@jini-ai/chat 0.6.12 → 0.6.15-rc.1
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 +30 -0
- package/dist/contracts/chat-bubble-style.d.ts +3 -2
- package/dist/contracts/chat-bubble-style.js +3 -2
- package/dist/core/accepted-input.d.ts +20 -1
- package/dist/core/accepted-input.js +65 -17
- package/dist/core/ag-ui/entry.d.ts +2 -4
- package/dist/core/ag-ui/entry.js +11 -5
- package/dist/core/assistant-client/ag-ui-transport.js +6 -1
- package/dist/core/assistant-client/durable-run-subscription.d.ts +2 -1
- package/dist/core/assistant-client/durable-run-subscription.js +3 -1
- package/dist/core/assistant-client/transport.js +5 -1
- package/dist/core/assistant-content.d.ts +8 -1
- package/dist/core/assistant-content.js +17 -4
- package/dist/core/durable-runs/continuation.js +6 -2
- package/dist/core/events.d.ts +3 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +1 -0
- package/dist/core/question-answer.d.ts +8 -0
- package/dist/core/question-answer.js +16 -4
- package/dist/core/run-activity.d.ts +1 -1
- package/dist/core/run-activity.js +46 -4
- package/dist/core/run-events/entry.js +5 -2
- package/dist/embed-widget/vanilla/style.css +1 -1
- package/dist/react/components/CopyMessageButton.d.ts +18 -0
- package/dist/react/components/CopyMessageButton.js +22 -0
- package/dist/react/components/Markdown.js +24 -5
- package/dist/react/components/McpUiSurfaceCard.js +50 -17
- package/dist/react/components/MessageList.hooks.d.ts +2 -1
- package/dist/react/components/MessageList.hooks.js +14 -4
- package/dist/react/components/MessageList.js +5 -3
- package/dist/react/components/MessageRow.d.ts +3 -1
- package/dist/react/components/MessageRow.js +66 -38
- package/dist/react/components/MidRunUserMessage.d.ts +1 -1
- package/dist/react/components/MidRunUserMessage.js +11 -7
- package/dist/react/components/QuestionForm.js +22 -2
- package/dist/react/components/ToolCard.js +21 -8
- package/dist/react/components/mcp-ui-question-input.d.ts +20 -0
- package/dist/react/components/mcp-ui-question-input.js +72 -0
- package/dist/react/ext-event-renderer-registry.d.ts +2 -0
- package/dist/react/features/chat-pane/components/ChatPane.js +10 -3
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +6 -0
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +70 -15
- package/dist/react/hooks/useAnsweredCards.js +36 -20
- package/dist/react/hooks/useConversation.d.ts +6 -0
- package/dist/react/hooks/useConversation.js +10 -3
- package/dist/react/index.d.ts +7 -0
- package/dist/react/index.js +4 -0
- package/dist/react/message-blocks.d.ts +6 -0
- package/dist/react/message-blocks.js +27 -1
- package/dist/server/durable-runs/accept.d.ts +4 -0
- package/dist/server/durable-runs/accept.js +8 -1
- package/dist/server/durable-runs/recover.js +5 -4
- package/package.json +15 -15
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { projectAcceptedInputEvents } from '../../core/accepted-input.js';
|
|
3
3
|
import { MidRunUserMessage, MID_RUN_USER_MESSAGE_EXT_EVENT_NAME } from './MidRunUserMessage.js';
|
|
4
|
+
import { useSurfaceAnswerContexts } from './mcp-ui-question-input.js';
|
|
4
5
|
import { terminalMessageNotice, continuingRunNotice } from '../../core/durable-projection.js';
|
|
5
6
|
import { usageEndLabel, usageLayout, usageLineMeetsActions, useMessageUsage, useUsageSummary } from './MessageRow.usage.js';
|
|
6
7
|
/**
|
|
@@ -36,7 +37,7 @@ import { usageEndLabel, usageLayout, usageLineMeetsActions, useMessageUsage, use
|
|
|
36
37
|
* see that module's header for the conditions.
|
|
37
38
|
*
|
|
38
39
|
* Both message roles also carry a per-message copy affordance
|
|
39
|
-
* (`CopyMessageButton`
|
|
40
|
+
* (`CopyMessageButton`), rendered inside a `.jini-message-actions` row.
|
|
40
41
|
* Assistant rows render that row as an extension seam: `branch`/thumbs-up/
|
|
41
42
|
* thumbs-down are meant to join `copy` there later, as siblings in the same
|
|
42
43
|
* row, once their backends (conversation branching, feedback storage) exist
|
|
@@ -57,16 +58,15 @@ import React, { memo, useMemo } from 'react';
|
|
|
57
58
|
import { assistantContentFromEvents, isAwaitingAnswer, isTerminalRunStatus, splitOnQuestionForms, stripArtifact } from '../../core/index.js';
|
|
58
59
|
import { useToolTimeline } from '../hooks/useToolTimeline.js';
|
|
59
60
|
import { useExtEventGroups } from '../hooks/useExtEventGroups.js';
|
|
60
|
-
import { useCopyToClipboard } from '../hooks/useCopyToClipboard.js';
|
|
61
61
|
import { useRunActivity } from '../hooks/useRunActivity.js';
|
|
62
62
|
import { useAnsweredCards } from '../hooks/useAnsweredCards.js';
|
|
63
|
-
import { interleaveMessageBlocks } from '../message-blocks.js';
|
|
63
|
+
import { interleaveMessageBlocks, splitMidRunBlocks } from '../message-blocks.js';
|
|
64
64
|
import { useT, useExtEventRendererRegistry } from '../hooks/context.js';
|
|
65
65
|
import { ExtEventErrorBoundary } from './ExtEventErrorBoundary.js';
|
|
66
66
|
import { AttachmentPreviewModal } from './AttachmentPreviewModal.js';
|
|
67
67
|
import { SentAttachmentButton } from './SentAttachment.js';
|
|
68
68
|
import { useAttachmentGallery } from './AttachmentGallery.hooks.js';
|
|
69
|
-
import {
|
|
69
|
+
import { CopyMessageButton } from './CopyMessageButton.js';
|
|
70
70
|
import { Markdown } from './Markdown.js';
|
|
71
71
|
import { ToolCard } from './ToolCard.js';
|
|
72
72
|
import { QuestionForm } from './QuestionForm.js';
|
|
@@ -149,25 +149,9 @@ function ThinkingBlock({ text }) {
|
|
|
149
149
|
function messageAwaitingAnswer({ runStreaming, runInProgress, events }, { isAnsweredCard } = {}) {
|
|
150
150
|
return runStreaming === true && runInProgress && isAwaitingAnswer({ events }, { isAnsweredCard });
|
|
151
151
|
}
|
|
152
|
-
function AssistantMessageFooter({ message, usageEvent, showUsageTotal, terminalNotice, pending, runInProgress, runStreaming, visibleContent, isAnsweredCard }, _optional = {}) {
|
|
152
|
+
function AssistantMessageFooter({ message, usageEvent, showUsageTotal, terminalNotice, pending, runInProgress, runStreaming, runSending, visibleContent, isAnsweredCard }, _optional = {}) {
|
|
153
153
|
const t = useT();
|
|
154
|
-
return _jsxs(_Fragment, { children: [usageEvent ? _jsx(UsageSummary, { usage: usageEvent, showTotal: showUsageTotal, message: message, awaitingAnswer: isAwaitingAnswer({ events: message.events }, { isAnsweredCard }) }) : null, terminalNotice ? _jsx("div", { className: "jini-message-error", role: "status", children: t(terminalNotice) }) : null, !pending && !runInProgress ? (_jsx("div", { className: "jini-message-actions jini-message-actions--assistant", children: _jsx(CopyMessageButton, { text: visibleContent, label: t('Copy message') }) })) : null, runInProgress ? _jsx(RunActivityLine, { events: message.events, active: pending || (runStreaming === true && runInProgress), isAnsweredCard: isAnsweredCard }) : null] });
|
|
155
|
-
}
|
|
156
|
-
/**
|
|
157
|
-
* Quiet, icon-only copy affordance shared by the user- and assistant-message
|
|
158
|
-
* action rows below. Delegates the actual clipboard write (and its
|
|
159
|
-
* secure-context fallback) to `useCopyToClipboard`; this component only owns
|
|
160
|
-
* the icon swap and the accessible announcement of the transient state.
|
|
161
|
-
* @param props.text - Exact string to copy, verbatim.
|
|
162
|
-
* @param props.label - Idle accessible name (already translated by the caller).
|
|
163
|
-
* @complexity O(1) — one child hook call, no branching beyond the copied-state read.
|
|
164
|
-
*/
|
|
165
|
-
function CopyMessageButton({ text, label }) {
|
|
166
|
-
const t = useT();
|
|
167
|
-
const { copied, copy } = useCopyToClipboard();
|
|
168
|
-
return (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "jini-message-action-btn", "aria-label": copied ? t('Copied') : label, onClick: () => {
|
|
169
|
-
void copy(text);
|
|
170
|
-
}, children: _jsx(Icon, { name: copied ? 'check' : 'copy' }) }), _jsx("span", { className: "jini-message-copy-status", "aria-live": "polite", children: copied ? t('Copied to clipboard') : '' })] }));
|
|
154
|
+
return _jsxs(_Fragment, { children: [usageEvent ? _jsx(UsageSummary, { usage: usageEvent, showTotal: showUsageTotal, message: message, awaitingAnswer: isAwaitingAnswer({ events: message.events }, { isAnsweredCard }) }) : null, terminalNotice ? _jsx("div", { className: "jini-message-error", role: "status", children: t(terminalNotice) }) : null, !pending && !runInProgress ? (_jsx("div", { className: "jini-message-actions jini-message-actions--assistant", children: _jsx(CopyMessageButton, { text: visibleContent, label: t('Copy message') }) })) : null, runInProgress ? _jsx(RunActivityLine, { events: message.events, active: pending || (runStreaming === true && runInProgress), isAnsweredCard: isAnsweredCard, sending: runSending }) : null] });
|
|
171
155
|
}
|
|
172
156
|
/** Stable empty list for a message with no attachments. */
|
|
173
157
|
const NO_ATTACHMENTS = [];
|
|
@@ -176,13 +160,14 @@ const NO_ATTACHMENTS = [];
|
|
|
176
160
|
* re-renders per composer keystroke) only re-renders rows whose props actually changed. Messages
|
|
177
161
|
* are replaced, never mutated, so identity is a sound change signal.
|
|
178
162
|
*/
|
|
179
|
-
export const MessageRow = memo(function MessageRow({ showUsageTotal = true, showUsageLine = true, toolCallDisplay = 'show', showThinking = false, runStreaming = false, runSucceeded = false, questionFormInteractive = false, ...props }) {
|
|
163
|
+
export const MessageRow = memo(function MessageRow({ showUsageTotal = true, showUsageLine = true, toolCallDisplay = 'show', showThinking = false, runStreaming = false, runSending = false, runSucceeded = false, questionFormInteractive = false, ...props }) {
|
|
164
|
+
const answerContexts = useSurfaceAnswerContexts({ events: props.message.events });
|
|
180
165
|
const message = useMemo(() => props.message.events === undefined ? props.message : {
|
|
181
|
-
...props.message, events: projectAcceptedInputEvents({ events: props.message.events }),
|
|
182
|
-
}, [props.message]);
|
|
166
|
+
...props.message, events: projectAcceptedInputEvents({ events: props.message.events }, answerContexts),
|
|
167
|
+
}, [props.message, answerContexts]);
|
|
183
168
|
if (message.role === 'user')
|
|
184
169
|
return _jsx(UserMessageRow, { ...props, message: message });
|
|
185
|
-
return _jsx(AssistantMessageRow, { ...props, message: message, showUsageTotal, showUsageLine, toolCallDisplay, showThinking, runStreaming, runSucceeded, questionFormInteractive });
|
|
170
|
+
return _jsx(AssistantMessageRow, { ...props, message: message, showUsageTotal, showUsageLine, toolCallDisplay, showThinking, runStreaming, runSending, runSucceeded, questionFormInteractive });
|
|
186
171
|
});
|
|
187
172
|
function UserMessageRow({ message, renderAttachment }) {
|
|
188
173
|
const t = useT();
|
|
@@ -193,7 +178,7 @@ function UserMessageRow({ message, renderAttachment }) {
|
|
|
193
178
|
const gallery = useAttachmentGallery({ attachments: message.attachments ?? NO_ATTACHMENTS });
|
|
194
179
|
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: gallery.open, 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') }) }), gallery.opened ? (_jsx(AttachmentPreviewModal, { attachment: gallery.opened, onClose: gallery.close, navigation: gallery.navigation })) : null] }));
|
|
195
180
|
}
|
|
196
|
-
function AssistantMessageRow({ message, usageSummary, showUsageTotal, showUsageLine, toolCallDisplay, showThinking, runStreaming, runSucceeded, questionFormInteractive, questionFormSubmittedAnswers, onQuestionFormSubmit, projectFileNames, onRequestOpenFile, }) {
|
|
181
|
+
function AssistantMessageRow({ message, usageSummary, showUsageTotal, showUsageLine, toolCallDisplay, showThinking, runStreaming, runSending, runSucceeded, questionFormInteractive, questionFormSubmittedAnswers, onQuestionFormSubmit, projectFileNames, onRequestOpenFile, }) {
|
|
197
182
|
const registry = useExtEventRendererRegistry();
|
|
198
183
|
const messageUsage = useMessageUsage({ message, ...(usageSummary === undefined ? {} : { summary: usageSummary }) });
|
|
199
184
|
const timeline = useToolTimeline({ events: message.events }, { runStreaming, runSucceeded });
|
|
@@ -219,17 +204,20 @@ function AssistantMessageRow({ message, usageSummary, showUsageTotal, showUsageL
|
|
|
219
204
|
? _jsx(ToolCallGroup, { rows: rows, renderRow: renderToolCard, failureOnly: toolCallDisplay === 'hidden' }, key)
|
|
220
205
|
: _jsx("div", { className: "jini-message-tools", children: rows.map(renderToolCard) }, key));
|
|
221
206
|
const flatTools = flatToolGroups({ message, rows: timeline.rows, registry, displayBlocks }, { toolCallDisplay });
|
|
207
|
+
const flatExtGroups = extGroups.filter(group => group.name !== MID_RUN_USER_MESSAGE_EXT_EVENT_NAME);
|
|
222
208
|
const renderExtGroup = (group) => {
|
|
209
|
+
const transcriptRow = group.name === MID_RUN_USER_MESSAGE_EXT_EVENT_NAME;
|
|
223
210
|
const renderer = registry.get({ name: group.name });
|
|
224
211
|
if (!renderer && group.name === MID_RUN_USER_MESSAGE_EXT_EVENT_NAME)
|
|
225
|
-
return _jsx(MidRunUserMessage, { events: group.events, runStreaming:
|
|
212
|
+
return _jsx(MidRunUserMessage, { events: group.events, runStreaming: runInProgress, transcriptRow: true }, group.slot);
|
|
226
213
|
if (!renderer)
|
|
227
214
|
return null;
|
|
228
215
|
const answerText = answeredCards.answerText(group.slot);
|
|
229
216
|
const node = renderer({
|
|
230
217
|
name: group.name,
|
|
231
218
|
events: group.events,
|
|
232
|
-
runStreaming: answeredCards.runStreaming,
|
|
219
|
+
runStreaming: group.name === MID_RUN_USER_MESSAGE_EXT_EVENT_NAME ? runInProgress : answeredCards.runStreaming,
|
|
220
|
+
...(group.name === MID_RUN_USER_MESSAGE_EXT_EVENT_NAME ? { transcriptRow: true } : {}),
|
|
233
221
|
runSucceeded,
|
|
234
222
|
awaitingAnswer,
|
|
235
223
|
runId: message.runId,
|
|
@@ -237,8 +225,7 @@ function AssistantMessageRow({ message, usageSummary, showUsageTotal, showUsageL
|
|
|
237
225
|
answered: answeredCards.isAnswered(group.slot),
|
|
238
226
|
onAnswered: () => answeredCards.markAnswered(group.slot),
|
|
239
227
|
onAnswerText: (text) => answeredCards.markAnswered(group.slot, text),
|
|
240
|
-
...(answeredCards
|
|
241
|
-
...(answerText === undefined ? {} : { answerText }),
|
|
228
|
+
...answerRenderProps({ answeredCards, slot: group.slot, answerText }),
|
|
242
229
|
});
|
|
243
230
|
if (!node)
|
|
244
231
|
return null;
|
|
@@ -246,7 +233,7 @@ function AssistantMessageRow({ message, usageSummary, showUsageTotal, showUsageL
|
|
|
246
233
|
// `key` includes the event count so a group that failed on an earlier, shorter event list
|
|
247
234
|
// gets a fresh boundary instance (not the still-tripped one) once a new event actually
|
|
248
235
|
// arrives for it, instead of staying tombstoned for the message's whole lifetime.
|
|
249
|
-
_jsx(ExtEventErrorBoundary, { name: group.name, children: _jsx("div", { children: node }) }, `${group.slot}:${group.events.length}`));
|
|
236
|
+
_jsx(ExtEventErrorBoundary, { name: group.name, children: transcriptRow ? node : _jsx("div", { children: node }) }, `${group.slot}:${group.events.length}`));
|
|
250
237
|
};
|
|
251
238
|
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}`)));
|
|
252
239
|
const renderBlock = (block) => {
|
|
@@ -262,25 +249,66 @@ function AssistantMessageRow({ message, usageSummary, showUsageTotal, showUsageL
|
|
|
262
249
|
return renderTools(block.rows, block.key);
|
|
263
250
|
if (block.kind === 'thinking')
|
|
264
251
|
return _jsx(ThinkingBlock, { text: block.text }, block.key);
|
|
252
|
+
return renderExtBlock(block);
|
|
253
|
+
};
|
|
254
|
+
// A mid-run bubble is its own transcript row, so it skips the ext-events wrapper and its spacing.
|
|
255
|
+
const renderExtBlock = (block) => {
|
|
265
256
|
const group = extGroups.find((g) => g.slot === block.slot);
|
|
266
257
|
if (!group)
|
|
267
258
|
return null;
|
|
259
|
+
if (group.name === MID_RUN_USER_MESSAGE_EXT_EVENT_NAME)
|
|
260
|
+
return _jsx(React.Fragment, { children: renderExtGroup(group) }, block.key);
|
|
268
261
|
return (_jsx("div", { className: "jini-message-ext-events", children: renderExtGroup(group) }, block.key));
|
|
269
262
|
};
|
|
270
|
-
|
|
263
|
+
const renderAssistantSection = ({ section, key, footer }) => (_jsxs("div", { className: usageLineMeetsActions({ footer: footerUsage, terminalNotice, pending, runInProgress }, { showTotal: showUsageTotal })
|
|
271
264
|
? 'jini-message jini-message-assistant jini-message-assistant--usage-actions'
|
|
272
|
-
: 'jini-message jini-message-assistant', "data-message-id": message.id, "data-run-status": message.runStatus, "data-agent-element": `chat-message-${message.id}`, "data-agent-role": "region", "data-agent-label": "A reply from the assistant", children: [message.agentName ? _jsx("div", { className: "jini-message-agent", children: message.agentName }) : null,
|
|
273
|
-
?
|
|
274
|
-
: (_jsxs(_Fragment, { children: [renderTextSegments(visibleContent, 'text'), flatTools.map((group) => renderTools(group.rows, group.key)),
|
|
265
|
+
: '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, section
|
|
266
|
+
? section.map(renderBlock)
|
|
267
|
+
: (_jsxs(_Fragment, { children: [renderTextSegments(visibleContent, 'text'), flatTools.map((group) => renderTools(group.rows, group.key)), flatExtGroups.length > 0 ? _jsx("div", { className: "jini-message-ext-events", children: flatExtGroups.map(renderExtGroup) }) : null] })), footer && _jsx(AssistantMessageFooter, { message, usageEvent: footerUsage, showUsageTotal, terminalNotice, pending, runInProgress, runStreaming, runSending, visibleContent, isAnsweredCard: answeredCards.isAnsweredCard })] }, key));
|
|
268
|
+
// The lossless fallback keeps the host's text intact; user receipts still own separate rows,
|
|
269
|
+
// in their original receipt order, outside the assistant's actions and usage footer.
|
|
270
|
+
if (blocks === null)
|
|
271
|
+
return _jsxs(_Fragment, { children: [renderAssistantSection({ section: null, key: message.id, footer: true }), extGroups.filter(group => group.name === MID_RUN_USER_MESSAGE_EXT_EVENT_NAME).map(renderExtGroup)] });
|
|
272
|
+
// A reply boundary without reported usage renders nothing; it must not create an empty row's gap.
|
|
273
|
+
const visibleBlocks = blocks.filter(block => block.kind !== 'reply-end' || (showUsageLine && usage.replies[block.index] !== undefined));
|
|
274
|
+
const { sections, footerIndex } = transcriptSections({ blocks: visibleBlocks, runInProgress });
|
|
275
|
+
return _jsx(_Fragment, { children: sections.map((section, index) => isUserSection({ section })
|
|
276
|
+
? _jsx(React.Fragment, { children: section.map(renderBlock) }, section[0].key)
|
|
277
|
+
: renderAssistantSection({ section, key: section[0]?.key ?? message.id, footer: index === footerIndex })) });
|
|
278
|
+
}
|
|
279
|
+
/** Preserve optional render-prop presence for custom extension renderers. */
|
|
280
|
+
function answerRenderProps({ answeredCards, slot, answerText }, _options = {}) {
|
|
281
|
+
return { ...(answeredCards.isRecorded(slot) ? { answerRecorded: true } : {}),
|
|
282
|
+
...(answerText === undefined ? {} : { answerText }) };
|
|
283
|
+
}
|
|
284
|
+
/** Place the one footer on the last assistant section, never below a completed trailing input. */
|
|
285
|
+
function transcriptSections({ blocks, runInProgress }, _options = {}) {
|
|
286
|
+
const sections = splitMidRunBlocks({ blocks });
|
|
287
|
+
if (sections.length === 0)
|
|
288
|
+
sections.push([]);
|
|
289
|
+
// A live trailing input starts the next reply. A completed run needs no empty assistant row
|
|
290
|
+
// (its actions/usage used to leave a large gap below the final user bubble).
|
|
291
|
+
if (runInProgress && isUserSection({ section: sections.at(-1) }))
|
|
292
|
+
sections.push([]);
|
|
293
|
+
const footerIndex = sections.findLastIndex(section => !isUserSection({ section }));
|
|
294
|
+
return { sections, footerIndex };
|
|
295
|
+
}
|
|
296
|
+
/** A user receipt occupies its own section; every other block belongs to an assistant row. */
|
|
297
|
+
function isUserSection({ section }, _options = {}) {
|
|
298
|
+
const block = section?.[0];
|
|
299
|
+
return block?.kind === 'ext' && block.name === MID_RUN_USER_MESSAGE_EXT_EVENT_NAME;
|
|
275
300
|
}
|
|
276
301
|
/**
|
|
277
302
|
* The live "what is it doing" line under a running reply (see `core/run-activity.ts`). Replaces the
|
|
278
303
|
* old "Thinking…" placeholder, which vanished for good as soon as the first tool row appeared.
|
|
279
304
|
* Kept a separate component so its one-second tick re-renders only this line, not the whole row.
|
|
280
305
|
*/
|
|
281
|
-
function RunActivityLine({ events, active, isAnsweredCard }) {
|
|
306
|
+
function RunActivityLine({ events, active, isAnsweredCard, sending }) {
|
|
307
|
+
const t = useT();
|
|
282
308
|
const activityLabel = useRunActivity({ events: events, active: active }, { isAnsweredCard });
|
|
283
|
-
|
|
309
|
+
// Until the server accepts the start the turn is still on its way: "Thinking…" there read as the
|
|
310
|
+
// model working while the text had not even arrived (owner, 2026-10-27). No timeout.
|
|
311
|
+
const label = active && sending ? t('Sending…') : continuingRunNotice({ events, active }, {}) ?? activityLabel;
|
|
284
312
|
if (label === null)
|
|
285
313
|
return null;
|
|
286
314
|
return (_jsx("div", { className: "jini-message-pending jini-run-activity", "aria-live": "off", children: label }));
|
|
@@ -7,7 +7,7 @@ export declare function midRunUserMessageSlotKey({ data }: {
|
|
|
7
7
|
data: unknown;
|
|
8
8
|
}): string | undefined;
|
|
9
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, runStreaming }: Pick<ExtEventRenderProps, 'events'> & Partial<Pick<ExtEventRenderProps, 'runStreaming'>>): import("react").JSX.Element | null;
|
|
10
|
+
export declare function MidRunUserMessage({ events, runStreaming, transcriptRow }: Pick<ExtEventRenderProps, 'events'> & Partial<Pick<ExtEventRenderProps, 'runStreaming' | 'transcriptRow'>>): import("react").JSX.Element | null;
|
|
11
11
|
/**
|
|
12
12
|
* Registers {@link MidRunUserMessage} against the ext-event registry.
|
|
13
13
|
*
|
|
@@ -3,15 +3,16 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
3
3
|
* @module MidRunUserMessage
|
|
4
4
|
*
|
|
5
5
|
* Renders a message the human sent while a run was going (`ChatPane`'s `deliverMidRunMessage`) as a
|
|
6
|
-
* user
|
|
6
|
+
* separate user row between assistant sections, at the point in the stream where the live agent received it.
|
|
7
7
|
* The daemon records each one as a `user_message` run event (`AgentExecutor.sendUserMessage`), which
|
|
8
8
|
* the run-event translator surfaces as `kind: 'ext'` with that name; this claims it.
|
|
9
9
|
*
|
|
10
|
-
* It is an event of the assistant turn, not a `ChatMessage` of its own, because the agent folded it
|
|
10
|
+
* It is stored as an event of the assistant turn, not a `ChatMessage` of its own, because the agent folded it
|
|
11
11
|
* into the turn it was already running — there is no separate reply to hang off it.
|
|
12
12
|
*/
|
|
13
13
|
import { acceptedInputEvent } from '../../core/accepted-input.js';
|
|
14
14
|
import { SentAttachmentButton } from './SentAttachment.js';
|
|
15
|
+
import { CopyMessageButton } from './CopyMessageButton.js';
|
|
15
16
|
import { AttachmentPreviewModal } from './AttachmentPreviewModal.js';
|
|
16
17
|
import { useAttachmentGallery } from './AttachmentGallery.hooks.js';
|
|
17
18
|
import { useT } from '../hooks/context.js';
|
|
@@ -29,7 +30,7 @@ export function midRunUserMessageSlotKey({ data }) {
|
|
|
29
30
|
return field(data, 'id');
|
|
30
31
|
}
|
|
31
32
|
/** The bubble for one mid-run message — the same classes as a sent user message, so it reads as one. */
|
|
32
|
-
export function MidRunUserMessage({ events, runStreaming = false }) {
|
|
33
|
+
export function MidRunUserMessage({ events, runStreaming = false, transcriptRow = false }) {
|
|
33
34
|
const data = events.at(-1);
|
|
34
35
|
const text = field(data, 'text');
|
|
35
36
|
if (text === undefined)
|
|
@@ -38,13 +39,16 @@ export function MidRunUserMessage({ events, runStreaming = false }) {
|
|
|
38
39
|
// MOVE: A single live delivery may contain several queued inputs; display their original receipts.
|
|
39
40
|
const inputs = receipt?.data.inputs ?? [{ id: field(data, 'id') ?? 'mid-run', text }];
|
|
40
41
|
// A run that ended without reading the message hands it to the next turn; it is no longer queued here.
|
|
41
|
-
return _jsx(_Fragment, { children: inputs.map(input => _jsx(MidRunInput, { input: input, queued: runStreaming && (input.deliveryState ?? receipt?.data.deliveryState) === 'queued' }, input.id)) });
|
|
42
|
+
return _jsx(_Fragment, { children: inputs.map(input => _jsx(MidRunInput, { input: input, transcriptRow: transcriptRow, queued: runStreaming && (input.deliveryState ?? receipt?.data.deliveryState) === 'queued' }, input.id)) });
|
|
42
43
|
}
|
|
43
|
-
|
|
44
|
+
/** One receipt's text, files and shared copy action; queued state belongs to this input alone.
|
|
45
|
+
* @complexity O(a) time/space for a attachments; clipboard and gallery effects stay with their owners.
|
|
46
|
+
*/
|
|
47
|
+
function MidRunInput({ input, queued, transcriptRow }) {
|
|
44
48
|
const t = useT();
|
|
45
49
|
const attachments = input.attachments ?? [];
|
|
46
50
|
const gallery = useAttachmentGallery({ attachments });
|
|
47
|
-
return (_jsxs("div", { className:
|
|
51
|
+
return (_jsxs("div", { className: `jini-message jini-message-user${transcriptRow ? '' : ' jini-message-user--mid-run'}`, "data-agent-role": "region", "data-agent-label": "A message from the user, sent while the agent was working", children: [queued && _jsx("div", { className: "jini-message-user__queued", role: "status", children: t('Queued — read after the current step') }), _jsx("div", { className: "jini-message-content", children: input.text }), attachments.length > 0 && _jsx("div", { className: "jini-message-attachments", children: attachments.map(attachment => _jsx(SentAttachmentButton, { attachment: attachment, onOpen: gallery.open }, attachment.path)) }), _jsx("div", { className: "jini-message-actions jini-message-actions--user", children: _jsx(CopyMessageButton, { text: input.text, label: t('Copy message') }) }), gallery.opened !== null && _jsx(AttachmentPreviewModal, { attachment: gallery.opened, onClose: gallery.close, navigation: gallery.navigation })] }));
|
|
48
52
|
}
|
|
49
53
|
/**
|
|
50
54
|
* Registers {@link MidRunUserMessage} against the ext-event registry.
|
|
@@ -52,6 +56,6 @@ function MidRunInput({ input, queued }) {
|
|
|
52
56
|
* @returns An unregister handle, so a test or a hot reload can dispose cleanly.
|
|
53
57
|
*/
|
|
54
58
|
export function registerMidRunUserMessageRenderer() {
|
|
55
|
-
return registerExtEventRenderer({ name: MID_RUN_USER_MESSAGE_EXT_EVENT_NAME, renderer: (props) => _jsx(MidRunUserMessage, { events: props.events, runStreaming: props.runStreaming }) }, { slotKey: midRunUserMessageSlotKey });
|
|
59
|
+
return registerExtEventRenderer({ name: MID_RUN_USER_MESSAGE_EXT_EVENT_NAME, renderer: (props) => _jsx(MidRunUserMessage, { events: props.events, runStreaming: props.runStreaming, transcriptRow: props.transcriptRow === true }) }, { slotKey: midRunUserMessageSlotKey });
|
|
56
60
|
}
|
|
57
61
|
//# sourceMappingURL=MidRunUserMessage.js.map
|
|
@@ -17,6 +17,7 @@ import { useUserTextGuard } from '../hooks/useUserTextGuard.js';
|
|
|
17
17
|
import { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from 'react';
|
|
18
18
|
import { formatFormAnswers, formOptionValueForLabel, redactQuestionAnswers, isSecretQuestion, withoutSecretQuestionDefaults, questionTextInputProps, questionTextareaProps } from '../../core/index.js';
|
|
19
19
|
import { useT } from '../hooks/context.js';
|
|
20
|
+
import { SecretInput, SecretTextarea } from '@jini-ai/ui';
|
|
20
21
|
export const QuestionForm = forwardRef(function QuestionForm({ form: incomingForm, interactive, submittedAnswers, hideInternalSubmit = false, draftAnswers, onReadyChange, onDraftChange, onAnswerChange, onSubmit, redactUserText, onSecretRedacted }, ref) {
|
|
21
22
|
const t = useT();
|
|
22
23
|
const form = useMemo(() => withoutSecretQuestionDefaults({ form: incomingForm }), [incomingForm]);
|
|
@@ -172,6 +173,25 @@ function DirectionCardsControl(ctx) {
|
|
|
172
173
|
return null;
|
|
173
174
|
return (_jsxs(_Fragment, { children: [_jsx("div", { className: "qf-direction-cards", children: q.cards.map((card) => (_jsx(DirectionCardView, { card: card, formId: formId, questionId: q.id, selected: value === card.id || value === card.label, disabled: locked, onSelect: () => update(q.id, card.id) }, card.id))) }), singleCustomChoice(ctx)] }));
|
|
174
175
|
}
|
|
176
|
+
/** A credential-shaped text question gets the shared eye toggle (hidden by default). */
|
|
177
|
+
function TextControl({ q, value, locked, update, t }) {
|
|
178
|
+
const shared = { className: 'qf-input', value: stringValue(value), placeholder: q.placeholder, disabled: locked, onChange: (e) => update(q.id, e.target.value) };
|
|
179
|
+
const props = questionTextInputProps({ question: q });
|
|
180
|
+
if (props.type === 'password') {
|
|
181
|
+
const { type: _type, ...secretProps } = props;
|
|
182
|
+
return _jsx(SecretInput, { ...secretProps, ...shared, "aria-label": q.label, label: q.label, toggleLabels: { show: `${t('Show')} ${q.label}`, hide: `${t('Hide')} ${q.label}` } });
|
|
183
|
+
}
|
|
184
|
+
return _jsx("input", { ...props, ...shared });
|
|
185
|
+
}
|
|
186
|
+
/** A credential-shaped textarea gets the same eye toggle; `SecretTextarea` owns the masking style. */
|
|
187
|
+
function TextareaControl({ q, value, locked, update, t }) {
|
|
188
|
+
const shared = { className: 'qf-textarea', value: stringValue(value), placeholder: q.placeholder, disabled: locked, rows: 3, onChange: (e) => update(q.id, e.target.value) };
|
|
189
|
+
const props = questionTextareaProps({ question: q });
|
|
190
|
+
if (isSecretQuestion({ question: q })) {
|
|
191
|
+
return _jsx(SecretTextarea, { ...props, ...shared, "aria-label": q.label, label: q.label, toggleLabels: { show: `${t('Show')} ${q.label}`, hide: `${t('Hide')} ${q.label}` } });
|
|
192
|
+
}
|
|
193
|
+
return _jsx("textarea", { ...props, ...shared });
|
|
194
|
+
}
|
|
175
195
|
/** date/time/datetime-local and url/email/tel share one plain input whose `type` is the question type. */
|
|
176
196
|
function TypedInputControl({ q, value, locked, update }) {
|
|
177
197
|
return _jsx("input", { type: q.type, className: "qf-input", value: stringValue(value), placeholder: q.placeholder, disabled: locked, onChange: (e) => update(q.id, e.target.value) });
|
|
@@ -181,7 +201,7 @@ const QUESTION_CONTROLS = {
|
|
|
181
201
|
checkbox: CheckboxControl,
|
|
182
202
|
select: SelectControl,
|
|
183
203
|
'direction-cards': DirectionCardsControl,
|
|
184
|
-
text:
|
|
204
|
+
text: TextControl,
|
|
185
205
|
number: ({ q, value, locked, update }) => _jsx("input", { type: "number", className: "qf-input", value: stringValue(value), placeholder: q.placeholder, min: q.min, max: q.max, step: q.step, disabled: locked, onChange: (e) => update(q.id, e.target.value) }),
|
|
186
206
|
range: ({ q, value, locked, update }) => (_jsxs("div", { className: "qf-range-wrap", children: [_jsx("input", { type: "range", className: "qf-range", value: rangeValue(q, value), min: q.min, max: q.max, step: q.step, disabled: locked, onChange: (e) => update(q.id, e.target.value) }), _jsx("output", { className: "qf-range-value", children: rangeValue(q, value) })] })),
|
|
187
207
|
date: TypedInputControl,
|
|
@@ -193,7 +213,7 @@ const QUESTION_CONTROLS = {
|
|
|
193
213
|
color: ({ q, value, locked, update }) => _jsx("input", { type: "color", className: "qf-color", value: normalizeColorInputValue(value), disabled: locked, onChange: (e) => update(q.id, e.target.value) }),
|
|
194
214
|
file: ({ q, value, locked, selectFiles }) => (_jsxs("div", { className: "qf-file-wrap", children: [_jsx("input", { type: "file", className: "qf-file", multiple: q.multiple, accept: q.accept, disabled: locked, onChange: (e) => selectFiles(q, e.target.files) }), fileValueLabel(value) ? _jsx("div", { className: "qf-file-summary", children: fileValueLabel(value) }) : null] })),
|
|
195
215
|
switch: ({ q, value, locked, update }) => (_jsxs("label", { className: "qf-switch", children: [_jsx("input", { type: "checkbox", role: "switch", checked: value === 'true', disabled: locked, onChange: (e) => update(q.id, e.target.checked ? 'true' : 'false') }), _jsx("span", { "aria-hidden": true })] })),
|
|
196
|
-
textarea:
|
|
216
|
+
textarea: TextareaControl,
|
|
197
217
|
};
|
|
198
218
|
function OptionCopy({ option }) {
|
|
199
219
|
return (_jsxs("span", { className: "qf-chip-copy", children: [_jsx("span", { children: option.label }), option.description ? _jsx("span", { className: "qf-chip-desc", children: option.description }) : null] }));
|
|
@@ -21,7 +21,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
21
21
|
* generic rendering for it.
|
|
22
22
|
*/
|
|
23
23
|
import { useId, useState } from 'react';
|
|
24
|
-
import { describeCollapsedToolCall, describeToolCallDetail,
|
|
24
|
+
import { COLLAPSED_DETAIL_MAX_CHARS, describeCollapsedToolCall, describeToolCallDetail, isDelegatedWrapperToolName, isTodoWriteToolName, parseTodoWriteInput, redactUserText, toRenderProps } from '../../core/index.js';
|
|
25
25
|
import { useT, useToolRendererRegistry } from '../hooks/context.js';
|
|
26
26
|
import { Icon } from './Icon.js';
|
|
27
27
|
import { TodoCard } from './TodoCard.js';
|
|
@@ -146,7 +146,7 @@ function DelegatedToolCard({ name, input, result, runStreaming, runSucceeded })
|
|
|
146
146
|
const args = isDelegatedWrapperToolName({ name: name }) ? wrapper.input : input;
|
|
147
147
|
const target = primaryTarget(args);
|
|
148
148
|
const isRunning = runStreaming && !result;
|
|
149
|
-
return (_jsx(ExpandableCard, { className: "op-jini-tool", call: { input: args ?? {}, result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }, head: _jsxs(_Fragment, { children: [_jsx(ResultBadge, { result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }), _jsxs("span", { className: `op-title${isRunning ? ' shimmer-text' : ''}`, children: [t('Tool call'), " \u00B7 ", humanizeToolId(toolId)] }), target ? _jsx("span", { className: "op-meta", children: target }) : null] }), after: _jsx(FileErrorDetail, { result: result }), children: _jsx(ToolCallDetail, { input: args ?? {}, result: result, runStreaming: runStreaming, runSucceeded: runSucceeded, children: _jsx(ToolResultMedia, { result: result }) }) }));
|
|
149
|
+
return (_jsx(ExpandableCard, { className: "op-jini-tool", call: { input: args ?? {}, result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }, head: _jsxs(_Fragment, { children: [_jsx(ResultBadge, { result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }), _jsxs("span", { className: `op-title${isRunning ? ' shimmer-text' : ''}`, children: [t('Tool call'), " \u00B7 ", humanizeToolId(toolId)] }), target ? _jsx("span", { className: "op-meta", children: headSummary({ text: target }) }) : null] }), after: _jsx(FileErrorDetail, { result: result }), children: _jsx(ToolCallDetail, { input: args ?? {}, result: result, runStreaming: runStreaming, runSucceeded: runSucceeded, children: _jsx(ToolResultMedia, { result: result }) }) }));
|
|
150
150
|
}
|
|
151
151
|
function SearchToolsCard({ input, result, runStreaming, runSucceeded }) {
|
|
152
152
|
const t = useT();
|
|
@@ -211,7 +211,7 @@ function BashCard({ input, result, runStreaming, runSucceeded }) {
|
|
|
211
211
|
const command = obj.command ?? '';
|
|
212
212
|
const desc = obj.description;
|
|
213
213
|
const isRunning = runStreaming && !result;
|
|
214
|
-
return (_jsx(ExpandableCard, { className: "op-bash", call: { input: input, result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }, head: _jsxs(_Fragment, { children: [_jsx(ResultBadge, { result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }), _jsx("span", { className: `op-title${isRunning ? ' shimmer-text' : ''}`, children: t('Bash') }), desc ? _jsx("span", { className: "op-meta op-desc", children: desc }) : null] }), children:
|
|
214
|
+
return (_jsx(ExpandableCard, { className: "op-bash", call: { input: input, result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }, head: _jsxs(_Fragment, { children: [_jsx(ResultBadge, { result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }), _jsx("span", { className: `op-title${isRunning ? ' shimmer-text' : ''}`, children: t('Bash') }), desc ? _jsx("span", { className: "op-meta op-desc", children: desc }) : null] }), children: _jsx(ToolCallDetail, { input: command, result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }) }));
|
|
215
215
|
}
|
|
216
216
|
function GlobCard({ input, result, runStreaming, runSucceeded }) {
|
|
217
217
|
const t = useT();
|
|
@@ -236,7 +236,7 @@ function WebSearchCard({ input, result, runStreaming, runSucceeded }) {
|
|
|
236
236
|
/** Any tool this package has no family card for — an MCP tool, or a card tool such as `assistant_ask_choice` whose form renders elsewhere in the turn. The row still opens to the full call. */
|
|
237
237
|
function GenericCard({ name, input, result, runStreaming, runSucceeded }) {
|
|
238
238
|
const summary = describeInput(input);
|
|
239
|
-
return (_jsx(ExpandableCard, { className: "op-generic", call: { input: input, result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }, head: _jsxs(_Fragment, { children: [_jsx(ResultBadge, { result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }), _jsx("span", { className: "op-title", children: name }), summary ? _jsx("span", { className: "op-meta", children:
|
|
239
|
+
return (_jsx(ExpandableCard, { className: "op-generic", call: { input: input, result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }, head: _jsxs(_Fragment, { children: [_jsx(ResultBadge, { result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }), _jsx("span", { className: "op-title", children: name }), summary ? _jsx("span", { className: "op-meta", children: headSummary({ text: summary }) }) : null] }), after: _jsx(ToolResultMedia, { result: result }), children: _jsx(ToolCallDetail, { input: input, result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }) }));
|
|
240
240
|
}
|
|
241
241
|
/**
|
|
242
242
|
* The one row shell every card shares: a real `<button>` head (Enter/Space toggle it) that reports
|
|
@@ -260,7 +260,9 @@ function CollapsedReaderText({ call }) {
|
|
|
260
260
|
/** The call's input, its result (or why there is none), and any extra `children` — see `describeToolCallDetail` for the redaction and formatting rules. */
|
|
261
261
|
function ToolCallDetail({ input, result, runStreaming, runSucceeded, children }) {
|
|
262
262
|
const t = useT();
|
|
263
|
-
|
|
263
|
+
// Only mounted on expansion: keep the compact preview cheap without losing any of the stored
|
|
264
|
+
// call when the person opens it. The core formatter still owns formatting and secret redaction.
|
|
265
|
+
const detail = describeToolCallDetail({ input: input, result: result }, { runStreaming: runStreaming, runSucceeded: runSucceeded, maxChars: Infinity });
|
|
264
266
|
return (_jsxs("div", { className: "op-card-detail", children: [detail.inputText !== null ? _jsx("pre", { className: "op-command op-input", children: detail.inputText }) : null, detail.outputText !== null ? _jsx("pre", { className: "op-output", children: detail.outputText }) : null, detail.noteKey !== null ? _jsx("p", { className: "op-detail-note", children: t(detail.noteKey) }) : null, children] }));
|
|
265
267
|
}
|
|
266
268
|
function ResultBadge({ result, runStreaming, runSucceeded }) {
|
|
@@ -270,7 +272,7 @@ function ResultBadge({ result, runStreaming, runSucceeded }) {
|
|
|
270
272
|
if (!result && !runSucceeded)
|
|
271
273
|
return (_jsx("span", { className: "op-status op-status-error", title: t('Error'), children: _jsx(Icon, { name: "close", size: 14 }) }));
|
|
272
274
|
if (result?.isError)
|
|
273
|
-
return (_jsx("span", { className: "op-status op-status-error", title: result.
|
|
275
|
+
return (_jsx("span", { className: "op-status op-status-error", title: describeToolCallDetail({ input: null, result }, { maxChars: COLLAPSED_DETAIL_MAX_CHARS }).outputText ?? t('Error'), children: _jsx(Icon, { name: "close", size: 14 }) }));
|
|
274
276
|
return (_jsx("span", { className: "op-status op-status-ok", title: t('Done'), children: _jsx(Icon, { name: "check", size: 14 }) }));
|
|
275
277
|
}
|
|
276
278
|
/**
|
|
@@ -295,9 +297,12 @@ function ToolResultMedia({ result }) {
|
|
|
295
297
|
return (_jsx("div", { className: "op-media", children: images.map((block, index) => (_jsx("img", { className: "op-media-image", src: `data:${block.mimeType};base64,${block.data}`, alt: "", style: { maxWidth: '100%', height: 'auto', display: 'block' } }, index))) }));
|
|
296
298
|
}
|
|
297
299
|
function FileErrorDetail({ result }) {
|
|
298
|
-
if (!result?.isError
|
|
300
|
+
if (!result?.isError)
|
|
299
301
|
return null;
|
|
300
|
-
|
|
302
|
+
// This block and the badge tooltip stay mounted even when collapsed. Share the reader's cap
|
|
303
|
+
// and redactor so large failures cannot retain their full output in those alternate DOM sinks.
|
|
304
|
+
const detail = describeToolCallDetail({ input: null, result }, { maxChars: COLLAPSED_DETAIL_MAX_CHARS });
|
|
305
|
+
return detail.outputText === null ? null : _jsx("pre", { className: "op-output", children: detail.outputText });
|
|
301
306
|
}
|
|
302
307
|
function describeInput(input) {
|
|
303
308
|
if (input == null)
|
|
@@ -319,6 +324,14 @@ function describeInput(input) {
|
|
|
319
324
|
return '';
|
|
320
325
|
}
|
|
321
326
|
}
|
|
327
|
+
/**
|
|
328
|
+
* A head's one-line summary of an arbitrary argument. Heads stay mounted for every collapsed row,
|
|
329
|
+
* and the fallback argument can be a 20k-char payload or a key, so it gets the reader text's
|
|
330
|
+
* order: redact first (a cut could split a key past its pattern), then cap.
|
|
331
|
+
*/
|
|
332
|
+
function headSummary({ text }) {
|
|
333
|
+
return truncate(redactUserText({ text: text }).text, 200);
|
|
334
|
+
}
|
|
322
335
|
function truncate(s, n) {
|
|
323
336
|
if (s.length <= n)
|
|
324
337
|
return s;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Read display context from a standalone form; its host may not have emitted a tool call. */
|
|
2
|
+
import { type UIResource } from '@jini-ai/ui/mcp-ui';
|
|
3
|
+
import type { AgentEvent } from '../../core/events.js';
|
|
4
|
+
import type { AcceptedInputProjectionOptions } from '../../core/accepted-input.js';
|
|
5
|
+
/** Feed recovered form context into the existing receipt projection, including after history reload.
|
|
6
|
+
* Grouping owns tool/surface association; this hook only supplies missing presentation context.
|
|
7
|
+
* @complexity O(e + h + c) for events, parsed HTML and controls; no mutation or I/O.
|
|
8
|
+
*/
|
|
9
|
+
export declare function useSurfaceAnswerContexts({ events }: {
|
|
10
|
+
events: AgentEvent[] | undefined;
|
|
11
|
+
}, _options?: {}): AcceptedInputProjectionOptions;
|
|
12
|
+
/** Parse inert markup only: never mount the document, execute scripts or read callback secrets.
|
|
13
|
+
* @returns Title and offered labels in the existing question-answer input shape.
|
|
14
|
+
* @complexity O(h + c) time/space for HTML length h and choice controls c; no I/O.
|
|
15
|
+
*/
|
|
16
|
+
export declare function surfaceQuestionInput({ resource, input }: {
|
|
17
|
+
resource: UIResource | undefined;
|
|
18
|
+
input: unknown;
|
|
19
|
+
}, _options?: {}): unknown;
|
|
20
|
+
//# sourceMappingURL=mcp-ui-question-input.d.ts.map
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/** Read display context from a standalone form; its host may not have emitted a tool call. */
|
|
2
|
+
import { parseUIResource, readActionPlan } from '@jini-ai/ui/mcp-ui';
|
|
3
|
+
import { useMemo, useRef } from 'react';
|
|
4
|
+
import { SURFACE_QUESTION_META_KEY } from '../../core/question-answer.js';
|
|
5
|
+
import { useExtEventGroups } from '../hooks/useExtEventGroups.js';
|
|
6
|
+
import { useExtEventRendererRegistry } from '../hooks/context.js';
|
|
7
|
+
/** Feed recovered form context into the existing receipt projection, including after history reload.
|
|
8
|
+
* Grouping owns tool/surface association; this hook only supplies missing presentation context.
|
|
9
|
+
* @complexity O(e + h + c) for events, parsed HTML and controls; no mutation or I/O.
|
|
10
|
+
*/
|
|
11
|
+
export function useSurfaceAnswerContexts({ events }, _options = {}) {
|
|
12
|
+
const registry = useExtEventRendererRegistry();
|
|
13
|
+
const groups = useExtEventGroups({ events }, { slotOf: (name, data) => registry.slot({ name, data }) });
|
|
14
|
+
const cache = useRef(new Map());
|
|
15
|
+
return useMemo(() => ({ answerContexts: new Map(groups.flatMap(group => {
|
|
16
|
+
if (group.name !== 'mcp-ui' || group.call?.id === undefined)
|
|
17
|
+
return [];
|
|
18
|
+
// Retain the question that offered the values, even when a later document shows its outcome.
|
|
19
|
+
const data = group.events[0];
|
|
20
|
+
let cached = cache.current.get(data);
|
|
21
|
+
if (cached === undefined || cached.sourceInput !== group.call.input) {
|
|
22
|
+
const input = surfaceQuestionInput({ resource: parseUIResource(data), input: group.call.input });
|
|
23
|
+
cached = { sourceInput: group.call.input, context: { input, includeTitle: input !== group.call.input } };
|
|
24
|
+
cache.current.set(data, cached);
|
|
25
|
+
}
|
|
26
|
+
return [[group.call.id, cached.context]];
|
|
27
|
+
})) }), [groups]);
|
|
28
|
+
}
|
|
29
|
+
/** Parse inert markup only: never mount the document, execute scripts or read callback secrets.
|
|
30
|
+
* @returns Title and offered labels in the existing question-answer input shape.
|
|
31
|
+
* @complexity O(h + c) time/space for HTML length h and choice controls c; no I/O.
|
|
32
|
+
*/
|
|
33
|
+
export function surfaceQuestionInput({ resource, input }, _options = {}) {
|
|
34
|
+
const context = surfaceQuestionContext({ resource });
|
|
35
|
+
if (hasQuestionOptions({ input: context }))
|
|
36
|
+
return context;
|
|
37
|
+
if (hasQuestionOptions({ input }))
|
|
38
|
+
return input;
|
|
39
|
+
if (resource === undefined || typeof DOMParser === 'undefined')
|
|
40
|
+
return input;
|
|
41
|
+
const { title, options } = markupQuestion({ resource });
|
|
42
|
+
if (!title && options.length === 0)
|
|
43
|
+
return input;
|
|
44
|
+
// The call's explicit question remains authoritative. Markup fills the standalone form's gap.
|
|
45
|
+
return { title, options, ...(typeof input === 'object' && input !== null ? input : {}) };
|
|
46
|
+
}
|
|
47
|
+
/** Read producer-supplied display context without parsing or executing the surface. O(1). */
|
|
48
|
+
function surfaceQuestionContext({ resource }, _options = {}) {
|
|
49
|
+
return resource?.resource._meta?.[SURFACE_QUESTION_META_KEY];
|
|
50
|
+
}
|
|
51
|
+
/** The title and offered choices an inert parse of the surface document shows. */
|
|
52
|
+
function markupQuestion({ resource }, _options = {}) {
|
|
53
|
+
const document = new DOMParser().parseFromString(resource.resource.text, 'text/html');
|
|
54
|
+
const title = readActionPlan(resource)?.title ?? document.querySelector('h1')?.textContent?.trim();
|
|
55
|
+
const labels = new Map([...document.querySelectorAll('label')].map(label => [label.htmlFor, label.textContent?.trim() ?? '']));
|
|
56
|
+
const options = [...document.querySelectorAll('input[type="radio"], input[type="checkbox"], option')].map(control => ({ value: control.value, label: optionText({ labels, control }) }));
|
|
57
|
+
return { title, options };
|
|
58
|
+
}
|
|
59
|
+
/** Explicit question options already belong to the shared answer mapper; no markup fallback needed. */
|
|
60
|
+
function hasQuestionOptions({ input }, _options = {}) {
|
|
61
|
+
if (typeof input !== 'object' || input === null)
|
|
62
|
+
return false;
|
|
63
|
+
const question = input;
|
|
64
|
+
return [question, question.singleSelect, question.multiSelect].some(group => typeof group === 'object' && group !== null && Array.isArray(group.options));
|
|
65
|
+
}
|
|
66
|
+
/** Resolve native label associations without interpolating caller-controlled ids into selectors. */
|
|
67
|
+
function optionText({ labels, control }, _options = {}) {
|
|
68
|
+
if (control.tagName === 'OPTION')
|
|
69
|
+
return control.textContent?.trim() ?? '';
|
|
70
|
+
return labels.get(control.id) ?? control.closest('label')?.textContent?.trim() ?? '';
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=mcp-ui-question-input.js.map
|
|
@@ -41,6 +41,8 @@ export interface ExtEventRenderProps {
|
|
|
41
41
|
events: readonly unknown[];
|
|
42
42
|
/** Whether the owning run is still streaming — more events with this `name` may still arrive. */
|
|
43
43
|
runStreaming: boolean;
|
|
44
|
+
/** The event is a user row alongside assistant sections, with ordinary transcript spacing. */
|
|
45
|
+
transcriptRow?: boolean;
|
|
44
46
|
runSucceeded: boolean;
|
|
45
47
|
/**
|
|
46
48
|
* Whether the owning run is waiting on the person — a card (form, confirm, sign-in) sits on a
|
|
@@ -119,11 +119,18 @@ function ChatPaneTypedAnswerNotice({ typedAnswerNotice, t, }) {
|
|
|
119
119
|
return null;
|
|
120
120
|
return (_jsx("div", { className: "jini-chat-pane__error", role: "alert", children: t(describeTypedAnswerNotice({ notice: typedAnswerNotice })) }));
|
|
121
121
|
}
|
|
122
|
+
/** A send the server never accepted. Its text went back into the composer if that was still empty;
|
|
123
|
+
* Retry sends it again either way (see `useChatPane`'s `retrySend`). */
|
|
124
|
+
function ChatPaneSendError({ sendError, onRetry, t }) {
|
|
125
|
+
if (!sendError)
|
|
126
|
+
return null;
|
|
127
|
+
return (_jsxs("div", { className: "jini-chat-pane__error", role: "alert", children: [t('Not sent:'), " ", sendError.message, ' ', _jsx("button", { type: "button", className: "jini-chat-pane__error-action", onClick: onRetry, children: t('Retry') })] }));
|
|
128
|
+
}
|
|
122
129
|
/** The pane's stack of mutually-independent error/status banners — each condition is its own
|
|
123
130
|
* source of truth, so they are rendered as siblings rather than folded into one derived state. */
|
|
124
|
-
function ChatPaneStatusMessages({ attachmentError, dropReadError, workingDirectoryError, workingDirectoryPending, workingDirectoryInvalid, runtimeInventoryError, typedAnswerNotice, ...availability }) {
|
|
131
|
+
function ChatPaneStatusMessages({ sendError, onRetrySend, attachmentError, dropReadError, workingDirectoryError, workingDirectoryPending, workingDirectoryInvalid, runtimeInventoryError, typedAnswerNotice, ...availability }) {
|
|
125
132
|
const { t } = availability;
|
|
126
|
-
return (_jsxs(_Fragment, { children: [_jsx(ChatPaneAvailabilityStatus, { ...availability }), attachmentError ? (_jsx("div", { className: "jini-chat-pane__error", role: "alert", children: attachmentError.message })) : null, dropReadError ? (_jsx("div", { className: "jini-chat-pane__error", role: "alert", children: dropReadError })) : null, workingDirectoryError ? (_jsx("div", { className: "jini-chat-pane__error", role: "alert", children: workingDirectoryError.message })) : null, _jsx(ChatPaneWorkingDirectoryStatus, { workingDirectoryPending: workingDirectoryPending, workingDirectoryInvalid: workingDirectoryInvalid, t: t }), runtimeInventoryError && !availability.connectingAgents ? (_jsx("div", { className: "jini-chat-pane__error", role: "alert", children: runtimeInventoryError.message })) : null, _jsx(ChatPaneTypedAnswerNotice, { typedAnswerNotice: typedAnswerNotice, t: t })] }));
|
|
133
|
+
return (_jsxs(_Fragment, { children: [_jsx(ChatPaneAvailabilityStatus, { ...availability }), _jsx(ChatPaneSendError, { sendError: sendError, onRetry: onRetrySend, t: t }), attachmentError ? (_jsx("div", { className: "jini-chat-pane__error", role: "alert", children: attachmentError.message })) : null, dropReadError ? (_jsx("div", { className: "jini-chat-pane__error", role: "alert", children: dropReadError })) : null, workingDirectoryError ? (_jsx("div", { className: "jini-chat-pane__error", role: "alert", children: workingDirectoryError.message })) : null, _jsx(ChatPaneWorkingDirectoryStatus, { workingDirectoryPending: workingDirectoryPending, workingDirectoryInvalid: workingDirectoryInvalid, t: t }), runtimeInventoryError && !availability.connectingAgents ? (_jsx("div", { className: "jini-chat-pane__error", role: "alert", children: runtimeInventoryError.message })) : null, _jsx(ChatPaneTypedAnswerNotice, { typedAnswerNotice: typedAnswerNotice, t: t })] }));
|
|
127
134
|
}
|
|
128
135
|
function ChatPaneWorkingDirectoryBlock({ workingDirectoryAccess, workingDirectoryControlPlacement, pane, t, }) {
|
|
129
136
|
if (workingDirectoryAccess && workingDirectoryControlPlacement === 'below') {
|
|
@@ -400,6 +407,6 @@ export function ChatPane(props) {
|
|
|
400
407
|
const selectSuggestion = useChatPaneSuggestion({ rootRef, setDraft: pane.composer.setDraft });
|
|
401
408
|
const openLinkInNewTab = useChatPaneNewTabLinks();
|
|
402
409
|
const { pendingPrompts, projectFileNameSet } = useChatPaneTranscriptProps({ pane }, definedProps({ source: { projectFileNames: p.projectFileNames } }));
|
|
403
|
-
return (_jsxs("section", { className: chatPaneClassName(p.variant, p.className), style: p.style, "data-activity": pane.activity, ref: rootRef, onClickCapture: openLinkInNewTab, children: [_jsx("style", { "data-jini-chat-pane-styles": "true", children: CHAT_PANE_STYLES }), resolveChatPaneHeader(p.header, p.title, pane.reset, t), p.agentPluginsMode !== 'off' && _jsx(AgentPluginUsage, { conversationId: pane.conversation.conversationId, messages: pane.conversation.messages }), _jsxs("div", { className: "jini-chat-pane__body", children: [_jsx(ChatPaneTranscript, { pane: pane, emptyState: p.emptyState, embed: surface.embed, suggestions: p.suggestions, onSelectSuggestion: selectSuggestion, pendingPrompts: pendingPrompts, projectFileNameSet: projectFileNameSet, toolCallDisplay: p.toolCallDisplay, showUsageLine: p.showUsageLine, showThinking: p.showThinking, t: t }), _jsxs("div", { className: "jini-chat-pane__controls", ref: controlsRef, children: [showsControlsSuggestions(surface.embed, pane.conversation.messages.length, p.emptyState) && (_jsx(ChatPaneSuggestionsRow, { suggestions: p.suggestions, onSelect: selectSuggestion, t: t })), pane.secretRedacted ? _jsx("div", { className: "jini-chat-pane__notice", role: "status", children: t(SECRET_REDACTED_NOTICE) }) : null, _jsx(ChatPaneStatusMessages, { unavailable: unavailable, scanningAgents: runtimeView.scanningAgents, connectingAgents: runtimeView.connectingAgents, executionMode: p.executionMode, apiModeAvailable: p.apiModeAvailable, onExecutionModeChange: p.onExecutionModeChange, attachmentError: pane.attachmentError, dropReadError: fileDrop.dropReadError, workingDirectoryError: pane.workingDirectoryError, workingDirectoryPending: pane.workingDirectoryPending, workingDirectoryInvalid: pane.workingDirectoryInvalid, runtimeInventoryError: inventory.runtimeInventoryError, typedAnswerNotice: pane.typedAnswerNotice, t: t }), _jsx(ChatPaneComposerArea, { fileDrop: fileDrop, uploadAttachments: surface.uploadAttachments, pane: pane, disabled: p.disabled, unavailable: unavailable, placeholder: composerPlaceholder, slots: slots, attachmentAccept: p.attachmentAccept, workingDirectoryAccess: p.workingDirectoryAccess, workingDirectoryControlPlacement: surface.workingDirectoryControlPlacement, t: t })] })] }), p.footer] }));
|
|
410
|
+
return (_jsxs("section", { className: chatPaneClassName(p.variant, p.className), style: p.style, "data-activity": pane.activity, ref: rootRef, onClickCapture: openLinkInNewTab, children: [_jsx("style", { "data-jini-chat-pane-styles": "true", children: CHAT_PANE_STYLES }), resolveChatPaneHeader(p.header, p.title, pane.reset, t), p.agentPluginsMode !== 'off' && _jsx(AgentPluginUsage, { conversationId: pane.conversation.conversationId, messages: pane.conversation.messages }), _jsxs("div", { className: "jini-chat-pane__body", children: [_jsx(ChatPaneTranscript, { pane: pane, emptyState: p.emptyState, embed: surface.embed, suggestions: p.suggestions, onSelectSuggestion: selectSuggestion, pendingPrompts: pendingPrompts, projectFileNameSet: projectFileNameSet, toolCallDisplay: p.toolCallDisplay, showUsageLine: p.showUsageLine, showThinking: p.showThinking, t: t }), _jsxs("div", { className: "jini-chat-pane__controls", ref: controlsRef, children: [showsControlsSuggestions(surface.embed, pane.conversation.messages.length, p.emptyState) && (_jsx(ChatPaneSuggestionsRow, { suggestions: p.suggestions, onSelect: selectSuggestion, t: t })), pane.secretRedacted ? _jsx("div", { className: "jini-chat-pane__notice", role: "status", children: t(SECRET_REDACTED_NOTICE) }) : null, _jsx(ChatPaneStatusMessages, { unavailable: unavailable, scanningAgents: runtimeView.scanningAgents, connectingAgents: runtimeView.connectingAgents, executionMode: p.executionMode, apiModeAvailable: p.apiModeAvailable, onExecutionModeChange: p.onExecutionModeChange, sendError: pane.conversation.sendError, onRetrySend: () => void pane.retrySend(), attachmentError: pane.attachmentError, dropReadError: fileDrop.dropReadError, workingDirectoryError: pane.workingDirectoryError, workingDirectoryPending: pane.workingDirectoryPending, workingDirectoryInvalid: pane.workingDirectoryInvalid, runtimeInventoryError: inventory.runtimeInventoryError, typedAnswerNotice: pane.typedAnswerNotice, t: t }), _jsx(ChatPaneComposerArea, { fileDrop: fileDrop, uploadAttachments: surface.uploadAttachments, pane: pane, disabled: p.disabled, unavailable: unavailable, placeholder: composerPlaceholder, slots: slots, attachmentAccept: p.attachmentAccept, workingDirectoryAccess: p.workingDirectoryAccess, workingDirectoryControlPlacement: surface.workingDirectoryControlPlacement, t: t })] })] }), p.footer] }));
|
|
404
411
|
}
|
|
405
412
|
//# sourceMappingURL=ChatPane.js.map
|
|
@@ -82,6 +82,12 @@ export interface UseChatPaneResult extends UseChatPaneWorkingDirectoryResult {
|
|
|
82
82
|
* silent no-op.
|
|
83
83
|
*/
|
|
84
84
|
sendPrompt: (prompt: string) => Promise<void>;
|
|
85
|
+
/**
|
|
86
|
+
* Sends the newest refused composer send again (see `conversation.sendError`). When that text is
|
|
87
|
+
* back in the composer it goes out from there like {@link send}; when something new was typed
|
|
88
|
+
* since, the refused text goes out on its own and the composer keeps the new typing.
|
|
89
|
+
*/
|
|
90
|
+
retrySend: () => Promise<void>;
|
|
85
91
|
/**
|
|
86
92
|
* The prompt waiting for the in-flight run to finish, or `null` when nothing is queued. Exposed
|
|
87
93
|
* so the pane can SHOW it — a queued turn that is invisible is indistinguishable from one that
|