@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
|
@@ -7,7 +7,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
7
7
|
* for the `chat-fab`/`chat-fab-open` classes.
|
|
8
8
|
*/
|
|
9
9
|
import { useChatFabDrag } from '../hooks/useChatFabDrag.js';
|
|
10
|
-
export function ChatFab({ open, onToggle, label = 'chat' }) {
|
|
10
|
+
export function ChatFab({ open, onToggle, label = 'chat', 'aria-label': accessibleLabel, 'aria-controls': controls, 'aria-haspopup': popup }) {
|
|
11
11
|
const { buttonRef, position, onPointerDown, onPointerMove, onPointerUp, onPointerCancel } = useChatFabDrag({ onClick: onToggle });
|
|
12
12
|
return (_jsx("button", { ref: buttonRef, type: "button", className: `chat-fab${open ? ' chat-fab-open' : ''}`,
|
|
13
13
|
// Not `onClick`: the pointer gesture already decides click-versus-drag, and keeping both
|
|
@@ -19,6 +19,6 @@ export function ChatFab({ open, onToggle, label = 'chat' }) {
|
|
|
19
19
|
event.preventDefault();
|
|
20
20
|
onToggle();
|
|
21
21
|
}
|
|
22
|
-
}, style: position === null ? undefined : { left: position.x, top: position.y, right: 'auto', bottom: 'auto' }, "aria-expanded": open, "aria-label": open ? `Hide ${label}` : `Show ${label}
|
|
22
|
+
}, style: position === null ? undefined : { left: position.x, top: position.y, right: 'auto', bottom: 'auto' }, "aria-expanded": open, "aria-label": accessibleLabel ?? (open ? `Hide ${label}` : `Show ${label}`), "aria-controls": controls, "aria-haspopup": popup, children: _jsx("span", { "aria-hidden": "true", children: open ? '×' : '💬' }) }));
|
|
23
23
|
}
|
|
24
24
|
//# sourceMappingURL=ChatFab.js.map
|
|
@@ -26,6 +26,13 @@ export interface ConversationListProps {
|
|
|
26
26
|
/** Disables the "New" action — e.g. while the current conversation is still empty. */
|
|
27
27
|
createDisabled?: boolean;
|
|
28
28
|
emptyState?: React.ReactNode;
|
|
29
|
+
/** Replaces the trigger's accessible name (default "Conversations — {active title}"). */
|
|
30
|
+
triggerLabel?: string;
|
|
31
|
+
/**
|
|
32
|
+
* `false` drops the trigger's native `title` tooltip, for a host that draws its own tooltip over
|
|
33
|
+
* the icon (the browser's dark tip would otherwise show on top of it). Defaults to true.
|
|
34
|
+
*/
|
|
35
|
+
nativeTitle?: boolean;
|
|
29
36
|
}
|
|
30
|
-
export declare function ConversationList({ conversations, activeConversationId, onSelect, onCreate, onDelete, onRename, onSearch, confirmDelete, createDisabled, emptyState, }: ConversationListProps): import("react").JSX.Element;
|
|
37
|
+
export declare function ConversationList({ conversations: savedConversations, activeConversationId, onSelect, onCreate, onDelete, onRename, onSearch, confirmDelete, createDisabled, emptyState, triggerLabel, nativeTitle, }: ConversationListProps): import("react").JSX.Element;
|
|
31
38
|
//# sourceMappingURL=ConversationList.d.ts.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ConversationListItem } from './ConversationList.js';
|
|
2
|
+
/** Project saved/search titles for presentation without renaming durable conversations.
|
|
3
|
+
* The switcher has no prompt or title provenance, so keep wording and casing while stripping
|
|
4
|
+
* markup through the same owner as the panel title. Caller arrays and objects stay untouched.
|
|
5
|
+
* @param input.conversations - The loaded conversations.
|
|
6
|
+
* @param input.serverResults - Optional server search results, or null before a response.
|
|
7
|
+
* @returns Plain-text display lists and raw titles for lossless editing. Hosts ellipsize rows in CSS.
|
|
8
|
+
* @complexity O(n + sum(t²)) worst-case cleanup time; O(n + sum(t)) space for n items of length t.
|
|
9
|
+
*/
|
|
10
|
+
export declare function useConversationListTitles({ conversations, serverResults }: {
|
|
11
|
+
conversations: ConversationListItem[];
|
|
12
|
+
serverResults: ConversationListItem[] | null;
|
|
13
|
+
}, _options?: {}): {
|
|
14
|
+
conversations: ConversationListItem[];
|
|
15
|
+
serverResults: ConversationListItem[] | null;
|
|
16
|
+
rawTitles: ReadonlyMap<string, string | null>;
|
|
17
|
+
};
|
|
18
|
+
//# sourceMappingURL=ConversationList.hooks.d.ts.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { stripConversationTitleMarkdown } from '../../core/persistence/title.js';
|
|
3
|
+
/** Project saved/search titles for presentation without renaming durable conversations.
|
|
4
|
+
* The switcher has no prompt or title provenance, so keep wording and casing while stripping
|
|
5
|
+
* markup through the same owner as the panel title. Caller arrays and objects stay untouched.
|
|
6
|
+
* @param input.conversations - The loaded conversations.
|
|
7
|
+
* @param input.serverResults - Optional server search results, or null before a response.
|
|
8
|
+
* @returns Plain-text display lists and raw titles for lossless editing. Hosts ellipsize rows in CSS.
|
|
9
|
+
* @complexity O(n + sum(t²)) worst-case cleanup time; O(n + sum(t)) space for n items of length t.
|
|
10
|
+
*/
|
|
11
|
+
export function useConversationListTitles({ conversations, serverResults }, _options = {}) {
|
|
12
|
+
return useMemo(() => {
|
|
13
|
+
const rawTitles = new Map();
|
|
14
|
+
const display = (items) => items.map(item => ({
|
|
15
|
+
...item, title: item.title === null ? null : stripConversationTitleMarkdown({ text: item.title }) || null,
|
|
16
|
+
}));
|
|
17
|
+
// Search results can contain unloaded rows or newer titles for loaded ids; visible results win.
|
|
18
|
+
for (const item of conversations)
|
|
19
|
+
rawTitles.set(item.id, item.title);
|
|
20
|
+
for (const item of serverResults ?? [])
|
|
21
|
+
rawTitles.set(item.id, item.title);
|
|
22
|
+
return { conversations: display(conversations), serverResults: serverResults === null ? null : display(serverResults), rawTitles };
|
|
23
|
+
}, [conversations, serverResults]);
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=ConversationList.hooks.js.map
|
|
@@ -24,6 +24,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
24
24
|
import { useCallback, useDeferredValue, useEffect, useMemo, useRef, useState } from 'react';
|
|
25
25
|
import { useT } from '../hooks/context.js';
|
|
26
26
|
import { Icon } from './Icon.js';
|
|
27
|
+
import { useConversationListTitles } from './ConversationList.hooks.js';
|
|
28
|
+
import { conversationTriggerAttributes } from './ConversationList.rules.js';
|
|
27
29
|
/**
|
|
28
30
|
* How long a click waits to see whether it is really the first half of a double-click.
|
|
29
31
|
*
|
|
@@ -63,13 +65,15 @@ function relativeTime(updatedAt, now) {
|
|
|
63
65
|
return `${hours}h ago`;
|
|
64
66
|
return `${Math.round(hours / 24)}d ago`;
|
|
65
67
|
}
|
|
66
|
-
export function ConversationList({ conversations, activeConversationId, onSelect, onCreate, onDelete, onRename, onSearch, confirmDelete, createDisabled = false, emptyState, }) {
|
|
68
|
+
export function ConversationList({ conversations: savedConversations, activeConversationId, onSelect, onCreate, onDelete, onRename, onSearch, confirmDelete, createDisabled = false, emptyState, triggerLabel, nativeTitle = true, }) {
|
|
67
69
|
const t = useT();
|
|
68
70
|
const [open, setOpen] = useState(false);
|
|
69
71
|
const [query, setQuery] = useState('');
|
|
70
|
-
const [
|
|
72
|
+
const [savedServerResults, setServerResults] = useState(null);
|
|
73
|
+
const { conversations, serverResults, rawTitles } = useConversationListTitles({ conversations: savedConversations, serverResults: savedServerResults });
|
|
71
74
|
const [editingId, setEditingId] = useState(null);
|
|
72
75
|
const [draft, setDraft] = useState('');
|
|
76
|
+
const originalDraftRef = useRef('');
|
|
73
77
|
const rootRef = useRef(null);
|
|
74
78
|
const deferredQuery = useDeferredValue(query);
|
|
75
79
|
/**
|
|
@@ -169,7 +173,8 @@ export function ConversationList({ conversations, activeConversationId, onSelect
|
|
|
169
173
|
// No optimistic title update happens here — the row keeps rendering `item.title` from
|
|
170
174
|
// props until the host's own state updates, so a rejection cannot leave the UI showing a
|
|
171
175
|
// title the server didn't accept. Reporting the failure is what closes the remaining gap.
|
|
172
|
-
|
|
176
|
+
// Merely opening and leaving the editor must not normalize a deliberately saved title.
|
|
177
|
+
if (next && draft !== originalDraftRef.current)
|
|
173
178
|
runConversationListHostEffect('onRename', () => onRename(id, next));
|
|
174
179
|
}, [draft, onRename]);
|
|
175
180
|
const requestDelete = useCallback((item) => {
|
|
@@ -188,7 +193,7 @@ export function ConversationList({ conversations, activeConversationId, onSelect
|
|
|
188
193
|
});
|
|
189
194
|
}, [confirmDelete, onDelete, t]);
|
|
190
195
|
const active = conversations.find((c) => c.id === activeConversationId) ?? null;
|
|
191
|
-
return (_jsxs("div", { className: "jini-conv", ref: rootRef, children: [_jsx("button", { type: "button", className: `jini-conv-trigger${open ? ' jini-conv-trigger-open' : ''}`, "aria-haspopup": "menu", "aria-expanded": open,
|
|
196
|
+
return (_jsxs("div", { className: "jini-conv", ref: rootRef, children: [_jsx("button", { type: "button", className: `jini-conv-trigger${open ? ' jini-conv-trigger-open' : ''}`, "aria-haspopup": "menu", "aria-expanded": open, ...conversationTriggerAttributes({ activeTitle: active?.title, t }, { label: triggerLabel, nativeTitle }), "data-testid": "conversation-trigger", onClick: () => setOpen((v) => !v), children: _jsx(Icon, { name: "comment", size: 16 }) }), open ? (_jsxs("div", { className: "jini-conv-menu", role: "menu", "data-testid": "conversation-menu", children: [_jsxs("div", { className: "jini-conv-head", children: [_jsxs("span", { className: "jini-conv-heading", children: [t('Conversations'), _jsx("span", { className: "jini-conv-count", "data-testid": "conversation-count", children: conversations.length })] }), _jsxs("button", { type: "button", className: "jini-conv-new", disabled: createDisabled, "data-testid": "conversation-new", onClick: () => {
|
|
192
197
|
if (createDisabled)
|
|
193
198
|
return;
|
|
194
199
|
// No optimistic row is inserted — the list still comes straight from props — so a
|
|
@@ -211,8 +216,10 @@ export function ConversationList({ conversations, activeConversationId, onSelect
|
|
|
211
216
|
}, DOUBLE_CLICK_GRACE_MS);
|
|
212
217
|
}, onDoubleClick: () => {
|
|
213
218
|
cancelPendingSelect();
|
|
219
|
+
// Snapshot the raw seed: a host refresh during editing is not a user edit.
|
|
220
|
+
originalDraftRef.current = rawTitles.get(item.id) ?? '';
|
|
214
221
|
setEditingId(item.id);
|
|
215
|
-
setDraft(
|
|
222
|
+
setDraft(originalDraftRef.current);
|
|
216
223
|
}, children: [_jsx("span", { className: "jini-conv-item-title", children: item.title ?? t('Untitled') }), _jsxs("span", { className: "jini-conv-item-meta", children: [item.messageCount === undefined ? '' : `${item.messageCount} msg · `, relativeTime(item.updatedAt, now)] })] })), _jsx("button", { type: "button", className: "jini-conv-item-delete", "aria-label": t('Delete conversation'), "data-testid": `conversation-delete-${item.id}`, onClick: (event) => {
|
|
217
224
|
event.stopPropagation();
|
|
218
225
|
requestDelete(item);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Accessible name and optional native tooltip for the conversation switcher's icon-only trigger. */
|
|
2
|
+
export interface ConversationTriggerAttributes {
|
|
3
|
+
'aria-label': string;
|
|
4
|
+
title?: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Resolves the switcher trigger's `aria-label` and native `title`. By default both name the active
|
|
8
|
+
* conversation. A host that draws its own tooltip over the icon passes `nativeTitle: false`, so the
|
|
9
|
+
* browser's dark native tip does not show on top of it, and `label` to rename the control (for
|
|
10
|
+
* example "Chats") instead of stripping the attribute at runtime.
|
|
11
|
+
* @param input.activeTitle - The active conversation's display title, or null/undefined when none.
|
|
12
|
+
* @param input.t - The package translator.
|
|
13
|
+
* @param options.label - Replaces the default accessible name (and the native title, when kept).
|
|
14
|
+
* @param options.nativeTitle - `false` omits the native `title` attribute. Defaults to true.
|
|
15
|
+
* @returns Props to spread onto the trigger button.
|
|
16
|
+
* @complexity O(1).
|
|
17
|
+
*/
|
|
18
|
+
export declare function conversationTriggerAttributes({ activeTitle, t }: {
|
|
19
|
+
activeTitle: string | null | undefined;
|
|
20
|
+
t: (key: string) => string;
|
|
21
|
+
}, { label, nativeTitle }?: {
|
|
22
|
+
label?: string | undefined;
|
|
23
|
+
nativeTitle?: boolean | undefined;
|
|
24
|
+
}): ConversationTriggerAttributes;
|
|
25
|
+
//# sourceMappingURL=ConversationList.rules.d.ts.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves the switcher trigger's `aria-label` and native `title`. By default both name the active
|
|
3
|
+
* conversation. A host that draws its own tooltip over the icon passes `nativeTitle: false`, so the
|
|
4
|
+
* browser's dark native tip does not show on top of it, and `label` to rename the control (for
|
|
5
|
+
* example "Chats") instead of stripping the attribute at runtime.
|
|
6
|
+
* @param input.activeTitle - The active conversation's display title, or null/undefined when none.
|
|
7
|
+
* @param input.t - The package translator.
|
|
8
|
+
* @param options.label - Replaces the default accessible name (and the native title, when kept).
|
|
9
|
+
* @param options.nativeTitle - `false` omits the native `title` attribute. Defaults to true.
|
|
10
|
+
* @returns Props to spread onto the trigger button.
|
|
11
|
+
* @complexity O(1).
|
|
12
|
+
*/
|
|
13
|
+
export function conversationTriggerAttributes({ activeTitle, t }, { label, nativeTitle = true } = {}) {
|
|
14
|
+
const ariaLabel = label ?? (activeTitle ? `${t('Conversations')} — ${activeTitle}` : t('Conversations'));
|
|
15
|
+
if (!nativeTitle)
|
|
16
|
+
return { 'aria-label': ariaLabel };
|
|
17
|
+
return { 'aria-label': ariaLabel, title: label ?? activeTitle ?? t('Conversations') };
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=ConversationList.rules.js.map
|
|
@@ -30,7 +30,7 @@ export interface McpUiSurfaceCardProps extends ExtEventRenderProps {
|
|
|
30
30
|
expiryClock?: SurfaceExpiryClock;
|
|
31
31
|
}
|
|
32
32
|
/** Registered against `ext-event-renderer-registry.ts`'s `'mcp-ui'` name — see module doc. */
|
|
33
|
-
export declare function McpUiSurfaceCard({ events, sandboxProxyUrl, onToolCall, onOpenLink, maxHeight, call, runStreaming, expiryClock }: McpUiSurfaceCardProps): import("react").JSX.Element;
|
|
33
|
+
export declare function McpUiSurfaceCard({ events, sandboxProxyUrl, onToolCall, onOpenLink, maxHeight, call, runStreaming, expiryClock, answered, onAnswered }: McpUiSurfaceCardProps): import("react").JSX.Element;
|
|
34
34
|
/**
|
|
35
35
|
* The ext-event slot key for MCP-UI: one slot per `ui://` URI. Each resource is its own card, so a
|
|
36
36
|
* run's second card renders where it arrived instead of inside the first card's slot, while a
|
|
@@ -66,7 +66,7 @@ import { McpUiHost, parseUIResource, readActionPlan, readExpiresAt, readPreferre
|
|
|
66
66
|
import { registerExtEventRenderer } from '../ext-event-renderer-registry.js';
|
|
67
67
|
import { useT } from '../hooks/context.js';
|
|
68
68
|
import { useSurfaceExpiry } from '../hooks/useSurfaceExpiry.js';
|
|
69
|
-
import { closedSurfaceLabel } from './mcp-ui-surface-state.js';
|
|
69
|
+
import { closedSurfaceLabel, reportDeliveredAnswer } from './mcp-ui-surface-state.js';
|
|
70
70
|
/** The `kind: 'ext'` event name this renderer claims. */
|
|
71
71
|
export const MCP_UI_EXT_EVENT_NAME = 'mcp-ui';
|
|
72
72
|
/** Collapses the event stream to the newest resource per `ui://` URI, in first-appearance order. */
|
|
@@ -120,18 +120,22 @@ function ClosedSurface({ call, label }) {
|
|
|
120
120
|
return (_jsx("div", { className: "mcpui-surface-card mcpui-surface-card-closed", role: "status", children: typeof title === 'string' && title.trim() !== '' ? `${title}: ${label}` : label }));
|
|
121
121
|
}
|
|
122
122
|
/** Registered against `ext-event-renderer-registry.ts`'s `'mcp-ui'` name — see module doc. */
|
|
123
|
-
export function McpUiSurfaceCard({ events, sandboxProxyUrl, onToolCall, onOpenLink, maxHeight, call, runStreaming, expiryClock }) {
|
|
123
|
+
export function McpUiSurfaceCard({ events, sandboxProxyUrl, onToolCall, onOpenLink, maxHeight, call, runStreaming, expiryClock, answered, onAnswered }) {
|
|
124
124
|
const t = useT();
|
|
125
125
|
const resources = useMemo(() => latestResourcesByUri(events), [events]);
|
|
126
126
|
// One card per slot is the registered shape (`mcpUiSurfaceSlotKey`), so `call` belongs to exactly
|
|
127
127
|
// that card. A group holding several URIs has no single call to judge them by, and stays live.
|
|
128
128
|
const single = resources.length === 1 ? resources[0] : undefined;
|
|
129
|
-
|
|
129
|
+
// An answered card has no deadline left: its countdown goes, and the deadline passing while the
|
|
130
|
+
// answer's work still runs must not relabel it "This question expired".
|
|
131
|
+
const expiry = useSurfaceExpiry({ expiresAtMs: single === undefined || answered === true ? undefined : readExpiresAt(single) }, expiryClock === undefined ? {} : { clock: expiryClock });
|
|
130
132
|
const closed = single === undefined
|
|
131
133
|
? undefined
|
|
132
134
|
: closedSurfaceLabel({ t, call, runStreaming, documentsForUri: events.length, expired: expiry?.expired === true });
|
|
133
135
|
if (closed !== undefined)
|
|
134
136
|
return _jsx(ClosedSurface, { call: call, label: closed });
|
|
137
|
+
// Reports the person's delivered answer to the row, which stops the "Waiting for your answer" line.
|
|
138
|
+
const toolCall = onToolCall === undefined || onAnswered === undefined ? onToolCall : reportDeliveredAnswer({ onToolCall, onAnswered });
|
|
135
139
|
if (resources.length === 0) {
|
|
136
140
|
// Visible rather than silent: an `ext` event named `mcp-ui` that carries no parseable resource
|
|
137
141
|
// is a producer bug, and a transcript that renders nothing for it hides the bug behind what
|
|
@@ -150,7 +154,7 @@ export function McpUiSurfaceCard({ events, sandboxProxyUrl, onToolCall, onOpenLi
|
|
|
150
154
|
// the previous iframe session, so the handshake never re-ran and the frame kept serving
|
|
151
155
|
// the OLD document's state. The URI stays in the key because it is real identity; the
|
|
152
156
|
// length was never a substitute for content.
|
|
153
|
-
sessionKey: `${resource.resource.uri}:${resource.resource.text}`, ...(
|
|
157
|
+
sessionKey: `${resource.resource.uri}:${resource.resource.text}`, ...(toolCall === undefined ? {} : { onToolCall: toolCall }), ...(onOpenLink === undefined ? {} : { onOpenLink }), ...(height === undefined || Number.isNaN(height) ? {} : { initialHeight: height }), ...(maxHeight === undefined ? {} : { maxHeight }) }), plan !== undefined && _jsx(PendingSurfaceMirror, { uri: resource.resource.uri, plan: plan, t: t })] }, resource.resource.uri));
|
|
154
158
|
}), expiry?.expired === false && (_jsx("p", { className: "mcpui-surface-expiry", role: "timer", children: t('Expires in {time}', { time: expiry.remaining }) }))] }));
|
|
155
159
|
}
|
|
156
160
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ChatMessage } from '../../core/index.js';
|
|
2
2
|
import { type MessageRowProps } from './MessageRow.js';
|
|
3
|
-
export interface MessageListProps extends Pick<MessageRowProps, 'projectFileNames' | 'onRequestOpenFile' | 'renderAttachment'> {
|
|
3
|
+
export interface MessageListProps extends Pick<MessageRowProps, 'projectFileNames' | 'onRequestOpenFile' | 'renderAttachment' | 'showUsageTotal' | 'showUsageLine' | 'toolCallDisplay' | 'showThinking'> {
|
|
4
4
|
messages: ChatMessage[];
|
|
5
5
|
isStreaming?: boolean;
|
|
6
6
|
/** Mirrors `useConversation().scrollIntent` — when `true`, this component scrolls to the bottom on mount/update. */
|
|
@@ -10,6 +10,10 @@ export interface MessageListAutoScroll {
|
|
|
10
10
|
containerRef: RefObject<HTMLDivElement | null>;
|
|
11
11
|
onScroll: (event: UIEvent<HTMLElement>) => void;
|
|
12
12
|
}
|
|
13
|
+
/** Recompute costs once per immutable transcript snapshot, including saved history on reload. */
|
|
14
|
+
export declare function useConversationUsage({ messages }: {
|
|
15
|
+
messages: readonly ChatMessage[];
|
|
16
|
+
}, _optional?: {}): Map<string, import("../../core/turn-usage.js").ChatUsageSummary>;
|
|
13
17
|
export declare function useMessageListAutoScroll({ messages, scrollIntent }: {
|
|
14
18
|
messages: ChatMessage[];
|
|
15
19
|
scrollIntent: boolean;
|
|
@@ -4,8 +4,13 @@
|
|
|
4
4
|
* lives here (presentational), not in the headless `useConversation` hook, per that hook's own
|
|
5
5
|
* "transport-agnostic hooks never touch DOM" contract.
|
|
6
6
|
*/
|
|
7
|
-
import { useEffect, useRef } from 'react';
|
|
7
|
+
import { useEffect, useMemo, useRef } from 'react';
|
|
8
|
+
import { conversationUsage } from '../../core/turn-usage.js';
|
|
8
9
|
import { nextStickToBottom, startsNewTurn } from './MessageList.rules.js';
|
|
10
|
+
/** Recompute costs once per immutable transcript snapshot, including saved history on reload. */
|
|
11
|
+
export function useConversationUsage({ messages }, _optional = {}) {
|
|
12
|
+
return useMemo(() => conversationUsage({ messages }), [messages]);
|
|
13
|
+
}
|
|
9
14
|
export function useMessageListAutoScroll({ messages, scrollIntent }, { onScrolled } = {}) {
|
|
10
15
|
const containerRef = useRef(null);
|
|
11
16
|
// Whether the transcript was scrolled to its bottom as of the last user scroll or programmatic
|
|
@@ -13,7 +13,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
13
13
|
import { memo } from 'react';
|
|
14
14
|
import { isTerminalRunStatus } from '../../core/index.js';
|
|
15
15
|
import { useT } from '../hooks/context.js';
|
|
16
|
-
import { useMessageListAutoScroll } from './MessageList.hooks.js';
|
|
16
|
+
import { useMessageListAutoScroll, useConversationUsage } from './MessageList.hooks.js';
|
|
17
17
|
import { MessageRow } from './MessageRow.js';
|
|
18
18
|
/**
|
|
19
19
|
* Memoized: the pane re-renders on every composer keystroke, and re-rendering a long transcript
|
|
@@ -21,13 +21,14 @@ import { MessageRow } from './MessageRow.js';
|
|
|
21
21
|
* 2026-10-08). Callers must pass stable props — `ChatPane` memoizes `pendingPrompt` and
|
|
22
22
|
* `projectFileNames` for exactly this.
|
|
23
23
|
*/
|
|
24
|
-
export const MessageList = memo(function MessageList({ messages, isStreaming = false, scrollIntent = false, onScrolled, activeQuestionFormMessageId = null, questionFormSubmittedAnswersByMessageId, onQuestionFormSubmit, projectFileNames, onRequestOpenFile, renderAttachment, pendingPrompt = null, }) {
|
|
24
|
+
export const MessageList = memo(function MessageList({ messages, isStreaming = false, scrollIntent = false, onScrolled, activeQuestionFormMessageId = null, questionFormSubmittedAnswersByMessageId, onQuestionFormSubmit, projectFileNames, onRequestOpenFile, renderAttachment, showUsageTotal = true, showUsageLine = true, toolCallDisplay = 'show', showThinking = false, pendingPrompt = null, }) {
|
|
25
25
|
const t = useT();
|
|
26
|
+
const usageByMessage = useConversationUsage({ messages });
|
|
26
27
|
const { containerRef, onScroll } = useMessageListAutoScroll({ messages, scrollIntent }, onScrolled !== undefined ? { onScrolled } : {});
|
|
27
28
|
return (_jsxs("div", { className: "jini-message-list", ref: containerRef, onScroll: onScroll, "data-agent-element": "chat-transcript", "data-agent-role": "list", "data-agent-label": "The conversation transcript, oldest message first", children: [messages.map((message) => {
|
|
28
29
|
const isLast = message.id === messages[messages.length - 1]?.id;
|
|
29
30
|
const runStreaming = isLast && isStreaming && !isTerminalRunStatus({ status: message.runStatus });
|
|
30
|
-
return (_jsx(MessageRow, { message: message, runStreaming: runStreaming, runSucceeded: message.runStatus === 'succeeded', questionFormInteractive: message.id === activeQuestionFormMessageId, ...(questionFormSubmittedAnswersByMessageId?.[message.id] !== undefined ? { questionFormSubmittedAnswers: questionFormSubmittedAnswersByMessageId[message.id] } : {}), ...(onQuestionFormSubmit !== undefined ? { onQuestionFormSubmit: (text, answers) => onQuestionFormSubmit(message.id, text, answers) } : {}), ...(projectFileNames !== undefined ? { projectFileNames } : {}), ...(onRequestOpenFile !== undefined ? { onRequestOpenFile } : {}), ...(renderAttachment !== undefined ? { renderAttachment } : {}) }, message.id));
|
|
31
|
+
return (_jsx(MessageRow, { message: message, ...(usageByMessage.has(message.id) ? { usageSummary: usageByMessage.get(message.id) } : {}), showUsageTotal: showUsageTotal, showUsageLine: showUsageLine, toolCallDisplay: toolCallDisplay, showThinking: showThinking, runStreaming: runStreaming, runSucceeded: message.runStatus === 'succeeded', questionFormInteractive: message.id === activeQuestionFormMessageId, ...(questionFormSubmittedAnswersByMessageId?.[message.id] !== undefined ? { questionFormSubmittedAnswers: questionFormSubmittedAnswersByMessageId[message.id] } : {}), ...(onQuestionFormSubmit !== undefined ? { onQuestionFormSubmit: (text, answers) => onQuestionFormSubmit(message.id, text, answers) } : {}), ...(projectFileNames !== undefined ? { projectFileNames } : {}), ...(onRequestOpenFile !== undefined ? { onRequestOpenFile } : {}), ...(renderAttachment !== undefined ? { renderAttachment } : {}) }, message.id));
|
|
31
32
|
}), pendingPrompt ? (_jsxs("div", { className: "jini-chat-pane__queued", role: "status", "aria-label": t('Message queued — not yet sent, will send once the current run finishes'), "data-testid": "chat-pane-queued", "data-agent-element": "chat-message-pending", "data-agent-role": "region", "data-agent-label": "A message queued to send once the current run finishes", children: [_jsx("div", { className: "jini-chat-pane__queued-text", children: pendingPrompt.text }), _jsx("div", { className: "jini-chat-pane__queued-actions", children: _jsx("button", { type: "button", className: "jini-chat-pane__queued-cancel", onClick: pendingPrompt.onCancel, title: t('Cancel queued message'), children: t('Cancel') }) })] })) : null] }));
|
|
32
33
|
});
|
|
33
34
|
//# sourceMappingURL=MessageList.js.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ChatUsageSummary } from '../../core/turn-usage.js';
|
|
1
2
|
/**
|
|
2
3
|
* @module MessageRow
|
|
3
4
|
*
|
|
@@ -50,8 +51,24 @@
|
|
|
50
51
|
*/
|
|
51
52
|
import React, { type ReactNode } from 'react';
|
|
52
53
|
import type { ChatAttachment, ChatMessage } from '../../core/index.js';
|
|
54
|
+
import { type ToolCallDisplay } from '../tool-call-display.js';
|
|
53
55
|
export interface MessageRowProps {
|
|
54
56
|
message: ChatMessage;
|
|
57
|
+
/** Per-turn charge and prefix total derived from the complete conversation's persisted events. */
|
|
58
|
+
usageSummary?: ChatUsageSummary;
|
|
59
|
+
/** Append the chat's running total as of each reply ("· total $3.95") to its Done line. On by default
|
|
60
|
+
* (owner, 2026-10-10: keep the total); pass `false` to show only each turn's own cost. */
|
|
61
|
+
showUsageTotal?: boolean;
|
|
62
|
+
/** Show the "Done · time · cost" line(s) at all. On by default; `false` hides every one, total
|
|
63
|
+
* included (a host's "Done line" display choice, 2026-10-11). */
|
|
64
|
+
showUsageLine?: boolean;
|
|
65
|
+
/** How tool calls show (`../tool-call-display.ts`): every card (`'show'`, the default), one
|
|
66
|
+
* fold-up line per turn (`'collapsed'`), or only failed ones (`'hidden'`). */
|
|
67
|
+
toolCallDisplay?: ToolCallDisplay;
|
|
68
|
+
/** Show the model's reasoning (`thinking` events) as a fold-up block where it happened. Off by
|
|
69
|
+
* default — the transcript never showed it before this option. Needs the interleaved layout; a
|
|
70
|
+
* message that falls back to the flat layout shows none. */
|
|
71
|
+
showThinking?: boolean;
|
|
55
72
|
/** Whether this message's own run is still streaming. */
|
|
56
73
|
runStreaming?: boolean;
|
|
57
74
|
runSucceeded?: boolean;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { terminalMessageNotice, continuingRunNotice } from '../../core/durable-projection.js';
|
|
3
|
-
import {
|
|
3
|
+
import { usageLayout, usageLineMeetsActions, useMessageUsage, useUsageSummary } from './MessageRow.usage.js';
|
|
4
4
|
/**
|
|
5
5
|
* @module MessageRow
|
|
6
6
|
*
|
|
@@ -57,21 +57,19 @@ import { useToolTimeline } from '../hooks/useToolTimeline.js';
|
|
|
57
57
|
import { useExtEventGroups } from '../hooks/useExtEventGroups.js';
|
|
58
58
|
import { useCopyToClipboard } from '../hooks/useCopyToClipboard.js';
|
|
59
59
|
import { useRunActivity } from '../hooks/useRunActivity.js';
|
|
60
|
+
import { useAnsweredCards } from '../hooks/useAnsweredCards.js';
|
|
60
61
|
import { interleaveMessageBlocks } from '../message-blocks.js';
|
|
61
62
|
import { useT } from '../hooks/context.js';
|
|
62
63
|
import { extEventSlot, getExtEventRenderer } from '../ext-event-renderer-registry.js';
|
|
63
64
|
import { ExtEventErrorBoundary } from './ExtEventErrorBoundary.js';
|
|
64
65
|
import { AttachmentPreviewModal } from './AttachmentPreviewModal.js';
|
|
66
|
+
import { SentAttachmentButton } from './SentAttachment.js';
|
|
65
67
|
import { Icon } from './Icon.js';
|
|
66
68
|
import { Markdown } from './Markdown.js';
|
|
67
69
|
import { ToolCard } from './ToolCard.js';
|
|
68
70
|
import { QuestionForm } from './QuestionForm.js';
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const minutes = Math.floor(totalSeconds / 60);
|
|
72
|
-
const seconds = totalSeconds % 60;
|
|
73
|
-
return minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
|
|
74
|
-
}
|
|
71
|
+
import { ToolCallGroup } from './ToolCallGroup.js';
|
|
72
|
+
import { applyToolCallDisplay, toolRowsForDisplay } from '../tool-call-display.js';
|
|
75
73
|
/**
|
|
76
74
|
* True while an assistant message is still fully pending — no visible text
|
|
77
75
|
* and no tool rows have arrived yet. Shared by the "Thinking…" placeholder
|
|
@@ -108,21 +106,17 @@ function isPendingWithNoContent(message, visibleContent, toolRowCount) {
|
|
|
108
106
|
function isRunInProgress(runStatus) {
|
|
109
107
|
return runStatus !== undefined && !isTerminalRunStatus({ status: runStatus });
|
|
110
108
|
}
|
|
111
|
-
/** One "Done · 6m 29s ·
|
|
112
|
-
|
|
109
|
+
/** One "Done · 6m 29s · $0.41 · total $2.50" line from reported usage, never estimated (the total is
|
|
110
|
+
* left off when `showTotal` is false). Partial usage stays partial; output tokens are available to
|
|
111
|
+
* assistive technology and in the tooltip. */
|
|
112
|
+
function UsageSummary({ usage, showTotal }) {
|
|
113
113
|
const t = useT();
|
|
114
|
-
const
|
|
115
|
-
if (
|
|
116
|
-
parts.push(formatDuration(usage.durationMs));
|
|
117
|
-
if (usage.outputTokens !== undefined)
|
|
118
|
-
parts.push(t('{n} out', { n: usage.outputTokens }));
|
|
119
|
-
if (usage.costUsd !== undefined)
|
|
120
|
-
parts.push(`$${usage.costUsd.toFixed(4)}`);
|
|
121
|
-
if (parts.length === 0)
|
|
114
|
+
const { text, title, ariaLabel } = useUsageSummary({ usage }, { showTotal });
|
|
115
|
+
if (!text)
|
|
122
116
|
return null;
|
|
123
|
-
return (_jsxs("div", { className: "jini-message-usage", children: [_jsx("span", { className: "jini-message-usage-dot", "aria-hidden": true, children: "\u25CF" }), t('Done'), " \u00B7 ",
|
|
117
|
+
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('Done'), " \u00B7 ", text] }));
|
|
124
118
|
}
|
|
125
|
-
function messageBlocks({ visibleContent, message, rows },
|
|
119
|
+
function messageBlocks({ visibleContent, message, rows }, { toolCallDisplay = 'show', includeThinking = false } = {}) {
|
|
126
120
|
// Interleaving is skipped outright when an artifact was stripped. `stripArtifact` operates on the
|
|
127
121
|
// whole string and trims, so the surviving text no longer lines up with the per-run offsets the
|
|
128
122
|
// event walk produces, and an artifact block could legitimately span a tool call. Rather than
|
|
@@ -130,14 +124,22 @@ function messageBlocks({ visibleContent, message, rows }, _optional = {}) {
|
|
|
130
124
|
// messages are dominated by the artifact panel anyway, so the ordering matters least there.
|
|
131
125
|
if (visibleContent !== message.content)
|
|
132
126
|
return null;
|
|
133
|
-
|
|
127
|
+
const blocks = interleaveMessageBlocks({ events: message.events, content: message.content, rows }, { slotOf: (ev) => extEventSlot({ name: ev.name, data: ev.data }), includeThinking });
|
|
128
|
+
return blocks ? applyToolCallDisplay({ blocks, mode: toolCallDisplay }) : null;
|
|
129
|
+
}
|
|
130
|
+
/** The model's reasoning, folded under a "Thinking" summary so it never crowds the answer. */
|
|
131
|
+
function ThinkingBlock({ text }) {
|
|
132
|
+
const t = useT();
|
|
133
|
+
if (!text.trim())
|
|
134
|
+
return null;
|
|
135
|
+
return (_jsxs("details", { className: "jini-message-thinking", children: [_jsx("summary", { children: t('Thinking') }), _jsx("div", { className: "jini-message-thinking__text", children: text })] }));
|
|
134
136
|
}
|
|
135
|
-
function messageAwaitingAnswer({ runStreaming, runInProgress, events },
|
|
136
|
-
return runStreaming === true && runInProgress && isAwaitingAnswer({ events });
|
|
137
|
+
function messageAwaitingAnswer({ runStreaming, runInProgress, events }, { isAnsweredCard } = {}) {
|
|
138
|
+
return runStreaming === true && runInProgress && isAwaitingAnswer({ events }, { isAnsweredCard });
|
|
137
139
|
}
|
|
138
|
-
function AssistantMessageFooter({ message, usageEvent, terminalNotice, pending, runInProgress, runStreaming, visibleContent }, _optional = {}) {
|
|
140
|
+
function AssistantMessageFooter({ message, usageEvent, showUsageTotal, terminalNotice, pending, runInProgress, runStreaming, visibleContent, isAnsweredCard }, _optional = {}) {
|
|
139
141
|
const t = useT();
|
|
140
|
-
return _jsxs(_Fragment, { children: [usageEvent ? _jsx(UsageSummary, { usage: usageEvent }) : 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) }) : null] });
|
|
142
|
+
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] });
|
|
141
143
|
}
|
|
142
144
|
/**
|
|
143
145
|
* Quiet, icon-only copy affordance shared by the user- and assistant-message
|
|
@@ -160,26 +162,34 @@ function CopyMessageButton({ text, label }) {
|
|
|
160
162
|
* re-renders per composer keystroke) only re-renders rows whose props actually changed. Messages
|
|
161
163
|
* are replaced, never mutated, so identity is a sound change signal.
|
|
162
164
|
*/
|
|
163
|
-
export const MessageRow = memo(function MessageRow({ message, runStreaming = false, runSucceeded = false, questionFormInteractive = false, questionFormSubmittedAnswers, onQuestionFormSubmit, projectFileNames, onRequestOpenFile, renderAttachment, }) {
|
|
165
|
+
export const MessageRow = memo(function MessageRow({ message, usageSummary, showUsageTotal = true, showUsageLine = true, toolCallDisplay = 'show', showThinking = false, runStreaming = false, runSucceeded = false, questionFormInteractive = false, questionFormSubmittedAnswers, onQuestionFormSubmit, projectFileNames, onRequestOpenFile, renderAttachment, }) {
|
|
164
166
|
const t = useT();
|
|
167
|
+
const messageUsage = useMessageUsage({ message, ...(usageSummary === undefined ? {} : { summary: usageSummary }) });
|
|
165
168
|
const timeline = useToolTimeline({ events: message.events }, { runStreaming, runSucceeded });
|
|
166
169
|
const extGroups = useExtEventGroups({ events: message.events }, { slotOf: (name, data) => extEventSlot({ name, data }) });
|
|
170
|
+
const answeredCards = useAnsweredCards({ slotOf: extEventSlot });
|
|
167
171
|
// Which attachment chip (if any) this row's preview modal is open for. Row-local, not
|
|
168
172
|
// module-level: two attachments in the same message can never be "open" at once, but two
|
|
169
173
|
// different messages opening their own attachments independently is fine and expected.
|
|
170
174
|
const [openAttachment, setOpenAttachment] = useState(null);
|
|
171
175
|
if (message.role === 'user') {
|
|
172
|
-
return (_jsxs("div", { className: "jini-message jini-message-user", "data-message-id": message.id, "data-agent-element": `chat-message-${message.id}`, "data-agent-role": "region", "data-agent-label": "A message from the user", children: [message.attachments && message.attachments.length > 0 ? (_jsx("div", { className: "jini-message-attachments", children: message.attachments.map((a) => (_jsx(
|
|
176
|
+
return (_jsxs("div", { className: "jini-message jini-message-user", "data-message-id": message.id, "data-agent-element": `chat-message-${message.id}`, "data-agent-role": "region", "data-agent-label": "A message from the user", children: [message.attachments && message.attachments.length > 0 ? (_jsx("div", { className: "jini-message-attachments", children: message.attachments.map((a) => (_jsx(SentAttachmentButton, { attachment: a, onOpen: setOpenAttachment, renderAttachment: renderAttachment }, a.path))) })) : null, _jsx("div", { className: "jini-message-content", children: message.content }), _jsx("div", { className: "jini-message-actions jini-message-actions--user", children: _jsx(CopyMessageButton, { text: message.content, label: t('Copy message') }) }), openAttachment ? (_jsx(AttachmentPreviewModal, { attachment: openAttachment, onClose: () => setOpenAttachment(null) })) : null] }));
|
|
173
177
|
}
|
|
174
178
|
const terminalNotice = terminalMessageNotice({ message }, {});
|
|
175
179
|
const visibleContent = stripArtifact({ content: message.content });
|
|
176
180
|
const segments = splitOnQuestionForms({ input: visibleContent });
|
|
177
|
-
const
|
|
178
|
-
const blocks = messageBlocks({ visibleContent, message, rows: timeline.rows }, {});
|
|
181
|
+
const blocks = messageBlocks({ visibleContent, message, rows: timeline.rows }, { toolCallDisplay, includeThinking: showThinking });
|
|
179
182
|
const pending = isPendingWithNoContent(message, visibleContent, timeline.rows.length);
|
|
180
183
|
const runInProgress = isRunInProgress(message.runStatus);
|
|
181
|
-
const awaitingAnswer = messageAwaitingAnswer({ runStreaming, runInProgress, events: message.events }, {});
|
|
184
|
+
const awaitingAnswer = messageAwaitingAnswer({ runStreaming, runInProgress, events: message.events }, { isAnsweredCard: answeredCards.isAnsweredCard });
|
|
185
|
+
const usage = usageLayout({ usage: messageUsage, interleaved: blocks !== null, runInProgress }, { runStreaming });
|
|
186
|
+
const footerUsage = showUsageLine ? usage.footer : undefined;
|
|
182
187
|
const renderToolCard = (row) => (_jsx(ToolCard, { use: row.use, result: row.result, runStreaming: runStreaming, runSucceeded: runSucceeded, ...(projectFileNames !== undefined ? { projectFileNames } : {}), ...(onRequestOpenFile !== undefined ? { onRequestOpenFile } : {}) }, row.id));
|
|
188
|
+
// One turn's cards: a fold-up line when collapsed, the plain card list otherwise.
|
|
189
|
+
const renderTools = (rows, key) => (toolCallDisplay === 'collapsed'
|
|
190
|
+
? _jsx(ToolCallGroup, { rows: rows, renderRow: renderToolCard }, key)
|
|
191
|
+
: _jsx("div", { className: "jini-message-tools", children: rows.map(renderToolCard) }, key));
|
|
192
|
+
const flatToolRows = toolRowsForDisplay({ rows: timeline.rows, mode: toolCallDisplay });
|
|
183
193
|
const renderExtGroup = (group) => {
|
|
184
194
|
const renderer = getExtEventRenderer({ name: group.name });
|
|
185
195
|
if (!renderer)
|
|
@@ -192,6 +202,8 @@ export const MessageRow = memo(function MessageRow({ message, runStreaming = fal
|
|
|
192
202
|
awaitingAnswer,
|
|
193
203
|
runId: message.runId,
|
|
194
204
|
...(group.call === undefined ? {} : { call: group.call }),
|
|
205
|
+
answered: answeredCards.isAnswered(group.slot),
|
|
206
|
+
onAnswered: () => answeredCards.markAnswered(group.slot),
|
|
195
207
|
});
|
|
196
208
|
if (!node)
|
|
197
209
|
return null;
|
|
@@ -202,28 +214,35 @@ export const MessageRow = memo(function MessageRow({ message, runStreaming = fal
|
|
|
202
214
|
_jsx(ExtEventErrorBoundary, { name: group.name, children: _jsx("div", { children: node }) }, `${group.slot}:${group.events.length}`));
|
|
203
215
|
};
|
|
204
216
|
const renderTextSegments = (text, keyPrefix) => splitOnQuestionForms({ input: text }).map((segment, i) => segment.kind === 'text' ? (segment.text.trim() ? (_jsx("div", { className: "jini-message-content", children: _jsx(Markdown, { children: segment.text }) }, `${keyPrefix}-${i}`)) : null) : (_jsx(QuestionForm, { form: segment.form, interactive: questionFormInteractive, ...(questionFormSubmittedAnswers !== undefined ? { submittedAnswers: questionFormSubmittedAnswers } : {}), ...(onQuestionFormSubmit !== undefined ? { onSubmit: onQuestionFormSubmit } : {}) }, `${keyPrefix}-${i}`)));
|
|
205
|
-
return (_jsxs("div", { className:
|
|
217
|
+
return (_jsxs("div", { className: usageLineMeetsActions({ footer: footerUsage, terminalNotice, pending, runInProgress })
|
|
218
|
+
? 'jini-message jini-message-assistant jini-message-assistant--usage-actions'
|
|
219
|
+
: '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
|
|
206
220
|
? blocks.map((block) => {
|
|
207
221
|
if (block.kind === 'text') {
|
|
208
222
|
return _jsx(React.Fragment, { children: renderTextSegments(block.text, block.key) }, block.key);
|
|
209
223
|
}
|
|
210
|
-
if (block.kind === '
|
|
211
|
-
|
|
224
|
+
if (block.kind === 'reply-end') {
|
|
225
|
+
const reply = usage.replies[block.index];
|
|
226
|
+
return reply && showUsageLine ? _jsx(UsageSummary, { usage: reply, showTotal: showUsageTotal }, block.key) : null;
|
|
212
227
|
}
|
|
228
|
+
if (block.kind === 'tools')
|
|
229
|
+
return renderTools(block.rows, block.key);
|
|
230
|
+
if (block.kind === 'thinking')
|
|
231
|
+
return _jsx(ThinkingBlock, { text: block.text }, block.key);
|
|
213
232
|
const group = extGroups.find((g) => g.slot === block.slot);
|
|
214
233
|
if (!group)
|
|
215
234
|
return null;
|
|
216
235
|
return (_jsx("div", { className: "jini-message-ext-events", children: renderExtGroup(group) }, block.key));
|
|
217
236
|
})
|
|
218
|
-
: (_jsxs(_Fragment, { children: [segments.map((segment, i) => segment.kind === 'text' ? (segment.text.trim() ? (_jsx("div", { className: "jini-message-content", children: _jsx(Markdown, { children: segment.text }) }, i)) : null) : (_jsx(QuestionForm, { form: segment.form, interactive: questionFormInteractive, ...(questionFormSubmittedAnswers !== undefined ? { submittedAnswers: questionFormSubmittedAnswers } : {}), ...(onQuestionFormSubmit !== undefined ? { onSubmit: onQuestionFormSubmit } : {}) }, i))),
|
|
237
|
+
: (_jsxs(_Fragment, { children: [segments.map((segment, i) => segment.kind === 'text' ? (segment.text.trim() ? (_jsx("div", { className: "jini-message-content", children: _jsx(Markdown, { children: segment.text }) }, i)) : null) : (_jsx(QuestionForm, { form: segment.form, interactive: questionFormInteractive, ...(questionFormSubmittedAnswers !== undefined ? { submittedAnswers: questionFormSubmittedAnswers } : {}), ...(onQuestionFormSubmit !== undefined ? { onSubmit: onQuestionFormSubmit } : {}) }, i))), flatToolRows.length > 0 ? renderTools(flatToolRows) : null, 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 })] }));
|
|
219
238
|
});
|
|
220
239
|
/**
|
|
221
240
|
* The live "what is it doing" line under a running reply (see `core/run-activity.ts`). Replaces the
|
|
222
241
|
* old "Thinking…" placeholder, which vanished for good as soon as the first tool row appeared.
|
|
223
242
|
* Kept a separate component so its one-second tick re-renders only this line, not the whole row.
|
|
224
243
|
*/
|
|
225
|
-
function RunActivityLine({ events, active }) {
|
|
226
|
-
const activityLabel = useRunActivity({ events: events, active: active });
|
|
244
|
+
function RunActivityLine({ events, active, isAnsweredCard }) {
|
|
245
|
+
const activityLabel = useRunActivity({ events: events, active: active }, { isAnsweredCard });
|
|
227
246
|
const label = continuingRunNotice({ events, active }, {}) ?? activityLabel;
|
|
228
247
|
if (label === null)
|
|
229
248
|
return null;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ChatMessage } from '../../core/messages.js';
|
|
2
|
+
import { type ChatUsageSummary } from '../../core/turn-usage.js';
|
|
3
|
+
/** A standalone row has one turn; MessageList supplies the full conversation projection. */
|
|
4
|
+
export declare function useMessageUsage({ message, summary }: {
|
|
5
|
+
message: ChatMessage;
|
|
6
|
+
summary?: ChatUsageSummary;
|
|
7
|
+
}, _optional?: {}): ChatUsageSummary | undefined;
|
|
8
|
+
/** Which "Done" lines a message shows. Each finished reply gets its own line where it ends; the reply
|
|
9
|
+
* still running gets none, so a stale line never sits above live work (owner, 2026-10-10). Per-reply
|
|
10
|
+
* lines need the ordered block layout to know where a reply ends; the flat fallback keeps one
|
|
11
|
+
* whole-message line under the message once the run is over. A row still streaming counts as running
|
|
12
|
+
* even when its `runStatus` was never set, so the previous reply's line cannot slide down under the
|
|
13
|
+
* live tool rows above "Running …" (owner bug, 2026-10-10). O(r) for r replies.
|
|
14
|
+
* @example usageLayout({ usage, interleaved: true, runInProgress: true }).footer // undefined */
|
|
15
|
+
export declare function usageLayout({ usage, interleaved, runInProgress }: {
|
|
16
|
+
usage: ChatUsageSummary | undefined;
|
|
17
|
+
interleaved: boolean;
|
|
18
|
+
runInProgress: boolean;
|
|
19
|
+
}, { runStreaming }?: {
|
|
20
|
+
runStreaming?: boolean;
|
|
21
|
+
}): {
|
|
22
|
+
footer: ChatUsageSummary | undefined;
|
|
23
|
+
replies: readonly (ChatUsageSummary | undefined)[];
|
|
24
|
+
};
|
|
25
|
+
/** Whether the footer's "Done" line renders directly above the copy row: a finished turn with a
|
|
26
|
+
* non-empty usage line and no notice between them. Rows get a modifier class from this so a host can
|
|
27
|
+
* lay the two out on one line with a plain class selector — a `:has(> usage + actions)` selector made
|
|
28
|
+
* every keystroke restyle the whole page in long chats (2026-10-11). O(1).
|
|
29
|
+
* @example usageLineMeetsActions({ footer: { kind: 'usage', costUsd: 0.1 }, terminalNotice: null, pending: false, runInProgress: false }) // true */
|
|
30
|
+
export declare function usageLineMeetsActions({ footer, terminalNotice, pending, runInProgress }: {
|
|
31
|
+
footer: ChatUsageSummary | undefined;
|
|
32
|
+
terminalNotice: string | null;
|
|
33
|
+
pending: boolean;
|
|
34
|
+
runInProgress: boolean;
|
|
35
|
+
}, _optional?: {}): boolean;
|
|
36
|
+
/** Build footer display values outside JSX; token counts remain available in the tooltip and accessible footer label.
|
|
37
|
+
* Each line carries the chat's running total as of that reply, "Done · 16s · $0.39 · total $3.95" (owner,
|
|
38
|
+
* 2026-10-10: keep the total, always shown, even on the first turn). `showTotal: false` turns it off. */
|
|
39
|
+
export declare function useUsageSummary({ usage }: {
|
|
40
|
+
usage: ChatUsageSummary;
|
|
41
|
+
}, { showTotal }?: {
|
|
42
|
+
showTotal?: boolean;
|
|
43
|
+
}): {
|
|
44
|
+
text: string;
|
|
45
|
+
title: string | undefined;
|
|
46
|
+
ariaLabel: string | undefined;
|
|
47
|
+
};
|
|
48
|
+
//# sourceMappingURL=MessageRow.usage.d.ts.map
|