@theoremjs/react 0.3.4-next.542.g59bcbcf → 0.3.4-next.548.gbb156b2

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.
package/README.md CHANGED
@@ -435,6 +435,8 @@ function MyChat() {
435
435
 
436
436
  To show the transcript and the composer, use `ChatTranscript` and `ChatComposerBar` from `/ui`. You can also draw your own from `chat.blocks`.
437
437
 
438
+ `ChatTranscript` places `citation` blocks after the answer text by default (the Sources strip under the prose). Pass `bodyKindPlacement={{}}` when citations must stay in stream order between text segments (inline citations).
439
+
438
440
  The hooks and the client write no English text. They return kinds, codes, and states. You write the words.
439
441
 
440
442
  - A failure is a `ClientFailure`: `{ error, errorKind, errorInternal? }`. Show `error` to the user. It is the wording of the profile. `errorKind` and `errorInternal` are for your code.
@@ -58,6 +58,16 @@ export type ComposedAssistantTurn = {
58
58
  /** True when the disclosure control should appear. */
59
59
  hasTrace: boolean;
60
60
  };
61
+ /** Pin a body kind before or after kinds with no placement (stream order among peers). */
62
+ export type BodyKindPlacement = 'first' | 'last';
63
+ export type ComposeAssistantTurnOptions = {
64
+ /**
65
+ * Where body kinds sit relative to each other. Omit a kind (or pass `null`)
66
+ * to keep stream order among peers. Pass `{}` for pure stream order.
67
+ * When the option is omitted entirely, {@link DEFAULT_BODY_KIND_PLACEMENT} applies.
68
+ */
69
+ bodyKindPlacement?: Partial<Record<TranscriptBlock['kind'], BodyKindPlacement | null | undefined>>;
70
+ };
61
71
  /**
62
72
  * Blocks as turns: neighbours from the same side share one, unless a block is
63
73
  * in `breaks`, which always starts its own. A message whose turn stopped short
@@ -86,8 +96,10 @@ export declare function workStatus(args: {
86
96
  * - `text` before/between tools → narration; text and answer kinds after the
87
97
  * latest tool → body (while streaming too, so the answer streams formatted)
88
98
  * - No tools: thoughts still go to trace; remaining kinds → body
99
+ * - Body kinds then follow {@link ComposeAssistantTurnOptions.bodyKindPlacement}
100
+ * (default: citations last)
89
101
  */
90
- export declare function composeAssistantTurn(blocks: readonly TranscriptBlock[]): ComposedAssistantTurn;
102
+ export declare function composeAssistantTurn(blocks: readonly TranscriptBlock[], options?: ComposeAssistantTurnOptions): ComposedAssistantTurn;
91
103
  /** The trailing user group while its reply has not started streaming back. */
92
104
  export declare function pendingPromptOf(groups: readonly TranscriptTurnGroup[]): TranscriptTurnGroup | undefined;
93
105
  export type AssistantTurnTiming = {
@@ -16,6 +16,31 @@ function addUsage(used, more) {
16
16
  const partial = !used.cost || !more.cost || used.cost.partial || more.cost.partial;
17
17
  return { total, cost: { usd, ...(partial ? { partial: true } : {}) } };
18
18
  }
19
+ /**
20
+ * Theorem's Sources strip: citations under the answer. For inline citations that
21
+ * must stay between text segments, pass `{ bodyKindPlacement: {} }`.
22
+ */
23
+ const DEFAULT_BODY_KIND_PLACEMENT = {
24
+ citation: 'last',
25
+ };
26
+ /** Stable-partition body blocks by {@link BodyKindPlacement}; peers keep stream order. */
27
+ function placeBodyBlocks(body, placement) {
28
+ if (!placement)
29
+ return [...body];
30
+ const first = [];
31
+ const middle = [];
32
+ const last = [];
33
+ for (const block of body) {
34
+ const place = placement[block.kind];
35
+ if (place === 'first')
36
+ first.push(block);
37
+ else if (place === 'last')
38
+ last.push(block);
39
+ else
40
+ middle.push(block);
41
+ }
42
+ return [...first, ...middle, ...last];
43
+ }
19
44
  const WORDED = new Set(WORDED_STOP_KINDS);
20
45
  const USER_KINDS = new Set(['user-text', 'user-attachment', 'user-voice']);
21
46
  function isUserTranscriptBlock(block) {
@@ -139,8 +164,10 @@ function classifyNonGateBlock(block, args, trace, body) {
139
164
  * - `text` before/between tools → narration; text and answer kinds after the
140
165
  * latest tool → body (while streaming too, so the answer streams formatted)
141
166
  * - No tools: thoughts still go to trace; remaining kinds → body
167
+ * - Body kinds then follow {@link ComposeAssistantTurnOptions.bodyKindPlacement}
168
+ * (default: citations last)
142
169
  */
143
- export function composeAssistantTurn(blocks) {
170
+ export function composeAssistantTurn(blocks, options) {
144
171
  const visible = blocks.filter((block) => !isHiddenTranscriptBlock(block));
145
172
  // why: One card at a time: the gate answered next. The step's later gates wait in the trace.
146
173
  const nextGate = visible.find(isGatedTool);
@@ -153,10 +180,11 @@ export function composeAssistantTurn(blocks) {
153
180
  for (const [index, block] of nonGate.entries()) {
154
181
  classifyNonGateBlock(block, { hasTools, index, lastToolIndex }, trace, body);
155
182
  }
183
+ const placement = options?.bodyKindPlacement ?? DEFAULT_BODY_KIND_PLACEMENT;
156
184
  return {
157
185
  trace,
158
186
  gatedTools,
159
- body,
187
+ body: placeBodyBlocks(body, placement),
160
188
  hasTrace: trace.length > 0,
161
189
  };
162
190
  }
@@ -10,7 +10,7 @@ export type UseTheoremChatOptions = ChatOptions;
10
10
  export declare function useTheoremChat(options: UseTheoremChatOptions): {
11
11
  slotChoices: {
12
12
  onChange: (name: string, value: string) => void;
13
- declared: Record<string, string[]>;
13
+ declared: Record<string, import("@theoremjs/web/client").ChatSlotDecl>;
14
14
  values: Record<string, string>;
15
15
  } | undefined;
16
16
  setDraftText: (value: import("@theoremjs/web/client").ChatUpdate<string>) => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theoremjs/react",
3
- "version": "0.3.4-next.542.g59bcbcf",
3
+ "version": "0.3.4-next.548.gbb156b2",
4
4
  "description": "React bindings for Theorem: headless hooks + transport, an Astryx chat UI, and a Web-standard server handler.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -60,8 +60,8 @@
60
60
  "@stylexjs/stylex": "0.19.0",
61
61
  "react": "^19.0.0",
62
62
  "react-dom": "^19.0.0",
63
- "@theoremjs/agents": "^0.3.4-next.542.g59bcbcf",
64
- "@theoremjs/web": "^0.3.4-next.542.g59bcbcf"
63
+ "@theoremjs/agents": "^0.3.4-next.548.gbb156b2",
64
+ "@theoremjs/web": "^0.3.4-next.548.gbb156b2"
65
65
  },
66
66
  "peerDependenciesMeta": {
67
67
  "@astryxdesign/core": {
@@ -4,8 +4,16 @@ import type { ClientFailure } from '@theoremjs/web/client';
4
4
  /** A slot the profile declares: its allowed values, the one chosen, and how to choose another. */
5
5
  /** The effort selector's icon. The studio's Agents list shows the same one. */
6
6
  export declare const EFFORT_ICON: import("react").ForwardRefExoticComponent<import("@tabler/icons-react").IconProps & import("react").RefAttributes<SVGSVGElement>>;
7
+ export type SlotChoiceOption = {
8
+ value: string;
9
+ description?: string;
10
+ };
11
+ export type SlotDeclView = {
12
+ label?: string;
13
+ choices: readonly SlotChoiceOption[];
14
+ };
7
15
  export type SlotChoices = {
8
- declared: Readonly<Record<string, readonly string[]>>;
16
+ declared: Readonly<Record<string, SlotDeclView>>;
9
17
  values: Readonly<Record<string, string>>;
10
18
  onChange: (name: string, value: string) => void;
11
19
  };
@@ -93,7 +93,18 @@ function SlotSelectors(props) {
93
93
  if (!props.choices)
94
94
  return null;
95
95
  const { declared, values, onChange } = props.choices;
96
- return Object.entries(declared).map(([name, allowed]) => (_jsx(Selector, { label: name, isLabelHidden: true, size: "sm", variant: "ghost", startIcon: _jsx(IconAdjustmentsHorizontal, { size: 14 }), placement: "above", isDisabled: props.isDisabled, value: values[name] ?? '', options: allowed.map((value) => ({ value, label: value })), onChange: (value) => onChange(name, value) }, name)));
96
+ return Object.entries(declared).map(([name, decl]) => {
97
+ // why: Group title belongs in the open menu (section heading), not beside the
98
+ // closed trigger — same chrome as Model/Effort. Descriptions stay Astryx
99
+ // secondary + one-line ellipsis; wrap would blow the composer dropdown open.
100
+ const title = decl.label?.trim() || name;
101
+ const choices = decl.choices.map((choice) => ({
102
+ value: choice.value,
103
+ label: choice.value,
104
+ description: choice.description,
105
+ }));
106
+ return (_jsx(Selector, { label: title, isLabelHidden: true, size: "sm", variant: "ghost", startIcon: _jsx(IconAdjustmentsHorizontal, { size: 14 }), placement: "above", isDisabled: props.isDisabled, value: values[name] ?? '', options: [{ type: 'section', title, options: choices }], onChange: (value) => onChange(name, value) }, name));
107
+ });
97
108
  }
98
109
  function GenerationSelect(props) {
99
110
  const { models, efforts } = generationOptions(props.iface, props.selectedModel);
@@ -3,6 +3,7 @@ import type { LabelOverrides } from '@theoremjs/agents';
3
3
  import { type TranscriptBlock } from '@theoremjs/agents/interface';
4
4
  import type { AnsweringGate } from '@theoremjs/web/client';
5
5
  import { type ReactNode } from 'react';
6
+ import { type ComposeAssistantTurnOptions } from '../client/transcript-groups.ts';
6
7
  import { type ToolDecision } from './ToolGateCard.tsx';
7
8
  export type ChatTranscriptProps = {
8
9
  blocks: readonly TranscriptBlock[];
@@ -32,6 +33,11 @@ export type ChatTranscriptProps = {
32
33
  * on an agent tool's call row what the agent it ran used. Off by default.
33
34
  */
34
35
  usage?: boolean;
36
+ /**
37
+ * Where body kinds sit in a reply. Default puts `citation` last (Sources under
38
+ * the prose). Pass `{}` to keep stream order for inline citations.
39
+ */
40
+ bodyKindPlacement?: ComposeAssistantTurnOptions['bodyKindPlacement'];
35
41
  /** The profile's labels (`iface.labels`): a stopped turn says why in its words. */
36
42
  labels?: LabelOverrides;
37
43
  };
@@ -4,12 +4,14 @@ import { ChatMessage, ChatMessageBubble, ChatMessageList, ChatMessageMetadata, C
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
+ import { DropdownMenu } from '@astryxdesign/core/DropdownMenu';
7
8
  import { HStack } from '@astryxdesign/core/HStack';
8
9
  import { Icon } from '@astryxdesign/core/Icon';
9
10
  import { IconButton } from '@astryxdesign/core/IconButton';
10
11
  import { useLocale } from '@astryxdesign/core/i18n';
11
12
  import { useLightbox } from '@astryxdesign/core/Lightbox';
12
13
  import { Markdown } from '@astryxdesign/core/Markdown';
14
+ import { OverflowList } from '@astryxdesign/core/OverflowList';
13
15
  import { Skeleton } from '@astryxdesign/core/Skeleton';
14
16
  import { Text } from '@astryxdesign/core/Text';
15
17
  import { Thumbnail } from '@astryxdesign/core/Thumbnail';
@@ -292,27 +294,96 @@ function MediaBlock({ block }) {
292
294
  }
293
295
  return _jsx(Text, { color: "secondary", children: block.mimeType });
294
296
  }
295
- /** Runs of images/videos become one gallery row; everything else is a row of its own. */
297
+ /**
298
+ * Runs of images/videos become one gallery row; consecutive citation blocks
299
+ * become one sources row; everything else is a row of its own.
300
+ */
296
301
  function bodyRows(t, body) {
297
302
  const rows = [];
298
303
  for (const block of body) {
299
304
  const item = galleryItemOf(t, block);
300
- const last = rows.at(-1);
301
- if (item && last?.kind === 'gallery')
302
- last.items.push(item);
303
- else if (item)
304
- rows.push({ kind: 'gallery', items: [item] });
305
+ if (item)
306
+ addToGallery(rows, item);
307
+ else if (block.kind === 'citation')
308
+ addToSources(rows, citationsFromBlock(block));
305
309
  else
306
310
  rows.push({ kind: 'block', block });
307
311
  }
308
312
  return rows;
309
313
  }
310
- function Sources({ block }) {
314
+ function addToGallery(rows, item) {
315
+ const last = rows.at(-1);
316
+ if (last?.kind === 'gallery')
317
+ last.items.push(item);
318
+ else
319
+ rows.push({ kind: 'gallery', items: [item] });
320
+ }
321
+ function addToSources(rows, citations) {
322
+ if (citations.length === 0)
323
+ return;
324
+ const last = rows.at(-1);
325
+ if (last?.kind === 'sources')
326
+ last.citations.push(...citations);
327
+ else
328
+ rows.push({ kind: 'sources', citations: [...citations] });
329
+ }
330
+ /** Keeps the overflow chip looking like a Citation, not a default button. */
331
+ const SOURCES_MORE_TRIGGER = {
332
+ appearance: 'none',
333
+ background: 'none',
334
+ border: 'none',
335
+ padding: 0,
336
+ margin: 0,
337
+ font: 'inherit',
338
+ color: 'inherit',
339
+ cursor: 'pointer',
340
+ display: 'inline-flex',
341
+ verticalAlign: 'baseline',
342
+ };
343
+ /** Favicon size matches Citation’s icon slot. */
344
+ const SOURCE_FAVICON = {
345
+ width: 16,
346
+ height: 16,
347
+ borderRadius: 9999,
348
+ display: 'block',
349
+ };
350
+ /**
351
+ * As wide as the row, and no wider — same as studio’s Add agent menu
352
+ * (`studio/ui/studio-screen.tsx` `ROW_WIDE` / `oneLine`).
353
+ */
354
+ const SOURCE_TITLE_WIDE = { inlineSize: 0, minInlineSize: '100%' };
355
+ /** One-line source title; what does not fit shows on hover. */
356
+ function sourceTitle(title) {
357
+ return (_jsx(Text, { maxLines: 1, hasTruncateTooltip: "end", style: SOURCE_TITLE_WIDE, children: title }));
358
+ }
359
+ /** One chip; the rest open in a scrollable DropdownMenu with favicons. */
360
+ function Sources({ citations }) {
311
361
  const t = useLabels();
312
- const citations = citationsFromBlock(block);
313
362
  if (citations.length === 0)
314
363
  return null;
315
- return (_jsx(HStack, { gap: 1, wrap: "wrap", "aria-label": t('@theorem.transcript.sources'), children: citations.map((citation, i) => (_jsx(Citation, { source: { title: citation.title, url: citation.href, src: citation.icon }, number: i + 1 }, citation.key))) }));
364
+ return (_jsx(OverflowList, { gap: 1, maxVisibleItems: 1, "aria-label": t('@theorem.transcript.sources'), overflowRenderer: (hidden) => {
365
+ const more = hidden.flatMap(({ index }) => {
366
+ const citation = citations[index];
367
+ return citation ? [citation] : [];
368
+ });
369
+ if (more.length === 0)
370
+ return null;
371
+ const favicon = more.find((citation) => citation.icon)?.icon;
372
+ const moreLabel = t('@theorem.transcript.sources_more', { count: more.length });
373
+ return (_jsx(DropdownMenu, { hasChevron: false,
374
+ // why: menuWidth is a floor; a fixed width is what makes titles ellipsize.
375
+ style: { width: 280 }, placement: "below", alignment: "start", renderTrigger: (props) => (_jsx("button", { type: "button", ...props, style: SOURCES_MORE_TRIGGER, "aria-label": moreLabel, children: _jsx(Citation, { source: {
376
+ title: moreLabel,
377
+ ...(favicon ? { src: favicon } : {}),
378
+ }, number: citations.length - more.length + 1 }) })), items: more.map((citation) => ({
379
+ id: citation.key,
380
+ label: sourceTitle(citation.title),
381
+ ...(citation.icon
382
+ ? { icon: _jsx("img", { src: citation.icon, alt: "", style: SOURCE_FAVICON }) }
383
+ : {}),
384
+ ...(citation.href ? { href: citation.href, target: '_blank' } : {}),
385
+ })) }));
386
+ }, children: citations.map((citation, i) => (_jsx(Citation, { source: { title: citation.title, url: citation.href, src: citation.icon }, number: i + 1 }, citation.key))) }));
316
387
  }
317
388
  /**
318
389
  * Streamed text (the reply, or the latest thought). Astryx reveals it while
@@ -330,13 +401,12 @@ function BodyBlock({ block, streaming }) {
330
401
  return _jsx(ToolCall, { tool: block.tool });
331
402
  return _jsx(ResultBlock, { block: block });
332
403
  }
333
- /** Non-streaming answer rows: errors, sources, structured output, and media. */
404
+ /** Non-streaming answer rows: errors, structured output, and media. Citations coalesce in bodyRows. */
334
405
  function ResultBlock({ block }) {
335
406
  switch (block.kind) {
336
407
  case 'error':
337
- return null;
338
408
  case 'citation':
339
- return _jsx(Sources, { block: block });
409
+ return null;
340
410
  case 'structured':
341
411
  return _jsx(ShapedData, { value: block.value });
342
412
  case 'media':
@@ -345,21 +415,16 @@ function ResultBlock({ block }) {
345
415
  return null;
346
416
  }
347
417
  }
348
- // why: Astryx sets a call's name in the code font; labels are sentences, so the row takes the body
349
- // font and the detail restores the code font kept on the wrapper.
350
- const CODE_FONT_KEPT = {
351
- display: 'contents',
352
- '--theorem-font-code': 'var(--font-family-code)',
353
- };
418
+ // why: Astryx draws call names in the code font; labels are sentences, so the
419
+ // whole tool call (row and expanded detail) uses the body font instead.
354
420
  const CODE_FONT_AS_BODY = { '--font-family-code': 'var(--font-family-body)' };
355
- const CODE_FONT_RESTORED = { '--font-family-code': 'var(--theorem-font-code)' };
356
421
  function ToolCalls({ calls }) {
357
- return (_jsx("div", { style: CODE_FONT_KEPT, children: _jsx(ChatToolCalls, { calls: calls, style: CODE_FONT_AS_BODY }) }));
422
+ return _jsx(ChatToolCalls, { calls: calls, style: CODE_FONT_AS_BODY });
358
423
  }
359
424
  /** A call's detail: what it ran with, then what came back. */
360
425
  function toolDetail(t, tool, result) {
361
426
  const input = tool.edited ? (_jsx(ShapedData, { value: tool.edited.to, title: t('@theorem.transcript.tool_input_edited') })) : (_jsx(ShapedData, { value: tool.arguments, title: t('@theorem.transcript.tool_input') }));
362
- return (_jsxs(VStack, { gap: 2, style: CODE_FONT_RESTORED, children: [_jsx(Text, { type: "code", size: "sm", color: "secondary", children: tool.name }), input, result] }));
427
+ return (_jsxs(VStack, { gap: 2, children: [_jsx(Text, { size: "sm", color: "secondary", children: tool.name }), input, result] }));
363
428
  }
364
429
  /** How long a finished call ran, when both ends were seen. */
365
430
  function toolDuration(t, tool) {
@@ -493,6 +558,8 @@ function TurnStatus(props) {
493
558
  function BodyRowView({ row, streaming, imageOutput, }) {
494
559
  if (row.kind === 'block')
495
560
  return _jsx(BodyBlock, { block: row.block, streaming: streaming });
561
+ if (row.kind === 'sources')
562
+ return _jsx(Sources, { citations: row.citations });
496
563
  if (imageOutput)
497
564
  return _jsx(GeneratedGallery, { items: row.items, ratio: imageOutput.ratio });
498
565
  return _jsx(MediaGallery, { items: row.items });
@@ -508,11 +575,13 @@ function AssistantTurn(props) {
508
575
  const chrome = useChromeReveal(props.isNewest === true);
509
576
  const showUsage = useContext(ShowUsage);
510
577
  const elapsedMs = useTurnElapsed(props.streaming, props.startedAt, props.workedMs);
511
- const { trace, gatedTools, body, hasTrace } = composeAssistantTurn(props.blocks);
578
+ const { trace, gatedTools, body, hasTrace } = composeAssistantTurn(props.blocks, props.bodyKindPlacement !== undefined
579
+ ? { bodyKindPlacement: props.bodyKindPlacement }
580
+ : undefined);
512
581
  const rows = bodyRows(t, body);
513
582
  const status = workStatusLabel(t, workStatus({ streaming: props.streaming, hasTrace, elapsedMs }));
514
583
  const copyText = assistantTurnCopyText(body.length > 0 ? body : props.blocks);
515
- return (_jsx(ChatMessage, { sender: "assistant", name: props.handle, ...chrome.handlers, metadata: props.streaming ? undefined : (_jsx(MessageChrome, { sender: "assistant", at: props.at, copyText: copyText, error: props.error, stopped: props.stopped, usage: showUsage ? props.usage : undefined, revealed: chrome.revealed })), 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
584
+ return (_jsx(ChatMessage, { sender: "assistant", name: props.handle, ...chrome.handlers, metadata: props.streaming ? undefined : (_jsx(MessageChrome, { sender: "assistant", at: props.at, copyText: copyText, error: props.error, stopped: props.stopped, usage: showUsage ? props.usage : undefined, revealed: chrome.revealed })), 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' || row.kind === 'sources' ? row.kind : row.block.kind).map(({ item: row, index: i, key }) => (_jsx(BodyRowView
516
585
  // why: Not the block id: the committed turn re-mints ids, and a
517
586
  // remount would cut the text reveal short.
518
587
  , { row: row, streaming: props.streaming && i === rows.length - 1, imageOutput: props.imageOutput }, key))), _jsx(PendingImage, { imageOutput: props.imageOutput, streaming: props.streaming, rows: rows })] }) }));
@@ -543,7 +612,7 @@ function isBareFailure(group) {
543
612
  export function ChatTranscript(props) {
544
613
  return (_jsx(TheoremLabelsProvider, { children: _jsx(ShowUsage.Provider, { value: props.usage === true, children: _jsx(ChatTranscriptBody, { ...props }) }) }));
545
614
  }
546
- function ChatTranscriptBody({ blocks, handle, streaming = false, delivery, onToolDecision, answering = null, onAuthenticated, emptyState, dividers, imageOutput, labels, }) {
615
+ function ChatTranscriptBody({ blocks, handle, streaming = false, delivery, onToolDecision, answering = null, onAuthenticated, emptyState, dividers, imageOutput, bodyKindPlacement, labels, }) {
547
616
  const groups = useMemo(() => groupTranscriptBlocks(blocks, dividers && new Set(Object.keys(dividers))), [blocks, dividers]);
548
617
  const timeOf = useFirstSeen(groups.map((group, index) => (group.kind === 'user' ? group.key : replyKey(groups, index))));
549
618
  const spans = useTurnSpans(streaming, groups.findLast((group) => group.kind === 'user')?.key);
@@ -556,7 +625,7 @@ function ChatTranscriptBody({ blocks, handle, streaming = false, delivery, onToo
556
625
  onToolDecision,
557
626
  onAuthenticated,
558
627
  };
559
- const turn = { handle, handlers, imageOutput };
628
+ const turn = { handle, handlers, imageOutput, bodyKindPlacement };
560
629
  const lastUser = groups.findLastIndex((group) => group.kind === 'user');
561
630
  const userTurn = (group, index) => {
562
631
  const next = groups[index + 1];
@@ -53,8 +53,8 @@ export type ConsoleFrameProps = {
53
53
  children: ReactNode;
54
54
  };
55
55
  /**
56
- * A console's page, for decisions and hosts alike: the trace's toggle in the
57
- * header, and one raised panel, flush with the host's, scrolling inside.
56
+ * A console's page, for decisions and hosts alike: the trace toggle floats over
57
+ * the content (same as chat), and one raised panel scrolls inside.
58
58
  */
59
59
  export declare function ConsoleFrame({ inspector, maxWidth, flush, columns, requestLabel, className, style, children, }: ConsoleFrameProps): import("react").JSX.Element;
60
60
  /** One card of a response with several: its request and result sit in it without cards of their own. */
@@ -41,12 +41,13 @@ function ConsoleSingle({ flush, wide, maxWidth, children, }) {
41
41
  return (_jsx(VStack, { height: "100%", paddingInlineStart: PANEL_EDGE, style: { ...(flush ? FLUSH_CARD : undefined), paddingInlineEnd: paneEdge(PANEL_EDGE) }, 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
42
  }
43
43
  /**
44
- * A console's page, for decisions and hosts alike: the trace's toggle in the
45
- * header, and one raised panel, flush with the host's, scrolling inside.
44
+ * A console's page, for decisions and hosts alike: the trace toggle floats over
45
+ * the content (same as chat), and one raised panel scrolls inside.
46
46
  */
47
47
  export function ConsoleFrame({ inspector, maxWidth, flush = false, columns = false, requestLabel, className, style, children, }) {
48
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(VStack, { height: "100%", paddingBlockStart: PANEL_EDGE, children: _jsx(ConsoleBody, { layoutRef: layoutRef, flush: flush, columns: columns, maxWidth: maxWidth, requestLabel: requestLabel, children: children }) }) }) }) }));
49
+ const header = inspector.toggle ? (_jsx(SidePanelHeader, { floating: true, children: inspector.toggle })) : undefined;
50
+ return (_jsx(WithTrace, { inspector: inspector, children: _jsx(Layout, { ref: layoutRef, height: "fill", className: className, style: { position: 'relative', ...style }, header: header, content: _jsx(LayoutContent, { padding: 0, isScrollable: false, children: _jsx(ConsoleBody, { layoutRef: layoutRef, flush: flush, columns: columns, maxWidth: maxWidth, requestLabel: requestLabel, children: children }) }) }) }));
50
51
  }
51
52
  /** The console's panels: a request and response side by side when there is room, else one column. */
52
53
  function ConsoleBody({ layoutRef, flush, columns, maxWidth, requestLabel, children, }) {
package/ui/labels.d.ts CHANGED
@@ -220,6 +220,11 @@ export declare const THEOREM_UI_CATALOG: {
220
220
  readonly defaultMessage: "Sources";
221
221
  readonly description: "The row of source chips (screen readers).";
222
222
  };
223
+ readonly '@theorem.transcript.sources_more': {
224
+ readonly defaultMessage: "{count, plural, one {+# other} other {+# others}}";
225
+ readonly description: "Overflow chip that opens the menu of remaining source citations.";
226
+ readonly params: readonly ["count"];
227
+ };
223
228
  readonly '@theorem.transcript.tool_input': {
224
229
  readonly defaultMessage: "Input";
225
230
  readonly description: "A tool call's arguments, in its detail.";
package/ui/labels.js CHANGED
@@ -204,6 +204,11 @@ export const THEOREM_UI_CATALOG = {
204
204
  defaultMessage: 'Sources',
205
205
  description: 'The row of source chips (screen readers).',
206
206
  },
207
+ '@theorem.transcript.sources_more': {
208
+ defaultMessage: '{count, plural, one {+# other} other {+# others}}',
209
+ description: 'Overflow chip that opens the menu of remaining source citations.',
210
+ params: ['count'],
211
+ },
207
212
  '@theorem.transcript.tool_input': {
208
213
  defaultMessage: 'Input',
209
214
  description: "A tool call's arguments, in its detail.",