@jini-ai/chat 0.5.6 → 0.5.8
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/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/durable-projection.d.ts +3 -1
- package/dist/core/durable-projection.js +40 -7
- 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 +4 -0
- package/dist/core/index.js +4 -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.d.ts +12 -1
- package/dist/core/run-events/entry.js +19 -6
- package/dist/core/tool-call-detail.d.ts +51 -0
- package/dist/core/tool-call-detail.js +67 -0
- package/dist/core/turn-usage.d.ts +50 -0
- package/dist/core/turn-usage.js +114 -0
- 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/chat-pane.d.ts +2 -2
- package/dist/react/chat-pane.js +1 -1
- 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/Composer.d.ts +2 -0
- package/dist/react/components/Composer.js +5 -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/Markdown.js +6 -6
- package/dist/react/components/McpUiSurfaceCard.d.ts +1 -1
- package/dist/react/components/McpUiSurfaceCard.js +8 -4
- package/dist/react/components/MessageList.d.ts +8 -13
- 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 +23 -4
- package/dist/react/components/MessageRow.d.ts +23 -1
- package/dist/react/components/MessageRow.js +61 -36
- 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 +17 -0
- package/dist/react/components/MidRunUserMessage.js +41 -0
- 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 +41 -22
- 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 +20 -7
- package/dist/react/features/chat-pane/create-mid-run-message-poster.d.ts +44 -0
- package/dist/react/features/chat-pane/create-mid-run-message-poster.js +69 -0
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +4 -1
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +65 -10
- package/dist/react/features/chat-pane/hooks/useChatPaneNewTabLinks.hooks.d.ts +25 -0
- package/dist/react/features/chat-pane/hooks/useChatPaneNewTabLinks.hooks.js +70 -0
- 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/index.d.ts +2 -1
- package/dist/react/features/chat-pane/index.js +1 -0
- package/dist/react/features/chat-pane/rules.d.ts +27 -0
- package/dist/react/features/chat-pane/rules.js +32 -0
- package/dist/react/features/chat-pane/styles.d.ts +1 -1
- package/dist/react/features/chat-pane/styles.js +92 -1
- package/dist/react/features/chat-pane/types.d.ts +39 -1
- package/dist/react/hooks/composer-draft-cache.d.ts +15 -0
- package/dist/react/hooks/composer-draft-cache.js +20 -0
- package/dist/react/hooks/useAnsweredCards.d.ts +20 -0
- package/dist/react/hooks/useAnsweredCards.js +23 -0
- package/dist/react/hooks/useComposer.d.ts +14 -0
- package/dist/react/hooks/useComposer.js +78 -6
- 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 +8 -2
- package/dist/react/index.js +6 -1
- 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/server/durable-runs/recover.js +19 -11
- package/dist/server/run-finalizer/entry.js +2 -1
- package/dist/store/ports.d.ts +25 -2
- package/dist/store/runtime/durable-run-store.js +5 -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/runtime/run-ledger.js +5 -2
- 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 -18
|
@@ -1,5 +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 { usageLayout, usageLineMeetsActions, useMessageUsage, useUsageSummary } from './MessageRow.usage.js';
|
|
3
4
|
/**
|
|
4
5
|
* @module MessageRow
|
|
5
6
|
*
|
|
@@ -50,27 +51,25 @@ import { terminalMessageNotice, continuingRunNotice } from '../../core/durable-p
|
|
|
50
51
|
* not appear until the whole message is done, not merely until its current
|
|
51
52
|
* chunk stopped arriving.
|
|
52
53
|
*/
|
|
53
|
-
import React, { useState } from 'react';
|
|
54
|
+
import React, { memo, useState } from 'react';
|
|
54
55
|
import { isAwaitingAnswer, isTerminalRunStatus, splitOnQuestionForms, stripArtifact } from '../../core/index.js';
|
|
55
56
|
import { useToolTimeline } from '../hooks/useToolTimeline.js';
|
|
56
57
|
import { useExtEventGroups } from '../hooks/useExtEventGroups.js';
|
|
57
58
|
import { useCopyToClipboard } from '../hooks/useCopyToClipboard.js';
|
|
58
59
|
import { useRunActivity } from '../hooks/useRunActivity.js';
|
|
60
|
+
import { useAnsweredCards } from '../hooks/useAnsweredCards.js';
|
|
59
61
|
import { interleaveMessageBlocks } from '../message-blocks.js';
|
|
60
62
|
import { useT } from '../hooks/context.js';
|
|
61
63
|
import { extEventSlot, getExtEventRenderer } from '../ext-event-renderer-registry.js';
|
|
62
64
|
import { ExtEventErrorBoundary } from './ExtEventErrorBoundary.js';
|
|
63
65
|
import { AttachmentPreviewModal } from './AttachmentPreviewModal.js';
|
|
66
|
+
import { SentAttachmentButton } from './SentAttachment.js';
|
|
64
67
|
import { Icon } from './Icon.js';
|
|
65
68
|
import { Markdown } from './Markdown.js';
|
|
66
69
|
import { ToolCard } from './ToolCard.js';
|
|
67
70
|
import { QuestionForm } from './QuestionForm.js';
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const minutes = Math.floor(totalSeconds / 60);
|
|
71
|
-
const seconds = totalSeconds % 60;
|
|
72
|
-
return minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
|
|
73
|
-
}
|
|
71
|
+
import { ToolCallGroup } from './ToolCallGroup.js';
|
|
72
|
+
import { applyToolCallDisplay, toolRowsForDisplay } from '../tool-call-display.js';
|
|
74
73
|
/**
|
|
75
74
|
* True while an assistant message is still fully pending — no visible text
|
|
76
75
|
* and no tool rows have arrived yet. Shared by the "Thinking…" placeholder
|
|
@@ -107,21 +106,17 @@ function isPendingWithNoContent(message, visibleContent, toolRowCount) {
|
|
|
107
106
|
function isRunInProgress(runStatus) {
|
|
108
107
|
return runStatus !== undefined && !isTerminalRunStatus({ status: runStatus });
|
|
109
108
|
}
|
|
110
|
-
/** One "Done · 6m 29s ·
|
|
111
|
-
|
|
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 }) {
|
|
112
113
|
const t = useT();
|
|
113
|
-
const
|
|
114
|
-
if (
|
|
115
|
-
parts.push(formatDuration(usage.durationMs));
|
|
116
|
-
if (usage.outputTokens !== undefined)
|
|
117
|
-
parts.push(t('{n} out', { n: usage.outputTokens }));
|
|
118
|
-
if (usage.costUsd !== undefined)
|
|
119
|
-
parts.push(`$${usage.costUsd.toFixed(4)}`);
|
|
120
|
-
if (parts.length === 0)
|
|
114
|
+
const { text, title, ariaLabel } = useUsageSummary({ usage }, { showTotal });
|
|
115
|
+
if (!text)
|
|
121
116
|
return null;
|
|
122
|
-
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] }));
|
|
123
118
|
}
|
|
124
|
-
function messageBlocks({ visibleContent, message, rows },
|
|
119
|
+
function messageBlocks({ visibleContent, message, rows }, { toolCallDisplay = 'show', includeThinking = false } = {}) {
|
|
125
120
|
// Interleaving is skipped outright when an artifact was stripped. `stripArtifact` operates on the
|
|
126
121
|
// whole string and trims, so the surviving text no longer lines up with the per-run offsets the
|
|
127
122
|
// event walk produces, and an artifact block could legitimately span a tool call. Rather than
|
|
@@ -129,14 +124,22 @@ function messageBlocks({ visibleContent, message, rows }, _optional = {}) {
|
|
|
129
124
|
// messages are dominated by the artifact panel anyway, so the ordering matters least there.
|
|
130
125
|
if (visibleContent !== message.content)
|
|
131
126
|
return null;
|
|
132
|
-
|
|
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 })] }));
|
|
133
136
|
}
|
|
134
|
-
function messageAwaitingAnswer({ runStreaming, runInProgress, events },
|
|
135
|
-
return runStreaming === true && runInProgress && isAwaitingAnswer({ events });
|
|
137
|
+
function messageAwaitingAnswer({ runStreaming, runInProgress, events }, { isAnsweredCard } = {}) {
|
|
138
|
+
return runStreaming === true && runInProgress && isAwaitingAnswer({ events }, { isAnsweredCard });
|
|
136
139
|
}
|
|
137
|
-
function AssistantMessageFooter({ message, usageEvent, terminalNotice, pending, runInProgress, runStreaming, visibleContent }, _optional = {}) {
|
|
140
|
+
function AssistantMessageFooter({ message, usageEvent, showUsageTotal, terminalNotice, pending, runInProgress, runStreaming, visibleContent, isAnsweredCard }, _optional = {}) {
|
|
138
141
|
const t = useT();
|
|
139
|
-
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] });
|
|
140
143
|
}
|
|
141
144
|
/**
|
|
142
145
|
* Quiet, icon-only copy affordance shared by the user- and assistant-message
|
|
@@ -154,26 +157,39 @@ function CopyMessageButton({ text, label }) {
|
|
|
154
157
|
void copy(text);
|
|
155
158
|
}, children: _jsx(Icon, { name: copied ? 'check' : 'copy' }) }), _jsx("span", { className: "jini-message-copy-status", "aria-live": "polite", children: copied ? t('Copied to clipboard') : '' })] }));
|
|
156
159
|
}
|
|
157
|
-
|
|
160
|
+
/**
|
|
161
|
+
* Memoized so a transcript re-render (a streaming update to the newest message, or a parent that
|
|
162
|
+
* re-renders per composer keystroke) only re-renders rows whose props actually changed. Messages
|
|
163
|
+
* are replaced, never mutated, so identity is a sound change signal.
|
|
164
|
+
*/
|
|
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, }) {
|
|
158
166
|
const t = useT();
|
|
167
|
+
const messageUsage = useMessageUsage({ message, ...(usageSummary === undefined ? {} : { summary: usageSummary }) });
|
|
159
168
|
const timeline = useToolTimeline({ events: message.events }, { runStreaming, runSucceeded });
|
|
160
169
|
const extGroups = useExtEventGroups({ events: message.events }, { slotOf: (name, data) => extEventSlot({ name, data }) });
|
|
170
|
+
const answeredCards = useAnsweredCards({ slotOf: extEventSlot });
|
|
161
171
|
// Which attachment chip (if any) this row's preview modal is open for. Row-local, not
|
|
162
172
|
// module-level: two attachments in the same message can never be "open" at once, but two
|
|
163
173
|
// different messages opening their own attachments independently is fine and expected.
|
|
164
174
|
const [openAttachment, setOpenAttachment] = useState(null);
|
|
165
175
|
if (message.role === 'user') {
|
|
166
|
-
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] }));
|
|
167
177
|
}
|
|
168
178
|
const terminalNotice = terminalMessageNotice({ message }, {});
|
|
169
179
|
const visibleContent = stripArtifact({ content: message.content });
|
|
170
180
|
const segments = splitOnQuestionForms({ input: visibleContent });
|
|
171
|
-
const
|
|
172
|
-
const blocks = messageBlocks({ visibleContent, message, rows: timeline.rows }, {});
|
|
181
|
+
const blocks = messageBlocks({ visibleContent, message, rows: timeline.rows }, { toolCallDisplay, includeThinking: showThinking });
|
|
173
182
|
const pending = isPendingWithNoContent(message, visibleContent, timeline.rows.length);
|
|
174
183
|
const runInProgress = isRunInProgress(message.runStatus);
|
|
175
|
-
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;
|
|
176
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 });
|
|
177
193
|
const renderExtGroup = (group) => {
|
|
178
194
|
const renderer = getExtEventRenderer({ name: group.name });
|
|
179
195
|
if (!renderer)
|
|
@@ -186,6 +202,8 @@ export function MessageRow({ message, runStreaming = false, runSucceeded = false
|
|
|
186
202
|
awaitingAnswer,
|
|
187
203
|
runId: message.runId,
|
|
188
204
|
...(group.call === undefined ? {} : { call: group.call }),
|
|
205
|
+
answered: answeredCards.isAnswered(group.slot),
|
|
206
|
+
onAnswered: () => answeredCards.markAnswered(group.slot),
|
|
189
207
|
});
|
|
190
208
|
if (!node)
|
|
191
209
|
return null;
|
|
@@ -196,28 +214,35 @@ export function MessageRow({ message, runStreaming = false, runSucceeded = false
|
|
|
196
214
|
_jsx(ExtEventErrorBoundary, { name: group.name, children: _jsx("div", { children: node }) }, `${group.slot}:${group.events.length}`));
|
|
197
215
|
};
|
|
198
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}`)));
|
|
199
|
-
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
|
|
200
220
|
? blocks.map((block) => {
|
|
201
221
|
if (block.kind === 'text') {
|
|
202
222
|
return _jsx(React.Fragment, { children: renderTextSegments(block.text, block.key) }, block.key);
|
|
203
223
|
}
|
|
204
|
-
if (block.kind === '
|
|
205
|
-
|
|
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;
|
|
206
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);
|
|
207
232
|
const group = extGroups.find((g) => g.slot === block.slot);
|
|
208
233
|
if (!group)
|
|
209
234
|
return null;
|
|
210
235
|
return (_jsx("div", { className: "jini-message-ext-events", children: renderExtGroup(group) }, block.key));
|
|
211
236
|
})
|
|
212
|
-
: (_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))),
|
|
213
|
-
}
|
|
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 })] }));
|
|
238
|
+
});
|
|
214
239
|
/**
|
|
215
240
|
* The live "what is it doing" line under a running reply (see `core/run-activity.ts`). Replaces the
|
|
216
241
|
* old "Thinking…" placeholder, which vanished for good as soon as the first tool row appeared.
|
|
217
242
|
* Kept a separate component so its one-second tick re-renders only this line, not the whole row.
|
|
218
243
|
*/
|
|
219
|
-
function RunActivityLine({ events, active }) {
|
|
220
|
-
const activityLabel = useRunActivity({ events: events, active: active });
|
|
244
|
+
function RunActivityLine({ events, active, isAnsweredCard }) {
|
|
245
|
+
const activityLabel = useRunActivity({ events: events, active: active }, { isAnsweredCard });
|
|
221
246
|
const label = continuingRunNotice({ events, active }, {}) ?? activityLabel;
|
|
222
247
|
if (label === null)
|
|
223
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
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { conversationUsage } from '../../core/turn-usage.js';
|
|
3
|
+
import { formatUsageCost } from '../../core/usage-format.js';
|
|
4
|
+
import { useT } from '../hooks/context.js';
|
|
5
|
+
/** A standalone row has one turn; MessageList supplies the full conversation projection. */
|
|
6
|
+
export function useMessageUsage({ message, summary }, _optional = {}) {
|
|
7
|
+
return useMemo(() => summary ?? conversationUsage({ messages: [message] }).get(message.id), [message, summary]);
|
|
8
|
+
}
|
|
9
|
+
/** Which "Done" lines a message shows. Each finished reply gets its own line where it ends; the reply
|
|
10
|
+
* still running gets none, so a stale line never sits above live work (owner, 2026-10-10). Per-reply
|
|
11
|
+
* lines need the ordered block layout to know where a reply ends; the flat fallback keeps one
|
|
12
|
+
* whole-message line under the message once the run is over. A row still streaming counts as running
|
|
13
|
+
* even when its `runStatus` was never set, so the previous reply's line cannot slide down under the
|
|
14
|
+
* live tool rows above "Running …" (owner bug, 2026-10-10). O(r) for r replies.
|
|
15
|
+
* @example usageLayout({ usage, interleaved: true, runInProgress: true }).footer // undefined */
|
|
16
|
+
export function usageLayout({ usage, interleaved, runInProgress }, { runStreaming = false } = {}) {
|
|
17
|
+
const segments = interleaved ? usage?.segments : undefined;
|
|
18
|
+
const last = segments ? segments.at(-1) : usage;
|
|
19
|
+
return { footer: runInProgress || runStreaming ? undefined : last, replies: segments ? segments.slice(0, -1) : [] };
|
|
20
|
+
}
|
|
21
|
+
/** Whether the footer's "Done" line renders directly above the copy row: a finished turn with a
|
|
22
|
+
* non-empty usage line and no notice between them. Rows get a modifier class from this so a host can
|
|
23
|
+
* lay the two out on one line with a plain class selector — a `:has(> usage + actions)` selector made
|
|
24
|
+
* every keystroke restyle the whole page in long chats (2026-10-11). O(1).
|
|
25
|
+
* @example usageLineMeetsActions({ footer: { kind: 'usage', costUsd: 0.1 }, terminalNotice: null, pending: false, runInProgress: false }) // true */
|
|
26
|
+
export function usageLineMeetsActions({ footer, terminalNotice, pending, runInProgress }, _optional = {}) {
|
|
27
|
+
const hasText = footer !== undefined && (footer.durationMs !== undefined || footer.costUsd !== undefined);
|
|
28
|
+
return hasText && !terminalNotice && !pending && !runInProgress;
|
|
29
|
+
}
|
|
30
|
+
/** Build footer display values outside JSX; token counts remain available in the tooltip and accessible footer label.
|
|
31
|
+
* Each line carries the chat's running total as of that reply, "Done · 16s · $0.39 · total $3.95" (owner,
|
|
32
|
+
* 2026-10-10: keep the total, always shown, even on the first turn). `showTotal: false` turns it off. */
|
|
33
|
+
export function useUsageSummary({ usage }, { showTotal = true } = {}) {
|
|
34
|
+
const t = useT();
|
|
35
|
+
const parts = [];
|
|
36
|
+
if (usage.durationMs !== undefined) {
|
|
37
|
+
const seconds = Math.round(usage.durationMs / 1000);
|
|
38
|
+
parts.push(seconds >= 60 ? `${Math.floor(seconds / 60)}m ${seconds % 60}s` : `${seconds}s`);
|
|
39
|
+
}
|
|
40
|
+
if (usage.costUsd !== undefined) {
|
|
41
|
+
parts.push(formatUsageCost({ costUsd: usage.costUsd, ...(showTotal ? { totalCostUsd: usage.totalCostUsd ?? usage.costUsd } : {}) }));
|
|
42
|
+
}
|
|
43
|
+
const text = parts.join(' · ');
|
|
44
|
+
const title = usage.outputTokens === undefined ? undefined : t('{n} out', { n: usage.outputTokens });
|
|
45
|
+
return { text, title, ariaLabel: title === undefined ? undefined : `${t('Done')} · ${text} · ${title}` };
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=MessageRow.usage.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type ExtEventRenderProps } from '../ext-event-renderer-registry.js';
|
|
2
|
+
/** The ext-event name the daemon's mid-run `user_message` run event arrives under. One source with
|
|
3
|
+
* the core reply split (`replySegments`), which ends a reply's usage at this same event. */
|
|
4
|
+
export declare const MID_RUN_USER_MESSAGE_EXT_EVENT_NAME = "user_message";
|
|
5
|
+
/** One slot per message id, so each mid-run message renders where it arrived rather than folding into the first. */
|
|
6
|
+
export declare function midRunUserMessageSlotKey({ data }: {
|
|
7
|
+
data: unknown;
|
|
8
|
+
}): string | undefined;
|
|
9
|
+
/** The bubble for one mid-run message — the same classes as a sent user message, so it reads as one. */
|
|
10
|
+
export declare function MidRunUserMessage({ events }: Pick<ExtEventRenderProps, 'events'>): import("react").JSX.Element | null;
|
|
11
|
+
/**
|
|
12
|
+
* Registers {@link MidRunUserMessage} against the ext-event registry.
|
|
13
|
+
*
|
|
14
|
+
* @returns An unregister handle, so a test or a hot reload can dispose cleanly.
|
|
15
|
+
*/
|
|
16
|
+
export declare function registerMidRunUserMessageRenderer(): () => void;
|
|
17
|
+
//# sourceMappingURL=MidRunUserMessage.d.ts.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* @module MidRunUserMessage
|
|
4
|
+
*
|
|
5
|
+
* Renders a message the human sent while a run was going (`ChatPane`'s `deliverMidRunMessage`) as a
|
|
6
|
+
* user bubble inside the running turn, at the point in the stream where the live agent received it.
|
|
7
|
+
* The daemon records each one as a `user_message` run event (`AgentExecutor.sendUserMessage`), which
|
|
8
|
+
* the run-event translator surfaces as `kind: 'ext'` with that name; this claims it.
|
|
9
|
+
*
|
|
10
|
+
* It is an event of the assistant turn, not a `ChatMessage` of its own, because the agent folded it
|
|
11
|
+
* into the turn it was already running — there is no separate reply to hang off it.
|
|
12
|
+
*/
|
|
13
|
+
import { MID_RUN_USER_MESSAGE_EVENT_NAME } from '../../core/turn-usage.js';
|
|
14
|
+
import { registerExtEventRenderer } from '../ext-event-renderer-registry.js';
|
|
15
|
+
/** The ext-event name the daemon's mid-run `user_message` run event arrives under. One source with
|
|
16
|
+
* the core reply split (`replySegments`), which ends a reply's usage at this same event. */
|
|
17
|
+
export const MID_RUN_USER_MESSAGE_EXT_EVENT_NAME = MID_RUN_USER_MESSAGE_EVENT_NAME;
|
|
18
|
+
function field(data, key) {
|
|
19
|
+
const value = typeof data === 'object' && data !== null ? data[key] : undefined;
|
|
20
|
+
return typeof value === 'string' && value !== '' ? value : undefined;
|
|
21
|
+
}
|
|
22
|
+
/** One slot per message id, so each mid-run message renders where it arrived rather than folding into the first. */
|
|
23
|
+
export function midRunUserMessageSlotKey({ data }) {
|
|
24
|
+
return field(data, 'id');
|
|
25
|
+
}
|
|
26
|
+
/** The bubble for one mid-run message — the same classes as a sent user message, so it reads as one. */
|
|
27
|
+
export function MidRunUserMessage({ events }) {
|
|
28
|
+
const text = field(events.at(-1), 'text');
|
|
29
|
+
if (text === undefined)
|
|
30
|
+
return null;
|
|
31
|
+
return (_jsx("div", { className: "jini-message jini-message-user jini-message-user--mid-run", "data-agent-role": "region", "data-agent-label": "A message from the user, sent while the agent was working", children: _jsx("div", { className: "jini-message-content", children: text }) }));
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Registers {@link MidRunUserMessage} against the ext-event registry.
|
|
35
|
+
*
|
|
36
|
+
* @returns An unregister handle, so a test or a hot reload can dispose cleanly.
|
|
37
|
+
*/
|
|
38
|
+
export function registerMidRunUserMessageRenderer() {
|
|
39
|
+
return registerExtEventRenderer({ name: MID_RUN_USER_MESSAGE_EXT_EVENT_NAME, renderer: (props) => _jsx(MidRunUserMessage, { events: props.events }) }, { slotKey: midRunUserMessageSlotKey });
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=MidRunUserMessage.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { ChatAttachment } from '../../core/index.js';
|
|
3
|
+
import { type ObjectUrlPort } from './SentAttachment.hooks.js';
|
|
4
|
+
export interface SentAttachmentButtonProps {
|
|
5
|
+
attachment: ChatAttachment;
|
|
6
|
+
/** Opens the row's preview modal for this attachment. */
|
|
7
|
+
onOpen: (attachment: ChatAttachment) => void;
|
|
8
|
+
/** Host-supplied chip content; when given it wins over the built-in thumbnail/filename. */
|
|
9
|
+
renderAttachment?: ((attachment: ChatAttachment) => ReactNode) | undefined;
|
|
10
|
+
/** Blob-URL port for the thumbnail; defaults to the platform's (see `SentAttachment.hooks.ts`). */
|
|
11
|
+
objectUrls?: ObjectUrlPort | undefined;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* One attachment on a sent user message: a small image thumbnail when this browser still holds the
|
|
15
|
+
* image's bytes, otherwise the filename chip. Either way a click opens the preview modal, and the
|
|
16
|
+
* accessible name stays "Open {name}".
|
|
17
|
+
*/
|
|
18
|
+
export declare function SentAttachmentButton({ attachment, onOpen, renderAttachment, objectUrls }: SentAttachmentButtonProps): import("react").JSX.Element;
|
|
19
|
+
//# sourceMappingURL=SentAttachment.d.ts.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { ChatAttachment } from '../../core/index.js';
|
|
2
|
+
/** Blob-URL port, so a test (jsdom has no `URL.createObjectURL`) can supply a fake. */
|
|
3
|
+
export interface ObjectUrlPort {
|
|
4
|
+
create: (file: File) => string;
|
|
5
|
+
revoke: (url: string) => void;
|
|
6
|
+
}
|
|
7
|
+
/** The platform's Blob URLs, or `undefined` where they do not exist (jsdom, non-browser hosts). */
|
|
8
|
+
export declare function platformObjectUrls(): ObjectUrlPort | undefined;
|
|
9
|
+
/**
|
|
10
|
+
* The cached file to draw as a thumbnail, or `undefined` when the attachment should stay a chip:
|
|
11
|
+
* not an image (same `looksLikeImageAttachment` rule the preview modal uses), or no bytes cached.
|
|
12
|
+
* @complexity O(1).
|
|
13
|
+
*/
|
|
14
|
+
export declare function sentAttachmentThumbnailSource({ attachment }: {
|
|
15
|
+
attachment: Pick<ChatAttachment, 'kind' | 'name' | 'path'>;
|
|
16
|
+
}, { getSource }?: {
|
|
17
|
+
getSource?: (required: {
|
|
18
|
+
path: string;
|
|
19
|
+
}) => File | undefined;
|
|
20
|
+
}): File | undefined;
|
|
21
|
+
export interface SentAttachmentThumbnail {
|
|
22
|
+
/** Object URL to put in the `<img>`, or `undefined` to render the filename chip. */
|
|
23
|
+
thumbnailUrl: string | undefined;
|
|
24
|
+
/** The `<img>`'s `onError`: an undecodable file falls back to the chip instead of a broken image. */
|
|
25
|
+
onThumbnailError: () => void;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Creates the thumbnail's object URL on mount and revokes it on unmount (an effect, not `useMemo`,
|
|
29
|
+
* so a Strict Mode double render cannot leak a URL - same reasoning as the preview modal's hook).
|
|
30
|
+
* @param input.hostRendered - True when a host `renderAttachment` draws this chip.
|
|
31
|
+
* @param options.objectUrls - Blob-URL port; defaults to the platform's, `undefined` where absent.
|
|
32
|
+
* @param options.getSource - Preview-cache lookup; defaults to the shared cache.
|
|
33
|
+
* @complexity O(1) per attachment.
|
|
34
|
+
*/
|
|
35
|
+
export declare function useSentAttachmentThumbnail({ attachment, hostRendered }: {
|
|
36
|
+
attachment: ChatAttachment;
|
|
37
|
+
hostRendered?: boolean;
|
|
38
|
+
}, { objectUrls, getSource }?: {
|
|
39
|
+
objectUrls?: ObjectUrlPort | undefined;
|
|
40
|
+
getSource?: (required: {
|
|
41
|
+
path: string;
|
|
42
|
+
}) => File | undefined;
|
|
43
|
+
}): SentAttachmentThumbnail;
|
|
44
|
+
//# sourceMappingURL=SentAttachment.hooks.d.ts.map
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Thumbnail state for one attachment on a SENT user message (`SentAttachment.tsx`).
|
|
3
|
+
*
|
|
4
|
+
* A sent image shows as a small thumbnail instead of a filename chip, so the operator can see at a
|
|
5
|
+
* glance which screenshots went with the turn. The bytes come from the same
|
|
6
|
+
* `attachment-preview-cache.ts` the preview modal reads: only files this browser staged for upload in
|
|
7
|
+
* this page session are there (the server cannot hand them back - see that module's doc). Anything
|
|
8
|
+
* else - a non-image, a reload, the cache's eviction cap, a browser that cannot decode the format -
|
|
9
|
+
* stays the filename chip, which still opens the preview modal's honest fallback.
|
|
10
|
+
*/
|
|
11
|
+
import { useEffect, useState } from 'react';
|
|
12
|
+
import { getAttachmentPreviewSource } from '../hooks/attachment-preview-cache.js';
|
|
13
|
+
import { canCreateObjectUrl, looksLikeImageAttachment } from './AttachmentPreviewModal.hooks.js';
|
|
14
|
+
/** The platform's Blob URLs, or `undefined` where they do not exist (jsdom, non-browser hosts). */
|
|
15
|
+
export function platformObjectUrls() {
|
|
16
|
+
if (!canCreateObjectUrl())
|
|
17
|
+
return undefined;
|
|
18
|
+
return { create: (file) => URL.createObjectURL(file), revoke: (url) => URL.revokeObjectURL(url) };
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The cached file to draw as a thumbnail, or `undefined` when the attachment should stay a chip:
|
|
22
|
+
* not an image (same `looksLikeImageAttachment` rule the preview modal uses), or no bytes cached.
|
|
23
|
+
* @complexity O(1).
|
|
24
|
+
*/
|
|
25
|
+
export function sentAttachmentThumbnailSource({ attachment }, { getSource = getAttachmentPreviewSource } = {}) {
|
|
26
|
+
if (!looksLikeImageAttachment({ attachment }))
|
|
27
|
+
return undefined;
|
|
28
|
+
return getSource({ path: attachment.path });
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Creates the thumbnail's object URL on mount and revokes it on unmount (an effect, not `useMemo`,
|
|
32
|
+
* so a Strict Mode double render cannot leak a URL - same reasoning as the preview modal's hook).
|
|
33
|
+
* @param input.hostRendered - True when a host `renderAttachment` draws this chip.
|
|
34
|
+
* @param options.objectUrls - Blob-URL port; defaults to the platform's, `undefined` where absent.
|
|
35
|
+
* @param options.getSource - Preview-cache lookup; defaults to the shared cache.
|
|
36
|
+
* @complexity O(1) per attachment.
|
|
37
|
+
*/
|
|
38
|
+
export function useSentAttachmentThumbnail({ attachment, hostRendered = false }, { objectUrls = platformObjectUrls(), getSource = getAttachmentPreviewSource } = {}) {
|
|
39
|
+
const [failed, setFailed] = useState(false);
|
|
40
|
+
const [thumbnailUrl, setThumbnailUrl] = useState(undefined);
|
|
41
|
+
// A host `renderAttachment` owns the chip's content, so no thumbnail URL is made for it.
|
|
42
|
+
const source = failed || hostRendered || !objectUrls ? undefined : sentAttachmentThumbnailSource({ attachment }, { getSource });
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (!source || !objectUrls) {
|
|
45
|
+
setThumbnailUrl(undefined);
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
const url = objectUrls.create(source);
|
|
49
|
+
setThumbnailUrl(url);
|
|
50
|
+
return () => objectUrls.revoke(url);
|
|
51
|
+
// `objectUrls` is a fresh object per render by default; the file is what the URL is made from.
|
|
52
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
53
|
+
}, [source]);
|
|
54
|
+
return { thumbnailUrl, onThumbnailError: () => setFailed(true) };
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=SentAttachment.hooks.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useT } from '../hooks/context.js';
|
|
3
|
+
import { useSentAttachmentThumbnail } from './SentAttachment.hooks.js';
|
|
4
|
+
/**
|
|
5
|
+
* One attachment on a sent user message: a small image thumbnail when this browser still holds the
|
|
6
|
+
* image's bytes, otherwise the filename chip. Either way a click opens the preview modal, and the
|
|
7
|
+
* accessible name stays "Open {name}".
|
|
8
|
+
*/
|
|
9
|
+
export function SentAttachmentButton({ attachment, onOpen, renderAttachment, objectUrls }) {
|
|
10
|
+
const t = useT();
|
|
11
|
+
const { thumbnailUrl, onThumbnailError } = useSentAttachmentThumbnail({ attachment, hostRendered: renderAttachment !== undefined }, objectUrls === undefined ? {} : { objectUrls });
|
|
12
|
+
return (_jsx("button", { type: "button", className: `jini-message-attachment-chip${thumbnailUrl ? ' is-thumbnail' : ''}`, onClick: () => onOpen(attachment), title: t('Open {name}', { name: attachment.name }), "aria-label": t('Open {name}', { name: attachment.name }), children: renderAttachment ? (renderAttachment(attachment)) : thumbnailUrl ? (_jsx("img", { className: "jini-message-attachment-thumb", src: thumbnailUrl, alt: "", onError: onThumbnailError })) : (attachment.name) }));
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=SentAttachment.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { ToolDisplayRow } from '../tool-call-display.js';
|
|
3
|
+
/**
|
|
4
|
+
* One turn's tool cards folded into a single line ("Used 3 tools ▸") that opens on click — the
|
|
5
|
+
* `toolCallDisplay: 'collapsed'` layout. Props in, JSX out; state and copy are in
|
|
6
|
+
* `useToolCallGroup`.
|
|
7
|
+
*/
|
|
8
|
+
export declare function ToolCallGroup<Row extends ToolDisplayRow>({ rows, renderRow }: {
|
|
9
|
+
rows: readonly Row[];
|
|
10
|
+
renderRow: (row: Row) => ReactNode;
|
|
11
|
+
}): import("react").JSX.Element;
|
|
12
|
+
//# sourceMappingURL=ToolCallGroup.d.ts.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type ToolDisplayRow } from '../tool-call-display.js';
|
|
2
|
+
export interface ToolCallGroupState {
|
|
3
|
+
/** Whether the turn's tool cards are showing under the line. Starts closed. */
|
|
4
|
+
open: boolean;
|
|
5
|
+
toggle: () => void;
|
|
6
|
+
/** "Used 3 tools", "Used 3 tools · 1 failed", or "Running Bash…" while a tool runs. */
|
|
7
|
+
label: string;
|
|
8
|
+
running: boolean;
|
|
9
|
+
failed: boolean;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* State and copy for one collapsed tool line (`toolCallDisplay: 'collapsed'`). The open flag lives
|
|
13
|
+
* in the line's own component, so it lasts as long as the message stays in the transcript — the
|
|
14
|
+
* whole time its conversation is open — and resets when the pane remounts.
|
|
15
|
+
* @complexity O(n) in rows, for the summary.
|
|
16
|
+
*/
|
|
17
|
+
export declare function useToolCallGroup({ rows }: {
|
|
18
|
+
rows: readonly ToolDisplayRow[];
|
|
19
|
+
}, _optional?: {}): ToolCallGroupState;
|
|
20
|
+
//# sourceMappingURL=ToolCallGroup.hooks.d.ts.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { useCallback, useState } from 'react';
|
|
2
|
+
import { useT } from '../hooks/context.js';
|
|
3
|
+
import { summarizeToolGroup, toolGroupLabel } from '../tool-call-display.js';
|
|
4
|
+
/**
|
|
5
|
+
* State and copy for one collapsed tool line (`toolCallDisplay: 'collapsed'`). The open flag lives
|
|
6
|
+
* in the line's own component, so it lasts as long as the message stays in the transcript — the
|
|
7
|
+
* whole time its conversation is open — and resets when the pane remounts.
|
|
8
|
+
* @complexity O(n) in rows, for the summary.
|
|
9
|
+
*/
|
|
10
|
+
export function useToolCallGroup({ rows }, _optional = {}) {
|
|
11
|
+
const t = useT();
|
|
12
|
+
const [open, setOpen] = useState(false);
|
|
13
|
+
const toggle = useCallback(() => setOpen((value) => !value), []);
|
|
14
|
+
const summary = summarizeToolGroup({ rows });
|
|
15
|
+
return {
|
|
16
|
+
open,
|
|
17
|
+
toggle,
|
|
18
|
+
label: toolGroupLabel({ summary, t }),
|
|
19
|
+
running: summary.kind === 'running',
|
|
20
|
+
failed: summary.kind === 'done' && summary.failed > 0,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=ToolCallGroup.hooks.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useToolCallGroup } from './ToolCallGroup.hooks.js';
|
|
3
|
+
import { Icon } from './Icon.js';
|
|
4
|
+
/**
|
|
5
|
+
* One turn's tool cards folded into a single line ("Used 3 tools ▸") that opens on click — the
|
|
6
|
+
* `toolCallDisplay: 'collapsed'` layout. Props in, JSX out; state and copy are in
|
|
7
|
+
* `useToolCallGroup`.
|
|
8
|
+
*/
|
|
9
|
+
export function ToolCallGroup({ rows, renderRow }) {
|
|
10
|
+
const group = useToolCallGroup({ rows });
|
|
11
|
+
return (_jsxs("div", { className: "jini-tool-group", "data-failed": group.failed ? 'true' : undefined, children: [_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: group.label }), _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
|
+
}
|
|
13
|
+
//# sourceMappingURL=ToolCallGroup.js.map
|