@jini-ai/chat 0.6.7 → 0.6.11
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 +135 -1
- package/dist/core/accepted-input.d.ts +2 -0
- package/dist/core/accepted-input.js +34 -3
- package/dist/core/assistant-client/durable-run-subscription.d.ts +4 -2
- package/dist/core/assistant-client/durable-run-subscription.js +36 -7
- package/dist/core/assistant-client/transport.js +3 -1
- package/dist/core/persistence/title.d.ts +8 -16
- package/dist/core/persistence/title.js +20 -63
- package/dist/core/question-answer.js +2 -2
- package/dist/core/run-events/entry.d.ts +1 -1
- package/dist/core/run-events/entry.js +2 -1
- package/dist/core/turn-usage.js +19 -2
- package/dist/react/components/Markdown.js +2 -2
- package/dist/react/components/McpUiSurfaceCard.js +10 -7
- package/dist/react/components/MessageList.hooks.js +5 -1
- package/dist/react/components/MessageRow.d.ts +1 -1
- package/dist/react/components/MessageRow.js +41 -24
- package/dist/react/components/MessageRow.usage.d.ts +9 -1
- package/dist/react/components/MessageRow.usage.js +22 -5
- package/dist/react/components/ToolCallGroup.d.ts +4 -3
- package/dist/react/components/ToolCallGroup.hooks.d.ts +3 -1
- package/dist/react/components/ToolCallGroup.hooks.js +2 -1
- package/dist/react/components/ToolCallGroup.js +7 -5
- package/dist/react/features/chat-pane/create-mid-run-message-poster.d.ts +3 -3
- package/dist/react/features/chat-pane/create-mid-run-message-poster.js +8 -5
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +69 -17
- package/dist/react/features/chat-pane/types.d.ts +9 -3
- package/dist/react/tool-call-display.d.ts +12 -6
- package/dist/react/tool-call-display.js +42 -15
- package/dist/server/durable-runs/recover.js +59 -23
- package/dist/store/sql/store.d.ts +2 -2
- package/dist/store/sql/store.js +11 -3
- package/package.json +15 -15
|
@@ -428,9 +428,9 @@ function renderInline(text) {
|
|
|
428
428
|
else if (mdLink)
|
|
429
429
|
nodes.push(renderMarkdownLink(mdLink, key++));
|
|
430
430
|
else if (bold)
|
|
431
|
-
nodes.push(_jsx("strong", { children: bold.slice(2, -2) }, key++));
|
|
431
|
+
nodes.push(_jsx("strong", { children: renderInline(bold.slice(2, -2)) }, key++));
|
|
432
432
|
else if (italic)
|
|
433
|
-
nodes.push(_jsx("em", { children: italic.slice(1, -1) }, key++));
|
|
433
|
+
nodes.push(_jsx("em", { children: renderInline(italic.slice(1, -1)) }, key++));
|
|
434
434
|
else if (link)
|
|
435
435
|
nodes.push(_jsx("a", { href: link, ...NEW_TAB, children: link }, key++));
|
|
436
436
|
// `whole` is `match[0]` — a successful `RegExpExecArray` always has a
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
3
|
* @module components/McpUiSurfaceCard
|
|
4
4
|
*
|
|
@@ -120,8 +120,10 @@ function PendingSurfaceMirror({ uri, plan, t }) {
|
|
|
120
120
|
}
|
|
121
121
|
/** A closed card: the question's title, when the call gave one, and why it is closed. */
|
|
122
122
|
function ClosedSurface({ call, label }) {
|
|
123
|
-
const
|
|
124
|
-
|
|
123
|
+
const input = call?.input;
|
|
124
|
+
const title = [input?.title, input?.question, input?.prompt]
|
|
125
|
+
.find(value => typeof value === 'string' && value.trim() !== '');
|
|
126
|
+
return (_jsx("div", { className: "mcpui-surface-card mcpui-surface-card-closed", role: "status", children: typeof title === 'string' ? `${title}: ${label}` : label }));
|
|
125
127
|
}
|
|
126
128
|
/** The live card's answer lifecycle: its deadline and delivery notification, using their existing owners. */
|
|
127
129
|
function surfaceInteraction({ resource, answered, onToolCall, onAnswered, onAnswerText, call }, _options = {}) {
|
|
@@ -144,16 +146,17 @@ export function McpUiSurfaceCard({ events, sandboxProxyUrl, onToolCall, onOpenLi
|
|
|
144
146
|
const closed = single === undefined
|
|
145
147
|
? undefined
|
|
146
148
|
: closedSurfaceLabel({ t, call, runStreaming, documentsForUri: events.length, expired: expiry?.expired === true });
|
|
147
|
-
const answer =
|
|
149
|
+
const answer = questionAnswerFromResult({ result: call?.result, input: call?.input }) ?? answerText;
|
|
148
150
|
return _jsx(SurfaceCardContent, { resources: resources, single: single, answerRecorded: answerRecorded, answer: answer, closed: closed, call: call, t: t, sandboxProxyUrl: sandboxProxyUrl, toolCall: toolCall, onOpenLink: onOpenLink, maxHeight: maxHeight, expiry: expiry });
|
|
149
151
|
}
|
|
150
152
|
/** Rendering priority stays receipt, answer, closure, then the live documents. */
|
|
151
153
|
function SurfaceCardContent({ resources, single, answerRecorded, answer, closed, call, t, sandboxProxyUrl, toolCall, onOpenLink, maxHeight, expiry }) {
|
|
152
|
-
// MOVE: The accepted-input receipt owns its bubble;
|
|
154
|
+
// MOVE: The accepted-input receipt owns its bubble; the closed card retains the question's context.
|
|
153
155
|
if (single !== undefined && answerRecorded)
|
|
154
|
-
return
|
|
156
|
+
return _jsx(ClosedSurface, { call: call, label: answer ?? closed ?? t('Answered') });
|
|
155
157
|
if (single !== undefined && answer !== undefined) {
|
|
156
|
-
|
|
158
|
+
const label = answer === 'Declined' ? t('Declined') : answer;
|
|
159
|
+
return _jsxs(_Fragment, { children: [_jsx(ClosedSurface, { call: call, label: label }), _jsx(MidRunUserMessage, { events: [{ text: label }] })] });
|
|
157
160
|
}
|
|
158
161
|
if (closed !== undefined)
|
|
159
162
|
return _jsx(ClosedSurface, { call: call, label: closed });
|
|
@@ -6,11 +6,15 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
8
8
|
import { conversationUsage } from '../../core/turn-usage.js';
|
|
9
|
+
import { projectAcceptedInputEvents } from '../../core/accepted-input.js';
|
|
9
10
|
import { nextStickToBottom, startsNewTurn } from './MessageList.rules.js';
|
|
10
11
|
const prefersReducedMotion = () => typeof window !== 'undefined' && (window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false);
|
|
11
12
|
/** Recompute costs once per immutable transcript snapshot, including saved history on reload. */
|
|
12
13
|
export function useConversationUsage({ messages }, _optional = {}) {
|
|
13
|
-
|
|
14
|
+
// Usage and rendering must see the same answer boundaries, including receipts rebuilt on reload.
|
|
15
|
+
return useMemo(() => conversationUsage({ messages: messages.map(message => message.events === undefined ? message : {
|
|
16
|
+
...message, events: projectAcceptedInputEvents({ events: message.events }),
|
|
17
|
+
}) }), [messages]);
|
|
14
18
|
}
|
|
15
19
|
export function useMessageListAutoScroll({ messages, scrollIntent }, { onScrolled, reducedMotion = prefersReducedMotion } = {}) {
|
|
16
20
|
const containerRef = useRef(null);
|
|
@@ -63,7 +63,7 @@ export interface MessageRowProps {
|
|
|
63
63
|
* included (a host's "Done line" display choice, 2026-10-11). */
|
|
64
64
|
showUsageLine?: boolean;
|
|
65
65
|
/** How tool calls show (`../tool-call-display.ts`): every card (`'show'`, the default), one
|
|
66
|
-
* fold-up line per turn (`'collapsed'`), or
|
|
66
|
+
* fold-up line per turn (`'collapsed'`), or compact unrecovered failures (`'hidden'`). */
|
|
67
67
|
toolCallDisplay?: ToolCallDisplay;
|
|
68
68
|
/** Show the model's reasoning (`thinking` events) as a fold-up block where it happened. Off by
|
|
69
69
|
* default — the transcript never showed it before this option. Needs the interleaved layout; a
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import { projectAcceptedInputEvents } from '../../core/accepted-input.js';
|
|
3
3
|
import { MidRunUserMessage, MID_RUN_USER_MESSAGE_EXT_EVENT_NAME } from './MidRunUserMessage.js';
|
|
4
4
|
import { terminalMessageNotice, continuingRunNotice } from '../../core/durable-projection.js';
|
|
5
|
-
import { usageLayout, usageLineMeetsActions, useMessageUsage, useUsageSummary } from './MessageRow.usage.js';
|
|
5
|
+
import { usageEndLabel, usageLayout, usageLineMeetsActions, useMessageUsage, useUsageSummary } from './MessageRow.usage.js';
|
|
6
6
|
/**
|
|
7
7
|
* @module MessageRow
|
|
8
8
|
*
|
|
@@ -54,7 +54,7 @@ import { usageLayout, usageLineMeetsActions, useMessageUsage, useUsageSummary }
|
|
|
54
54
|
* chunk stopped arriving.
|
|
55
55
|
*/
|
|
56
56
|
import React, { memo, useMemo } from 'react';
|
|
57
|
-
import { isAwaitingAnswer, isTerminalRunStatus, splitOnQuestionForms, stripArtifact } from '../../core/index.js';
|
|
57
|
+
import { assistantContentFromEvents, isAwaitingAnswer, isTerminalRunStatus, splitOnQuestionForms, stripArtifact } from '../../core/index.js';
|
|
58
58
|
import { useToolTimeline } from '../hooks/useToolTimeline.js';
|
|
59
59
|
import { useExtEventGroups } from '../hooks/useExtEventGroups.js';
|
|
60
60
|
import { useCopyToClipboard } from '../hooks/useCopyToClipboard.js';
|
|
@@ -111,23 +111,33 @@ function isRunInProgress(runStatus) {
|
|
|
111
111
|
/** One "Done · 6m 29s · $0.41 · total $2.50" line from reported usage, never estimated (the total is
|
|
112
112
|
* left off when `showTotal` is false). Partial usage stays partial; output tokens are available to
|
|
113
113
|
* assistive technology and in the tooltip. */
|
|
114
|
-
function UsageSummary({ usage, showTotal }) {
|
|
114
|
+
function UsageSummary({ usage, showTotal, message, awaitingAnswer }) {
|
|
115
115
|
const t = useT();
|
|
116
|
-
const {
|
|
116
|
+
const label = usageEndLabel({ usage, ...(message === undefined ? {} : { message }), ...(awaitingAnswer === undefined ? {} : { awaitingAnswer }) });
|
|
117
|
+
const { text, title, ariaLabel } = useUsageSummary({ usage }, { showTotal, label });
|
|
117
118
|
if (!text)
|
|
118
119
|
return null;
|
|
119
|
-
return (_jsxs("div", { className: "jini-message-usage", title: title, role: "note", "aria-label": ariaLabel, children: [_jsx("span", { className: "jini-message-usage-dot", "aria-hidden": true, children: "\u25CF" }), t(
|
|
120
|
+
return (_jsxs("div", { className: "jini-message-usage", title: title, role: "note", "aria-label": ariaLabel, children: [label === 'Done' ? _jsx("span", { className: "jini-message-usage-dot", "aria-hidden": true, children: "\u25CF" }) : null, t(label), " \u00B7 ", text] }));
|
|
120
121
|
}
|
|
121
|
-
function messageBlocks({
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
122
|
+
function messageBlocks({ message, rows, registry, content = message.content }, { toolCallDisplay = 'show', includeThinking = false } = {}) {
|
|
123
|
+
const blocks = interleaveMessageBlocks({ events: message.events, content, rows }, { slotOf: (ev) => registry.slot({ name: ev.name, data: ev.data }), includeThinking });
|
|
124
|
+
return blocks ? applyToolCallDisplay({ blocks, mode: toolCallDisplay }, { runFailed: message.runStatus === 'failed' }) : null;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* The flat layout's tool groups. Even when host text prevents interleaving, event ordering can still
|
|
128
|
+
* prove tool recovery and reply boundaries, so hidden mode reconstructs the blocks from the events —
|
|
129
|
+
* only for this display decision; the host's text is never replaced. Otherwise one flat list.
|
|
130
|
+
* @complexity O(n) in events + rows.
|
|
131
|
+
*/
|
|
132
|
+
function flatToolGroups({ message, rows, registry, displayBlocks }, { toolCallDisplay }) {
|
|
133
|
+
if (toolCallDisplay === 'hidden') {
|
|
134
|
+
const recovery = displayBlocks
|
|
135
|
+
?? messageBlocks({ message, rows, registry, content: assistantContentFromEvents({ events: message.events ?? [] }) }, { toolCallDisplay });
|
|
136
|
+
if (recovery)
|
|
137
|
+
return recovery.flatMap((block) => (block.kind === 'tools' ? [{ rows: block.rows, key: block.key }] : []));
|
|
138
|
+
}
|
|
139
|
+
const flat = toolRowsForDisplay({ rows, mode: toolCallDisplay }, { runFailed: message.runStatus === 'failed' });
|
|
140
|
+
return flat.length > 0 ? [{ rows: flat, key: 'tools' }] : [];
|
|
131
141
|
}
|
|
132
142
|
/** The model's reasoning, folded under a "Thinking" summary so it never crowds the answer. */
|
|
133
143
|
function ThinkingBlock({ text }) {
|
|
@@ -141,7 +151,7 @@ function messageAwaitingAnswer({ runStreaming, runInProgress, events }, { isAnsw
|
|
|
141
151
|
}
|
|
142
152
|
function AssistantMessageFooter({ message, usageEvent, showUsageTotal, terminalNotice, pending, runInProgress, runStreaming, visibleContent, isAnsweredCard }, _optional = {}) {
|
|
143
153
|
const t = useT();
|
|
144
|
-
return _jsxs(_Fragment, { children: [usageEvent ? _jsx(UsageSummary, { usage: usageEvent, showTotal: showUsageTotal }) : null, terminalNotice ? _jsx("div", { className: "jini-message-error", role: "status", children: t(terminalNotice) }) : null, !pending && !runInProgress ? (_jsx("div", { className: "jini-message-actions jini-message-actions--assistant", children: _jsx(CopyMessageButton, { text: visibleContent, label: t('Copy message') }) })) : null, runInProgress ? _jsx(RunActivityLine, { events: message.events, active: pending || (runStreaming === true && runInProgress), isAnsweredCard: isAnsweredCard }) : null] });
|
|
154
|
+
return _jsxs(_Fragment, { children: [usageEvent ? _jsx(UsageSummary, { usage: usageEvent, showTotal: showUsageTotal, message: message, awaitingAnswer: isAwaitingAnswer({ events: message.events }, { isAnsweredCard }) }) : null, terminalNotice ? _jsx("div", { className: "jini-message-error", role: "status", children: t(terminalNotice) }) : null, !pending && !runInProgress ? (_jsx("div", { className: "jini-message-actions jini-message-actions--assistant", children: _jsx(CopyMessageButton, { text: visibleContent, label: t('Copy message') }) })) : null, runInProgress ? _jsx(RunActivityLine, { events: message.events, active: pending || (runStreaming === true && runInProgress), isAnsweredCard: isAnsweredCard }) : null] });
|
|
145
155
|
}
|
|
146
156
|
/**
|
|
147
157
|
* Quiet, icon-only copy affordance shared by the user- and assistant-message
|
|
@@ -191,18 +201,24 @@ function AssistantMessageRow({ message, usageSummary, showUsageTotal, showUsageL
|
|
|
191
201
|
const answeredCards = useAnsweredCards({ slotOf: registry.slot, events: message.events ?? [], groups: extGroups, runStreaming, runStatus: message.runStatus });
|
|
192
202
|
const terminalNotice = terminalMessageNotice({ message }, {});
|
|
193
203
|
const visibleContent = stripArtifact({ content: message.content });
|
|
194
|
-
const
|
|
204
|
+
const displayBlocks = messageBlocks({ message, rows: timeline.rows, registry }, { toolCallDisplay, includeThinking: showThinking });
|
|
205
|
+
// Artifact stripping trims/replaces text, invalidating the event offsets for rendering. Keep the
|
|
206
|
+
// flat layout there; displayBlocks still supplies the recovery decision from the original events.
|
|
207
|
+
const blocks = visibleContent === message.content ? displayBlocks : null;
|
|
195
208
|
const pending = isPendingWithNoContent(message, visibleContent, timeline.rows.length);
|
|
196
209
|
const runInProgress = isRunInProgress(message.runStatus);
|
|
197
210
|
const awaitingAnswer = messageAwaitingAnswer({ runStreaming, runInProgress, events: message.events }, { isAnsweredCard: answeredCards.isAnsweredCard });
|
|
198
|
-
const usage = usageLayout({ usage: messageUsage, interleaved: blocks !== null, runInProgress }, {
|
|
211
|
+
const usage = usageLayout({ usage: messageUsage, interleaved: blocks !== null, runInProgress }, {
|
|
212
|
+
runStreaming: runStreaming && !isTerminalRunStatus({ status: message.runStatus }),
|
|
213
|
+
});
|
|
199
214
|
const footerUsage = showUsageLine ? usage.footer : undefined;
|
|
215
|
+
const lastFinishedReply = !runInProgress && !usage.footer ? usage.replies.findLastIndex(value => value !== undefined) : -1;
|
|
200
216
|
const renderToolCard = (row) => (_jsx(ToolCard, { use: row.use, result: row.result, runStreaming: runStreaming, runSucceeded: runSucceeded, ...(projectFileNames !== undefined ? { projectFileNames } : {}), ...(onRequestOpenFile !== undefined ? { onRequestOpenFile } : {}) }, row.id));
|
|
201
|
-
//
|
|
202
|
-
const renderTools = (rows, key) => (toolCallDisplay
|
|
203
|
-
? _jsx(ToolCallGroup, { rows: rows, renderRow: renderToolCard }, key)
|
|
217
|
+
// Hidden mode reuses the same disclosure and card renderer for unrecovered failures.
|
|
218
|
+
const renderTools = (rows, key) => (toolCallDisplay !== 'show'
|
|
219
|
+
? _jsx(ToolCallGroup, { rows: rows, renderRow: renderToolCard, failureOnly: toolCallDisplay === 'hidden' }, key)
|
|
204
220
|
: _jsx("div", { className: "jini-message-tools", children: rows.map(renderToolCard) }, key));
|
|
205
|
-
const
|
|
221
|
+
const flatTools = flatToolGroups({ message, rows: timeline.rows, registry, displayBlocks }, { toolCallDisplay });
|
|
206
222
|
const renderExtGroup = (group) => {
|
|
207
223
|
const renderer = registry.get({ name: group.name });
|
|
208
224
|
if (!renderer && group.name === MID_RUN_USER_MESSAGE_EXT_EVENT_NAME)
|
|
@@ -239,7 +255,8 @@ function AssistantMessageRow({ message, usageSummary, showUsageTotal, showUsageL
|
|
|
239
255
|
}
|
|
240
256
|
if (block.kind === 'reply-end') {
|
|
241
257
|
const reply = usage.replies[block.index];
|
|
242
|
-
|
|
258
|
+
// A trailing input may have no reply: its predecessor's usage belongs above that input.
|
|
259
|
+
return reply && showUsageLine ? _jsx(UsageSummary, { usage: reply, showTotal: showUsageTotal, ...(block.index === lastFinishedReply ? { message } : {}) }, block.key) : null;
|
|
243
260
|
}
|
|
244
261
|
if (block.kind === 'tools')
|
|
245
262
|
return renderTools(block.rows, block.key);
|
|
@@ -254,7 +271,7 @@ function AssistantMessageRow({ message, usageSummary, showUsageTotal, showUsageL
|
|
|
254
271
|
? 'jini-message jini-message-assistant jini-message-assistant--usage-actions'
|
|
255
272
|
: 'jini-message jini-message-assistant', "data-message-id": message.id, "data-run-status": message.runStatus, "data-agent-element": `chat-message-${message.id}`, "data-agent-role": "region", "data-agent-label": "A reply from the assistant", children: [message.agentName ? _jsx("div", { className: "jini-message-agent", children: message.agentName }) : null, blocks
|
|
256
273
|
? blocks.map(renderBlock)
|
|
257
|
-
: (_jsxs(_Fragment, { children: [renderTextSegments(visibleContent, 'text'),
|
|
274
|
+
: (_jsxs(_Fragment, { children: [renderTextSegments(visibleContent, 'text'), flatTools.map((group) => renderTools(group.rows, group.key)), extGroups.length > 0 ? _jsx("div", { className: "jini-message-ext-events", children: extGroups.map(renderExtGroup) }) : null] })), _jsx(AssistantMessageFooter, { message, usageEvent: footerUsage, showUsageTotal, terminalNotice, pending, runInProgress, runStreaming, visibleContent, isAnsweredCard: answeredCards.isAnsweredCard })] }));
|
|
258
275
|
}
|
|
259
276
|
/**
|
|
260
277
|
* The live "what is it doing" line under a running reply (see `core/run-activity.ts`). Replaces the
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import type { ChatMessage } from '../../core/messages.js';
|
|
2
2
|
import { type ChatUsageSummary } from '../../core/turn-usage.js';
|
|
3
|
+
/** A run's terminal state wins over its last model response; interrupted usage never claims success.
|
|
4
|
+
* Missing lifecycle data keeps the legacy completed label. O(n) in terminal notice events. */
|
|
5
|
+
export declare function usageEndLabel({ usage, message, awaitingAnswer }: {
|
|
6
|
+
usage: ChatUsageSummary;
|
|
7
|
+
message?: ChatMessage;
|
|
8
|
+
awaitingAnswer?: boolean;
|
|
9
|
+
}, _options?: {}): string;
|
|
3
10
|
/** A standalone row has one turn; MessageList supplies the full conversation projection. */
|
|
4
11
|
export declare function useMessageUsage({ message, summary }: {
|
|
5
12
|
message: ChatMessage;
|
|
@@ -41,8 +48,9 @@ export declare function usageLineMeetsActions({ footer, terminalNotice, pending,
|
|
|
41
48
|
* the current turn's cost is unknown. `showTotal: false` turns the total off. */
|
|
42
49
|
export declare function useUsageSummary({ usage }: {
|
|
43
50
|
usage: ChatUsageSummary;
|
|
44
|
-
}, { showTotal }?: {
|
|
51
|
+
}, { showTotal, label }?: {
|
|
45
52
|
showTotal?: boolean;
|
|
53
|
+
label?: string;
|
|
46
54
|
}): {
|
|
47
55
|
text: string;
|
|
48
56
|
title: string | undefined;
|
|
@@ -1,7 +1,24 @@
|
|
|
1
1
|
import { useMemo } from 'react';
|
|
2
|
+
import { terminalMessageNotice } from '../../core/durable-projection.js';
|
|
2
3
|
import { conversationUsage } from '../../core/turn-usage.js';
|
|
3
4
|
import { formatUsageCost } from '../../core/usage-format.js';
|
|
4
|
-
import {
|
|
5
|
+
import { useI18n } from '../hooks/context.js';
|
|
6
|
+
/** A run's terminal state wins over its last model response; interrupted usage never claims success.
|
|
7
|
+
* Missing lifecycle data keeps the legacy completed label. O(n) in terminal notice events. */
|
|
8
|
+
export function usageEndLabel({ usage, message, awaitingAnswer = false }, _options = {}) {
|
|
9
|
+
const status = message?.runStatus;
|
|
10
|
+
if (status === 'failed')
|
|
11
|
+
return 'Failed';
|
|
12
|
+
if (status === 'canceled') {
|
|
13
|
+
return /stopped/i.test(terminalMessageNotice({ message: message }) ?? '') ? 'Stopped' : 'Canceled';
|
|
14
|
+
}
|
|
15
|
+
if (awaitingAnswer)
|
|
16
|
+
return 'Waiting for your answer';
|
|
17
|
+
const reason = usage.stopReason ?? '';
|
|
18
|
+
if (/error|fail/i.test(reason))
|
|
19
|
+
return 'Failed';
|
|
20
|
+
return /abort|interrupt|cancel|max_tokens|max_tool_turns|length|tool_use/i.test(reason) ? 'Interrupted' : 'Done';
|
|
21
|
+
}
|
|
5
22
|
/** A standalone row has one turn; MessageList supplies the full conversation projection. */
|
|
6
23
|
export function useMessageUsage({ message, summary }, _optional = {}) {
|
|
7
24
|
return useMemo(() => summary ?? conversationUsage({ messages: [message] }).get(message.id), [message, summary]);
|
|
@@ -31,13 +48,13 @@ export function usageLineMeetsActions({ footer, terminalNotice, pending, runInPr
|
|
|
31
48
|
* Each line carries the chat's running total as of that reply, "Done · 16s · $0.39 · total $3.95" (owner,
|
|
32
49
|
* 2026-10-10: keep the total, always shown, even on the first turn). Known totals remain visible when
|
|
33
50
|
* the current turn's cost is unknown. `showTotal: false` turns the total off. */
|
|
34
|
-
export function useUsageSummary({ usage }, { showTotal = true } = {}) {
|
|
35
|
-
const t =
|
|
51
|
+
export function useUsageSummary({ usage }, { showTotal = true, label = 'Done' } = {}) {
|
|
52
|
+
const { t, locale } = useI18n();
|
|
36
53
|
const parts = [usageDuration({ durationMs: usage.durationMs }), usageCost({ usage }, { showTotal })]
|
|
37
54
|
.filter((part) => part !== undefined);
|
|
38
55
|
const text = parts.join(' · ');
|
|
39
|
-
const title = usage.outputTokens === undefined ? undefined : t('{n}
|
|
40
|
-
return { text, title, ariaLabel: title === undefined ? undefined : `${t(
|
|
56
|
+
const title = usage.outputTokens === undefined ? undefined : t('{n} output tokens', { n: usage.outputTokens.toLocaleString(locale) });
|
|
57
|
+
return { text, title, ariaLabel: title === undefined ? undefined : `${t(label)} · ${text} · ${title}` };
|
|
41
58
|
}
|
|
42
59
|
function usageDuration({ durationMs }, _options = {}) {
|
|
43
60
|
if (durationMs === undefined)
|
|
@@ -2,11 +2,12 @@ import type { ReactNode } from 'react';
|
|
|
2
2
|
import type { ToolDisplayRow } from '../tool-call-display.js';
|
|
3
3
|
/**
|
|
4
4
|
* One turn's tool cards folded into a single line ("Used 3 tools ▸") that opens on click — the
|
|
5
|
-
* `toolCallDisplay: 'collapsed'` layout
|
|
6
|
-
* `useToolCallGroup
|
|
5
|
+
* `toolCallDisplay: 'collapsed'` layout, or the compact failure notice in 'hidden'. The disclosure
|
|
6
|
+
* state lives in `useToolCallGroup`; copy uses the host's translator.
|
|
7
7
|
*/
|
|
8
|
-
export declare function ToolCallGroup<Row extends ToolDisplayRow>({ rows, renderRow }: {
|
|
8
|
+
export declare function ToolCallGroup<Row extends ToolDisplayRow>({ rows, renderRow, failureOnly }: {
|
|
9
9
|
rows: readonly Row[];
|
|
10
|
+
failureOnly?: boolean;
|
|
10
11
|
renderRow: (row: Row) => ReactNode;
|
|
11
12
|
}): import("react").JSX.Element;
|
|
12
13
|
//# sourceMappingURL=ToolCallGroup.d.ts.map
|
|
@@ -3,8 +3,10 @@ export interface ToolCallGroupState {
|
|
|
3
3
|
/** Whether the turn's tool cards are showing under the line. Starts closed. */
|
|
4
4
|
open: boolean;
|
|
5
5
|
toggle: () => void;
|
|
6
|
-
/** "Used 3 tools",
|
|
6
|
+
/** "Used 3 tools", or "Running Bash…" while a tool runs. */
|
|
7
7
|
label: string;
|
|
8
|
+
/** The failure suffix, styled independently of the tool count. */
|
|
9
|
+
failureLabel: string | undefined;
|
|
8
10
|
running: boolean;
|
|
9
11
|
failed: boolean;
|
|
10
12
|
}
|
|
@@ -15,7 +15,8 @@ export function useToolCallGroup({ rows }, _optional = {}) {
|
|
|
15
15
|
return {
|
|
16
16
|
open,
|
|
17
17
|
toggle,
|
|
18
|
-
label: toolGroupLabel({ summary, t }),
|
|
18
|
+
label: toolGroupLabel({ summary: summary.kind === 'done' ? { ...summary, failed: 0 } : summary, t }),
|
|
19
|
+
failureLabel: summary.kind === 'done' && summary.failed > 0 ? t('{count} failed', { count: summary.failed }) : undefined,
|
|
19
20
|
running: summary.kind === 'running',
|
|
20
21
|
failed: summary.kind === 'done' && summary.failed > 0,
|
|
21
22
|
};
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useToolCallGroup } from './ToolCallGroup.hooks.js';
|
|
3
|
+
import { useT } from '../hooks/context.js';
|
|
3
4
|
import { Icon } from './Icon.js';
|
|
4
5
|
/**
|
|
5
6
|
* One turn's tool cards folded into a single line ("Used 3 tools ▸") that opens on click — the
|
|
6
|
-
* `toolCallDisplay: 'collapsed'` layout
|
|
7
|
-
* `useToolCallGroup
|
|
7
|
+
* `toolCallDisplay: 'collapsed'` layout, or the compact failure notice in 'hidden'. The disclosure
|
|
8
|
+
* state lives in `useToolCallGroup`; copy uses the host's translator.
|
|
8
9
|
*/
|
|
9
|
-
export function ToolCallGroup({ rows, renderRow }) {
|
|
10
|
+
export function ToolCallGroup({ rows, renderRow, failureOnly = false }) {
|
|
10
11
|
const group = useToolCallGroup({ rows });
|
|
11
|
-
|
|
12
|
+
const t = useT();
|
|
13
|
+
return (_jsxs("div", { className: "jini-tool-group", "data-failed": group.failed ? 'true' : undefined, children: [failureOnly ? _jsxs("span", { className: "jini-tool-group__label", children: [t('A step failed'), " "] }) : null, _jsxs("button", { type: "button", className: "jini-tool-group__toggle", "aria-expanded": group.open, onClick: group.toggle, children: [_jsx("span", { className: `jini-tool-group__label${group.running ? ' shimmer-text' : ''}`, children: failureOnly ? t('Show') : group.label }), !failureOnly && group.failureLabel ? _jsxs("span", { className: "jini-tool-group__failed", children: [" \u00B7 ", group.failureLabel] }) : null, _jsx(Icon, { name: group.open ? 'chevron-down' : 'chevron-right', size: 12 })] }), group.open ? _jsx("div", { className: "jini-message-tools", children: rows.map(renderRow) }) : null] }));
|
|
12
14
|
}
|
|
13
15
|
//# sourceMappingURL=ToolCallGroup.js.map
|
|
@@ -15,11 +15,11 @@
|
|
|
15
15
|
*
|
|
16
16
|
* ## Why it resolves an outcome instead of throwing
|
|
17
17
|
*
|
|
18
|
-
* The pane acts differently on each outcome and must never drop the text: `'unsupported'`
|
|
19
|
-
*
|
|
18
|
+
* The pane acts differently on each outcome and must never drop the text: `'unsupported'` queues
|
|
19
|
+
* the whole turn; `'not-running'` queues behind the
|
|
20
20
|
* ending run; `'failed'` retains the draft for retry. A 404 means
|
|
21
21
|
* the daemon never saw this run (a host-side run such as a BYOK API turn), so it is `'unsupported'`
|
|
22
|
-
* too: no live process to steer
|
|
22
|
+
* too: no live process to steer. Accepted inputs retain their fallback copy until consumed.
|
|
23
23
|
*/
|
|
24
24
|
import { type UserTextRedactionOptions } from '../../../core/user-text-redaction.js';
|
|
25
25
|
import type { DeliverMidRunMessage } from './types.js';
|
|
@@ -15,11 +15,11 @@
|
|
|
15
15
|
*
|
|
16
16
|
* ## Why it resolves an outcome instead of throwing
|
|
17
17
|
*
|
|
18
|
-
* The pane acts differently on each outcome and must never drop the text: `'unsupported'`
|
|
19
|
-
*
|
|
18
|
+
* The pane acts differently on each outcome and must never drop the text: `'unsupported'` queues
|
|
19
|
+
* the whole turn; `'not-running'` queues behind the
|
|
20
20
|
* ending run; `'failed'` retains the draft for retry. A 404 means
|
|
21
21
|
* the daemon never saw this run (a host-side run such as a BYOK API turn), so it is `'unsupported'`
|
|
22
|
-
* too: no live process to steer
|
|
22
|
+
* too: no live process to steer. Accepted inputs retain their fallback copy until consumed.
|
|
23
23
|
*/
|
|
24
24
|
import { redactUserText } from '../../../core/user-text-redaction.js';
|
|
25
25
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
@@ -31,8 +31,11 @@ async function conflictDelivery(response) {
|
|
|
31
31
|
}
|
|
32
32
|
/** Maps the route's response onto an outcome. Only 202 is a delivery. */
|
|
33
33
|
async function deliveryFor(response) {
|
|
34
|
-
if (response.status === 202)
|
|
35
|
-
|
|
34
|
+
if (response.status === 202) {
|
|
35
|
+
const body = await response.json().catch(() => null);
|
|
36
|
+
const id = body?.id;
|
|
37
|
+
return typeof id === 'string' && id.length > 0 ? { delivery: 'delivered', id } : 'delivered';
|
|
38
|
+
}
|
|
36
39
|
if (response.status === 409)
|
|
37
40
|
return conflictDelivery(response);
|
|
38
41
|
return response.status === 404 ? 'unsupported' : 'failed';
|
|
@@ -8,6 +8,35 @@ import { readCachedAttachmentBatchId, writeCachedAttachmentBatchId, } from '../.
|
|
|
8
8
|
import { useComposer } from '../../../hooks/useComposer.js';
|
|
9
9
|
import { useConversation, } from '../../../hooks/useConversation.js';
|
|
10
10
|
import { useChatPaneWorkingDirectory, } from './useChatPaneWorkingDirectory.hooks.js';
|
|
11
|
+
/** Filter during render so a consumed receipt followed by end in the same batch cannot flush twice.
|
|
12
|
+
* Legacy host receipts can replace optimistic IDs; the accepted-input owner retains their aliases.
|
|
13
|
+
* @complexity O(e + q) time and space for receipt events and locally queued inputs.
|
|
14
|
+
*/
|
|
15
|
+
function unconsumedQueuedInputs({ inputs, messages }) {
|
|
16
|
+
const consumed = new Set();
|
|
17
|
+
for (const message of messages) {
|
|
18
|
+
for (const event of projectAcceptedInputEvents({ events: message.events ?? [] })) {
|
|
19
|
+
const receipt = acceptedInputEvent({ event });
|
|
20
|
+
if (receipt?.data.deliveryState !== 'consumed')
|
|
21
|
+
continue;
|
|
22
|
+
for (const input of [receipt.data, ...(receipt.data.inputs ?? [])]) {
|
|
23
|
+
consumed.add(input.id);
|
|
24
|
+
for (const alias of input.aliases ?? [])
|
|
25
|
+
consumed.add(alias);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return inputs.filter(input => !consumed.has(input.liveMessageId ?? input.id));
|
|
30
|
+
}
|
|
31
|
+
/** The conversation copies run events onto its messages one effect after streaming stops, so a
|
|
32
|
+
* consumed receipt can still be missing from the transcript when the send blocker clears. A
|
|
33
|
+
* live-accepted copy therefore waits until the run's own message reaches a terminal status. */
|
|
34
|
+
function awaitingRunReceipts({ inputs, messages }) {
|
|
35
|
+
if (!inputs.some(input => input.liveMessageId !== undefined))
|
|
36
|
+
return false;
|
|
37
|
+
const status = messages.at(-1)?.runStatus;
|
|
38
|
+
return status === 'queued' || status === 'running';
|
|
39
|
+
}
|
|
11
40
|
/** The stand-in prompt for a turn that carries only attachments. */
|
|
12
41
|
const ATTACHMENTS_ONLY_PROMPT = 'Review the attached file(s).';
|
|
13
42
|
/**
|
|
@@ -149,11 +178,7 @@ export function useChatPane(options) {
|
|
|
149
178
|
const [activeUploadCount, setActiveUploadCount] = useState(0);
|
|
150
179
|
const [attachmentError, setAttachmentError] = useState(null);
|
|
151
180
|
// MOVE: Keep the signal with each sanitized input; re-redaction cannot detect the original paste.
|
|
152
|
-
const [
|
|
153
|
-
const queuedTurn = useMemo(() => queuedInputs.reduce((queued, input) => appendQueuedTurn({
|
|
154
|
-
queued, next: { text: input.text, secretRedacted: input.secretRedaction?.secretRedacted ?? false,
|
|
155
|
-
count: input.secretRedaction?.count ?? 0 },
|
|
156
|
-
}), null), [queuedInputs]);
|
|
181
|
+
const [storedQueuedInputs, setQueuedInputs] = useState([]);
|
|
157
182
|
// MOVE: Uploads belong to the input that submitted them, even if a later message is queued first.
|
|
158
183
|
const queuedUploadOwnersRef = useRef(new Map());
|
|
159
184
|
// A send during upload resumes once pixels arrive; a rejected live delivery waits for run end.
|
|
@@ -221,6 +246,16 @@ export function useChatPane(options) {
|
|
|
221
246
|
validateAttachments: options.validateAttachments,
|
|
222
247
|
} }));
|
|
223
248
|
const activity = resolveChatPaneActivity(selectedAgent, conversation);
|
|
249
|
+
const queuedInputs = useMemo(() => unconsumedQueuedInputs({ inputs: storedQueuedInputs, messages: conversation.messages }), [storedQueuedInputs, conversation.messages]);
|
|
250
|
+
const queuedTurn = useMemo(() => queuedInputs.reduce((queued, input) => appendQueuedTurn({
|
|
251
|
+
queued, next: { text: input.text, secretRedacted: input.secretRedaction?.secretRedacted ?? false,
|
|
252
|
+
count: input.secretRedaction?.count ?? 0 },
|
|
253
|
+
}), null), [queuedInputs]);
|
|
254
|
+
// Release consumed text/files locally as well as hiding them; next-turn fallback uses this same projection.
|
|
255
|
+
useEffect(() => {
|
|
256
|
+
if (queuedInputs.length !== storedQueuedInputs.length)
|
|
257
|
+
setQueuedInputs(queuedInputs);
|
|
258
|
+
}, [queuedInputs, storedQueuedInputs.length]);
|
|
224
259
|
useEffect(() => {
|
|
225
260
|
options.onActivityChange?.(activity);
|
|
226
261
|
}, [activity, options.onActivityChange]);
|
|
@@ -363,7 +398,7 @@ export function useChatPane(options) {
|
|
|
363
398
|
}
|
|
364
399
|
}, [composer, options.conversationId, options.uploadAttachments]);
|
|
365
400
|
/** Acceptance is the only writer of local recall and optimistic in-run bubbles. */
|
|
366
|
-
const recordAcceptedInput = useCallback(({ inputs, bubble },
|
|
401
|
+
const recordAcceptedInput = useCallback(({ inputs, bubble }, { receipt = 'optimistic' } = {}) => {
|
|
367
402
|
composer.history?.record({ inputs });
|
|
368
403
|
submittedComposerRef.current = composer;
|
|
369
404
|
if (bubble === undefined)
|
|
@@ -372,8 +407,9 @@ export function useChatPane(options) {
|
|
|
372
407
|
const last = messages.at(-1);
|
|
373
408
|
if (last?.role !== 'assistant')
|
|
374
409
|
return messages;
|
|
375
|
-
const event = acceptedInputUserEvent({ input: { id: inputs[0].id, text: bubble.text
|
|
376
|
-
|
|
410
|
+
const event = acceptedInputUserEvent({ input: { id: inputs[0].id, text: bubble.text,
|
|
411
|
+
...(inputs[0].deliveryState ? { deliveryState: inputs[0].deliveryState } : {}) },
|
|
412
|
+
inputs, ...bubble }, { receipt });
|
|
377
413
|
return [...messages.slice(0, -1), { ...last,
|
|
378
414
|
events: projectAcceptedInputEvents({ events: [...(last.events ?? []), event] }) }];
|
|
379
415
|
});
|
|
@@ -495,24 +531,37 @@ export function useChatPane(options) {
|
|
|
495
531
|
if (inputs === undefined && latestDraftRef.current === submittedDraft)
|
|
496
532
|
latestComposerRef.current.setDraft('');
|
|
497
533
|
}, []);
|
|
534
|
+
/** HTTP acceptance retains fallback ownership; only an actual consumption receipt removes it. */
|
|
535
|
+
const retainAcceptedMidRunInput = useCallback(({ outcome, inputs, turn, attachments, afterInputId }, _options = {}) => {
|
|
536
|
+
const accepted = inputs ?? [{ id: createChatLocalId({}), text: turn.text, attachments,
|
|
537
|
+
...(turn.secretRedacted ? { secretRedaction: { secretRedacted: true, count: turn.count } } : {}) }];
|
|
538
|
+
const id = typeof outcome === 'object' ? outcome.id : accepted[0].id;
|
|
539
|
+
// A known HTTP id is already authoritative. Do not let equal text reconcile an unrelated
|
|
540
|
+
// wire receipt into this bubble; legacy hosts alone still need optimistic reconciliation.
|
|
541
|
+
const retained = accepted.map((input, index) => ({ ...input,
|
|
542
|
+
...(index === 0 ? { id, aliases: [...(input.aliases ?? []), input.id] } : {}),
|
|
543
|
+
liveMessageId: id, deliveryState: 'queued',
|
|
544
|
+
}));
|
|
545
|
+
queuedConversationIdRef.current = options.conversationId;
|
|
546
|
+
setQueuedInputs(queued => [...queued, ...retained]);
|
|
547
|
+
recordAcceptedInput({ inputs: retained,
|
|
548
|
+
bubble: { text: turn.text, ...(afterInputId === undefined ? {} : { afterInputId }) } }, typeof outcome === 'object' ? { receipt: 'accepted' } : {});
|
|
549
|
+
}, [options.conversationId, recordAcceptedInput]);
|
|
498
550
|
const finishMidRunSend = useCallback(({ outcome, inputs, turn, attachments, afterInputId, submittedDraft }, _options = {}) => {
|
|
499
551
|
if (outcome === 'failed') {
|
|
500
552
|
restoreQueuedDraft({ inputs, text: turn.text, attachments });
|
|
501
553
|
setTypedAnswerNotice('failed');
|
|
502
554
|
return;
|
|
503
555
|
}
|
|
504
|
-
if (outcome === 'delivered') {
|
|
505
|
-
|
|
506
|
-
bubble: { text: turn.text, ...(afterInputId === undefined ? {} : { afterInputId }) } });
|
|
556
|
+
if (outcome === 'delivered' || typeof outcome === 'object') {
|
|
557
|
+
retainAcceptedMidRunInput({ outcome, inputs, turn, attachments, afterInputId });
|
|
507
558
|
clearDeliveredDraft({ inputs, submittedDraft });
|
|
508
559
|
consumeDeliveredAttachments({ attachments });
|
|
509
560
|
return;
|
|
510
561
|
}
|
|
511
562
|
enqueueTurn({ turn, attachments, ...(inputs === undefined ? {} : { inputs }) });
|
|
512
563
|
clearDeliveredDraft({ inputs, submittedDraft });
|
|
513
|
-
|
|
514
|
-
conversation.cancel();
|
|
515
|
-
}, [conversation.cancel, enqueueTurn, recordAcceptedInput, clearDeliveredDraft, consumeDeliveredAttachments, restoreQueuedDraft]);
|
|
564
|
+
}, [enqueueTurn, retainAcceptedMidRunInput, clearDeliveredDraft, consumeDeliveredAttachments, restoreQueuedDraft]);
|
|
516
565
|
// A delivered steer belongs to the existing run; unsupported turns use the same accepted queue.
|
|
517
566
|
// A failed delivery keeps the working draft so retry cannot silently become another run.
|
|
518
567
|
const sendIntoRunningTurn = useCallback(async ({ deliver, runId, turn, inputs, attachments = composer.attachments }, _options = {}) => {
|
|
@@ -544,7 +593,7 @@ export function useChatPane(options) {
|
|
|
544
593
|
if (uploadPending)
|
|
545
594
|
queuedForUploadRef.current = queuedTurn === null || queuedForUploadRef.current;
|
|
546
595
|
// MOVE: The existing queue owns its files; a later steer must join it to preserve send order.
|
|
547
|
-
const target = uploadPending ||
|
|
596
|
+
const target = uploadPending || queuedInputs.some(input => input.liveMessageId === undefined) ? null : findMidRunTarget({
|
|
548
597
|
deliver: options.deliverMidRunMessage,
|
|
549
598
|
blocker: sendBlocker,
|
|
550
599
|
attachmentCount: composer.attachments.length,
|
|
@@ -565,6 +614,7 @@ export function useChatPane(options) {
|
|
|
565
614
|
enqueueTurn,
|
|
566
615
|
options.deliverMidRunMessage,
|
|
567
616
|
queuedTurn,
|
|
617
|
+
queuedInputs,
|
|
568
618
|
sendBlocker,
|
|
569
619
|
sendIntoRunningTurn,
|
|
570
620
|
sendPrompt,
|
|
@@ -659,7 +709,7 @@ export function useChatPane(options) {
|
|
|
659
709
|
deliver: options.deliverMidRunMessage,
|
|
660
710
|
blocker: sendBlocker, attachmentCount: queuedInputs.flatMap(input => input.attachments).length, messages: conversation.messages,
|
|
661
711
|
}) : null;
|
|
662
|
-
if (sendBlocker !== null
|
|
712
|
+
if (target === null && (sendBlocker !== null || awaitingRunReceipts({ inputs: queuedInputs, messages: conversation.messages })))
|
|
663
713
|
return;
|
|
664
714
|
queuedForUploadRef.current = false;
|
|
665
715
|
const inputs = queuedInputs;
|
|
@@ -712,7 +762,9 @@ export function useChatPane(options) {
|
|
|
712
762
|
send,
|
|
713
763
|
sendPrompt,
|
|
714
764
|
queuedPrompt: queuedTurn?.text ?? null,
|
|
715
|
-
|
|
765
|
+
// Accepted live messages already have queued bubbles in the transcript. Their private fallback
|
|
766
|
+
// copy must not create a second composer row whose Cancel button cannot revoke daemon input.
|
|
767
|
+
queuedMessages: queuedInputs.filter(input => input.liveMessageId === undefined),
|
|
716
768
|
cancelQueuedMessage,
|
|
717
769
|
cancelQueued,
|
|
718
770
|
typedAnswerNotice,
|
|
@@ -176,11 +176,16 @@ export type DeliverTypedAnswer = ((input: {
|
|
|
176
176
|
};
|
|
177
177
|
/**
|
|
178
178
|
* What happened to a message sent while a run streams (see {@link ChatPaneProps.deliverMidRunMessage}):
|
|
179
|
-
* `'delivered'` — the live agent
|
|
179
|
+
* `{delivery:'delivered', id}` — accepted by the live agent; keep the local copy until an id-matched
|
|
180
|
+
* consumed event. Legacy hosts may still return `'delivered'`; their receipts reconcile optimistic ids.
|
|
181
|
+
* `'unsupported'` — this agent cannot
|
|
180
182
|
* take input mid-run (it reads one prompt per turn); `'not-running'` — the run has no live process
|
|
181
183
|
* any more (it is ending); `'failed'` — the host could not tell (network error, unexpected status).
|
|
182
184
|
*/
|
|
183
|
-
export type MidRunMessageDelivery = 'delivered' | 'unsupported' | 'not-running' | 'failed'
|
|
185
|
+
export type MidRunMessageDelivery = 'delivered' | 'unsupported' | 'not-running' | 'failed' | {
|
|
186
|
+
readonly delivery: 'delivered';
|
|
187
|
+
readonly id: string;
|
|
188
|
+
};
|
|
184
189
|
/** Delivers a complete turn to the live agent; unsupported attachments must reject the whole turn. */
|
|
185
190
|
export type DeliverMidRunMessage = (input: {
|
|
186
191
|
runId: string;
|
|
@@ -312,7 +317,8 @@ export interface ChatPaneProps extends UserTextRedactionOptions {
|
|
|
312
317
|
/**
|
|
313
318
|
* Transcript display choices (a host's chat settings, 2026-10-11), passed straight to
|
|
314
319
|
* `MessageList`: how tool calls show (`'show'` default, `'collapsed'` one line per turn, `'hidden'`
|
|
315
|
-
*
|
|
320
|
+
* hides recovered calls and folds unrecovered failures — see `tool-call-display.ts`), whether the
|
|
321
|
+
* "Done · time · cost" lines show
|
|
316
322
|
* (default `true`), and whether the model's reasoning shows (default `false`).
|
|
317
323
|
*/
|
|
318
324
|
toolCallDisplay?: ToolCallDisplay;
|