@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.
- package/CHANGELOG.md +4 -0
- package/dist/core/ag-ui/entry.js +2 -0
- package/dist/core/assistant-client/durable-run-subscription.d.ts +4 -1
- package/dist/core/assistant-client/durable-run-subscription.js +13 -4
- package/dist/core/events.d.ts +3 -0
- package/dist/core/history-tool-metadata.d.ts +23 -0
- package/dist/core/history-tool-metadata.js +13 -0
- package/dist/core/index.d.ts +3 -0
- package/dist/core/index.js +3 -0
- package/dist/core/persistence/title.d.ts +24 -0
- package/dist/core/persistence/title.js +60 -9
- package/dist/core/run-activity.d.ts +15 -2
- package/dist/core/run-activity.js +6 -4
- package/dist/core/run-events/entry.js +2 -0
- package/dist/core/tool-call-detail.d.ts +15 -0
- package/dist/core/tool-call-detail.js +18 -0
- package/dist/core/turn-usage.d.ts +42 -3
- package/dist/core/turn-usage.js +101 -4
- package/dist/core/usage-format.d.ts +10 -0
- package/dist/core/usage-format.js +15 -0
- package/dist/embed-widget/browser/lazy-loader.d.ts +34 -0
- package/dist/embed-widget/browser/lazy-loader.js +116 -0
- package/dist/embed-widget/react/dialog.hooks.d.ts +28 -0
- package/dist/embed-widget/react/dialog.hooks.js +54 -0
- package/dist/embed-widget/react/embed.d.ts +1 -0
- package/dist/embed-widget/react/embed.js +5 -3
- package/dist/embed-widget/react/header.d.ts +3 -1
- package/dist/embed-widget/react/header.hooks.d.ts +13 -0
- package/dist/embed-widget/react/header.hooks.js +32 -0
- package/dist/embed-widget/react/header.js +4 -14
- package/dist/embed-widget/vanilla/index.d.ts +63 -0
- package/dist/embed-widget/vanilla/index.js +319 -0
- package/dist/embed-widget/vanilla/state.d.ts +48 -0
- package/dist/embed-widget/vanilla/state.js +120 -0
- package/dist/embed-widget/vanilla/style.css +29 -0
- package/dist/react/components/A2uiSurfaceCard.d.ts +5 -1
- package/dist/react/components/A2uiSurfaceCard.js +9 -9
- package/dist/react/components/AttachmentPreviewModal.hooks.d.ts +7 -0
- package/dist/react/components/AttachmentPreviewModal.hooks.js +1 -1
- package/dist/react/components/ChatFab.d.ts +4 -1
- package/dist/react/components/ChatFab.js +2 -2
- package/dist/react/components/ConversationList.d.ts +8 -1
- package/dist/react/components/ConversationList.hooks.d.ts +18 -0
- package/dist/react/components/ConversationList.hooks.js +25 -0
- package/dist/react/components/ConversationList.js +12 -5
- package/dist/react/components/ConversationList.rules.d.ts +25 -0
- package/dist/react/components/ConversationList.rules.js +19 -0
- package/dist/react/components/McpUiSurfaceCard.d.ts +1 -1
- package/dist/react/components/McpUiSurfaceCard.js +8 -4
- package/dist/react/components/MessageList.d.ts +1 -1
- package/dist/react/components/MessageList.hooks.d.ts +4 -0
- package/dist/react/components/MessageList.hooks.js +6 -1
- package/dist/react/components/MessageList.js +4 -3
- package/dist/react/components/MessageRow.d.ts +17 -0
- package/dist/react/components/MessageRow.js +54 -35
- package/dist/react/components/MessageRow.usage.d.ts +48 -0
- package/dist/react/components/MessageRow.usage.js +47 -0
- package/dist/react/components/MidRunUserMessage.d.ts +2 -12
- package/dist/react/components/MidRunUserMessage.js +4 -2
- package/dist/react/components/SentAttachment.d.ts +19 -0
- package/dist/react/components/SentAttachment.hooks.d.ts +44 -0
- package/dist/react/components/SentAttachment.hooks.js +56 -0
- package/dist/react/components/SentAttachment.js +14 -0
- package/dist/react/components/ToolCallGroup.d.ts +12 -0
- package/dist/react/components/ToolCallGroup.hooks.d.ts +20 -0
- package/dist/react/components/ToolCallGroup.hooks.js +23 -0
- package/dist/react/components/ToolCallGroup.js +13 -0
- package/dist/react/components/ToolCard.js +25 -17
- package/dist/react/components/mcp-ui-surface-state.d.ts +21 -0
- package/dist/react/components/mcp-ui-surface-state.js +26 -0
- package/dist/react/ext-event-renderer-registry.d.ts +7 -0
- package/dist/react/features/chat-pane/components/ChatPane.d.ts +1 -1
- package/dist/react/features/chat-pane/components/ChatPane.js +11 -6
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +1 -0
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +16 -10
- package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.js +4 -0
- package/dist/react/features/chat-pane/hooks/useChatPaneSuggestion.hooks.d.ts +9 -0
- package/dist/react/features/chat-pane/hooks/useChatPaneSuggestion.hooks.js +11 -0
- package/dist/react/features/chat-pane/hooks/useChatPaneSurface.hooks.d.ts +17 -0
- package/dist/react/features/chat-pane/hooks/useChatPaneSurface.hooks.js +15 -0
- package/dist/react/features/chat-pane/rules.d.ts +14 -0
- package/dist/react/features/chat-pane/rules.js +18 -0
- package/dist/react/features/chat-pane/styles.d.ts +1 -1
- package/dist/react/features/chat-pane/styles.js +85 -1
- package/dist/react/features/chat-pane/types.d.ts +16 -1
- package/dist/react/hooks/useAnsweredCards.d.ts +20 -0
- package/dist/react/hooks/useAnsweredCards.js +23 -0
- package/dist/react/hooks/useConversation.d.ts +8 -0
- package/dist/react/hooks/useConversation.js +34 -0
- package/dist/react/hooks/useRunActivity.d.ts +5 -3
- package/dist/react/hooks/useRunActivity.js +4 -3
- package/dist/react/index.d.ts +4 -0
- package/dist/react/index.js +3 -0
- package/dist/react/message-blocks.d.ts +22 -3
- package/dist/react/message-blocks.js +25 -3
- package/dist/react/tool-call-display.d.ts +90 -0
- package/dist/react/tool-call-display.js +97 -0
- package/dist/store/ports.d.ts +25 -2
- package/dist/store/runtime/entry.d.ts +1 -0
- package/dist/store/runtime/entry.js +1 -0
- package/dist/store/runtime/history-tools.d.ts +27 -0
- package/dist/store/runtime/history-tools.js +169 -0
- package/dist/store/sql/store.js +52 -0
- package/dist/testing/index.d.ts +7 -0
- package/dist/testing/index.js +22 -0
- 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
|
-
|
|
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);
|
package/dist/react/index.d.ts
CHANGED
|
@@ -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
|
/**
|
package/dist/react/index.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|