@theoremjs/react 0.3.0 → 0.3.1

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 (258) hide show
  1. package/README.md +728 -202
  2. package/client/composer-hints.js +1 -1
  3. package/client/decision-images.d.ts +76 -0
  4. package/client/decision-images.js +139 -0
  5. package/client/decision-transport.d.ts +26 -4
  6. package/client/decision-transport.js +79 -6
  7. package/client/embedding-input.d.ts +84 -0
  8. package/client/embedding-input.js +194 -0
  9. package/client/embedding-space.d.ts +82 -0
  10. package/client/embedding-space.js +389 -0
  11. package/client/embedding-transport.d.ts +40 -0
  12. package/client/embedding-transport.js +71 -0
  13. package/client/failure.d.ts +7 -10
  14. package/client/failure.js +9 -9
  15. package/client/host-call.d.ts +1 -1
  16. package/client/host-call.js +7 -2
  17. package/client/host-transport.d.ts +2 -0
  18. package/client/host-transport.js +3 -2
  19. package/client/index.d.ts +19 -11
  20. package/client/index.js +14 -10
  21. package/client/ink-waveform.d.ts +1 -1
  22. package/client/ink-waveform.js +1 -2
  23. package/client/live/apply-live-turn-tool-event.js +1 -1
  24. package/client/live/live-captions.d.ts +30 -0
  25. package/client/live/live-captions.js +105 -1
  26. package/client/live/live-mic-forward.d.ts +1 -0
  27. package/client/live/live-mic-forward.js +11 -2
  28. package/client/live/live-page-tool.d.ts +34 -0
  29. package/client/live/live-page-tool.js +11 -0
  30. package/client/live/live-state.d.ts +4 -2
  31. package/client/live/live-state.js +2 -0
  32. package/client/live/live-video.js +1 -1
  33. package/client/live/run-live-tool-call.d.ts +7 -2
  34. package/client/live/run-live-tool-call.js +48 -10
  35. package/client/live/use-live-caption-log.d.ts +17 -0
  36. package/client/live/use-live-caption-log.js +51 -0
  37. package/client/live-client.d.ts +52 -4
  38. package/client/live-client.js +250 -59
  39. package/client/live-messages.d.ts +51 -13
  40. package/client/live-messages.js +42 -9
  41. package/client/page-dispatch.d.ts +15 -0
  42. package/client/page-dispatch.js +63 -0
  43. package/client/run-commit.d.ts +16 -43
  44. package/client/run-commit.js +26 -101
  45. package/client/run-session.d.ts +40 -41
  46. package/client/run-session.js +104 -112
  47. package/client/schema-fields.js +27 -6
  48. package/client/shaped-data.js +37 -8
  49. package/client/source-citations.js +1 -1
  50. package/client/space-geometry.d.ts +39 -0
  51. package/client/space-geometry.js +183 -0
  52. package/client/state-fields.js +1 -1
  53. package/client/tool-result.js +30 -12
  54. package/client/tool-resume.d.ts +18 -1
  55. package/client/tool-resume.js +4 -1
  56. package/client/trace-feed.d.ts +1 -1
  57. package/client/trace-story.d.ts +2 -2
  58. package/client/trace-story.js +43 -19
  59. package/client/trace-view.d.ts +2 -2
  60. package/client/trace-view.js +56 -16
  61. package/client/transcript-groups.d.ts +28 -2
  62. package/client/transcript-groups.js +63 -13
  63. package/client/transcript-schema.d.ts +10 -0
  64. package/client/transcript-schema.js +120 -0
  65. package/client/transport.d.ts +114 -26
  66. package/client/transport.js +106 -32
  67. package/client/turn-client.d.ts +17 -8
  68. package/client/turn-client.js +26 -31
  69. package/client/voice-recorder.d.ts +1 -1
  70. package/client/voice-recorder.js +2 -5
  71. package/client/wire-line.js +5 -5
  72. package/client/worklets/mic-capture.js +9 -9
  73. package/components/InkWaveform.d.ts +12 -1
  74. package/components/InkWaveform.js +65 -42
  75. package/components/live/use-live-runner-controls.d.ts +3 -4
  76. package/components/live/use-live-runner-controls.js +6 -10
  77. package/components/live/use-live-runner-model.d.ts +10 -6
  78. package/components/live/use-live-runner-model.js +29 -14
  79. package/components/live/use-live-runner-ui.d.ts +10 -8
  80. package/components/live/use-live-runner-ui.js +19 -11
  81. package/components/live/use-live-session-client.d.ts +4 -4
  82. package/components/live/use-live-session-client.js +11 -8
  83. package/components/use-composer-voice.d.ts +4 -4
  84. package/components/use-composer-voice.js +8 -8
  85. package/components/use-editor-selection.js +4 -1
  86. package/components/use-voice-playback.d.ts +2 -1
  87. package/components/use-voice-playback.js +4 -4
  88. package/hooks/use-described.d.ts +5 -1
  89. package/hooks/use-described.js +11 -9
  90. package/hooks/use-embedding-index.d.ts +27 -0
  91. package/hooks/use-embedding-index.js +43 -0
  92. package/hooks/use-one-call.d.ts +31 -0
  93. package/hooks/use-one-call.js +76 -0
  94. package/hooks/use-page-tool-warnings.d.ts +6 -0
  95. package/hooks/use-page-tool-warnings.js +25 -0
  96. package/hooks/use-second-ticker.d.ts +2 -0
  97. package/hooks/use-second-ticker.js +13 -0
  98. package/hooks/use-theorem-chat-actions.d.ts +25 -21
  99. package/hooks/use-theorem-chat-actions.js +67 -45
  100. package/hooks/use-theorem-chat-state.d.ts +6 -6
  101. package/hooks/use-theorem-chat-state.js +13 -9
  102. package/hooks/use-theorem-chat.d.ts +48 -14
  103. package/hooks/use-theorem-chat.js +329 -141
  104. package/hooks/use-theorem-decision.d.ts +5 -10
  105. package/hooks/use-theorem-decision.js +4 -67
  106. package/hooks/use-theorem-embedding.d.ts +18 -0
  107. package/hooks/use-theorem-embedding.js +12 -0
  108. package/hooks/use-theorem-host.d.ts +5 -4
  109. package/hooks/use-theorem-host.js +26 -8
  110. package/hooks/use-theorem-interface.d.ts +6 -3
  111. package/hooks/use-theorem-interface.js +12 -22
  112. package/index.d.ts +6 -3
  113. package/index.js +5 -2
  114. package/live.d.ts +1 -0
  115. package/live.js +1 -1
  116. package/package.json +6 -5
  117. package/server/checkpoint-ownership.d.ts +6 -0
  118. package/server/checkpoint-ownership.js +42 -0
  119. package/server/conversation-store.d.ts +92 -0
  120. package/server/conversation-store.js +131 -0
  121. package/server/conversation.d.ts +193 -0
  122. package/server/conversation.js +552 -0
  123. package/server/decision-handler.d.ts +12 -4
  124. package/server/decision-handler.js +29 -38
  125. package/server/embedding-handler.d.ts +33 -0
  126. package/server/embedding-handler.js +45 -0
  127. package/server/handler.d.ts +58 -20
  128. package/server/handler.js +441 -238
  129. package/server/mod.d.ts +15 -6
  130. package/server/mod.js +13 -5
  131. package/server/page-dispatch.d.ts +22 -0
  132. package/server/page-dispatch.js +127 -0
  133. package/server/request-body.d.ts +2 -0
  134. package/server/request-body.js +42 -0
  135. package/server/request-check.d.ts +6 -1
  136. package/server/request-check.js +27 -12
  137. package/server/session-authority.d.ts +8 -0
  138. package/server/session-authority.js +78 -0
  139. package/server/session-boundary.d.ts +26 -0
  140. package/server/session-boundary.js +64 -0
  141. package/server/session-identity.d.ts +16 -0
  142. package/server/session-identity.js +41 -0
  143. package/server/session-limits.d.ts +12 -0
  144. package/server/session-limits.js +59 -0
  145. package/server/session-store.d.ts +69 -20
  146. package/server/session-store.js +220 -5
  147. package/server/single-action.d.ts +21 -0
  148. package/server/single-action.js +30 -0
  149. package/server/steer-inbox.js +1 -1
  150. package/server/tool-operations.d.ts +6 -0
  151. package/server/tool-operations.js +132 -0
  152. package/server/turn-input.d.ts +10 -0
  153. package/server/turn-input.js +29 -0
  154. package/styles/ink-controls.css +78 -78
  155. package/ui/CardWall.d.ts +10 -0
  156. package/ui/CardWall.js +61 -0
  157. package/ui/ChatComposerBar.d.ts +28 -5
  158. package/ui/ChatComposerBar.js +85 -30
  159. package/ui/ChatTranscript.d.ts +14 -1
  160. package/ui/ChatTranscript.js +110 -57
  161. package/ui/ConsoleFrame.d.ts +61 -4
  162. package/ui/ConsoleFrame.js +109 -9
  163. package/ui/DecisionAnswers.d.ts +40 -2
  164. package/ui/DecisionAnswers.js +166 -35
  165. package/ui/DecisionImages.d.ts +16 -0
  166. package/ui/DecisionImages.js +46 -0
  167. package/ui/EmbeddingDocuments.d.ts +78 -0
  168. package/ui/EmbeddingDocuments.js +278 -0
  169. package/ui/EmbeddingQuery.d.ts +27 -0
  170. package/ui/EmbeddingQuery.js +95 -0
  171. package/ui/EmbeddingResults.d.ts +41 -0
  172. package/ui/EmbeddingResults.js +165 -0
  173. package/ui/EmbeddingSpace.d.ts +26 -0
  174. package/ui/EmbeddingSpace.js +106 -0
  175. package/ui/LiveCaptionsPanel.d.ts +22 -0
  176. package/ui/LiveCaptionsPanel.js +27 -0
  177. package/ui/LiveRunner.d.ts +6 -4
  178. package/ui/LiveRunner.js +42 -55
  179. package/ui/PaneState.d.ts +33 -0
  180. package/ui/PaneState.js +50 -0
  181. package/ui/SchemaFields.d.ts +1 -1
  182. package/ui/SchemaFields.js +54 -26
  183. package/ui/ShapedData.js +73 -28
  184. package/ui/SidePanel.d.ts +74 -5
  185. package/ui/SidePanel.js +82 -9
  186. package/ui/StateFields.d.ts +10 -2
  187. package/ui/StateFields.js +56 -13
  188. package/ui/TheoremChat.d.ts +65 -14
  189. package/ui/TheoremChat.js +66 -78
  190. package/ui/TheoremDecision.d.ts +12 -20
  191. package/ui/TheoremDecision.js +78 -35
  192. package/ui/TheoremEmbedding.d.ts +27 -0
  193. package/ui/TheoremEmbedding.js +122 -0
  194. package/ui/TheoremHost.d.ts +22 -5
  195. package/ui/TheoremHost.js +127 -37
  196. package/ui/ToolGateCard.d.ts +5 -1
  197. package/ui/ToolGateCard.js +30 -23
  198. package/ui/ToolResult.js +12 -10
  199. package/ui/ToolResultCharts.js +29 -12
  200. package/ui/TraceCharts.d.ts +2 -2
  201. package/ui/TraceCharts.js +48 -17
  202. package/ui/TraceGuardrails.d.ts +3 -2
  203. package/ui/TraceGuardrails.js +24 -6
  204. package/ui/TraceInspector.d.ts +11 -1
  205. package/ui/TraceInspector.js +45 -28
  206. package/ui/TraceInspectorPanel.d.ts +31 -9
  207. package/ui/TraceInspectorPanel.js +77 -39
  208. package/ui/TraceOverview.d.ts +7 -3
  209. package/ui/TraceOverview.js +44 -14
  210. package/ui/TraceSpanDetail.js +24 -22
  211. package/ui/TraceStory.js +9 -8
  212. package/ui/TraceValues.js +19 -9
  213. package/ui/TraceWaterfall.js +12 -9
  214. package/ui/VoiceNote.js +4 -4
  215. package/ui/approval-heading.d.ts +4 -0
  216. package/ui/approval-heading.js +9 -0
  217. package/ui/arrive.d.ts +18 -0
  218. package/ui/arrive.js +46 -0
  219. package/ui/built/theme.css +86 -68
  220. package/ui/built/theme.css.d.ts +9 -0
  221. package/ui/built/theorem.d.ts +3 -3
  222. package/ui/built/theorem.js +34 -5
  223. package/ui/built/theorem.variants.d.ts +8 -2
  224. package/ui/code-language.d.ts +12 -0
  225. package/ui/code-language.js +50 -0
  226. package/ui/disclosure-motion.js +53 -9
  227. package/ui/embedding-space-gl.d.ts +37 -0
  228. package/ui/embedding-space-gl.js +663 -0
  229. package/ui/embedding-space-view.d.ts +8 -0
  230. package/ui/embedding-space-view.js +8 -0
  231. package/ui/glide.d.ts +7 -0
  232. package/ui/glide.js +44 -0
  233. package/ui/icons.js +2 -1
  234. package/ui/index.d.ts +20 -9
  235. package/ui/index.js +20 -10
  236. package/ui/labels-provider.js +5 -2
  237. package/ui/labels.d.ts +374 -85
  238. package/ui/labels.js +1028 -188
  239. package/ui/markdown-plugins.d.ts +9 -0
  240. package/ui/markdown-plugins.js +13 -0
  241. package/ui/mcp-icon.d.ts +6 -0
  242. package/ui/mcp-icon.js +8 -0
  243. package/ui/row-keys.d.ts +19 -0
  244. package/ui/row-keys.js +33 -0
  245. package/ui/theme.d.ts +1 -1
  246. package/ui/theme.js +2 -2
  247. package/ui/theorem-theme.js +32 -10
  248. package/ui/trace-palette.d.ts +2 -16
  249. package/ui/trace-palette.js +2 -16
  250. package/ui/trace-patterns.d.ts +11 -3
  251. package/ui/trace-patterns.js +16 -3
  252. package/ui/transcript-copy-text.js +1 -0
  253. package/ui/wall-columns.d.ts +8 -0
  254. package/ui/wall-columns.js +19 -0
  255. package/client/walk-away.d.ts +0 -34
  256. package/client/walk-away.js +0 -48
  257. package/server/walk-away.d.ts +0 -22
  258. package/server/walk-away.js +0 -41
@@ -1,32 +1,40 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { AspectRatio } from '@astryxdesign/core/AspectRatio';
3
- import { ChatMessage, ChatMessageBubble, ChatMessageList, ChatMessageMetadata, ChatToolCalls, } from '@astryxdesign/core/Chat';
3
+ import { ChatMessage, ChatMessageBubble, ChatMessageList, ChatMessageMetadata, ChatSystemMessage, ChatToolCalls, } from '@astryxdesign/core/Chat';
4
4
  import { Citation } from '@astryxdesign/core/Citation';
5
5
  import { ClickableCard } from '@astryxdesign/core/ClickableCard';
6
6
  import { Collapsible } from '@astryxdesign/core/Collapsible';
7
7
  import { HStack } from '@astryxdesign/core/HStack';
8
+ import { useStreamingText } from '@astryxdesign/core/hooks';
8
9
  import { Icon } from '@astryxdesign/core/Icon';
9
10
  import { IconButton } from '@astryxdesign/core/IconButton';
11
+ import { useLocale } from '@astryxdesign/core/i18n';
10
12
  import { useLightbox } from '@astryxdesign/core/Lightbox';
11
- import { useStreamingText } from '@astryxdesign/core/hooks';
12
13
  import { Markdown } from '@astryxdesign/core/Markdown';
13
14
  import { Skeleton } from '@astryxdesign/core/Skeleton';
14
15
  import { Text } from '@astryxdesign/core/Text';
15
16
  import { Thumbnail } from '@astryxdesign/core/Thumbnail';
16
17
  import { Timestamp } from '@astryxdesign/core/Timestamp';
17
18
  import { Token } from '@astryxdesign/core/Token';
19
+ import { Tooltip as HoverTip } from '@astryxdesign/core/Tooltip';
18
20
  import { VStack } from '@astryxdesign/core/VStack';
19
21
  import { IconCheck, IconCopy } from '@tabler/icons-react';
20
- import { useEffect, useMemo, useRef, useState } from 'react';
22
+ import { stopText } from '@theoremjs/agents/interface';
23
+ import { createContext, useContext, useEffect, useMemo, useRef, useState, } from 'react';
21
24
  import { citationsFromBlock } from "../client/source-citations.js";
22
- import { assistantTurnCopyText, assistantTurnTiming, composeAssistantTurn, groupTranscriptBlocks, pendingPromptOf, promptReplyKey, replyKey, toolCallLabel, workStatus, } from "../client/transcript-groups.js";
25
+ import { assistantTurnCopyText, assistantTurnTiming, composeAssistantTurn, groupTranscriptBlocks, pendingPromptOf, promptReplyKey, promptTime, replyKey, toolCallLabel, workStatus, } from "../client/transcript-groups.js";
26
+ import { useSecondTicker } from "../hooks/use-second-ticker.js";
23
27
  import { useDisclosureMotion } from "./disclosure-motion.js";
24
- import { workDuration, workStatusLabel } from "./labels.js";
28
+ import { usageLine, workDuration, workStatusLabel } from "./labels.js";
25
29
  import { TheoremLabelsProvider, useLabels } from "./labels-provider.js";
30
+ import { useMarkdownPlugins } from "./markdown-plugins.js";
31
+ import { keyedByContent } from "./row-keys.js";
26
32
  import { ShapedData } from "./ShapedData.js";
27
- import { transcriptBlockCopyText } from "./transcript-copy-text.js";
28
33
  import { ApprovalCard, AuthChallengeCard } from "./ToolGateCard.js";
34
+ import { transcriptBlockCopyText } from "./transcript-copy-text.js";
29
35
  import { VoiceNote } from "./VoiceNote.js";
36
+ /** Whether the transcript shows usage (`ChatTranscriptProps.usage`). */
37
+ const ShowUsage = createContext(false);
30
38
  /** Largest a generated image renders in the transcript. */
31
39
  const GENERATED_IMAGE_MAX_WIDTH = 512;
32
40
  /** First-seen time per group key, so timestamps don't jump while streaming. */
@@ -39,18 +47,6 @@ function useFirstSeen(keys) {
39
47
  }
40
48
  return (key) => times.current.get(key) ?? now;
41
49
  }
42
- /** `Date.now()`, refreshed every second while `isActive`. */
43
- function useSecondTicker(isActive) {
44
- const [now, setNow] = useState(() => Date.now());
45
- useEffect(() => {
46
- if (!isActive)
47
- return;
48
- setNow(Date.now());
49
- const timer = globalThis.setInterval(() => setNow(Date.now()), 1_000);
50
- return () => globalThis.clearInterval(timer);
51
- }, [isActive]);
52
- return now;
53
- }
54
50
  /**
55
51
  * Each turn's span, keyed by the user group that started it. A turn stops at
56
52
  * a gate and streams again under the same prompt once it's answered; the wait
@@ -58,7 +54,7 @@ function useSecondTicker(isActive) {
58
54
  */
59
55
  function useTurnSpans(streaming, lastUserKey) {
60
56
  const spans = useRef(new Map());
61
- // The first send mounts the transcript already streaming; that's the start.
57
+ // why: The first send mounts the transcript already streaming; that's the start.
62
58
  const wasStreaming = useRef(false);
63
59
  if (lastUserKey && streaming !== wasStreaming.current) {
64
60
  const now = Date.now();
@@ -106,13 +102,36 @@ function MessageTime({ at }) {
106
102
  }
107
103
  return _jsx(Timestamp, { value: at, format: "relative_short", isLive: true });
108
104
  }
109
- /** A message's time, copy button and status; a failure says why on the line beneath. */
105
+ /** What a reply or a called agent used, in the viewer's locale. */
106
+ function Usage({ tokens }) {
107
+ return usageLine(useLabels(), useLocale(), tokens);
108
+ }
109
+ /** Why a turn stopped short, drawn as the delivery statuses are. */
110
+ function Stopped({ label }) {
111
+ return (_jsxs(HStack, { gap: 1, vAlign: "center", children: [_jsx(Icon, { icon: "stop", size: "xsm", color: "inherit" }), _jsx("span", { children: label })] }));
112
+ }
113
+ /** A failed message's status, drawn as Astryx draws its error status; the reason shows on hover. */
114
+ function Failed({ reason }) {
115
+ const label = useLabels()('@theorem.transcript.failed');
116
+ return (_jsx(HoverTip, { content: reason, children: _jsxs(HStack, { gap: 1, vAlign: "center", style: { color: 'var(--color-error)' }, children: [_jsx(Icon, { icon: "error", size: "xsm", color: "inherit" }), _jsx("span", { children: label })] }) }));
117
+ }
118
+ /** A message's copy button and time, the button on the outside: right for the person, left for the agent. */
119
+ function CopyAndTime(props) {
120
+ const copy = _jsx(CopyButton, { text: props.copyText });
121
+ const time = _jsx(MessageTime, { at: props.at });
122
+ return (_jsxs(HStack, { gap: 1, vAlign: "center", children: [props.sender === 'user' ? time : copy, props.sender === 'user' ? copy : time] }));
123
+ }
124
+ /** A message's copy button (outermost), time and status; a failure says why on hovering its status. */
110
125
  function MessageChrome(props) {
111
- const failed = props.error !== undefined;
112
- const metadata = (_jsx(ChatMessageMetadata, { timestamp: _jsx(MessageTime, { at: props.at }), footer: _jsx(CopyButton, { text: props.copyText }), status: failed ? 'error' : props.status }));
113
- if (!failed)
114
- return metadata;
115
- return (_jsxs(VStack, { gap: 1, children: [metadata, _jsx(Text, { type: "supporting", color: "secondary", children: props.error })] }));
126
+ const usage = props.usage ? _jsx(Usage, { tokens: props.usage }) : undefined;
127
+ const aside = props.error !== undefined ? (_jsx(Failed, { reason: props.error })) : props.stopped !== undefined ? (_jsx(Stopped, { label: props.stopped })) : (usage);
128
+ return (_jsx(ChatMessageMetadata
129
+ // why: The row's first slot is the one nearest the margin.
130
+ , {
131
+ // why: The row's first slot is the one nearest the margin.
132
+ timestamp: _jsx(CopyAndTime, { sender: props.sender, at: props.at, copyText: props.copyText }),
133
+ // why: Astryx's statuses are a closed set, so these ride the footer.
134
+ footer: aside, status: props.error !== undefined || props.stopped !== undefined ? undefined : props.status }));
116
135
  }
117
136
  function dataUrl(mimeType, data) {
118
137
  return data === undefined ? undefined : `data:${mimeType};base64,${data}`;
@@ -184,7 +203,10 @@ function GeneratedGallery({ items, ratio }) {
184
203
  return (_jsxs(VStack, { gap: 2, width: "100%", children: [items.map((item, i) => item.media.type === 'image' ? (_jsx(GeneratedImage, { item: item, ratio: ratio, onOpen: () => lightbox.open(i) }, item.media.src)) : (_jsx(Thumbnail, { alt: item.media.alt, label: item.media.alt, onClick: () => lightbox.open(i) }, item.media.src))), lightbox.element] }));
185
204
  }
186
205
  /** An image takes seconds: a slow, smooth breath rather than the Skeleton's quick stepped blink. */
187
- const GENERATING_PULSE = { animationDuration: '1.8s', animationTimingFunction: 'ease-in-out' };
206
+ const GENERATING_PULSE = {
207
+ animationDuration: '1.8s',
208
+ animationTimingFunction: 'ease-in-out',
209
+ };
188
210
  /** Placeholder shaped like the image being generated. */
189
211
  function GeneratingImage({ ratio }) {
190
212
  const t = useLabels();
@@ -229,12 +251,13 @@ function StreamedMarkdown({ text, streaming, density, components, }) {
229
251
  const [revealing, setRevealing] = useState(streaming);
230
252
  if (streaming && !revealing)
231
253
  setRevealing(true);
254
+ const plugins = useMarkdownPlugins();
232
255
  const shown = useStreamingText(text, revealing);
233
256
  useEffect(() => {
234
257
  if (!streaming && shown.length >= text.length)
235
258
  setRevealing(false);
236
259
  }, [streaming, shown, text]);
237
- return (_jsx(Markdown, { isStreaming: revealing, density: density, components: components, children: text }));
260
+ return (_jsx(Markdown, { isStreaming: revealing, density: density, components: components, plugins: plugins, children: text }));
238
261
  }
239
262
  function BodyBlock({ block, streaming }) {
240
263
  if (block.kind === 'text')
@@ -243,10 +266,9 @@ function BodyBlock({ block, streaming }) {
243
266
  return _jsx(ToolCall, { tool: block.tool });
244
267
  return _jsx(ResultBlock, { block: block });
245
268
  }
246
- /** Non-streaming answer rows: errors, sources, structured output and media. */
269
+ /** Non-streaming answer rows: errors, sources, structured output, and media. */
247
270
  function ResultBlock({ block }) {
248
271
  switch (block.kind) {
249
- // A failure shows on the message's metadata line (MessageChrome).
250
272
  case 'error':
251
273
  return null;
252
274
  case 'citation':
@@ -259,7 +281,7 @@ function ResultBlock({ block }) {
259
281
  return null;
260
282
  }
261
283
  }
262
- // Astryx sets a call's name in the code font; labels are sentences, so the row takes the body
284
+ // why: Astryx sets a call's name in the code font; labels are sentences, so the row takes the body
263
285
  // font and the detail restores the code font kept on the wrapper.
264
286
  const CODE_FONT_KEPT = {
265
287
  display: 'contents',
@@ -281,16 +303,20 @@ function toolDuration(t, tool) {
281
303
  return {};
282
304
  return { duration: workDuration(t, tool.endedAt - tool.startedAt) };
283
305
  }
284
- function toolCallItem(t, id, tool) {
306
+ /** What the agent an agent tool ran used, after the call's name. Only agent tools report it. */
307
+ function toolUsageStats(state, showUsage) {
308
+ return showUsage && state.tokens ? { stats: _jsx(Usage, { tokens: state.tokens }) } : {};
309
+ }
310
+ function toolCallItem(t, id, tool, showUsage) {
285
311
  const base = { key: id, name: toolCallLabel(tool) };
286
312
  const { state } = tool;
287
313
  switch (state?.phase) {
288
314
  case 'error':
289
- // What failed, as the tool reported it; the whole failure is in the detail.
290
315
  return {
291
316
  ...base,
292
317
  status: 'error',
293
318
  target: state.failure.message,
319
+ ...toolUsageStats(state, showUsage),
294
320
  errorMessage: state.failure.message,
295
321
  resultDetail: toolDetail(t, tool, _jsx(ShapedData, { value: state.failure, title: t('@theorem.transcript.tool_error') })),
296
322
  };
@@ -299,6 +325,7 @@ function toolCallItem(t, id, tool) {
299
325
  ...base,
300
326
  status: 'complete',
301
327
  ...toolDuration(t, tool),
328
+ ...toolUsageStats(state, showUsage),
302
329
  resultDetail: toolDetail(t, tool, state.output === undefined ? undefined : (_jsx(ShapedData, { value: state.output, title: t('@theorem.transcript.tool_output') }))),
303
330
  };
304
331
  case 'running':
@@ -309,7 +336,8 @@ function toolCallItem(t, id, tool) {
309
336
  }
310
337
  function ToolCall({ tool }) {
311
338
  const t = useLabels();
312
- return _jsx(ToolCalls, { calls: [toolCallItem(t, tool.name, tool)] });
339
+ const showUsage = useContext(ShowUsage);
340
+ return _jsx(ToolCalls, { calls: [toolCallItem(t, tool.name, tool, showUsage)] });
313
341
  }
314
342
  /**
315
343
  * Thinking reads as quieter text than the reply. Astryx has no reasoning
@@ -322,6 +350,7 @@ const THOUGHT_MARKDOWN = {
322
350
  };
323
351
  function TraceList({ items, streaming }) {
324
352
  const t = useLabels();
353
+ const showUsage = useContext(ShowUsage);
325
354
  const rows = [];
326
355
  let tools = [];
327
356
  const flush = () => {
@@ -332,7 +361,7 @@ function TraceList({ items, streaming }) {
332
361
  };
333
362
  for (const [i, item] of items.entries()) {
334
363
  if (item.kind === 'tool') {
335
- tools.push(toolCallItem(t, item.id, item.block.tool));
364
+ tools.push(toolCallItem(t, item.id, item.block.tool, showUsage));
336
365
  continue;
337
366
  }
338
367
  flush();
@@ -341,17 +370,28 @@ function TraceList({ items, streaming }) {
341
370
  flush();
342
371
  return _jsx(VStack, { gap: 2, children: rows });
343
372
  }
344
- function GateCard({ block, handlers }) {
373
+ /** The gate a person decides. The page answers a page gate, so it has none. */
374
+ function personGate(tool) {
375
+ if (tool.state?.phase !== 'gate' || tool.state.gate.kind === 'page')
376
+ return null;
377
+ return tool.state.gate;
378
+ }
379
+ /** The answer on its way for this tool's gate, if the one being sent is this tool's. */
380
+ function answerSent(answering, tool) {
381
+ const ours = answering?.callId === tool.callId && answering?.operationId === tool.operationId;
382
+ return ours ? answering.action : null;
383
+ }
384
+ function GateCard({ block, handle, handlers, }) {
345
385
  const { tool } = block;
346
- if (tool.state?.phase !== 'gate')
386
+ const gate = personGate(tool);
387
+ if (!gate)
347
388
  return null;
348
- const { gate } = tool.state;
349
389
  const index = handlers.indexOf(block);
350
- const answer = handlers.answering?.callId === tool.callId ? handlers.answering.action : null;
390
+ const answer = answerSent(handlers.answering, tool);
351
391
  if (gate.kind === 'auth') {
352
392
  return (_jsx(AuthChallengeCard, { gate: gate, toolName: tool.name, submitted: answer === 'auth', onAuthenticated: (secret) => handlers.onAuthenticated?.(index, secret) }));
353
393
  }
354
- return (_jsx(ApprovalCard, { gate: gate, toolName: tool.name, input: tool.arguments, decided: answer === 'auth' ? null : answer, onDecision: (action) => handlers.onToolDecision?.(index, action) }));
394
+ return (_jsx(ApprovalCard, { gate: gate, toolName: tool.name, agent: handle, input: tool.arguments, decided: answer === 'auth' ? null : answer, onDecision: (action) => handlers.onToolDecision?.(index, action) }));
355
395
  }
356
396
  /** Live elapsed time while the turn streams; its final duration once it ends. */
357
397
  /** Live: counted from the start. Stopped: the work its blocks record. */
@@ -381,7 +421,7 @@ function TurnStatus(props) {
381
421
  return label;
382
422
  return (_jsx(Collapsible, { trigger: label, isOpen: traceOpen, onOpenChange: setTraceOpen, children: _jsx(TraceList, { items: props.trace, streaming: streaming }) }));
383
423
  }
384
- function BodyRowView({ row, streaming, imageOutput }) {
424
+ function BodyRowView({ row, streaming, imageOutput, }) {
385
425
  if (row.kind === 'block')
386
426
  return _jsx(BodyBlock, { block: row.block, streaming: streaming });
387
427
  if (imageOutput)
@@ -396,15 +436,16 @@ function PendingImage(props) {
396
436
  }
397
437
  function AssistantTurn(props) {
398
438
  const t = useLabels();
439
+ const showUsage = useContext(ShowUsage);
399
440
  const elapsedMs = useTurnElapsed(props.streaming, props.startedAt, props.workedMs);
400
441
  const { trace, gatedTools, body, hasTrace } = composeAssistantTurn(props.blocks);
401
442
  const rows = bodyRows(t, body);
402
443
  const status = workStatusLabel(t, workStatus({ streaming: props.streaming, hasTrace, elapsedMs }));
403
444
  const copyText = assistantTurnCopyText(body.length > 0 ? body : props.blocks);
404
- return (_jsx(ChatMessage, { sender: "assistant", name: props.handle, metadata: props.streaming ? undefined : _jsx(MessageChrome, { at: props.at, copyText: copyText, error: props.error }), children: _jsxs(VStack, { gap: 3, width: "100%", children: [_jsx(TurnStatus, { status: status, trace: trace, hasTrace: hasTrace, streaming: props.streaming }), gatedTools.map((block) => (_jsx(GateCard, { block: block, handlers: props.handlers }, block.id))), rows.map((row, i) => (_jsx(BodyRowView
405
- // By position: the committed turn re-mints block ids, and a
445
+ return (_jsx(ChatMessage, { sender: "assistant", name: props.handle, metadata: props.streaming ? undefined : (_jsx(MessageChrome, { sender: "assistant", at: props.at, copyText: copyText, error: props.error, stopped: props.stopped, usage: showUsage ? props.usage : undefined })), children: _jsxs(VStack, { gap: 3, width: "100%", children: [_jsx(TurnStatus, { status: status, trace: trace, hasTrace: hasTrace, streaming: props.streaming }), gatedTools.map((block) => (_jsx(GateCard, { block: block, handle: props.handle, handlers: props.handlers }, block.id))), keyedByContent(rows, (row) => (row.kind === 'gallery' ? 'gallery' : row.block.kind)).map(({ item: row, index: i, key }) => (_jsx(BodyRowView
446
+ // why: Not the block id: the committed turn re-mints ids, and a
406
447
  // remount would cut the text reveal short.
407
- , { row: row, streaming: props.streaming && i === rows.length - 1, imageOutput: props.imageOutput }, i))), _jsx(PendingImage, { imageOutput: props.imageOutput, streaming: props.streaming, rows: rows })] }) }));
448
+ , { row: row, streaming: props.streaming && i === rows.length - 1, imageOutput: props.imageOutput }, key))), _jsx(PendingImage, { imageOutput: props.imageOutput, streaming: props.streaming, rows: rows })] }) }));
408
449
  }
409
450
  function UserTurn(props) {
410
451
  const t = useLabels();
@@ -412,8 +453,8 @@ function UserTurn(props) {
412
453
  .map((block) => transcriptBlockCopyText(t, block))
413
454
  .filter(Boolean)
414
455
  .join('\n\n');
415
- const chrome = _jsx(MessageChrome, { at: props.at, copyText: copyText, status: props.status, error: props.error });
416
- // Astryx: metadata goes on the last bubble, or on the message when the last
456
+ const chrome = (_jsx(MessageChrome, { sender: "user", at: props.at, copyText: copyText, status: props.status, stopped: props.stopped, error: props.error }));
457
+ // why: Astryx: metadata goes on the last bubble, or on the message when the last
417
458
  // content is unbubbled (an attachment or voice note).
418
459
  const last = props.blocks.at(-1);
419
460
  const lastIsBubble = last?.kind === 'user-text';
@@ -429,15 +470,14 @@ function isBareFailure(group) {
429
470
  }
430
471
  /** Theorem transcript blocks rendered as Astryx chat messages. */
431
472
  export function ChatTranscript(props) {
432
- return (_jsx(TheoremLabelsProvider, { children: _jsx(ChatTranscriptBody, { ...props }) }));
473
+ return (_jsx(TheoremLabelsProvider, { children: _jsx(ShowUsage.Provider, { value: props.usage === true, children: _jsx(ChatTranscriptBody, { ...props }) }) }));
433
474
  }
434
- function ChatTranscriptBody({ blocks, handle, streaming = false, delivery, onToolDecision, answering = null, onAuthenticated, emptyState, imageOutput, }) {
435
- const groups = useMemo(() => groupTranscriptBlocks(blocks), [blocks]);
475
+ function ChatTranscriptBody({ blocks, handle, streaming = false, delivery, onToolDecision, answering = null, onAuthenticated, emptyState, dividers, imageOutput, labels, }) {
476
+ const groups = useMemo(() => groupTranscriptBlocks(blocks, dividers && new Set(Object.keys(dividers))), [blocks, dividers]);
436
477
  const timeOf = useFirstSeen(groups.map((group, index) => (group.kind === 'user' ? group.key : replyKey(groups, index))));
437
478
  const spans = useTurnSpans(streaming, groups.findLast((group) => group.kind === 'user')?.key);
438
479
  const pendingPrompt = streaming ? pendingPromptOf(groups) : undefined;
439
480
  const listRef = useRef(null);
440
- // Disclosures ease open and shut in place; opening one never moves the transcript.
441
481
  useDisclosureMotion(listRef);
442
482
  const handlers = {
443
483
  indexOf: (block) => blocks.findIndex((entry) => entry.id === block.id),
@@ -448,24 +488,37 @@ function ChatTranscriptBody({ blocks, handle, streaming = false, delivery, onToo
448
488
  const turn = { handle, handlers, imageOutput };
449
489
  const lastUser = groups.findLastIndex((group) => group.kind === 'user');
450
490
  const userTurn = (group, index) => {
451
- // A turn that failed before any reply shows it on the message itself.
452
491
  const next = groups[index + 1];
453
492
  const error = isBareFailure(next) ? failureOf(next) : undefined;
454
493
  const status = index === lastUser ? (delivery ?? undefined) : undefined;
455
- return _jsx(UserTurn, { blocks: group.blocks, at: timeOf(group.key), status: status, error: error }, group.key);
494
+ return (_jsx(UserTurn, { blocks: group.blocks, at: promptTime(groups, index, timeOf), status: status, stopped: stopText(group.stop, group.blocks, labels), error: error }, group.key));
456
495
  };
457
- const turns = groups.flatMap((group, index) => {
496
+ const turnOf = (group, index) => {
458
497
  if (group.kind === 'user')
459
498
  return userTurn(group, index);
460
499
  if (isBareFailure(group) && groups[index - 1]?.kind === 'user')
461
500
  return [];
462
- const { key, live, endedAt, ...timing } = assistantTurnTiming({ groups, index, streaming, timeOf, spans });
463
- // A reply is dated when it last stopped: a reply that just finished reads "now".
501
+ const { key, live, endedAt, ...timing } = assistantTurnTiming({
502
+ groups,
503
+ index,
504
+ streaming,
505
+ timeOf,
506
+ spans,
507
+ });
508
+ // why: A reply is dated when it last stopped: a reply that just finished reads "now".
464
509
  const at = endedAt ?? timeOf(key);
465
- return (_jsx(AssistantTurn, { ...turn, ...timing, blocks: group.blocks, streaming: live, at: at, error: failureOf(group) }, key));
510
+ return (_jsx(AssistantTurn, { ...turn, ...timing, blocks: group.blocks, streaming: live, at: at, error: failureOf(group), stopped: stopText(group.stop, group.blocks, labels), usage: group.usage }, key));
511
+ };
512
+ const turns = groups.flatMap((group, index) => {
513
+ const label = dividers?.[group.key];
514
+ if (label === undefined)
515
+ return turnOf(group, index);
516
+ return [
517
+ _jsx(ChatSystemMessage, { variant: "divider", children: label }, `divider:${group.key}`),
518
+ turnOf(group, index),
519
+ ].flat();
466
520
  });
467
- // Nothing streamed back yet: show the reply's "Working…" status right away.
468
- // It sits in the same keyed list as the streamed reply, so the reply stays
521
+ // why: It sits in the same keyed list as the streamed reply, so the reply stays
469
522
  // one message; a remount would read to the layout as a new message.
470
523
  if (pendingPrompt) {
471
524
  turns.push(_jsx(AssistantTurn, { ...turn, blocks: [], streaming: true, at: timeOf(pendingPrompt.key), startedAt: timeOf(pendingPrompt.key) }, promptReplyKey(pendingPrompt)));
@@ -1,8 +1,53 @@
1
- import type { CSSProperties, ReactNode } from 'react';
1
+ import type { DefinedTheme } from '@astryxdesign/core/theme';
2
+ import { type CSSProperties, type ReactNode } from 'react';
3
+ import type { HttpOptions } from '../client/transport.ts';
4
+ import type { TheoremLabels } from './labels.ts';
2
5
  import { type TraceInspector } from './TraceInspectorPanel.tsx';
6
+ /** The props a one-call console page shares: its look, its wording, its trace, and its layout. */
7
+ export type ConsolePageProps = {
8
+ /** Extra fetch options for the default HTTP transport (auth headers, custom fetch). */
9
+ http?: HttpOptions;
10
+ theme?: DefinedTheme;
11
+ mode?: 'system' | 'light' | 'dark';
12
+ /** Replacement lines by locale, for any `@theorem.*` or `@astryx.*` key. */
13
+ labels?: TheoremLabels;
14
+ /** Widest the column may grow, as a CSS length. Default `960px`. */
15
+ maxWidth?: string;
16
+ /**
17
+ * Drive the trace from the host's own control. The built-in toggle hides.
18
+ * The open trace takes the result's place, unless this page is inside
19
+ * `TracePlacement value="panel"`, where it docks. Omit to keep the toggle.
20
+ * Needs a profile that records traces.
21
+ */
22
+ trace?: boolean;
23
+ /**
24
+ * The page is already a raised panel. The console sits in it instead of
25
+ * painting the ground it uses on a flat page.
26
+ */
27
+ flush?: boolean;
28
+ /** The request on the left and the result on the right. */
29
+ columns?: boolean;
30
+ className?: string;
31
+ style?: CSSProperties;
32
+ };
3
33
  export type ConsoleFrameProps = {
4
34
  inspector: TraceInspector;
5
35
  maxWidth: string;
36
+ /**
37
+ * The page is already a raised panel. The console sits in that panel instead
38
+ * of painting the ground it uses when the page behind it is flat.
39
+ */
40
+ flush?: boolean;
41
+ /**
42
+ * The request and the response sit side by side. The request is a start
43
+ * panel at two fifths of that pair, so its handle is on the trailing edge
44
+ * and the response fills the other three fifths. Below the shell's drawer
45
+ * breakpoint they stack, request first. Two children: the request, then the
46
+ * response. One child keeps the single column.
47
+ */
48
+ columns?: boolean;
49
+ /** The request column's name, read by its scroll region and its resize handle. */
50
+ requestLabel?: string;
6
51
  className?: string;
7
52
  style?: CSSProperties;
8
53
  children: ReactNode;
@@ -11,7 +56,17 @@ export type ConsoleFrameProps = {
11
56
  * A console's page, for decisions and hosts alike: the trace's toggle in the
12
57
  * header, and one raised panel, flush with the host's, scrolling inside.
13
58
  */
14
- export declare function ConsoleFrame({ inspector, maxWidth, className, style, children }: ConsoleFrameProps): import("react").JSX.Element;
59
+ export declare function ConsoleFrame({ inspector, maxWidth, flush, columns, requestLabel, className, style, children, }: ConsoleFrameProps): import("react").JSX.Element;
60
+ /** One card of a response with several: its request and result sit in it without cards of their own. */
61
+ export declare function ConsoleSection({ children }: {
62
+ children: ReactNode;
63
+ }): import("react").JSX.Element;
64
+ /** The response. Beside the request it scrolls in the panel; a centred state fills the card, as an empty trace does. */
65
+ export declare function ResponseColumn({ label, fill, children, }: {
66
+ label: string;
67
+ fill?: boolean;
68
+ children: ReactNode;
69
+ }): string | number | bigint | boolean | import("react").JSX.Element | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | null | undefined;
15
70
  export type ConsoleView = 'fields' | 'json';
16
71
  export type RequestCardProps = {
17
72
  title: string;
@@ -26,7 +81,7 @@ export type RequestCardProps = {
26
81
  /** The request's card: its heading with the Fields/JSON switch, then the request and its run bar. */
27
82
  export declare function RequestCard({ title, view, hasFields, onView, onRun, children }: RequestCardProps): import("react").JSX.Element;
28
83
  /** The request as JSON text, marked while it doesn't parse. */
29
- export declare function JsonRequest({ label, text, isInvalid, onChange }: {
84
+ export declare function JsonRequest({ label, text, isInvalid, onChange, }: {
30
85
  label: string;
31
86
  text: string;
32
87
  isInvalid: boolean;
@@ -44,4 +99,6 @@ export type RunBarProps = {
44
99
  onStop: () => void;
45
100
  };
46
101
  /** A note on the request, Stop while it runs, and Run. */
47
- export declare function RunBar({ note, isReady, isRunning, runLabel, runningLabel, stopLabel, onRun, onStop }: RunBarProps): import("react").JSX.Element;
102
+ export declare function RunBar({ note, isReady, isRunning, runLabel, runningLabel, stopLabel, onRun, onStop, }: RunBarProps): import("react").JSX.Element;
103
+ /** A size in bytes as the reader's locale writes it: bytes under a kilobyte, else kilobytes. */
104
+ export declare function useBytes(): (bytes: number) => string;
@@ -3,20 +3,102 @@ import { Button } from '@astryxdesign/core/Button';
3
3
  import { Card } from '@astryxdesign/core/Card';
4
4
  import { Center } from '@astryxdesign/core/Center';
5
5
  import { HStack } from '@astryxdesign/core/HStack';
6
+ import { useLocale } from '@astryxdesign/core/i18n';
6
7
  import { Layout, LayoutContent } from '@astryxdesign/core/Layout';
8
+ import { useResizable } from '@astryxdesign/core/Resizable';
7
9
  import { SegmentedControl, SegmentedControlItem } from '@astryxdesign/core/SegmentedControl';
8
10
  import { Text } from '@astryxdesign/core/Text';
9
11
  import { TextArea } from '@astryxdesign/core/TextArea';
10
12
  import { VStack } from '@astryxdesign/core/VStack';
13
+ import { Children, createContext, use, useMemo, useRef, useSyncExternalStore, } from 'react';
11
14
  import { useLabels } from "./labels-provider.js";
12
- import { RAISED, SidePanelHeader } from "./SidePanel.js";
15
+ import { PanelBody, PanelScroll, RAISED, SidePanel, SidePanelHeader } from "./SidePanel.js";
16
+ import { textRows } from "./StateFields.js";
13
17
  import { WithTrace } from "./TraceInspectorPanel.js";
18
+ import { CONSOLE_CARD } from "./trace-patterns.js";
19
+ /** Cards on a flush console. The flat page uses the surface; a raised page uses the card. */
20
+ const FLUSH_CARD = {
21
+ '--theorem-console-card': 'var(--color-background-muted)',
22
+ };
23
+ /** Below the app shell's drawer breakpoint, where the two columns stack. */
24
+ const NARROW = '(width < 768px)';
25
+ /** Side by side, the panel card is the frame, so the request and response bring no card of their own. */
26
+ const ConsoleChrome = createContext(true);
27
+ function useNarrow() {
28
+ return useSyncExternalStore((change) => {
29
+ const query = globalThis.matchMedia(NARROW);
30
+ query.addEventListener('change', change);
31
+ return () => {
32
+ query.removeEventListener('change', change);
33
+ };
34
+ }, () => globalThis.matchMedia(NARROW).matches, () => false);
35
+ }
36
+ /**
37
+ * One column: the request, then the response under it, in the page's own card.
38
+ * `wide` is a pair stacked on a narrow page, which takes the page's width.
39
+ */
40
+ function ConsoleSingle({ flush, wide, maxWidth, children, }) {
41
+ return (_jsx(VStack, { height: "100%", paddingInline: 3, style: flush ? FLUSH_CARD : undefined, children: _jsx(Card, { variant: "transparent", height: "100%", padding: 0, style: flush ? { overflowY: 'auto' } : { ...RAISED, overflowY: 'auto' }, children: _jsx(Center, { axis: "horizontal", width: "100%", minHeight: "100%", children: _jsx(VStack, { width: "100%", maxWidth: wide ? undefined : maxWidth, gap: 3, padding: 4, children: children }) }) }) }));
42
+ }
14
43
  /**
15
44
  * A console's page, for decisions and hosts alike: the trace's toggle in the
16
45
  * header, and one raised panel, flush with the host's, scrolling inside.
17
46
  */
18
- export function ConsoleFrame({ inspector, maxWidth, className, style, children }) {
19
- return (_jsx(WithTrace, { inspector: inspector, children: _jsx(Layout, { height: "fill", className: className, style: style, header: inspector.toggle ? _jsx(SidePanelHeader, { children: inspector.toggle }) : undefined, content: _jsx(LayoutContent, { padding: 0, isScrollable: false, children: _jsx(VStack, { height: "100%", paddingInline: 3, children: _jsx(Card, { variant: "transparent", height: "100%", padding: 0, style: { ...RAISED, overflowY: 'auto' }, children: _jsx(Center, { axis: "horizontal", width: "100%", children: _jsx(VStack, { width: "100%", maxWidth: maxWidth, gap: 3, padding: 4, children: children }) }) }) }) }) }) }));
47
+ export function ConsoleFrame({ inspector, maxWidth, flush = false, columns = false, requestLabel, className, style, children, }) {
48
+ const layoutRef = useRef(null);
49
+ return (_jsx(WithTrace, { inspector: inspector, children: _jsx(Layout, { ref: layoutRef, height: "fill", className: className, style: style, header: inspector.toggle ? _jsx(SidePanelHeader, { children: inspector.toggle }) : undefined, content: _jsx(LayoutContent, { padding: 0, isScrollable: false, children: _jsx(ConsoleBody, { layoutRef: layoutRef, flush: flush, columns: columns, maxWidth: maxWidth, requestLabel: requestLabel, children: children }) }) }) }));
50
+ }
51
+ /** The console's panels: a request and response side by side when there is room, else one column. */
52
+ function ConsoleBody({ layoutRef, flush, columns, maxWidth, requestLabel, children, }) {
53
+ const narrow = useNarrow();
54
+ // why: Children.count counts a null slot; toArray drops it, which is the child that is actually drawn.
55
+ const drawn = Children.toArray(children).length;
56
+ const paired = columns && drawn > 1;
57
+ if (paired && !narrow) {
58
+ return (_jsx(ConsoleColumns, { requestLabel: requestLabel, basisWidth: layoutRef.current?.clientWidth ?? 0, children: children }));
59
+ }
60
+ return (_jsx(ConsoleSingle, { flush: flush, wide: paired, maxWidth: maxWidth, children: children }));
61
+ }
62
+ /**
63
+ * The request is the sized panel and the response fills what it leaves, so
64
+ * one handle keeps the pair at two fifths / three fifths. The request sizes
65
+ * against this inner layout, not the page, so a trace docked outside the pair
66
+ * cannot take the request's share.
67
+ */
68
+ function ConsoleColumns({ requestLabel = 'Request', basisWidth, children, }) {
69
+ const t = useLabels();
70
+ const pairRef = useRef(null);
71
+ // why: a percentage default resolves against a 1200px stand-in until the pair
72
+ // is measured, and the panel's width transition then eases between the two,
73
+ // reflowing the request. This layout is already on screen, so the first
74
+ // width is two fifths of it. 70% leaves the response a readable share, and a
75
+ // percentage maximum follows the pair when the trace opens.
76
+ const requestPanel = useResizable({
77
+ defaultSize: basisWidth > 0 ? Math.round(basisWidth * 0.4) : '40%',
78
+ minSize: 280,
79
+ maxSize: '70%',
80
+ containerRef: pairRef,
81
+ });
82
+ const [request, response] = Children.toArray(children);
83
+ const labels = {
84
+ name: requestLabel,
85
+ show: requestLabel,
86
+ hide: requestLabel,
87
+ resize: t('@theorem.panel.resize', { name: requestLabel }),
88
+ };
89
+ return (_jsx(ConsoleChrome, { value: false, children: _jsx(Layout, { ref: pairRef, height: "fill", start: _jsx(SidePanel, { side: "start", className: "theorem-console-request", labels: labels, resizable: requestPanel, open: true, children: _jsx(PanelScroll, { label: requestLabel, children: request }) }), content: _jsx(LayoutContent, { padding: 0, isScrollable: false, children: _jsx(PanelBody, { inset: "beside", className: "theorem-console-response", children: response }) }) }) }));
90
+ }
91
+ /** One card of a response with several: its request and result sit in it without cards of their own. */
92
+ export function ConsoleSection({ children }) {
93
+ return (_jsx(Card, { variant: "muted", padding: 0, className: "theorem-console-card", style: CONSOLE_CARD, children: _jsx(ConsoleChrome, { value: false, children: children }) }));
94
+ }
95
+ /** The response. Beside the request it scrolls in the panel; a centred state fills the card, as an empty trace does. */
96
+ export function ResponseColumn({ label, fill = false, children, }) {
97
+ const framed = use(ConsoleChrome);
98
+ if (!framed) {
99
+ return fill ? (children) : (_jsx(PanelScroll, { label: label, padding: 4, children: children }));
100
+ }
101
+ return (_jsx(Card, { variant: "muted", padding: fill ? 0 : 4, className: "theorem-console-card", style: CONSOLE_CARD, children: fill ? (children) : (_jsx(VStack, { height: "100%", gap: 3, children: children })) }));
20
102
  }
21
103
  /** ⌘/Ctrl+Enter runs the console's request. */
22
104
  function runShortcut(run) {
@@ -30,17 +112,35 @@ function runShortcut(run) {
30
112
  /** The request's card: its heading with the Fields/JSON switch, then the request and its run bar. */
31
113
  export function RequestCard({ title, view, hasFields, onView, onRun, children }) {
32
114
  const t = useLabels();
33
- return (_jsx(Card, { variant: "muted", padding: 4, style: { background: 'var(--color-background-surface)' }, children: _jsxs(VStack, { gap: 3, onKeyDown: runShortcut(onRun), children: [_jsxs(HStack, { gap: 2, hAlign: "between", vAlign: "center", children: [_jsx(Text, { type: "supporting", color: "secondary", as: "h3", children: title }), _jsxs(SegmentedControl, { label: t('@theorem.decision.view'), size: "sm", value: view, onChange: (next) => {
34
- onView(next === 'json' ? 'json' : 'fields');
35
- }, children: [_jsx(SegmentedControlItem, { value: "fields", label: t('@theorem.decision.fields'), isDisabled: !hasFields }), _jsx(SegmentedControlItem, { value: "json", label: t('@theorem.data.json') })] })] }), children] }) }));
115
+ const framed = use(ConsoleChrome);
116
+ const body = (_jsxs(VStack, { gap: 3, padding: framed ? undefined : 4, onKeyDown: runShortcut(onRun), children: [_jsxs(HStack, { gap: 2, hAlign: "between", vAlign: "center", children: [_jsx(Text, { type: "supporting", color: "secondary", as: "h3", children: title }), _jsxs(SegmentedControl, { label: t('@theorem.decision.view'), size: "sm", value: view, onChange: (next) => {
117
+ onView(next === 'json' ? 'json' : 'fields');
118
+ }, children: [_jsx(SegmentedControlItem, { value: "fields", label: t('@theorem.decision.fields'), isDisabled: !hasFields }), _jsx(SegmentedControlItem, { value: "json", label: t('@theorem.data.json') })] })] }), children] }));
119
+ if (!framed)
120
+ return body;
121
+ return (_jsx(Card, { variant: "muted", padding: 4, className: "theorem-console-card", style: CONSOLE_CARD, children: body }));
36
122
  }
37
123
  /** The field reads as code: the body font is swapped for the code font inside it. */
38
124
  const CODE_FONT = { '--font-family-body': 'var(--font-family-code)' };
39
125
  /** The request as JSON text, marked while it doesn't parse. */
40
- export function JsonRequest({ label, text, isInvalid, onChange }) {
41
- return (_jsx(TextArea, { label: label, isLabelHidden: true, value: text, onChange: onChange, rows: 8, hasSpellCheck: false, status: isInvalid ? { type: 'error' } : undefined, style: CODE_FONT }));
126
+ export function JsonRequest({ label, text, isInvalid, onChange, }) {
127
+ return (_jsx(TextArea, { label: label, isLabelHidden: true, value: text, onChange: onChange, rows: textRows(text, 8, 24), hasSpellCheck: false, status: isInvalid ? { type: 'error' } : undefined, controlStyle: CODE_FONT }));
42
128
  }
43
129
  /** A note on the request, Stop while it runs, and Run. */
44
- export function RunBar({ note, isReady, isRunning, runLabel, runningLabel, stopLabel, onRun, onStop }) {
130
+ export function RunBar({ note, isReady, isRunning, runLabel, runningLabel, stopLabel, onRun, onStop, }) {
45
131
  return (_jsxs(HStack, { gap: 3, hAlign: "between", vAlign: "center", children: [_jsx(Text, { type: "supporting", color: "secondary", style: isReady ? undefined : { color: 'var(--color-error)' }, hasTabularNumbers: true, children: note }), _jsxs(HStack, { gap: 2, vAlign: "center", children: [isRunning ? _jsx(Button, { label: stopLabel, variant: "ghost", onClick: onStop }) : null, _jsx(Button, { label: isRunning ? runningLabel : runLabel, variant: "primary", isLoading: isRunning, isDisabled: !isReady, onClick: onRun })] })] }));
46
132
  }
133
+ /** A size in bytes as the reader's locale writes it: bytes under a kilobyte, else kilobytes. */
134
+ export function useBytes() {
135
+ const locale = useLocale();
136
+ return useMemo(() => {
137
+ const b = new Intl.NumberFormat(locale, { style: 'unit', unit: 'byte', unitDisplay: 'narrow' });
138
+ const kb = new Intl.NumberFormat(locale, {
139
+ style: 'unit',
140
+ unit: 'kilobyte',
141
+ unitDisplay: 'narrow',
142
+ maximumFractionDigits: 1,
143
+ });
144
+ return (bytes) => (bytes < 1024 ? b.format(bytes) : kb.format(bytes / 1024));
145
+ }, [locale]);
146
+ }