@jini-ai/chat 0.5.7 → 0.6.0

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 (106) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/core/ag-ui/entry.js +2 -0
  3. package/dist/core/assistant-client/durable-run-subscription.d.ts +4 -1
  4. package/dist/core/assistant-client/durable-run-subscription.js +13 -4
  5. package/dist/core/events.d.ts +3 -0
  6. package/dist/core/history-tool-metadata.d.ts +23 -0
  7. package/dist/core/history-tool-metadata.js +13 -0
  8. package/dist/core/index.d.ts +3 -0
  9. package/dist/core/index.js +3 -0
  10. package/dist/core/persistence/title.d.ts +24 -0
  11. package/dist/core/persistence/title.js +60 -9
  12. package/dist/core/run-activity.d.ts +15 -2
  13. package/dist/core/run-activity.js +6 -4
  14. package/dist/core/run-events/entry.js +2 -0
  15. package/dist/core/tool-call-detail.d.ts +15 -0
  16. package/dist/core/tool-call-detail.js +18 -0
  17. package/dist/core/turn-usage.d.ts +42 -3
  18. package/dist/core/turn-usage.js +101 -4
  19. package/dist/core/usage-format.d.ts +10 -0
  20. package/dist/core/usage-format.js +15 -0
  21. package/dist/embed-widget/browser/lazy-loader.d.ts +34 -0
  22. package/dist/embed-widget/browser/lazy-loader.js +116 -0
  23. package/dist/embed-widget/react/dialog.hooks.d.ts +28 -0
  24. package/dist/embed-widget/react/dialog.hooks.js +54 -0
  25. package/dist/embed-widget/react/embed.d.ts +1 -0
  26. package/dist/embed-widget/react/embed.js +5 -3
  27. package/dist/embed-widget/react/header.d.ts +3 -1
  28. package/dist/embed-widget/react/header.hooks.d.ts +13 -0
  29. package/dist/embed-widget/react/header.hooks.js +32 -0
  30. package/dist/embed-widget/react/header.js +4 -14
  31. package/dist/embed-widget/vanilla/index.d.ts +63 -0
  32. package/dist/embed-widget/vanilla/index.js +319 -0
  33. package/dist/embed-widget/vanilla/state.d.ts +48 -0
  34. package/dist/embed-widget/vanilla/state.js +120 -0
  35. package/dist/embed-widget/vanilla/style.css +29 -0
  36. package/dist/react/components/A2uiSurfaceCard.d.ts +5 -1
  37. package/dist/react/components/A2uiSurfaceCard.js +9 -9
  38. package/dist/react/components/AttachmentPreviewModal.hooks.d.ts +7 -0
  39. package/dist/react/components/AttachmentPreviewModal.hooks.js +1 -1
  40. package/dist/react/components/ChatFab.d.ts +4 -1
  41. package/dist/react/components/ChatFab.js +2 -2
  42. package/dist/react/components/ConversationList.d.ts +8 -1
  43. package/dist/react/components/ConversationList.hooks.d.ts +18 -0
  44. package/dist/react/components/ConversationList.hooks.js +25 -0
  45. package/dist/react/components/ConversationList.js +12 -5
  46. package/dist/react/components/ConversationList.rules.d.ts +25 -0
  47. package/dist/react/components/ConversationList.rules.js +19 -0
  48. package/dist/react/components/McpUiSurfaceCard.d.ts +1 -1
  49. package/dist/react/components/McpUiSurfaceCard.js +8 -4
  50. package/dist/react/components/MessageList.d.ts +1 -1
  51. package/dist/react/components/MessageList.hooks.d.ts +4 -0
  52. package/dist/react/components/MessageList.hooks.js +6 -1
  53. package/dist/react/components/MessageList.js +4 -3
  54. package/dist/react/components/MessageRow.d.ts +17 -0
  55. package/dist/react/components/MessageRow.js +54 -35
  56. package/dist/react/components/MessageRow.usage.d.ts +48 -0
  57. package/dist/react/components/MessageRow.usage.js +47 -0
  58. package/dist/react/components/MidRunUserMessage.d.ts +2 -12
  59. package/dist/react/components/MidRunUserMessage.js +4 -2
  60. package/dist/react/components/SentAttachment.d.ts +19 -0
  61. package/dist/react/components/SentAttachment.hooks.d.ts +44 -0
  62. package/dist/react/components/SentAttachment.hooks.js +56 -0
  63. package/dist/react/components/SentAttachment.js +14 -0
  64. package/dist/react/components/ToolCallGroup.d.ts +12 -0
  65. package/dist/react/components/ToolCallGroup.hooks.d.ts +20 -0
  66. package/dist/react/components/ToolCallGroup.hooks.js +23 -0
  67. package/dist/react/components/ToolCallGroup.js +13 -0
  68. package/dist/react/components/ToolCard.js +25 -17
  69. package/dist/react/components/mcp-ui-surface-state.d.ts +21 -0
  70. package/dist/react/components/mcp-ui-surface-state.js +26 -0
  71. package/dist/react/ext-event-renderer-registry.d.ts +7 -0
  72. package/dist/react/features/chat-pane/components/ChatPane.d.ts +1 -1
  73. package/dist/react/features/chat-pane/components/ChatPane.js +11 -6
  74. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +1 -0
  75. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +16 -10
  76. package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.js +4 -0
  77. package/dist/react/features/chat-pane/hooks/useChatPaneSuggestion.hooks.d.ts +9 -0
  78. package/dist/react/features/chat-pane/hooks/useChatPaneSuggestion.hooks.js +11 -0
  79. package/dist/react/features/chat-pane/hooks/useChatPaneSurface.hooks.d.ts +17 -0
  80. package/dist/react/features/chat-pane/hooks/useChatPaneSurface.hooks.js +15 -0
  81. package/dist/react/features/chat-pane/rules.d.ts +14 -0
  82. package/dist/react/features/chat-pane/rules.js +18 -0
  83. package/dist/react/features/chat-pane/styles.d.ts +1 -1
  84. package/dist/react/features/chat-pane/styles.js +85 -1
  85. package/dist/react/features/chat-pane/types.d.ts +16 -1
  86. package/dist/react/hooks/useAnsweredCards.d.ts +20 -0
  87. package/dist/react/hooks/useAnsweredCards.js +23 -0
  88. package/dist/react/hooks/useConversation.d.ts +8 -0
  89. package/dist/react/hooks/useConversation.js +34 -0
  90. package/dist/react/hooks/useRunActivity.d.ts +5 -3
  91. package/dist/react/hooks/useRunActivity.js +4 -3
  92. package/dist/react/index.d.ts +4 -0
  93. package/dist/react/index.js +3 -0
  94. package/dist/react/message-blocks.d.ts +22 -3
  95. package/dist/react/message-blocks.js +25 -3
  96. package/dist/react/tool-call-display.d.ts +90 -0
  97. package/dist/react/tool-call-display.js +97 -0
  98. package/dist/store/ports.d.ts +25 -2
  99. package/dist/store/runtime/entry.d.ts +1 -0
  100. package/dist/store/runtime/entry.js +1 -0
  101. package/dist/store/runtime/history-tools.d.ts +27 -0
  102. package/dist/store/runtime/history-tools.js +169 -0
  103. package/dist/store/sql/store.js +52 -0
  104. package/dist/testing/index.d.ts +7 -0
  105. package/dist/testing/index.js +22 -0
  106. package/package.json +48 -23
@@ -31,7 +31,10 @@ export const CHAT_PANE_STYLES = `
31
31
  min-width: 0;
32
32
  min-height: 0;
33
33
  height: 100%;
34
+ /* clip after the hidden fallback: a hidden box still scrolls programmatically (a focus() or
35
+ scrollIntoView() inside it), which shifts the pinned composer up and leaves blank pane below. */
34
36
  overflow: hidden;
37
+ overflow: clip;
35
38
  color: var(--jini-chat-text);
36
39
  background: var(--jini-chat-bg);
37
40
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft YaHei UI", "Noto Sans", Roboto, "Helvetica Neue", Arial, sans-serif;
@@ -84,6 +87,7 @@ export const CHAT_PANE_STYLES = `
84
87
  position: relative;
85
88
  min-height: 0;
86
89
  overflow: hidden;
90
+ overflow: clip;
87
91
  }
88
92
  .jini-chat-pane .jini-message-list {
89
93
  height: 100%;
@@ -111,6 +115,30 @@ export const CHAT_PANE_STYLES = `
111
115
  font-size: 14px;
112
116
  line-height: 1.62;
113
117
  }
118
+ /*
119
+ * Off-screen rows skip style, layout and paint. Owner report 2026-10-11: typing in a long chat
120
+ * lagged. React was not the cost (no row re-renders per key); the browser was. A keystroke's DOM
121
+ * change can invalidate style for the whole document (a host :has() rule did, in the Tovu dock),
122
+ * and a transcript with ~2,000 tool cards and 3.5 MB of tool output then cost ~50 ms of style
123
+ * recalc per key. With this, only the rows on screen are recalculated (~8 ms). The DOM stays
124
+ * whole: find-in-page, textContent readers and screen readers still see every row.
125
+ * 'auto' in contain-intrinsic-size keeps a row's last real height once it has rendered; the
126
+ * auto-scroll ResizeObserver re-sticks to the bottom as estimated rows resolve.
127
+ * Containment clips paint at the row's edge, so overflow-clip-margin leaves room for focus rings
128
+ * (2px outline + 2px offset); @supports keeps browsers without it on the old, unclipped path.
129
+ */
130
+ @supports (overflow-clip-margin: 8px) {
131
+ .jini-chat-pane .jini-message {
132
+ content-visibility: auto;
133
+ contain-intrinsic-size: auto 160px;
134
+ overflow-clip-margin: 8px;
135
+ }
136
+ }
137
+ /* A mid-run user bubble is nested inside an assistant turn, so the preceding tool/text block
138
+ * has no message-row bottom margin. Give it the same separation as an ordinary next turn. */
139
+ .jini-chat-pane .jini-message-user--mid-run {
140
+ margin-top: 20px;
141
+ }
114
142
  /*
115
143
  * Bubble on the content, not the row: MessageRow.tsx renders attachments as siblings of
116
144
  * .jini-message-content inside .jini-message-user, each with their own chip styling -- bubbling
@@ -124,7 +152,9 @@ export const CHAT_PANE_STYLES = `
124
152
  padding: 10px 13px;
125
153
  width: fit-content;
126
154
  max-width: 88%;
127
- background: var(--jini-chat-subtle);
155
+ /* User bubbles share this style at every nesting depth, regardless of message length. */
156
+ background: var(--jini-chat-user-bubble, #1a1b1c);
157
+ color: var(--jini-chat-user-bubble-ink, #fff);
128
158
  border-radius: 12px 12px 3px 12px;
129
159
  }
130
160
  .jini-chat-pane .jini-message-user .jini-message-attachments {
@@ -149,6 +179,25 @@ export const CHAT_PANE_STYLES = `
149
179
  color: var(--jini-chat-text);
150
180
  border-color: var(--jini-chat-border-strong);
151
181
  }
182
+ /* A sent image this browser still holds (SentAttachment.tsx): a small square thumbnail instead of
183
+ * the filename pill. Same click target and preview modal; cover-cropped so mixed aspect ratios line up. */
184
+ .jini-chat-pane .jini-message-user .jini-message-attachment-chip.is-thumbnail {
185
+ width: 64px;
186
+ height: 64px;
187
+ padding: 0;
188
+ overflow: hidden;
189
+ border-radius: 10px;
190
+ }
191
+ .jini-chat-pane .jini-message-user .jini-message-attachment-chip.is-thumbnail:focus-visible {
192
+ outline: 2px solid var(--jini-chat-accent);
193
+ outline-offset: 2px;
194
+ }
195
+ .jini-chat-pane .jini-message-attachment-thumb {
196
+ display: block;
197
+ width: 100%;
198
+ height: 100%;
199
+ object-fit: cover;
200
+ }
152
201
  .jini-chat-pane .jini-message-agent {
153
202
  font-size: 11px;
154
203
  font-weight: 700;
@@ -582,6 +631,11 @@ export const CHAT_PANE_STYLES = `
582
631
  font-size: 13.5px;
583
632
  line-height: 1.6;
584
633
  }
634
+ /* Shared focus-zoom floor: embeds at every width, all panes on small/touch viewports. */
635
+ .jini-chat-embed .jini-chat-pane .jini-composer-input { font-size: 16px; }
636
+ @media (max-width: 640px), (pointer: coarse) {
637
+ .jini-chat-pane.jini-chat-pane .jini-composer-input { font-size: 16px; }
638
+ }
585
639
  .jini-chat-pane .jini-composer-input::placeholder { color: var(--jini-chat-faint); }
586
640
  .jini-chat-pane .jini-composer-footer {
587
641
  display: flex;
@@ -1291,6 +1345,36 @@ export const CHAT_PANE_STYLES = `
1291
1345
  margin-top: 2px;
1292
1346
  }
1293
1347
  .jini-chat-pane .jini-message-usage-dot { font-size: 8px; color: #4e875f; }
1348
+ /* toolCallDisplay 'collapsed' (ToolCallGroup.tsx): one quiet line per turn, cards open under it. */
1349
+ .jini-chat-pane .jini-tool-group { display: flex; flex-direction: column; gap: 6px; width: 100%; }
1350
+ .jini-chat-pane .jini-tool-group__toggle {
1351
+ display: inline-flex;
1352
+ align-items: center;
1353
+ gap: 4px;
1354
+ align-self: flex-start;
1355
+ max-width: 100%;
1356
+ padding: 2px 0;
1357
+ background: none;
1358
+ border: 0;
1359
+ font: inherit;
1360
+ font-size: 12px;
1361
+ color: var(--jini-chat-muted);
1362
+ cursor: pointer;
1363
+ }
1364
+ .jini-chat-pane .jini-tool-group__toggle:hover { color: inherit; }
1365
+ .jini-chat-pane .jini-tool-group__toggle:focus-visible { outline: 2px solid var(--jini-chat-accent); outline-offset: 2px; border-radius: 4px; }
1366
+ .jini-chat-pane .jini-tool-group__label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1367
+ .jini-chat-pane .jini-tool-group[data-failed="true"] .jini-tool-group__toggle { color: var(--danger); }
1368
+ /* showThinking (MessageRow.tsx's ThinkingBlock): the model's reasoning, folded, set apart from the answer. */
1369
+ .jini-chat-pane .jini-message-thinking { font-size: 12px; color: var(--jini-chat-muted); }
1370
+ .jini-chat-pane .jini-message-thinking > summary { cursor: pointer; width: fit-content; }
1371
+ .jini-chat-pane .jini-message-thinking__text {
1372
+ margin-top: 4px;
1373
+ padding-left: 10px;
1374
+ border-left: 2px solid var(--jini-chat-border);
1375
+ white-space: pre-wrap;
1376
+ word-break: break-word;
1377
+ }
1294
1378
  /*
1295
1379
  * Fenced code block ('Markdown.tsx''s 'renderBlock' case 'code') and inline 'code' span visual
1296
1380
  * treatment — previously entirely unstyled (bare UA-default '<pre><code>', no background, border,
@@ -2,6 +2,7 @@ import type { UserTextRedactionOptions } from '../../../core/user-text-redaction
2
2
  import type { CSSProperties, ReactNode, RefObject } from 'react';
3
3
  import type { ChatAttachment, ChatMessage } from '@jini-ai/chat';
4
4
  import type { ComposerSlots } from '../../slots.js';
5
+ import type { ToolCallDisplay } from '../../tool-call-display.js';
5
6
  import type { ComposerHistoryStoragePort } from '../../../core/composer-history.js';
6
7
  import type { ChatTransport, RunContext } from '@jini-ai/chat';
7
8
  export interface ChatPaneAgentOption {
@@ -16,6 +17,8 @@ export interface ChatPaneAgentOption {
16
17
  * coupling the React package to `@jini-ai/http-kit`.
17
18
  */
18
19
  export interface ChatPaneAgent {
20
+ /** Read cached models now and refresh while the host finishes its background probe. */
21
+ discoveryPending?: boolean;
19
22
  id: string;
20
23
  name: string;
21
24
  available?: boolean;
@@ -67,7 +70,7 @@ export interface ChatPaneRuntimeAccess {
67
70
  daemonOnline: () => Promise<boolean>;
68
71
  }
69
72
  export type ChatPaneActivity = 'unavailable' | 'ready' | 'queued' | 'streaming' | 'failed';
70
- export type ChatPaneVariant = 'workspace';
73
+ export type ChatPaneVariant = 'workspace' | 'embed';
71
74
  export type RuntimePickerPlacement = 'up' | 'down';
72
75
  /**
73
76
  * Native filesystem operations used by {@link ChatPane} to implement its
@@ -188,6 +191,7 @@ export interface ChatPaneProps extends UserTextRedactionOptions {
188
191
  title?: string;
189
192
  variant?: ChatPaneVariant;
190
193
  initialMessages?: ChatMessage[];
194
+ refreshSignal?: import('../../hooks/useConversation.js').UseConversationOptions['refreshSignal'];
191
195
  conversationId?: string | null;
192
196
  initialSelection?: ChatPaneAgentSelection;
193
197
  selection?: ChatPaneAgentSelection;
@@ -266,6 +270,8 @@ export interface ChatPaneProps extends UserTextRedactionOptions {
266
270
  */
267
271
  placeholders?: readonly string[];
268
272
  suggestions?: readonly string[];
273
+ /** Host greeting shown only while the transcript is empty; never persisted as a message. */
274
+ emptyState?: ReactNode;
269
275
  /** Controlled working-directory value. */
270
276
  workingDirectory?: string | null;
271
277
  /** Initial value used when `workingDirectory` is uncontrolled. */
@@ -296,6 +302,15 @@ export interface ChatPaneProps extends UserTextRedactionOptions {
296
302
  */
297
303
  workingDirectoryControlPlacement?: 'below' | 'composer' | 'none';
298
304
  projectFileNames?: ReadonlySet<string>;
305
+ /**
306
+ * Transcript display choices (a host's chat settings, 2026-10-11), passed straight to
307
+ * `MessageList`: how tool calls show (`'show'` default, `'collapsed'` one line per turn, `'hidden'`
308
+ * all but failed ones — see `tool-call-display.ts`), whether the "Done · time · cost" lines show
309
+ * (default `true`), and whether the model's reasoning shows (default `false`).
310
+ */
311
+ toolCallDisplay?: ToolCallDisplay;
312
+ showUsageLine?: boolean;
313
+ showThinking?: boolean;
299
314
  uploadAttachments?: (files: File[], options?: ChatPaneAttachmentUploadOptions) => Promise<ChatAttachment[]>;
300
315
  attachmentAccept?: string;
301
316
  disabled?: boolean;
@@ -0,0 +1,20 @@
1
+ import type { IsAnsweredCard } from '../../core/index.js';
2
+ export interface AnsweredCards {
3
+ /** Whether the slot's card was answered. */
4
+ isAnswered(slot: string): boolean;
5
+ /** Records the slot's card as answered. Idempotent. */
6
+ markAnswered(slot: string): void;
7
+ /** The same answer as `isAnswered`, keyed by an event — for `deriveRunActivity`. Stable while nothing new is answered. */
8
+ isAnsweredCard: IsAnsweredCard;
9
+ }
10
+ /**
11
+ * @param slotOf - Maps an ext event to its render slot (`extEventSlot`).
12
+ * @complexity O(1) per lookup; O(answered cards) space.
13
+ */
14
+ export declare function useAnsweredCards({ slotOf }: {
15
+ slotOf: (card: {
16
+ name: string;
17
+ data: unknown;
18
+ }) => string;
19
+ }): AnsweredCards;
20
+ //# sourceMappingURL=useAnsweredCards.d.ts.map
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @module useAnsweredCards
3
+ *
4
+ * Which of a message's cards the person has already answered, by ext-event slot. A held-open
5
+ * question's tool call stays open after the click while the confirmed work runs, so the event
6
+ * stream alone still reads "waiting for an answer"; the card reports its own delivered answer here
7
+ * and the activity line and countdown clear at once (owner, 2026-10-11).
8
+ */
9
+ import { useCallback, useState } from 'react';
10
+ /**
11
+ * @param slotOf - Maps an ext event to its render slot (`extEventSlot`).
12
+ * @complexity O(1) per lookup; O(answered cards) space.
13
+ */
14
+ export function useAnsweredCards({ slotOf }) {
15
+ const [answered, setAnswered] = useState(() => new Set());
16
+ const markAnswered = useCallback((slot) => {
17
+ setAnswered((current) => (current.has(slot) ? current : new Set(current).add(slot)));
18
+ }, []);
19
+ const isAnswered = useCallback((slot) => answered.has(slot), [answered]);
20
+ const isAnsweredCard = useCallback((card) => answered.has(slotOf(card)), [answered, slotOf]);
21
+ return { isAnswered, markAnswered, isAnsweredCard };
22
+ }
23
+ //# sourceMappingURL=useAnsweredCards.js.map
@@ -4,6 +4,14 @@ import type { ChatTransport, RunContext } from '../../core/index.js';
4
4
  export interface UseConversationOptions extends UserTextRedactionOptions {
5
5
  transport: ChatTransport;
6
6
  initialMessages?: ChatMessage[];
7
+ /** Optional host loader and browser ports, e.g. `{ loadMessages, visibility: document, focus: window }`; refresh stays passive. */
8
+ refreshSignal?: {
9
+ loadMessages: () => Promise<ChatMessage[]>;
10
+ visibility: EventTarget & {
11
+ readonly visibilityState: string;
12
+ };
13
+ focus: EventTarget;
14
+ };
7
15
  conversationId?: string | null;
8
16
  agentId?: string;
9
17
  /** Defaults to a `crypto.randomUUID()`-based id generator when available, else a counter. */
@@ -34,6 +34,40 @@ export function useConversation(options) {
34
34
  const setMessages = useCallback((updater) => {
35
35
  setMessagesState((prev) => (typeof updater === 'function' ? updater(prev) : updater));
36
36
  }, []);
37
+ // Map upserts preserve order in O(local + saved) time/space; cleanup fences late reads on switch/unmount/local runs.
38
+ useEffect(() => {
39
+ const source = options.refreshSignal;
40
+ if (!source || run.isStreaming)
41
+ return;
42
+ let disposed = false, pending = false, timer;
43
+ const refresh = async () => {
44
+ if (pending)
45
+ return;
46
+ pending = true;
47
+ try {
48
+ const saved = await source.loadMessages();
49
+ if (!disposed)
50
+ setMessagesState(prev => [...new Map([...prev, ...saved].map(message => [message.id, message])).values()]);
51
+ }
52
+ catch (error) {
53
+ if (!disposed)
54
+ console.error('[chat] conversation refresh failed', error);
55
+ }
56
+ finally {
57
+ pending = false;
58
+ }
59
+ };
60
+ const schedule = () => {
61
+ clearTimeout(timer);
62
+ if (source.visibility.visibilityState === 'visible')
63
+ timer = setTimeout(() => { void refresh(); }, 500);
64
+ };
65
+ const events = [[source.visibility, 'visibilitychange'], [source.focus, 'focus']];
66
+ for (const [target, event] of events)
67
+ target.addEventListener(event, schedule);
68
+ return () => { disposed = true; clearTimeout(timer); for (const [target, event] of events)
69
+ target.removeEventListener(event, schedule); };
70
+ }, [options.refreshSignal, run.isStreaming]);
37
71
  /**
38
72
  * The mount-time snapshot of `initialMessages`, captured once — the same "read once, never react
39
73
  * to a later identity change" contract `messages`'s own `useState` initializer above already has
@@ -1,17 +1,19 @@
1
- import type { AgentEvent } from '../../core/index.js';
1
+ import type { AgentEvent, IsAnsweredCard } from '../../core/index.js';
2
2
  /** Wall-clock source, swappable in tests. */
3
3
  export type NowFn = () => number;
4
4
  /**
5
5
  * @param events - The running message's events.
6
6
  * @param active - True while the turn is in progress; the clock only ticks while true.
7
7
  * @param now - Clock source (default `Date.now`).
8
+ * @param isAnsweredCard - Cards the person already answered (see `RunActivityOptions`).
8
9
  * @returns The activity line's text, or `null` when `active` is false.
9
- * @complexity O(n) in events per render (memoized on the events array).
10
+ * @complexity O(n) in events per render (memoized on the events array and the answered predicate).
10
11
  */
11
12
  export declare function useRunActivity({ events, active }: {
12
13
  events: readonly AgentEvent[] | undefined;
13
14
  active: boolean;
14
- }, { now }?: {
15
+ }, { now, isAnsweredCard }?: {
15
16
  now?: (NowFn) | undefined;
17
+ isAnsweredCard?: IsAnsweredCard | undefined;
16
18
  }): string | null;
17
19
  //# sourceMappingURL=useRunActivity.d.ts.map
@@ -12,12 +12,13 @@ import { useT } from './context.js';
12
12
  * @param events - The running message's events.
13
13
  * @param active - True while the turn is in progress; the clock only ticks while true.
14
14
  * @param now - Clock source (default `Date.now`).
15
+ * @param isAnsweredCard - Cards the person already answered (see `RunActivityOptions`).
15
16
  * @returns The activity line's text, or `null` when `active` is false.
16
- * @complexity O(n) in events per render (memoized on the events array).
17
+ * @complexity O(n) in events per render (memoized on the events array and the answered predicate).
17
18
  */
18
- export function useRunActivity({ events, active }, { now = Date.now } = {}) {
19
+ export function useRunActivity({ events, active }, { now = Date.now, isAnsweredCard } = {}) {
19
20
  const t = useT();
20
- const state = useMemo(() => deriveRunActivity({ events: events }), [events]);
21
+ const state = useMemo(() => deriveRunActivity({ events: events }, { isAnsweredCard }), [events, isAnsweredCard]);
21
22
  const [, setTick] = useState(0);
22
23
  const signal = useRef(null);
23
24
  const visible = useRef(null);
@@ -84,6 +84,10 @@ export { appendComposerDiscovery } from './components/composer-discovery.js';
84
84
  */
85
85
  export { interleaveMessageBlocks } from './message-blocks.js';
86
86
  export type { MessageBlock } from './message-blocks.js';
87
+ /** Transcript display modes for tool calls (`ChatPaneProps.toolCallDisplay`, 2026-10-11) — public so
88
+ * a custom message row can apply the same show / collapsed / hidden rules to its own blocks. */
89
+ export { applyToolCallDisplay, isFailedToolRow, summarizeToolGroup, toolGroupLabel, toolRowsForDisplay } from './tool-call-display.js';
90
+ export type { ToolCallDisplay, ToolDisplayRow, ToolGroupSummary } from './tool-call-display.js';
87
91
  export { TodoCard } from './components/TodoCard.js';
88
92
  export type { TodoCardProps } from './components/TodoCard.js';
89
93
  /**
@@ -78,6 +78,9 @@ export { appendComposerDiscovery } from './components/composer-discovery.js';
78
78
  * the one live instance §10 found of R10's documented transitive-reach scope limit.
79
79
  */
80
80
  export { interleaveMessageBlocks } from './message-blocks.js';
81
+ /** Transcript display modes for tool calls (`ChatPaneProps.toolCallDisplay`, 2026-10-11) — public so
82
+ * a custom message row can apply the same show / collapsed / hidden rules to its own blocks. */
83
+ export { applyToolCallDisplay, isFailedToolRow, summarizeToolGroup, toolGroupLabel, toolRowsForDisplay } from './tool-call-display.js';
81
84
  export { TodoCard } from './components/TodoCard.js';
82
85
  /**
83
86
  * The conversation switcher — new / select / delete / search / rename-on-double-click. Storage
@@ -35,8 +35,8 @@
35
35
  * so every check below prefers the former.
36
36
  *
37
37
  * It refuses when:
38
- * - there are no events, or no tool rows AND no ext events to interleave (nothing to do — the flat
39
- * path is identical);
38
+ * - there are no events, or no tool rows AND no ext events (AND no thinking, when the caller asked
39
+ * for it) to interleave (nothing to do — the flat path is identical);
40
40
  * - the concatenated text events do not reproduce `content` byte for byte. This is the load-bearing
41
41
  * check. A message persisted before the events schema settled, one rebuilt by a host that
42
42
  * assembles `content` some other way, or any future divergence between the two, all land here;
@@ -69,6 +69,21 @@ export type MessageBlock<Row> = {
69
69
  readonly name: string;
70
70
  readonly slot: string;
71
71
  readonly key: string;
72
+ }
73
+ /** Where reply `index` of a run ends: just before the mid-run user message that started the next
74
+ * reply (`isReplyBoundary`). `MessageRow` hangs that reply's own "Done" line here. */
75
+ | {
76
+ readonly kind: 'reply-end';
77
+ readonly index: number;
78
+ readonly key: string;
79
+ }
80
+ /** The model's reasoning (`kind: 'thinking'` events), where it happened. Emitted only when the
81
+ * caller passes `includeThinking` (a host's "show thinking" display choice, 2026-10-11);
82
+ * back-to-back thinking events with no text between them join into one block. */
83
+ | {
84
+ readonly kind: 'thinking';
85
+ readonly text: string;
86
+ readonly key: string;
72
87
  };
73
88
  /**
74
89
  * Rebuilds an assistant message as ordered blocks, or returns `null` if that cannot be done
@@ -79,6 +94,9 @@ export type MessageBlock<Row> = {
79
94
  * @param rows - The tool timeline rows, already deduped and paired by `useToolTimeline`. Only rows
80
95
  * present here are emitted, so a row the timeline chose to drop stays dropped.
81
96
  * @param slotOf - The render slot an ext event belongs to (default: its `name`).
97
+ * @param includeThinking - Emit `thinking` blocks for the model's reasoning (default `false`: thinking
98
+ * events are skipped, the layout every host had before this option). Thinking text is not part of
99
+ * `content`, so it never affects the lossless check below.
82
100
  * @returns Ordered blocks, or `null` to signal "render the flat layout instead".
83
101
  * @complexity O(n + m) in the event and row counts.
84
102
  */
@@ -88,10 +106,11 @@ export declare function interleaveMessageBlocks<Row extends {
88
106
  events: readonly AgentEvent[] | undefined;
89
107
  content: string;
90
108
  rows: readonly Row[];
91
- }, { slotOf }?: {
109
+ }, { slotOf, includeThinking }?: {
92
110
  slotOf?: (ev: {
93
111
  readonly name: string;
94
112
  readonly data: unknown;
95
113
  }) => string;
114
+ includeThinking?: boolean;
96
115
  }): MessageBlock<Row>[] | null;
97
116
  //# sourceMappingURL=message-blocks.d.ts.map
@@ -1,4 +1,4 @@
1
- import { assistantContentFromEvents, legacyAssistantContentFromEvents } from '../core/index.js';
1
+ import { assistantContentFromEvents, isReplyBoundary, legacyAssistantContentFromEvents } from '../core/index.js';
2
2
  /**
3
3
  * Rebuilds an assistant message as ordered blocks, or returns `null` if that cannot be done
4
4
  * losslessly.
@@ -8,14 +8,18 @@ import { assistantContentFromEvents, legacyAssistantContentFromEvents } from '..
8
8
  * @param rows - The tool timeline rows, already deduped and paired by `useToolTimeline`. Only rows
9
9
  * present here are emitted, so a row the timeline chose to drop stays dropped.
10
10
  * @param slotOf - The render slot an ext event belongs to (default: its `name`).
11
+ * @param includeThinking - Emit `thinking` blocks for the model's reasoning (default `false`: thinking
12
+ * events are skipped, the layout every host had before this option). Thinking text is not part of
13
+ * `content`, so it never affects the lossless check below.
11
14
  * @returns Ordered blocks, or `null` to signal "render the flat layout instead".
12
15
  * @complexity O(n + m) in the event and row counts.
13
16
  */
14
- export function interleaveMessageBlocks({ events, content, rows }, { slotOf = (ev) => ev.name } = {}) {
17
+ export function interleaveMessageBlocks({ events, content, rows }, { slotOf = (ev) => ev.name, includeThinking = false } = {}) {
15
18
  if (!events || events.length === 0)
16
19
  return null;
17
20
  const hasExtEvent = events.some((ev) => ev.kind === 'ext');
18
- if (rows.length === 0 && !hasExtEvent)
21
+ const hasThinking = includeThinking && events.some((ev) => ev.kind === 'thinking' && ev.text.trim() !== '');
22
+ if (rows.length === 0 && !hasExtEvent && !hasThinking)
19
23
  return null;
20
24
  // The reconstruction is only trustworthy if the text events are demonstrably the source `content`
21
25
  // was built from. Anything else and the offsets below are fiction.
@@ -28,6 +32,7 @@ export function interleaveMessageBlocks({ events, content, rows }, { slotOf = (e
28
32
  const emitted = new Set();
29
33
  const emittedExtSlots = new Set();
30
34
  let buffer = '';
35
+ let replies = 0;
31
36
  const flushText = () => {
32
37
  if (buffer.length === 0)
33
38
  return;
@@ -39,6 +44,23 @@ export function interleaveMessageBlocks({ events, content, rows }, { slotOf = (e
39
44
  buffer += ev.text;
40
45
  continue;
41
46
  }
47
+ if (ev.kind === 'thinking') {
48
+ if (!includeThinking)
49
+ continue;
50
+ flushText();
51
+ const last = blocks[blocks.length - 1];
52
+ if (last && last.kind === 'thinking')
53
+ blocks[blocks.length - 1] = { kind: 'thinking', text: last.text + ev.text, key: last.key };
54
+ else
55
+ blocks.push({ kind: 'thinking', text: ev.text, key: `thinking-${blocks.length}` });
56
+ continue;
57
+ }
58
+ if (isReplyBoundary({ event: ev })) {
59
+ // Emitted per boundary, before any slot dedupe below, so every reply gets its own end marker.
60
+ flushText();
61
+ blocks.push({ kind: 'reply-end', index: replies, key: `reply-end-${replies}` });
62
+ replies += 1;
63
+ }
42
64
  if (ev.kind === 'ext') {
43
65
  // One block per SLOT, at its first occurrence — later events in the same slot fold into it
44
66
  // (`useExtEventGroups`'s job in `MessageRow.tsx`), not a second block. A slot is the `name`
@@ -0,0 +1,90 @@
1
+ /**
2
+ * @module tool-call-display
3
+ *
4
+ * How a transcript shows tool calls (owner, 2026-10-11 — the Tovu dock's Chat settings → Display):
5
+ *
6
+ * - `'show'` — every tool card, where it happened. The layout every host had before this.
7
+ * - `'collapsed'` — each turn's tool cards fold into ONE line ("Used 3 tools") that expands on
8
+ * click; while a tool runs the line names it instead ("Running Bash…").
9
+ * - `'hidden'` — no tool cards at all, EXCEPT a failed one: an error must never vanish from the
10
+ * transcript just because the reader asked for a quieter view.
11
+ *
12
+ * Pure rules over `interleaveMessageBlocks`' output (or the flat row list when that refused), kept
13
+ * out of `MessageRow.tsx` so every mode is testable without rendering.
14
+ */
15
+ import type { ToolStatus } from '../core/index.js';
16
+ import type { MessageBlock } from './message-blocks.js';
17
+ export type ToolCallDisplay = 'show' | 'collapsed' | 'hidden';
18
+ /** The fields of a tool row these rules read — `ToolTimelineRow` satisfies it. */
19
+ export interface ToolDisplayRow {
20
+ readonly id: string;
21
+ readonly name: string;
22
+ readonly status: ToolStatus;
23
+ readonly use?: {
24
+ readonly input: unknown;
25
+ };
26
+ }
27
+ /**
28
+ * Whether a row is a failure the reader must always see: the tool returned an error, or the run
29
+ * ended without the tool ever answering (`deriveToolStatus` maps both to `'error'`).
30
+ * @complexity O(1).
31
+ */
32
+ export declare function isFailedToolRow({ row }: {
33
+ row: Pick<ToolDisplayRow, 'status'>;
34
+ }): boolean;
35
+ /**
36
+ * Applies a display mode to an interleaved message.
37
+ *
38
+ * `'collapsed'` merges every `tools` block of one turn into the turn's FIRST `tools` block (its
39
+ * position and key), so the turn shows a single fold-up line. A turn ends at a `reply-end` block —
40
+ * the mid-run user message that starts the next reply — so a message holding several replies keeps
41
+ * one line per reply. Keeping the first block's key keeps the line's open/closed state across
42
+ * streaming updates, since later blocks append after it.
43
+ *
44
+ * `'hidden'` keeps only failed rows and drops `tools` blocks left empty.
45
+ *
46
+ * @param input.blocks - `interleaveMessageBlocks` output.
47
+ * @param input.mode - The display mode.
48
+ * @returns New blocks; the input is never mutated. `'show'` returns `blocks` itself.
49
+ * @complexity O(n) in blocks + rows.
50
+ */
51
+ export declare function applyToolCallDisplay<Row extends ToolDisplayRow>({ blocks, mode }: {
52
+ blocks: readonly MessageBlock<Row>[];
53
+ mode: ToolCallDisplay;
54
+ }): readonly MessageBlock<Row>[];
55
+ /**
56
+ * The flat layout's counterpart (used when `interleaveMessageBlocks` refused): `'hidden'` keeps only
57
+ * failed rows; the other modes keep every row (`'collapsed'` folds them into one line at render).
58
+ * @complexity O(n) in rows.
59
+ */
60
+ export declare function toolRowsForDisplay<Row extends ToolDisplayRow>({ rows, mode }: {
61
+ rows: readonly Row[];
62
+ mode: ToolCallDisplay;
63
+ }): readonly Row[];
64
+ /** What a collapsed tool line says: the tool running now, or how many ran (and how many failed). */
65
+ export type ToolGroupSummary = {
66
+ readonly kind: 'running';
67
+ readonly name: string;
68
+ } | {
69
+ readonly kind: 'done';
70
+ readonly count: number;
71
+ readonly failed: number;
72
+ };
73
+ /**
74
+ * Summarises one turn's tool rows. The LAST running row wins, so a line over several in-flight calls
75
+ * names the newest one, matching what the run is doing now.
76
+ * @complexity O(n) in rows.
77
+ */
78
+ export declare function summarizeToolGroup({ rows }: {
79
+ rows: readonly ToolDisplayRow[];
80
+ }): ToolGroupSummary;
81
+ /**
82
+ * The collapsed line's text. Each phrase is its own translation key (the English string is the key,
83
+ * see `slots.ts`), with `{count}`/`{name}` filled by the translator.
84
+ * @complexity O(1).
85
+ */
86
+ export declare function toolGroupLabel({ summary, t }: {
87
+ summary: ToolGroupSummary;
88
+ t: (key: string, vars?: Record<string, string | number>) => string;
89
+ }): string;
90
+ //# sourceMappingURL=tool-call-display.d.ts.map
@@ -0,0 +1,97 @@
1
+ import { humanizeToolName } from '../core/index.js';
2
+ /**
3
+ * Whether a row is a failure the reader must always see: the tool returned an error, or the run
4
+ * ended without the tool ever answering (`deriveToolStatus` maps both to `'error'`).
5
+ * @complexity O(1).
6
+ */
7
+ export function isFailedToolRow({ row }) {
8
+ return row.status === 'error';
9
+ }
10
+ /** Running = no result yet while the run streams (`'executing'`), or not started (`'inProgress'`). */
11
+ function isRunningToolRow(row) {
12
+ return row.status === 'executing' || row.status === 'inProgress';
13
+ }
14
+ /**
15
+ * Applies a display mode to an interleaved message.
16
+ *
17
+ * `'collapsed'` merges every `tools` block of one turn into the turn's FIRST `tools` block (its
18
+ * position and key), so the turn shows a single fold-up line. A turn ends at a `reply-end` block —
19
+ * the mid-run user message that starts the next reply — so a message holding several replies keeps
20
+ * one line per reply. Keeping the first block's key keeps the line's open/closed state across
21
+ * streaming updates, since later blocks append after it.
22
+ *
23
+ * `'hidden'` keeps only failed rows and drops `tools` blocks left empty.
24
+ *
25
+ * @param input.blocks - `interleaveMessageBlocks` output.
26
+ * @param input.mode - The display mode.
27
+ * @returns New blocks; the input is never mutated. `'show'` returns `blocks` itself.
28
+ * @complexity O(n) in blocks + rows.
29
+ */
30
+ export function applyToolCallDisplay({ blocks, mode }) {
31
+ if (mode === 'show')
32
+ return blocks;
33
+ if (mode === 'hidden') {
34
+ const out = [];
35
+ for (const block of blocks) {
36
+ if (block.kind !== 'tools') {
37
+ out.push(block);
38
+ continue;
39
+ }
40
+ const failed = block.rows.filter((row) => isFailedToolRow({ row }));
41
+ if (failed.length > 0)
42
+ out.push({ kind: 'tools', rows: failed, key: block.key });
43
+ }
44
+ return out;
45
+ }
46
+ const out = [];
47
+ let turnToolsIndex = -1;
48
+ for (const block of blocks) {
49
+ if (block.kind === 'reply-end')
50
+ turnToolsIndex = -1;
51
+ if (block.kind !== 'tools') {
52
+ out.push(block);
53
+ continue;
54
+ }
55
+ const first = turnToolsIndex === -1 ? undefined : out[turnToolsIndex];
56
+ if (first && first.kind === 'tools') {
57
+ out[turnToolsIndex] = { kind: 'tools', rows: [...first.rows, ...block.rows], key: first.key };
58
+ continue;
59
+ }
60
+ turnToolsIndex = out.length;
61
+ out.push(block);
62
+ }
63
+ return out;
64
+ }
65
+ /**
66
+ * The flat layout's counterpart (used when `interleaveMessageBlocks` refused): `'hidden'` keeps only
67
+ * failed rows; the other modes keep every row (`'collapsed'` folds them into one line at render).
68
+ * @complexity O(n) in rows.
69
+ */
70
+ export function toolRowsForDisplay({ rows, mode }) {
71
+ return mode === 'hidden' ? rows.filter((row) => isFailedToolRow({ row })) : rows;
72
+ }
73
+ /**
74
+ * Summarises one turn's tool rows. The LAST running row wins, so a line over several in-flight calls
75
+ * names the newest one, matching what the run is doing now.
76
+ * @complexity O(n) in rows.
77
+ */
78
+ export function summarizeToolGroup({ rows }) {
79
+ for (let i = rows.length - 1; i >= 0; i -= 1) {
80
+ const row = rows[i];
81
+ if (isRunningToolRow(row))
82
+ return { kind: 'running', name: humanizeToolName({ name: row.name }, { input: row.use?.input }) };
83
+ }
84
+ return { kind: 'done', count: rows.length, failed: rows.filter((row) => isFailedToolRow({ row })).length };
85
+ }
86
+ /**
87
+ * The collapsed line's text. Each phrase is its own translation key (the English string is the key,
88
+ * see `slots.ts`), with `{count}`/`{name}` filled by the translator.
89
+ * @complexity O(1).
90
+ */
91
+ export function toolGroupLabel({ summary, t }) {
92
+ if (summary.kind === 'running')
93
+ return t('Running {name}…', { name: summary.name });
94
+ const used = summary.count === 1 ? t('Used 1 tool') : t('Used {count} tools', { count: summary.count });
95
+ return summary.failed > 0 ? `${used} · ${t('{count} failed', { count: summary.failed })}` : used;
96
+ }
97
+ //# sourceMappingURL=tool-call-display.js.map