@jini-ai/chat 0.6.15-rc.2 → 0.6.16
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/README.md +6 -1
- package/dist/core/accepted-input.js +20 -11
- package/dist/core/ag-ui/entry.js +3 -1
- package/dist/core/agent-plugin-chat-state.js +3 -1
- package/dist/core/antigravity/tool-presentation.d.ts +13 -0
- package/dist/core/antigravity/tool-presentation.js +20 -0
- package/dist/core/assistant-client/ag-ui-transport.js +14 -2
- package/dist/core/assistant-client/attachment-liveness.js +1 -1
- package/dist/core/assistant-client/durable-run-subscription.js +2 -1
- package/dist/core/assistant-client/transport.js +13 -7
- package/dist/core/assistant-content.js +2 -1
- package/dist/core/composer-history.js +2 -1
- package/dist/core/durable-projection.js +2 -1
- package/dist/core/durable-runs/continuation.js +2 -1
- package/dist/core/events.d.ts +5 -1
- package/dist/core/messages.d.ts +5 -1
- package/dist/core/partial-json.js +18 -9
- package/dist/core/persistence/title.d.ts +1 -1
- package/dist/core/persistence/title.js +25 -7
- package/dist/core/question-form/normalize.js +1 -1
- package/dist/core/question-form/scan.js +25 -15
- package/dist/core/question-form/secret-answers.js +2 -1
- package/dist/core/run-activity.js +8 -4
- package/dist/core/run-events/entry.js +6 -2
- package/dist/core/tool-events.d.ts +18 -8
- package/dist/core/tool-events.js +72 -16
- package/dist/core/tool-presentation.d.ts +14 -0
- package/dist/core/tool-presentation.js +8 -0
- package/dist/core/transcript.js +13 -6
- package/dist/core/turn-usage.js +7 -2
- package/dist/core/user-text-redaction.d.ts +4 -6
- package/dist/core/user-text-redaction.js +13 -6
- package/dist/core/util/manifest.js +13 -11
- package/dist/core/util/markdown-context.d.ts +14 -0
- package/dist/core/util/markdown-context.js +42 -4
- package/dist/core/util/markup-attributes.js +1 -1
- package/dist/core/util/parser.js +7 -7
- package/dist/core/util/pointer.js +22 -6
- package/dist/core/util/recover.js +5 -8
- package/dist/core/util/strip.js +12 -15
- package/dist/core/util/validate.js +30 -18
- package/dist/embed-widget/browser/frontend-session-bridge.d.ts +99 -0
- package/dist/embed-widget/browser/frontend-session-bridge.js +384 -0
- package/dist/embed-widget/browser/lazy-loader.js +16 -12
- package/dist/embed-widget/browser/session-store.d.ts +3 -9
- package/dist/embed-widget/browser/session-store.js +6 -38
- package/dist/embed-widget/react/dialog.hooks.js +2 -2
- package/dist/embed-widget/react/embed.d.ts +3 -10
- package/dist/embed-widget/react/embed.js +8 -52
- package/dist/embed-widget/react/header.hooks.js +1 -1
- package/dist/embed-widget/transports/fetch-sse.js +2 -1
- package/dist/embed-widget/vanilla/index.d.ts +0 -5
- package/dist/embed-widget/vanilla/index.js +23 -26
- package/dist/embed-widget/vanilla/state.d.ts +3 -11
- package/dist/embed-widget/vanilla/state.js +10 -39
- package/dist/embed-widget/vanilla/style.css +1 -1
- package/dist/react/agent-bridge/frontend-session-bridge.d.ts +2 -56
- package/dist/react/agent-bridge/frontend-session-bridge.js +2 -248
- package/dist/react/components/AttachmentPreviewModal.hooks.js +2 -1
- package/dist/react/components/ChatSettingsDialog.js +18 -9
- package/dist/react/components/Composer.js +5 -4
- package/dist/react/components/ConversationList.js +5 -4
- package/dist/react/components/Icon.d.ts +1 -1
- package/dist/react/components/Icon.js +1 -0
- package/dist/react/components/Markdown.d.ts +1 -0
- package/dist/react/components/Markdown.js +30 -27
- package/dist/react/components/McpUiSurfaceCard.js +2 -1
- package/dist/react/components/MessageList.d.ts +1 -1
- package/dist/react/components/MessageList.hooks.js +3 -2
- package/dist/react/components/MessageRow.d.ts +3 -0
- package/dist/react/components/MessageRow.js +32 -14
- package/dist/react/components/MessageRow.usage.js +3 -1
- package/dist/react/components/QuestionForm.js +4 -5
- package/dist/react/components/SentAttachment.js +4 -1
- package/dist/react/components/TodoCard.js +9 -1
- package/dist/react/components/ToolCard.d.ts +3 -1
- package/dist/react/components/ToolCard.js +84 -64
- package/dist/react/components/ToolCard.rules.d.ts +20 -0
- package/dist/react/components/ToolCard.rules.js +92 -0
- package/dist/react/components/composer-discovery.js +8 -8
- package/dist/react/features/chat-pane/components/AgentPluginUsage.js +1 -2
- package/dist/react/features/chat-pane/components/AgentRuntimePicker.d.ts +1 -1
- package/dist/react/features/chat-pane/components/AgentRuntimePicker.js +31 -9
- package/dist/react/features/chat-pane/components/ChatPane.js +7 -5
- package/dist/react/features/chat-pane/components/RuntimeDropUp.js +1 -1
- package/dist/react/features/chat-pane/create-daemon-attachment-uploader.js +1 -1
- package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.js +2 -2
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +3 -1
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +13 -5
- package/dist/react/features/chat-pane/hooks/useChatPaneAgentControl.hooks.js +2 -2
- package/dist/react/features/chat-pane/hooks/useChatPaneControlsHeight.hooks.js +1 -1
- package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.js +2 -2
- package/dist/react/features/chat-pane/hooks/useChatPaneWorkingDirectory.hooks.js +2 -2
- package/dist/react/features/chat-pane/hooks/useRuntimeDropUp.hooks.js +2 -2
- package/dist/react/features/chat-pane/model-options.js +4 -2
- package/dist/react/features/chat-pane/styles.d.ts +1 -1
- package/dist/react/features/chat-pane/styles.js +95 -5
- package/dist/react/features/chat-pane/types.d.ts +11 -0
- package/dist/react/hooks/assistant-chats.js +22 -8
- package/dist/react/hooks/useArtifactStream.js +2 -1
- package/dist/react/hooks/useComposer.js +2 -2
- package/dist/react/hooks/useComposerHistoryKeys.hooks.js +2 -2
- package/dist/react/hooks/useConversation.js +9 -4
- package/dist/react/hooks/useExtEventGroups.js +4 -2
- package/dist/react/hooks/useLatestOperation.js +2 -1
- package/dist/react/hooks/useQuestionForms.js +32 -22
- package/dist/react/hooks/useRunStream.js +4 -2
- package/dist/react/hooks/useUserTextGuard.d.ts +1 -0
- package/dist/react/hooks/useUserTextGuard.js +3 -1
- package/dist/react/message-blocks.js +4 -2
- package/dist/react/styles/reference.css +9 -0
- package/dist/react/tool-call-display.d.ts +11 -0
- package/dist/react/tool-call-display.js +59 -2
- package/dist/server/durable-runs/recover.js +13 -5
- package/dist/server/run-finalizer/entry.js +1 -0
- package/dist/store/runtime/chat-history-store.js +9 -3
- package/dist/store/runtime/durable-run-store.js +34 -24
- package/dist/store/runtime/run-ledger.js +7 -4
- package/dist/store/sql/boundary.js +3 -1
- package/dist/store/sql/cursor.js +8 -3
- package/dist/store/sql/store.js +36 -28
- package/package.json +93 -88
- package/dist/core/browser/page-actions.d.ts +0 -121
- package/dist/core/browser/page-actions.js +0 -115
- package/dist/embed-widget/browser/page-actions.d.ts +0 -2
- package/dist/embed-widget/browser/page-actions.js +0 -3
|
@@ -74,7 +74,7 @@ function splitTableCells(line) {
|
|
|
74
74
|
cells.push(tail);
|
|
75
75
|
return cells;
|
|
76
76
|
}
|
|
77
|
-
const TABLE_ALIGN_CELL =
|
|
77
|
+
const TABLE_ALIGN_CELL = /^:?-+:?$/;
|
|
78
78
|
// Every cell `TABLE_ALIGN_CELL` accepts starts and ends with `:` or `-`, so its
|
|
79
79
|
// first + last character is always exactly one of these four keys.
|
|
80
80
|
const COLUMN_ALIGN = { '::': 'center', '-:': 'right', ':-': 'left', '--': null };
|
|
@@ -117,7 +117,7 @@ function collectTableRows(lines, start) {
|
|
|
117
117
|
}
|
|
118
118
|
const ORDERED_ITEM = /^\s*\d+[.)]\s+/;
|
|
119
119
|
const FENCE = /^```/;
|
|
120
|
-
const HEADING = /^(#{1,3})\s
|
|
120
|
+
const HEADING = /^(#{1,3})\s+/;
|
|
121
121
|
const RULE = /^(-{3,}|\*{3,}|_{3,})\s*$/;
|
|
122
122
|
const QUOTE_LINE = /^>\s?/;
|
|
123
123
|
const BULLET_ITEM = /^\s*[-*]\s+/;
|
|
@@ -139,11 +139,12 @@ function parseHeading(lines, i) {
|
|
|
139
139
|
const heading = HEADING.exec(lines[i]);
|
|
140
140
|
if (!heading)
|
|
141
141
|
return null;
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
//
|
|
146
|
-
|
|
142
|
+
const content = /^(.*)$/.exec(lines[i].slice(heading[0].length));
|
|
143
|
+
if (!content)
|
|
144
|
+
return null;
|
|
145
|
+
// Both captures are mandatory in their respective patterns, so each successful match has a
|
|
146
|
+
// defined group 1 — same dead-fallback reasoning as the `lines[i]` accesses above.
|
|
147
|
+
return { block: { kind: 'h', level: heading[1].length, text: content[1] }, next: i + 1 };
|
|
147
148
|
}
|
|
148
149
|
function parseRule(lines, i) {
|
|
149
150
|
return RULE.test(lines[i]) ? { block: { kind: 'hr' }, next: i + 1 } : null;
|
|
@@ -268,13 +269,8 @@ function nextLooseListLine(lines, i) {
|
|
|
268
269
|
}
|
|
269
270
|
function isBlockStart(lines, i) {
|
|
270
271
|
const line = lines[i];
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|| RULE.test(line)
|
|
274
|
-
|| QUOTE_LINE.test(line)
|
|
275
|
-
|| BULLET_ITEM.test(line)
|
|
276
|
-
|| ORDERED_ITEM.test(line)
|
|
277
|
-
|| isTableStartAt(lines, i));
|
|
272
|
+
const simpleBlock = [FENCE, /^(#{1,3})\s+/, RULE, QUOTE_LINE, BULLET_ITEM, ORDERED_ITEM].some(pattern => pattern.test(line));
|
|
273
|
+
return simpleBlock || isTableStartAt(lines, i);
|
|
278
274
|
}
|
|
279
275
|
function renderBlock(block) {
|
|
280
276
|
switch (block.kind) {
|
|
@@ -297,6 +293,7 @@ function renderBlock(block) {
|
|
|
297
293
|
case 'hr':
|
|
298
294
|
return _jsx("hr", {});
|
|
299
295
|
}
|
|
296
|
+
return undefined;
|
|
300
297
|
}
|
|
301
298
|
// Fenced code block ('renderBlock' case 'code') plus its copy affordance. 'CopyCodeButton' below
|
|
302
299
|
// sits in its own small toolbar row above the '<pre>', not overlaid on top of it: an overlay
|
|
@@ -343,7 +340,7 @@ function useTableOverflow() {
|
|
|
343
340
|
// The wrap div renders unconditionally, so its ref is attached by the time this effect runs.
|
|
344
341
|
const el = wrapRef.current;
|
|
345
342
|
if (typeof ResizeObserver !== 'function')
|
|
346
|
-
return;
|
|
343
|
+
return undefined;
|
|
347
344
|
const check = () => setIsOverflowing(el.scrollWidth > el.clientWidth + TABLE_OVERFLOW_SLACK_PX);
|
|
348
345
|
const observer = new ResizeObserver(check);
|
|
349
346
|
observer.observe(el);
|
|
@@ -418,7 +415,7 @@ const MD_LINK_PARTS = /^\[([^\]\n]+)\]\((.+)\)$/u;
|
|
|
418
415
|
const INLINE_RE = new RegExp(String.raw `(\x60[^\x60]+\x60)|(${MD_LINK})|(\*\*[^*]+\*\*)|(\*[^*]+\*|${UNDERSCORE_EM})|(https?:\/\/[^\s)]+)`, 'gu');
|
|
419
416
|
// Only these targets become anchors; anything else (`javascript:`, `data:`, protocol-relative
|
|
420
417
|
// `//host`) stays literal text, since the text is untrusted model output.
|
|
421
|
-
const SAFE_LINK_HREF = /^(?:https?:\/\/|mailto:|\/(?!\/)|#)/iu;
|
|
418
|
+
export const SAFE_LINK_HREF = /^(?:https?:\/\/|mailto:|\/(?!\/)|#)/iu;
|
|
422
419
|
// Every link opens in a new tab — an in-app path too, so following one never navigates the tab the
|
|
423
420
|
// chat lives in (owner 2026-10-08). Only a `#` same-document anchor stays in place. ChatPane's
|
|
424
421
|
// click guard (`useChatPaneNewTabLinks`) covers links this renderer does not draw.
|
|
@@ -441,17 +438,9 @@ function renderInline(text) {
|
|
|
441
438
|
while (match !== null) {
|
|
442
439
|
if (match.index > lastIndex)
|
|
443
440
|
nodes.push(text.slice(lastIndex, match.index));
|
|
444
|
-
const
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
else if (mdLink)
|
|
448
|
-
nodes.push(renderMarkdownLink(mdLink, key++));
|
|
449
|
-
else if (bold)
|
|
450
|
-
nodes.push(_jsx("strong", { children: renderInline(bold.slice(2, -2)) }, key++));
|
|
451
|
-
else if (italic)
|
|
452
|
-
nodes.push(_jsx("em", { children: renderInline(italic.slice(1, -1)) }, key++));
|
|
453
|
-
else if (link)
|
|
454
|
-
nodes.push(_jsx("a", { href: link, ...NEW_TAB, children: link }, key++));
|
|
441
|
+
const whole = match[0];
|
|
442
|
+
nodes.push(renderInlineToken({ match, key }));
|
|
443
|
+
key += 1;
|
|
455
444
|
// `whole` is `match[0]` — a successful `RegExpExecArray` always has a
|
|
456
445
|
// defined (if possibly empty) full-match string at index 0, so this
|
|
457
446
|
// fallback is unreachable (same dead-fallback reasoning as above).
|
|
@@ -462,4 +451,18 @@ function renderInline(text) {
|
|
|
462
451
|
nodes.push(text.slice(lastIndex));
|
|
463
452
|
return nodes;
|
|
464
453
|
}
|
|
454
|
+
function renderInlineToken({ match, key }) {
|
|
455
|
+
const [, code, mdLink, bold, italic, link] = match;
|
|
456
|
+
if (code)
|
|
457
|
+
return _jsx("code", { children: code.slice(1, -1) }, key);
|
|
458
|
+
if (mdLink)
|
|
459
|
+
return renderMarkdownLink(mdLink, key);
|
|
460
|
+
if (bold)
|
|
461
|
+
return _jsx("strong", { children: renderInline(bold.slice(2, -2)) }, key);
|
|
462
|
+
if (italic)
|
|
463
|
+
return _jsx("em", { children: renderInline(italic.slice(1, -1)) }, key);
|
|
464
|
+
if (link)
|
|
465
|
+
return _jsx("a", { href: link, ...NEW_TAB, children: link }, key);
|
|
466
|
+
return null;
|
|
467
|
+
}
|
|
465
468
|
//# sourceMappingURL=Markdown.js.map
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { trimChars } from '@jini-ai/core/primitives';
|
|
2
3
|
/**
|
|
3
4
|
* @module components/McpUiSurfaceCard
|
|
4
5
|
*
|
|
@@ -98,7 +99,7 @@ function latestResourcesByUri(events) {
|
|
|
98
99
|
function pendingMirrorHandle(uri) {
|
|
99
100
|
// Never empty: only resources whose URI starts with `ui://` reach a card (`@jini-ai/ui/mcp-ui`'s
|
|
100
101
|
// resource parser rejects every other scheme), so the sanitized handle always keeps at least `ui`.
|
|
101
|
-
const sanitized = uri.toLowerCase().replace(/[^a-z0-9]+/g, '-')
|
|
102
|
+
const sanitized = trimChars({ text: uri.toLowerCase().replace(/[^a-z0-9]+/g, '-'), chars: '-' });
|
|
102
103
|
return `mcp-ui-pending-${sanitized}`;
|
|
103
104
|
}
|
|
104
105
|
/** The one label a mirror region carries — what is pending, and what an agent could report or wait on. */
|
|
@@ -6,7 +6,7 @@ interface PendingPrompt {
|
|
|
6
6
|
attachments?: ChatAttachment[];
|
|
7
7
|
onCancel: () => void;
|
|
8
8
|
}
|
|
9
|
-
export interface MessageListProps extends Pick<MessageRowProps, 'projectFileNames' | 'onRequestOpenFile' | 'renderAttachment' | 'showUsageTotal' | 'showUsageLine' | 'toolCallDisplay' | 'showThinking'> {
|
|
9
|
+
export interface MessageListProps extends Pick<MessageRowProps, 'projectFileNames' | 'onRequestOpenFile' | 'renderAttachment' | 'showUsageTotal' | 'showUsageLine' | 'toolCallDisplay' | 'showThinking' | 'sendingLabel'> {
|
|
10
10
|
messages: ChatMessage[];
|
|
11
11
|
isStreaming?: boolean;
|
|
12
12
|
/** Mirrors `useConversation().scrollIntent` — when `true`, this component scrolls to the bottom on mount/update. */
|
|
@@ -23,7 +23,8 @@ export function useConversationUsage({ messages }, _optional = {}) {
|
|
|
23
23
|
if (old && JSON.stringify(old) === JSON.stringify(summary))
|
|
24
24
|
next.set(id, old);
|
|
25
25
|
}
|
|
26
|
-
|
|
26
|
+
previous.current = next;
|
|
27
|
+
return next;
|
|
27
28
|
}, [messages]);
|
|
28
29
|
}
|
|
29
30
|
export function useMessageListAutoScroll({ messages, scrollIntent }, { onScrolled, reducedMotion = prefersReducedMotion } = {}) {
|
|
@@ -86,7 +87,7 @@ export function useMessageListAutoScroll({ messages, scrollIntent }, { onScrolle
|
|
|
86
87
|
useEffect(() => {
|
|
87
88
|
const el = containerRef.current;
|
|
88
89
|
if (!el || typeof ResizeObserver !== 'function')
|
|
89
|
-
return;
|
|
90
|
+
return undefined;
|
|
90
91
|
const observer = new ResizeObserver(() => {
|
|
91
92
|
if (!stickToBottomRef.current)
|
|
92
93
|
return;
|
|
@@ -73,6 +73,9 @@ export interface MessageRowProps {
|
|
|
73
73
|
runStreaming?: boolean;
|
|
74
74
|
/** Whether the server has yet to accept this reply's run (its user turn is still pending). */
|
|
75
75
|
runSending?: boolean;
|
|
76
|
+
/** Host-localized text shown verbatim only while this run is active and sending.
|
|
77
|
+
* Omit to use the translated `Sending…` label. */
|
|
78
|
+
sendingLabel?: string;
|
|
76
79
|
runSucceeded?: boolean;
|
|
77
80
|
/** Whether this message's question-form (if any) is still the active/answerable one. */
|
|
78
81
|
questionFormInteractive?: boolean;
|
|
@@ -71,7 +71,7 @@ import { Markdown } from './Markdown.js';
|
|
|
71
71
|
import { ToolCard } from './ToolCard.js';
|
|
72
72
|
import { QuestionForm } from './QuestionForm.js';
|
|
73
73
|
import { ToolCallGroup } from './ToolCallGroup.js';
|
|
74
|
-
import { applyToolCallDisplay, toolRowsForDisplay } from '../tool-call-display.js';
|
|
74
|
+
import { applyToolCallDisplay, retriedToolRowIds, toolRowsForDisplay } from '../tool-call-display.js';
|
|
75
75
|
/**
|
|
76
76
|
* True while an assistant message is still fully pending — no visible text
|
|
77
77
|
* and no tool rows have arrived yet. Shared by the "Thinking…" placeholder
|
|
@@ -140,6 +140,14 @@ function flatToolGroups({ message, rows, registry, displayBlocks }, { toolCallDi
|
|
|
140
140
|
return flat.length > 0 ? [{ rows: flat, key: 'tools' }] : [];
|
|
141
141
|
}
|
|
142
142
|
/** The model's reasoning, folded under a "Thinking" summary so it never crowds the answer. */
|
|
143
|
+
/** A host status belongs to the run that emitted it, regardless of the current picker selection. */
|
|
144
|
+
function RunAccessNotice({ events }) {
|
|
145
|
+
const t = useT();
|
|
146
|
+
const notice = events?.find(event => event.kind === 'status' && event.label === 'full_cli_access');
|
|
147
|
+
if (notice?.kind !== 'status' || !notice.detail)
|
|
148
|
+
return null;
|
|
149
|
+
return _jsx("div", { className: "jini-chat-pane__notice", role: "note", children: t(notice.detail) });
|
|
150
|
+
}
|
|
143
151
|
function ThinkingBlock({ text }) {
|
|
144
152
|
const t = useT();
|
|
145
153
|
if (!text.trim())
|
|
@@ -149,9 +157,9 @@ function ThinkingBlock({ text }) {
|
|
|
149
157
|
function messageAwaitingAnswer({ runStreaming, runInProgress, events }, { isAnsweredCard } = {}) {
|
|
150
158
|
return runStreaming === true && runInProgress && isAwaitingAnswer({ events }, { isAnsweredCard });
|
|
151
159
|
}
|
|
152
|
-
function AssistantMessageFooter({ message, usageEvent, showUsageTotal, terminalNotice, pending, runInProgress, runStreaming, runSending, visibleContent, isAnsweredCard }, _optional = {}) {
|
|
160
|
+
function AssistantMessageFooter({ message, usageEvent, showUsageTotal, terminalNotice, pending, runInProgress, runStreaming, runSending, sendingLabel, visibleContent, isAnsweredCard }, _optional = {}) {
|
|
153
161
|
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, sending: runSending }) : null] });
|
|
162
|
+
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, sendingLabel: sendingLabel }) : null] });
|
|
155
163
|
}
|
|
156
164
|
/** Stable empty list for a message with no attachments. */
|
|
157
165
|
const NO_ATTACHMENTS = [];
|
|
@@ -178,7 +186,7 @@ function UserMessageRow({ message, renderAttachment }) {
|
|
|
178
186
|
const gallery = useAttachmentGallery({ attachments: message.attachments ?? NO_ATTACHMENTS });
|
|
179
187
|
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] }));
|
|
180
188
|
}
|
|
181
|
-
function AssistantMessageRow({ message, usageSummary, showUsageTotal, showUsageLine, toolCallDisplay, showThinking, runStreaming, runSending, runSucceeded, questionFormInteractive, questionFormSubmittedAnswers, onQuestionFormSubmit, projectFileNames, onRequestOpenFile, }) {
|
|
189
|
+
function AssistantMessageRow({ message, usageSummary, showUsageTotal, showUsageLine, toolCallDisplay, showThinking, runStreaming, runSending, sendingLabel, runSucceeded, questionFormInteractive, questionFormSubmittedAnswers, onQuestionFormSubmit, projectFileNames, onRequestOpenFile, }) {
|
|
182
190
|
const registry = useExtEventRendererRegistry();
|
|
183
191
|
const messageUsage = useMessageUsage({ message, ...(usageSummary === undefined ? {} : { summary: usageSummary }) });
|
|
184
192
|
const timeline = useToolTimeline({ events: message.events }, { runStreaming, runSucceeded });
|
|
@@ -198,7 +206,8 @@ function AssistantMessageRow({ message, usageSummary, showUsageTotal, showUsageL
|
|
|
198
206
|
});
|
|
199
207
|
const footerUsage = showUsageLine ? usage.footer : undefined;
|
|
200
208
|
const lastFinishedReply = !runInProgress && !usage.footer ? usage.replies.findLastIndex(value => value !== undefined) : -1;
|
|
201
|
-
const
|
|
209
|
+
const retriedRows = retriedToolRowIds({ rows: timeline.rows });
|
|
210
|
+
const renderToolCard = (row) => (_jsx(ToolCard, { retried: retriedRows.has(row.id), use: row.use, result: row.result, runStreaming: runStreaming, runSucceeded: runSucceeded, ...(projectFileNames !== undefined ? { projectFileNames } : {}), ...(onRequestOpenFile !== undefined ? { onRequestOpenFile } : {}) }, row.id));
|
|
202
211
|
// Hidden mode reuses the same disclosure and card renderer for unrecovered failures.
|
|
203
212
|
const renderTools = (rows, key) => (toolCallDisplay !== 'show'
|
|
204
213
|
? _jsx(ToolCallGroup, { rows: rows, renderRow: renderToolCard, failureOnly: toolCallDisplay === 'hidden' }, key)
|
|
@@ -235,7 +244,11 @@ function AssistantMessageRow({ message, usageSummary, showUsageTotal, showUsageL
|
|
|
235
244
|
// arrives for it, instead of staying tombstoned for the message's whole lifetime.
|
|
236
245
|
_jsx(ExtEventErrorBoundary, { name: group.name, children: transcriptRow ? node : _jsx("div", { children: node }) }, `${group.slot}:${group.events.length}`));
|
|
237
246
|
};
|
|
238
|
-
const renderTextSegments = (text, keyPrefix) => splitOnQuestionForms({ input: text }).map((segment, i) =>
|
|
247
|
+
const renderTextSegments = (text, keyPrefix) => splitOnQuestionForms({ input: text }).map((segment, i) => {
|
|
248
|
+
if (segment.kind === 'text')
|
|
249
|
+
return segment.text.trim() ? (_jsx("div", { className: "jini-message-content", children: _jsx(Markdown, { children: segment.text }) }, `${keyPrefix}-${i}`)) : null;
|
|
250
|
+
return (_jsx(QuestionForm, { form: segment.form, interactive: questionFormInteractive, ...(questionFormSubmittedAnswers !== undefined ? { submittedAnswers: questionFormSubmittedAnswers } : {}), ...(onQuestionFormSubmit !== undefined ? { onSubmit: onQuestionFormSubmit } : {}) }, `${keyPrefix}-${i}`));
|
|
251
|
+
});
|
|
239
252
|
const renderBlock = (block) => {
|
|
240
253
|
if (block.kind === 'text') {
|
|
241
254
|
return _jsx(React.Fragment, { children: renderTextSegments(block.text, block.key) }, block.key);
|
|
@@ -243,7 +256,9 @@ function AssistantMessageRow({ message, usageSummary, showUsageTotal, showUsageL
|
|
|
243
256
|
if (block.kind === 'reply-end') {
|
|
244
257
|
const reply = usage.replies[block.index];
|
|
245
258
|
// A trailing input may have no reply: its predecessor's usage belongs above that input.
|
|
246
|
-
|
|
259
|
+
if (!reply || !showUsageLine)
|
|
260
|
+
return null;
|
|
261
|
+
return _jsx(UsageSummary, { usage: reply, showTotal: showUsageTotal, ...(block.index === lastFinishedReply ? { message } : {}) }, block.key);
|
|
247
262
|
}
|
|
248
263
|
if (block.kind === 'tools')
|
|
249
264
|
return renderTools(block.rows, block.key);
|
|
@@ -260,21 +275,23 @@ function AssistantMessageRow({ message, usageSummary, showUsageTotal, showUsageL
|
|
|
260
275
|
return _jsx(React.Fragment, { children: renderExtGroup(group) }, block.key);
|
|
261
276
|
return (_jsx("div", { className: "jini-message-ext-events", children: renderExtGroup(group) }, block.key));
|
|
262
277
|
};
|
|
278
|
+
// The live footer follows the run as transcript sections split; its clock must not remount.
|
|
263
279
|
const renderAssistantSection = ({ section, key, footer }) => (_jsxs("div", { className: usageLineMeetsActions({ footer: footerUsage, terminalNotice, pending, runInProgress }, { showTotal: showUsageTotal })
|
|
264
280
|
? 'jini-message jini-message-assistant jini-message-assistant--usage-actions'
|
|
265
281
|
: '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
282
|
? 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));
|
|
283
|
+
: (_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, sendingLabel, visibleContent, isAnsweredCard: answeredCards.isAnsweredCard })] }, footer ? message.id : key));
|
|
268
284
|
// The lossless fallback keeps the host's text intact; user receipts still own separate rows,
|
|
269
285
|
// in their original receipt order, outside the assistant's actions and usage footer.
|
|
286
|
+
// Keep the same child-array nesting as the interleaved path so React preserves the footer.
|
|
270
287
|
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)] });
|
|
288
|
+
return _jsxs(_Fragment, { children: [_jsx(RunAccessNotice, { events: message.events }), [renderAssistantSection({ section: null, key: message.id, footer: true })], extGroups.filter(group => group.name === MID_RUN_USER_MESSAGE_EXT_EVENT_NAME).map(renderExtGroup)] });
|
|
272
289
|
// A reply boundary without reported usage renders nothing; it must not create an empty row's gap.
|
|
273
290
|
const visibleBlocks = blocks.filter(block => block.kind !== 'reply-end' || (showUsageLine && usage.replies[block.index] !== undefined));
|
|
274
291
|
const { sections, footerIndex } = transcriptSections({ blocks: visibleBlocks, runInProgress });
|
|
275
|
-
return
|
|
276
|
-
|
|
277
|
-
|
|
292
|
+
return _jsxs(_Fragment, { children: [_jsx(RunAccessNotice, { events: message.events }), sections.map((section, index) => isUserSection({ section })
|
|
293
|
+
? _jsx(React.Fragment, { children: section.map(renderBlock) }, section[0].key)
|
|
294
|
+
: renderAssistantSection({ section, key: section[0]?.key ?? message.id, footer: index === footerIndex }))] });
|
|
278
295
|
}
|
|
279
296
|
/** Preserve optional render-prop presence for custom extension renderers. */
|
|
280
297
|
function answerRenderProps({ answeredCards, slot, answerText }, _options = {}) {
|
|
@@ -302,13 +319,14 @@ function isUserSection({ section }, _options = {}) {
|
|
|
302
319
|
* The live "what is it doing" line under a running reply (see `core/run-activity.ts`). Replaces the
|
|
303
320
|
* old "Thinking…" placeholder, which vanished for good as soon as the first tool row appeared.
|
|
304
321
|
* Kept a separate component so its one-second tick re-renders only this line, not the whole row.
|
|
322
|
+
* A host's already-localized sending label is used verbatim until acceptance.
|
|
305
323
|
*/
|
|
306
|
-
function RunActivityLine({ events, active, isAnsweredCard, sending }) {
|
|
324
|
+
function RunActivityLine({ events, active, isAnsweredCard, sending, sendingLabel }) {
|
|
307
325
|
const t = useT();
|
|
308
326
|
const activityLabel = useRunActivity({ events: events, active: active }, { isAnsweredCard });
|
|
309
327
|
// Until the server accepts the start the turn is still on its way: "Thinking…" there read as the
|
|
310
328
|
// 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;
|
|
329
|
+
const label = active && sending ? sendingLabel ?? t('Sending…') : continuingRunNotice({ events, active }, {}) ?? activityLabel;
|
|
312
330
|
if (label === null)
|
|
313
331
|
return null;
|
|
314
332
|
return (_jsx("div", { className: "jini-message-pending jini-run-activity", "aria-live": "off", children: label }));
|
|
@@ -44,7 +44,9 @@ export function usageLayout({ usage, interleaved, runInProgress }, { runStreamin
|
|
|
44
44
|
* every keystroke restyle the whole page in long chats (2026-10-11). O(1).
|
|
45
45
|
* @example usageLineMeetsActions({ footer: { kind: 'usage', costUsd: 0.1 }, terminalNotice: null, pending: false, runInProgress: false }) // true */
|
|
46
46
|
export function usageLineMeetsActions({ footer, terminalNotice, pending, runInProgress }, { showTotal = true } = {}) {
|
|
47
|
-
|
|
47
|
+
if (footer === undefined)
|
|
48
|
+
return false;
|
|
49
|
+
const hasText = footer.durationMs !== undefined || footer.costUsd !== undefined || (showTotal && footer.totalCostUsd !== undefined);
|
|
48
50
|
return hasText && !terminalNotice && !pending && !runInProgress;
|
|
49
51
|
}
|
|
50
52
|
/** Build footer display values outside JSX; token counts remain available in the tooltip and accessible footer label.
|
|
@@ -177,8 +177,8 @@ function DirectionCardsControl(ctx) {
|
|
|
177
177
|
function TextControl({ q, value, locked, update, t }) {
|
|
178
178
|
const shared = { className: 'qf-input', value: stringValue(value), placeholder: q.placeholder, disabled: locked, onChange: (e) => update(q.id, e.target.value) };
|
|
179
179
|
const props = questionTextInputProps({ question: q });
|
|
180
|
-
|
|
181
|
-
|
|
180
|
+
const { type, ...secretProps } = props;
|
|
181
|
+
if (type === 'password') {
|
|
182
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
183
|
}
|
|
184
184
|
return _jsx("input", { ...props, ...shared });
|
|
@@ -271,9 +271,8 @@ function collectFileSubmissions(form, fileAnswers) {
|
|
|
271
271
|
if (q.type !== 'file')
|
|
272
272
|
continue;
|
|
273
273
|
const files = fileAnswers[q.id] ?? [];
|
|
274
|
-
if (files.length
|
|
275
|
-
|
|
276
|
-
out.push({ questionId: q.id, questionLabel: q.label, files });
|
|
274
|
+
if (files.length > 0)
|
|
275
|
+
out.push({ questionId: q.id, questionLabel: q.label, files });
|
|
277
276
|
}
|
|
278
277
|
return out;
|
|
279
278
|
}
|
|
@@ -9,6 +9,9 @@ import { useSentAttachmentThumbnail } from './SentAttachment.hooks.js';
|
|
|
9
9
|
export function SentAttachmentButton({ attachment, onOpen, renderAttachment, objectUrls }) {
|
|
10
10
|
const t = useT();
|
|
11
11
|
const { thumbnailUrl, onThumbnailError } = useSentAttachmentThumbnail({ attachment, hostRendered: renderAttachment !== undefined }, objectUrls === undefined ? {} : { objectUrls });
|
|
12
|
-
|
|
12
|
+
const fallback = thumbnailUrl
|
|
13
|
+
? _jsx("img", { className: "jini-message-attachment-thumb", src: thumbnailUrl, alt: "", onError: onThumbnailError })
|
|
14
|
+
: attachment.name;
|
|
15
|
+
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) : fallback }));
|
|
13
16
|
}
|
|
14
17
|
//# sourceMappingURL=SentAttachment.js.map
|
|
@@ -18,7 +18,7 @@ export function TodoCard({ todos, runStreaming, onDismiss }) {
|
|
|
18
18
|
const [overrideExpanded, setOverrideExpanded] = useState(null);
|
|
19
19
|
const expanded = overrideExpanded ?? defaultExpanded;
|
|
20
20
|
const showDismiss = Boolean(onDismiss) && allComplete;
|
|
21
|
-
return (_jsxs("div", { className: `op-card op-todo${expanded ? '' : ' op-todo-collapsed'}`, children: [_jsxs("div", { className: "op-card-head op-todo-head", children: [_jsxs("button", { type: "button", className: "op-todo-toggle", "aria-expanded": expanded, onClick: () => setOverrideExpanded(!expanded), title: expanded ? t('Collapse todos') : t('Expand todos'), children: [_jsx("span", { className: "op-icon", "aria-hidden": true, children: "\u2610" }), _jsx("span", { className: "op-title", children: t('Todos') }), _jsxs("span", { className: "op-meta", children: [done, "/", todos.length] }), !expanded && inProgressTodo ? _jsx("span", { className: "op-todo-current", children: todoLabel({ todo: inProgressTodo }) }) : null, _jsx("span", { className: "op-todo-chev", "aria-hidden": true, children: expanded ? _jsx(Icon, { name: "chevron-down", size: 11 }) : _jsx(Icon, { name: "chevron-right", size: 11 }) })] }), showDismiss ? (_jsx("button", { type: "button", className: "op-todo-done", onClick: () => onDismiss?.(), title: t('Dismiss'), children: t('Done') })) : null] }), _jsx("div", { className: `accordion-collapsible${expanded ? ' open' : ''}`, children: _jsx("div", { className: "accordion-collapsible-inner", children: _jsx("ul", { className: "todo-list", children: todos.map((todo, i) => (_jsxs("li", { className: `todo-item todo-${todo.status}`, children: [_jsx("span", { className: "todo-check", "aria-hidden": true, children:
|
|
21
|
+
return (_jsxs("div", { className: `op-card op-todo${expanded ? '' : ' op-todo-collapsed'}`, children: [_jsxs("div", { className: "op-card-head op-todo-head", children: [_jsxs("button", { type: "button", className: "op-todo-toggle", "aria-expanded": expanded, onClick: () => setOverrideExpanded(!expanded), title: expanded ? t('Collapse todos') : t('Expand todos'), children: [_jsx("span", { className: "op-icon", "aria-hidden": true, children: "\u2610" }), _jsx("span", { className: "op-title", children: t('Todos') }), _jsxs("span", { className: "op-meta", children: [done, "/", todos.length] }), !expanded && inProgressTodo ? _jsx("span", { className: "op-todo-current", children: todoLabel({ todo: inProgressTodo }) }) : null, _jsx("span", { className: "op-todo-chev", "aria-hidden": true, children: expanded ? _jsx(Icon, { name: "chevron-down", size: 11 }) : _jsx(Icon, { name: "chevron-right", size: 11 }) })] }), showDismiss ? (_jsx("button", { type: "button", className: "op-todo-done", onClick: () => onDismiss?.(), title: t('Dismiss'), children: t('Done') })) : null] }), _jsx("div", { className: `accordion-collapsible${expanded ? ' open' : ''}`, children: _jsx("div", { className: "accordion-collapsible-inner", children: _jsx("ul", { className: "todo-list", children: todos.map((todo, i) => (_jsxs("li", { className: `todo-item todo-${todo.status}`, children: [_jsx("span", { className: "todo-check", "aria-hidden": true, children: todoStatusGlyph({ status: todo.status }) }), _jsx("span", { className: "todo-text", children: todoLabel({ todo }) })] }, i))) }) }) })] }));
|
|
22
22
|
}
|
|
23
23
|
function todoProgress({ todos }, { runStreaming = false } = {}) {
|
|
24
24
|
const hasPending = todos.some((todo) => todo.status === 'pending' || todo.status === 'in_progress');
|
|
@@ -32,4 +32,12 @@ function todoProgress({ todos }, { runStreaming = false } = {}) {
|
|
|
32
32
|
function todoLabel({ todo }, _options = {}) {
|
|
33
33
|
return todo.status === 'in_progress' && todo.activeForm ? todo.activeForm : todo.content;
|
|
34
34
|
}
|
|
35
|
+
function todoStatusGlyph({ status }) {
|
|
36
|
+
switch (status) {
|
|
37
|
+
case 'completed': return '✓';
|
|
38
|
+
case 'in_progress': return '◐';
|
|
39
|
+
case 'stopped': return '!';
|
|
40
|
+
default: return '○';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
35
43
|
//# sourceMappingURL=TodoCard.js.map
|
|
@@ -16,7 +16,9 @@ export interface ToolCardProps {
|
|
|
16
16
|
projectFileNames?: Set<string>;
|
|
17
17
|
/** Lifts a basename up to the host so it can focus the matching tab in its own file viewer. */
|
|
18
18
|
onRequestOpenFile?: (name: string) => void;
|
|
19
|
+
/** A later call of the same tool in this turn succeeded (`retriedToolRowIds`): a failure shows a neutral "Retried" badge, not red. */
|
|
20
|
+
retried?: boolean;
|
|
19
21
|
}
|
|
20
|
-
export declare function ToolCard({ use, result, runStreaming, runSucceeded, projectFileNames, onRequestOpenFile }: ToolCardProps): import("react").JSX.Element | null;
|
|
22
|
+
export declare function ToolCard({ use, result, runStreaming, runSucceeded, projectFileNames, onRequestOpenFile, retried }: ToolCardProps): import("react").JSX.Element | null;
|
|
21
23
|
export {};
|
|
22
24
|
//# sourceMappingURL=ToolCard.d.ts.map
|