@jini-ai/chat 0.6.6 → 0.6.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/dist/core/accepted-input.d.ts +54 -0
- package/dist/core/accepted-input.js +165 -0
- package/dist/core/agent-plugin-chat-state.d.ts +43 -0
- package/dist/core/agent-plugin-chat-state.js +48 -0
- package/dist/core/assistant-client/a2ui-action-poster.js +32 -28
- package/dist/core/assistant-client/messages.d.ts +1 -1
- package/dist/core/assistant-client/messages.js +2 -2
- package/dist/core/assistant-client/transport.d.ts +2 -2
- package/dist/core/assistant-client/transport.js +184 -97
- package/dist/core/assistant-content.d.ts +2 -1
- package/dist/core/assistant-content.js +9 -11
- package/dist/core/browser/page-actions.js +4 -2
- package/dist/core/composer-history.d.ts +16 -9
- package/dist/core/composer-history.js +28 -16
- package/dist/core/conversation-title.d.ts +3 -0
- package/dist/core/conversation-title.js +3 -0
- package/dist/core/durable-projection.js +20 -13
- package/dist/core/durable-runs/continuation.d.ts +4 -0
- package/dist/core/durable-runs/continuation.js +31 -6
- package/dist/core/durable-runs/ledger-contracts.d.ts +2 -1
- package/dist/core/durable-runs/ports.d.ts +6 -2
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +2 -0
- package/dist/core/messages.d.ts +2 -0
- package/dist/core/partial-json.d.ts +6 -7
- package/dist/core/partial-json.js +81 -47
- package/dist/core/persistence/title.js +4 -2
- package/dist/core/question-answer.d.ts +23 -0
- package/dist/core/question-answer.js +66 -0
- package/dist/core/question-form/format.d.ts +6 -6
- package/dist/core/question-form/format.js +16 -17
- package/dist/core/question-form/normalize.d.ts +18 -0
- package/dist/core/question-form/normalize.js +112 -122
- package/dist/core/question-form/scan.js +141 -203
- package/dist/core/run-activity.js +62 -49
- package/dist/core/run-events/entry.d.ts +10 -1
- package/dist/core/run-events/entry.js +117 -87
- package/dist/core/todos.d.ts +1 -1
- package/dist/core/todos.js +60 -85
- package/dist/core/tool-events.js +14 -12
- package/dist/core/transcript.js +62 -16
- package/dist/core/turn-usage.js +54 -32
- package/dist/core/user-text-redaction.js +3 -0
- package/dist/core/util/manifest.js +77 -82
- package/dist/core/util/markdown-context.js +51 -53
- package/dist/core/util/parser.js +136 -144
- package/dist/core/util/pointer.js +16 -16
- package/dist/core/util/strip.js +57 -67
- package/dist/core/util/validate.js +15 -17
- package/dist/embed-widget/browser/session-store.js +21 -12
- package/dist/embed-widget/transports/fetch-sse.js +108 -95
- package/dist/embed-widget/vanilla/index.js +81 -70
- package/dist/react/agent-bridge/frontend-session-bridge.js +80 -52
- package/dist/react/components/AttachmentPreviewModal.js +25 -1
- package/dist/react/components/ChatSettingsDialog.d.ts +13 -0
- package/dist/react/components/ChatSettingsDialog.hooks.d.ts +115 -0
- package/dist/react/components/ChatSettingsDialog.hooks.js +40 -0
- package/dist/react/components/ChatSettingsDialog.js +72 -0
- package/dist/react/components/ChatSettingsSegmented.d.ts +18 -0
- package/dist/react/components/ChatSettingsSegmented.js +11 -0
- package/dist/react/components/Composer.d.ts +5 -1
- package/dist/react/components/Composer.js +126 -82
- package/dist/react/components/ConversationList.js +25 -39
- package/dist/react/components/ConversationTitleEditor.d.ts +27 -0
- package/dist/react/components/ConversationTitleEditor.js +54 -0
- package/dist/react/components/Icon.d.ts +1 -1
- package/dist/react/components/Icon.js +25 -45
- package/dist/react/components/Markdown.js +134 -123
- package/dist/react/components/McpUiSurfaceCard.d.ts +2 -2
- package/dist/react/components/McpUiSurfaceCard.js +30 -9
- package/dist/react/components/MessageList.d.ts +13 -13
- package/dist/react/components/MessageList.hooks.d.ts +6 -1
- package/dist/react/components/MessageList.hooks.js +19 -3
- package/dist/react/components/MessageList.js +34 -10
- package/dist/react/components/MessageRow.js +47 -31
- package/dist/react/components/MessageRow.usage.js +16 -11
- package/dist/react/components/MidRunUserMessage.js +16 -3
- package/dist/react/components/QuestionForm.js +148 -76
- package/dist/react/components/QuestionsPanel.js +1 -1
- package/dist/react/components/TodoCard.js +12 -6
- package/dist/react/components/ToolCard.js +47 -40
- package/dist/react/components/composer-discovery.js +6 -1
- package/dist/react/components/mcp-ui-surface-state.d.ts +6 -11
- package/dist/react/components/mcp-ui-surface-state.js +23 -8
- package/dist/react/ext-event-renderer-registry.d.ts +8 -0
- package/dist/react/ext-event-renderer-registry.js +5 -2
- package/dist/react/features/chat-pane/components/AgentPluginUsage.d.ts +7 -0
- package/dist/react/features/chat-pane/components/AgentPluginUsage.js +45 -0
- package/dist/react/features/chat-pane/components/AgentRuntimePicker.js +55 -19
- package/dist/react/features/chat-pane/components/ChatPane.d.ts +1 -1
- package/dist/react/features/chat-pane/components/ChatPane.js +192 -74
- package/dist/react/features/chat-pane/components/RuntimeDropUp.d.ts +29 -0
- package/dist/react/features/chat-pane/components/RuntimeDropUp.js +45 -0
- package/dist/react/features/chat-pane/create-daemon-attachment-uploader.js +80 -47
- package/dist/react/features/chat-pane/create-mcp-ui-tool-caller.js +44 -40
- package/dist/react/features/chat-pane/create-mid-run-message-poster.d.ts +6 -6
- package/dist/react/features/chat-pane/create-mid-run-message-poster.js +8 -8
- package/dist/react/features/chat-pane/create-typed-answer-poster.d.ts +3 -3
- package/dist/react/features/chat-pane/create-typed-answer-poster.js +3 -3
- package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.d.ts +6 -1
- package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.js +11 -5
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +12 -7
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +340 -149
- package/dist/react/features/chat-pane/hooks/useChatPaneControlsHeight.hooks.js +7 -6
- package/dist/react/features/chat-pane/hooks/useChatPaneNewTabLinks.hooks.js +28 -10
- package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.d.ts +3 -3
- package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.js +60 -12
- package/dist/react/features/chat-pane/hooks/useRuntimeDropUp.hooks.d.ts +49 -0
- package/dist/react/features/chat-pane/hooks/useRuntimeDropUp.hooks.js +143 -0
- package/dist/react/features/chat-pane/model-options.d.ts +14 -0
- package/dist/react/features/chat-pane/model-options.js +45 -25
- package/dist/react/features/chat-pane/rules.d.ts +8 -9
- package/dist/react/features/chat-pane/rules.js +84 -46
- package/dist/react/features/chat-pane/styles.d.ts +1 -1
- package/dist/react/features/chat-pane/styles.js +152 -36
- package/dist/react/features/chat-pane/types.d.ts +17 -8
- package/dist/react/hooks/assistant-chats.js +12 -5
- package/dist/react/hooks/composer-draft-cache.js +116 -118
- package/dist/react/hooks/composer-history-storage.js +5 -5
- package/dist/react/hooks/fab-position.js +7 -4
- package/dist/react/hooks/useAnsweredCards.d.ts +21 -4
- package/dist/react/hooks/useAnsweredCards.js +38 -11
- package/dist/react/hooks/useArtifactStream.js +28 -22
- package/dist/react/hooks/useComposer.d.ts +20 -0
- package/dist/react/hooks/useComposer.js +22 -9
- package/dist/react/hooks/useComposerHistory.hooks.d.ts +6 -1
- package/dist/react/hooks/useComposerHistory.hooks.js +39 -19
- package/dist/react/hooks/useComposerHistoryKeys.hooks.js +24 -14
- package/dist/react/hooks/useConversation.d.ts +7 -0
- package/dist/react/hooks/useConversation.js +23 -10
- package/dist/react/hooks/useExtEventGroups.js +39 -39
- package/dist/react/hooks/useQuestionForms.js +21 -26
- package/dist/react/hooks/useRunStream.js +10 -2
- package/dist/react/index.d.ts +7 -1
- package/dist/react/index.js +9 -0
- package/dist/react/message-blocks.d.ts +6 -4
- package/dist/react/message-blocks.js +94 -72
- package/dist/react/slots.d.ts +2 -2
- package/dist/react/tool-call-display.js +19 -17
- package/dist/server/durable-runs/recover.js +132 -80
- package/dist/server/run-finalizer/entry.js +25 -15
- package/dist/store/legacy/sqlite/conversations.d.ts +11 -11
- package/dist/store/legacy/sqlite/conversations.js +69 -98
- package/dist/store/legacy/sqlite/messages.d.ts +10 -2
- package/dist/store/legacy/sqlite/messages.js +113 -71
- package/dist/store/runtime/chat-history-store.js +9 -3
- package/dist/store/runtime/durable-run-store.d.ts +2 -2
- package/dist/store/runtime/durable-run-store.js +165 -117
- package/dist/store/runtime/history-tools.js +118 -82
- package/dist/store/runtime/run-ledger.d.ts +4 -1
- package/dist/store/runtime/run-ledger.js +56 -16
- package/dist/store/sql/cursor.js +38 -18
- package/dist/store/sql/store.d.ts +18 -0
- package/dist/store/sql/store.js +46 -15
- package/package.json +12 -10
|
@@ -1,48 +1,28 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
const CROSS = _jsxs(_Fragment, { children: [_jsx("path", { d: "M20 4 4 20" }), _jsx("path", { d: "m4 4 16 16" })] });
|
|
3
|
+
// Each glyph's SVG children; `Icon` wraps them in the shared `<svg>` shell.
|
|
4
|
+
const ICON_GLYPHS = {
|
|
5
|
+
spinner: _jsx("path", { d: "M21 12a9 9 0 1 1-6.22-8.56" }),
|
|
6
|
+
check: _jsx("path", { d: "M20 6 9 17l-5-5" }),
|
|
7
|
+
close: CROSS,
|
|
8
|
+
x: CROSS,
|
|
9
|
+
'chevron-down': _jsx("path", { d: "m6 9 6 6 6-6" }),
|
|
10
|
+
/** Conversation-switcher trigger — a speech bubble reads as "chat history" without a label. */
|
|
11
|
+
comment: (_jsx("path", { d: "M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" })),
|
|
12
|
+
search: _jsxs(_Fragment, { children: [_jsx("circle", { cx: "11", cy: "11", r: "7" }), _jsx("path", { d: "m20 20-3.5-3.5" })] }),
|
|
13
|
+
plus: _jsxs(_Fragment, { children: [_jsx("path", { d: "M12 5v14" }), _jsx("path", { d: "M5 12h14" })] }),
|
|
14
|
+
'chevron-left': _jsx("path", { d: "m15 18-6-6 6-6" }),
|
|
15
|
+
'chevron-right': _jsx("path", { d: "m9 18 6-6-6-6" }),
|
|
16
|
+
attach: _jsx("path", { d: "m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48" }),
|
|
17
|
+
file: _jsxs(_Fragment, { children: [_jsx("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z" }), _jsx("path", { d: "M14 2v6h6" }), _jsx("path", { d: "M8 13h8M8 17h6" })] }),
|
|
18
|
+
image: _jsxs(_Fragment, { children: [_jsx("rect", { width: "18", height: "18", x: "3", y: "3", rx: "2" }), _jsx("circle", { cx: "9", cy: "9", r: "2" }), _jsx("path", { d: "m21 15-5-5L5 21" })] }),
|
|
19
|
+
pencil: _jsxs(_Fragment, { children: [_jsx("path", { d: "m16 3 5 5L8 21H3v-5L16 3Z" }), _jsx("path", { d: "m14 5 5 5" })] }),
|
|
20
|
+
/** Per-message copy affordance (MessageRow.tsx). Two overlapping squares -- the conventional copy glyph. */
|
|
21
|
+
copy: _jsxs(_Fragment, { children: [_jsx("rect", { width: "14", height: "14", x: "8", y: "8", rx: "2", ry: "2" }), _jsx("path", { d: "M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" })] }),
|
|
22
|
+
};
|
|
2
23
|
export function Icon({ name, size = 14, strokeWidth = 1.6, ...rest }) {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
viewBox: '0 0 24 24',
|
|
7
|
-
fill: 'none',
|
|
8
|
-
stroke: 'currentColor',
|
|
9
|
-
strokeWidth,
|
|
10
|
-
strokeLinecap: 'round',
|
|
11
|
-
strokeLinejoin: 'round',
|
|
12
|
-
'aria-hidden': true,
|
|
13
|
-
focusable: 'false',
|
|
14
|
-
...rest,
|
|
15
|
-
};
|
|
16
|
-
switch (name) {
|
|
17
|
-
case 'spinner':
|
|
18
|
-
return (_jsx("svg", { ...common, className: `jini-icon-spin ${rest.className ?? ''}`.trim(), children: _jsx("path", { d: "M21 12a9 9 0 1 1-6.22-8.56" }) }));
|
|
19
|
-
case 'check':
|
|
20
|
-
return (_jsx("svg", { ...common, children: _jsx("path", { d: "M20 6 9 17l-5-5" }) }));
|
|
21
|
-
case 'close':
|
|
22
|
-
case 'x':
|
|
23
|
-
return (_jsxs("svg", { ...common, children: [_jsx("path", { d: "M20 4 4 20" }), _jsx("path", { d: "m4 4 16 16" })] }));
|
|
24
|
-
case 'chevron-down':
|
|
25
|
-
return (_jsx("svg", { ...common, children: _jsx("path", { d: "m6 9 6 6 6-6" }) }));
|
|
26
|
-
/** Conversation-switcher trigger — a speech bubble reads as "chat history" without a label. */
|
|
27
|
-
case 'comment':
|
|
28
|
-
return (_jsx("svg", { ...common, children: _jsx("path", { d: "M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" }) }));
|
|
29
|
-
case 'search':
|
|
30
|
-
return (_jsxs("svg", { ...common, children: [_jsx("circle", { cx: "11", cy: "11", r: "7" }), _jsx("path", { d: "m20 20-3.5-3.5" })] }));
|
|
31
|
-
case 'plus':
|
|
32
|
-
return (_jsxs("svg", { ...common, children: [_jsx("path", { d: "M12 5v14" }), _jsx("path", { d: "M5 12h14" })] }));
|
|
33
|
-
case 'chevron-left':
|
|
34
|
-
return (_jsx("svg", { ...common, children: _jsx("path", { d: "m15 18-6-6 6-6" }) }));
|
|
35
|
-
case 'chevron-right':
|
|
36
|
-
return (_jsx("svg", { ...common, children: _jsx("path", { d: "m9 18 6-6-6-6" }) }));
|
|
37
|
-
case 'attach':
|
|
38
|
-
return (_jsx("svg", { ...common, children: _jsx("path", { d: "m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48" }) }));
|
|
39
|
-
case 'file':
|
|
40
|
-
return (_jsxs("svg", { ...common, children: [_jsx("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z" }), _jsx("path", { d: "M14 2v6h6" }), _jsx("path", { d: "M8 13h8M8 17h6" })] }));
|
|
41
|
-
case 'image':
|
|
42
|
-
return (_jsxs("svg", { ...common, children: [_jsx("rect", { width: "18", height: "18", x: "3", y: "3", rx: "2" }), _jsx("circle", { cx: "9", cy: "9", r: "2" }), _jsx("path", { d: "m21 15-5-5L5 21" })] }));
|
|
43
|
-
/** Per-message copy affordance (MessageRow.tsx). Two overlapping squares -- the conventional copy glyph. */
|
|
44
|
-
case 'copy':
|
|
45
|
-
return (_jsxs("svg", { ...common, children: [_jsx("rect", { width: "14", height: "14", x: "8", y: "8", rx: "2", ry: "2" }), _jsx("path", { d: "M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" })] }));
|
|
46
|
-
}
|
|
24
|
+
// The spinner alone puts its spin class in front of any caller-supplied one.
|
|
25
|
+
const className = name === 'spinner' ? `jini-icon-spin ${rest.className ?? ''}`.trim() : rest.className;
|
|
26
|
+
return (_jsx("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: strokeWidth, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, focusable: "false", ...rest, className: className, children: ICON_GLYPHS[name] }));
|
|
47
27
|
}
|
|
48
28
|
//# sourceMappingURL=Icon.js.map
|
|
@@ -46,45 +46,38 @@ export function Markdown({ children }) {
|
|
|
46
46
|
const blocks = parseBlocks(children);
|
|
47
47
|
return (_jsx(_Fragment, { children: blocks.map((block, i) => (_jsx(Fragment, { children: renderBlock(block) }, i))) }));
|
|
48
48
|
}
|
|
49
|
-
// GFM pipe-table cell split, ported
|
|
50
|
-
// `runtime/markdown.tsx`.
|
|
49
|
+
// GFM pipe-table cell split, ported logic-for-logic from OD's
|
|
50
|
+
// `runtime/markdown.tsx`. Tokenizes rather than a naive `split('|')`
|
|
51
51
|
// so it can honor three cell-content rules without placeholder substitution:
|
|
52
52
|
// `\|` is a literal pipe inside a cell, a `|` inside a backtick code span is
|
|
53
53
|
// cell content rather than a column boundary, and a single optional leading
|
|
54
54
|
// `|` plus an unescaped trailing `|` are row terminators, not empty cells.
|
|
55
|
+
// Always returns at least one cell (an empty row yields `['']`).
|
|
56
|
+
const TABLE_ROW_LEAD = /^ *\|?/;
|
|
57
|
+
const TABLE_CELL_TOKEN = /\\\||`|\||[^`|\\]+|\\/g;
|
|
55
58
|
function splitTableCells(line) {
|
|
56
59
|
const cells = [];
|
|
57
60
|
let cur = '';
|
|
58
61
|
let inCode = false;
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
i++;
|
|
62
|
-
if (line[i] === '|')
|
|
63
|
-
i++;
|
|
64
|
-
for (; i < line.length; i++) {
|
|
65
|
-
const ch = line[i];
|
|
66
|
-
if (ch === '\\' && line[i + 1] === '|') {
|
|
67
|
-
cur += '|';
|
|
68
|
-
i++;
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
71
|
-
if (ch === '`') {
|
|
72
|
-
inCode = !inCode;
|
|
73
|
-
cur += ch;
|
|
74
|
-
continue;
|
|
75
|
-
}
|
|
76
|
-
if (ch === '|' && !inCode) {
|
|
62
|
+
for (const token of line.replace(TABLE_ROW_LEAD, '').match(TABLE_CELL_TOKEN) ?? []) {
|
|
63
|
+
if (token === '|' && !inCode) {
|
|
77
64
|
cells.push(cur.trim());
|
|
78
65
|
cur = '';
|
|
79
66
|
continue;
|
|
80
67
|
}
|
|
81
|
-
|
|
68
|
+
if (token === '`')
|
|
69
|
+
inCode = !inCode;
|
|
70
|
+
cur += token === '\\|' ? '|' : token;
|
|
82
71
|
}
|
|
83
72
|
const tail = cur.trim();
|
|
84
73
|
if (cells.length === 0 || tail !== '')
|
|
85
74
|
cells.push(tail);
|
|
86
75
|
return cells;
|
|
87
76
|
}
|
|
77
|
+
const TABLE_ALIGN_CELL = /^:?-{1,}:?$/;
|
|
78
|
+
// Every cell `TABLE_ALIGN_CELL` accepts starts and ends with `:` or `-`, so its
|
|
79
|
+
// first + last character is always exactly one of these four keys.
|
|
80
|
+
const COLUMN_ALIGN = { '::': 'center', '-:': 'right', ':-': 'left', '--': null };
|
|
88
81
|
// Recognizes a GFM alignment row (`| --- | :---: | ---: |`) and returns the
|
|
89
82
|
// per-column alignment, or `null` if `line` isn't one — used both to detect
|
|
90
83
|
// where a table starts and to read its column alignment.
|
|
@@ -92,26 +85,17 @@ function parseTableAlignRow(line) {
|
|
|
92
85
|
if (!line.includes('|'))
|
|
93
86
|
return null;
|
|
94
87
|
const cells = splitTableCells(line);
|
|
95
|
-
if (cells.
|
|
88
|
+
if (!cells.every((cell) => TABLE_ALIGN_CELL.test(cell)))
|
|
96
89
|
return null;
|
|
97
|
-
|
|
98
|
-
for (const cell of cells) {
|
|
99
|
-
if (!/^:?-{1,}:?$/.test(cell))
|
|
100
|
-
return null;
|
|
101
|
-
const left = cell.startsWith(':');
|
|
102
|
-
const right = cell.endsWith(':');
|
|
103
|
-
aligns.push(left && right ? 'center' : right ? 'right' : left ? 'left' : null);
|
|
104
|
-
}
|
|
105
|
-
return aligns;
|
|
90
|
+
return cells.map((cell) => COLUMN_ALIGN[`${cell[0]}${cell.at(-1)}`]);
|
|
106
91
|
}
|
|
107
92
|
// A table starts at `i` when `lines[i]` is a pipe-bearing header row
|
|
108
93
|
// immediately followed by a valid alignment row at `lines[i + 1]`.
|
|
109
94
|
function isTableStartAt(lines, i) {
|
|
110
|
-
const header = lines[i];
|
|
111
95
|
const sep = lines[i + 1];
|
|
112
|
-
if (
|
|
96
|
+
if (sep === undefined)
|
|
113
97
|
return false;
|
|
114
|
-
if (!
|
|
98
|
+
if (!lines[i].includes('|'))
|
|
115
99
|
return false;
|
|
116
100
|
return parseTableAlignRow(sep) !== null;
|
|
117
101
|
}
|
|
@@ -124,7 +108,7 @@ function collectTableRows(lines, start) {
|
|
|
124
108
|
let i = start;
|
|
125
109
|
while (i < lines.length) {
|
|
126
110
|
const row = lines[i];
|
|
127
|
-
if (row
|
|
111
|
+
if (row.trim() === '' || !row.includes('|'))
|
|
128
112
|
break;
|
|
129
113
|
rows.push(splitTableCells(row));
|
|
130
114
|
i++;
|
|
@@ -132,95 +116,122 @@ function collectTableRows(lines, start) {
|
|
|
132
116
|
return { rows, next: i };
|
|
133
117
|
}
|
|
134
118
|
const ORDERED_ITEM = /^\s*\d+[.)]\s+/;
|
|
119
|
+
const FENCE = /^```/;
|
|
120
|
+
const HEADING = /^(#{1,3})\s+(.*)$/;
|
|
121
|
+
const RULE = /^(-{3,}|\*{3,}|_{3,})\s*$/;
|
|
122
|
+
const QUOTE_LINE = /^>\s?/;
|
|
123
|
+
const BULLET_ITEM = /^\s*[-*]\s+/;
|
|
124
|
+
function parseFence(lines, i) {
|
|
125
|
+
const line = lines[i];
|
|
126
|
+
if (!FENCE.test(line))
|
|
127
|
+
return null;
|
|
128
|
+
const lang = line.slice(3).trim() || null;
|
|
129
|
+
const body = [];
|
|
130
|
+
let j = i + 1;
|
|
131
|
+
while (j < lines.length && !FENCE.test(lines[j])) {
|
|
132
|
+
body.push(lines[j]);
|
|
133
|
+
j += 1;
|
|
134
|
+
}
|
|
135
|
+
// `j + 1` skips the closing fence (or end of input if unterminated).
|
|
136
|
+
return { block: { kind: 'code', lang, body: body.join('\n') }, next: j + 1 };
|
|
137
|
+
}
|
|
138
|
+
function parseHeading(lines, i) {
|
|
139
|
+
const heading = HEADING.exec(lines[i]);
|
|
140
|
+
if (!heading)
|
|
141
|
+
return null;
|
|
142
|
+
// Both capture groups are mandatory (non-optional) in this pattern, so
|
|
143
|
+
// whenever `heading` itself is truthy, `heading[1]`/`heading[2]` are
|
|
144
|
+
// always matched strings (never `undefined`) — same dead-fallback
|
|
145
|
+
// reasoning as the `lines[i]` accesses above.
|
|
146
|
+
return { block: { kind: 'h', level: heading[1].length, text: heading[2] }, next: i + 1 };
|
|
147
|
+
}
|
|
148
|
+
function parseRule(lines, i) {
|
|
149
|
+
return RULE.test(lines[i]) ? { block: { kind: 'hr' }, next: i + 1 } : null;
|
|
150
|
+
}
|
|
151
|
+
// Collects the run of consecutive lines matching `marker`, each with that
|
|
152
|
+
// marker stripped — the shared shape of blockquotes and bullet lists.
|
|
153
|
+
function collectMarked(lines, start, marker) {
|
|
154
|
+
const items = [];
|
|
155
|
+
let i = start;
|
|
156
|
+
while (i < lines.length && marker.test(lines[i])) {
|
|
157
|
+
items.push(lines[i].replace(marker, ''));
|
|
158
|
+
i += 1;
|
|
159
|
+
}
|
|
160
|
+
return { items, next: i };
|
|
161
|
+
}
|
|
162
|
+
function parseQuote(lines, i) {
|
|
163
|
+
if (!QUOTE_LINE.test(lines[i]))
|
|
164
|
+
return null;
|
|
165
|
+
const { items, next } = collectMarked(lines, i, QUOTE_LINE);
|
|
166
|
+
return { block: { kind: 'bq', text: items.join('\n') }, next };
|
|
167
|
+
}
|
|
168
|
+
function parseBulletList(lines, i) {
|
|
169
|
+
if (!BULLET_ITEM.test(lines[i]))
|
|
170
|
+
return null;
|
|
171
|
+
const { items, next } = collectMarked(lines, i, BULLET_ITEM);
|
|
172
|
+
return { block: { kind: 'ul', items }, next };
|
|
173
|
+
}
|
|
174
|
+
function parseTable(lines, i) {
|
|
175
|
+
if (!isTableStartAt(lines, i))
|
|
176
|
+
return null;
|
|
177
|
+
const aligns = parseTableAlignRow(lines[i + 1]);
|
|
178
|
+
const headers = splitTableCells(lines[i]);
|
|
179
|
+
const { rows, next } = collectTableRows(lines, i + 2);
|
|
180
|
+
return { block: { kind: 'table', aligns, headers, rows }, next };
|
|
181
|
+
}
|
|
182
|
+
function parseOrderedList(lines, i) {
|
|
183
|
+
const line = lines[i];
|
|
184
|
+
if (!ORDERED_ITEM.test(line))
|
|
185
|
+
return null;
|
|
186
|
+
const start = Number(/\d+/.exec(line)[0]);
|
|
187
|
+
const items = [];
|
|
188
|
+
let j = i;
|
|
189
|
+
while (j < lines.length && ORDERED_ITEM.test(lines[j])) {
|
|
190
|
+
items.push(lines[j].replace(ORDERED_ITEM, ''));
|
|
191
|
+
j = nextLooseListLine(lines, j + 1);
|
|
192
|
+
}
|
|
193
|
+
return { block: { kind: 'ol', start, items }, next: j };
|
|
194
|
+
}
|
|
195
|
+
// Tried in this order at each non-blank line; the first match wins, and a
|
|
196
|
+
// line none of them claims starts a paragraph.
|
|
197
|
+
const BLOCK_PARSERS = [
|
|
198
|
+
parseFence,
|
|
199
|
+
parseHeading,
|
|
200
|
+
parseRule,
|
|
201
|
+
parseQuote,
|
|
202
|
+
parseBulletList,
|
|
203
|
+
parseTable,
|
|
204
|
+
parseOrderedList,
|
|
205
|
+
];
|
|
206
|
+
function parseParagraph(lines, i) {
|
|
207
|
+
const paragraph = [];
|
|
208
|
+
let j = i;
|
|
209
|
+
while (j < lines.length && lines[j].trim() !== '' && !isBlockStart(lines, j)) {
|
|
210
|
+
paragraph.push(lines[j]);
|
|
211
|
+
j += 1;
|
|
212
|
+
}
|
|
213
|
+
return { block: { kind: 'p', text: paragraph.join('\n') }, next: j };
|
|
214
|
+
}
|
|
215
|
+
function parseBlockAt(lines, i) {
|
|
216
|
+
for (const parse of BLOCK_PARSERS) {
|
|
217
|
+
const parsed = parse(lines, i);
|
|
218
|
+
if (parsed)
|
|
219
|
+
return parsed;
|
|
220
|
+
}
|
|
221
|
+
return parseParagraph(lines, i);
|
|
222
|
+
}
|
|
135
223
|
function parseBlocks(input) {
|
|
136
224
|
const lines = input.split(/\r?\n/);
|
|
137
225
|
const blocks = [];
|
|
138
226
|
let i = 0;
|
|
139
|
-
// Every `lines[i]` access below is guarded by an `i < lines.length` check
|
|
140
|
-
// (either this loop's own condition or an inner `while`'s left-hand
|
|
141
|
-
// operand, short-circuited before the index is read) — `lines` is a
|
|
142
|
-
// `string[]` from `String.prototype.split`, which never contains
|
|
143
|
-
// `undefined` entries, so the index is always in range and always a
|
|
144
|
-
// string. The `?? ''`/non-null-assertion sites below are TS-only
|
|
145
|
-
// fallbacks for `noUncheckedIndexedAccess`-style indexing with no real
|
|
146
|
-
// runtime path; using `!` documents that instead of instrumenting an
|
|
147
|
-
// unreachable branch (same convention as `ToolCard.tsx`'s `baseName`
|
|
148
|
-
// comment).
|
|
149
227
|
while (i < lines.length) {
|
|
150
|
-
|
|
151
|
-
if (line.trim() === '') {
|
|
152
|
-
i += 1;
|
|
153
|
-
continue;
|
|
154
|
-
}
|
|
155
|
-
if (/^```/.test(line)) {
|
|
156
|
-
const lang = line.slice(3).trim() || null;
|
|
157
|
-
const body = [];
|
|
158
|
-
i += 1;
|
|
159
|
-
while (i < lines.length && !/^```/.test(lines[i])) {
|
|
160
|
-
body.push(lines[i]);
|
|
161
|
-
i += 1;
|
|
162
|
-
}
|
|
163
|
-
i += 1; // skip closing fence (or end of input if unterminated)
|
|
164
|
-
blocks.push({ kind: 'code', lang, body: body.join('\n') });
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
const heading = /^(#{1,3})\s+(.*)$/.exec(line);
|
|
168
|
-
if (heading) {
|
|
169
|
-
// Both capture groups are mandatory (non-optional) in this pattern, so
|
|
170
|
-
// whenever `heading` itself is truthy, `heading[1]`/`heading[2]` are
|
|
171
|
-
// always matched strings (never `undefined`) — same dead-fallback
|
|
172
|
-
// reasoning as the `lines[i]` accesses above.
|
|
173
|
-
blocks.push({ kind: 'h', level: heading[1].length, text: heading[2] });
|
|
228
|
+
if (lines[i].trim() === '') {
|
|
174
229
|
i += 1;
|
|
175
230
|
continue;
|
|
176
231
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
continue;
|
|
181
|
-
}
|
|
182
|
-
if (/^>\s?/.test(line)) {
|
|
183
|
-
const quoted = [];
|
|
184
|
-
while (i < lines.length && /^>\s?/.test(lines[i])) {
|
|
185
|
-
quoted.push(lines[i].replace(/^>\s?/, ''));
|
|
186
|
-
i += 1;
|
|
187
|
-
}
|
|
188
|
-
blocks.push({ kind: 'bq', text: quoted.join('\n') });
|
|
189
|
-
continue;
|
|
190
|
-
}
|
|
191
|
-
if (/^\s*[-*]\s+/.test(line)) {
|
|
192
|
-
const items = [];
|
|
193
|
-
while (i < lines.length && /^\s*[-*]\s+/.test(lines[i])) {
|
|
194
|
-
items.push(lines[i].replace(/^\s*[-*]\s+/, ''));
|
|
195
|
-
i += 1;
|
|
196
|
-
}
|
|
197
|
-
blocks.push({ kind: 'ul', items });
|
|
198
|
-
continue;
|
|
199
|
-
}
|
|
200
|
-
if (isTableStartAt(lines, i)) {
|
|
201
|
-
const aligns = parseTableAlignRow(lines[i + 1]);
|
|
202
|
-
const headers = splitTableCells(line);
|
|
203
|
-
const { rows, next } = collectTableRows(lines, i + 2);
|
|
204
|
-
blocks.push({ kind: 'table', aligns, headers, rows });
|
|
205
|
-
i = next;
|
|
206
|
-
continue;
|
|
207
|
-
}
|
|
208
|
-
if (ORDERED_ITEM.test(line)) {
|
|
209
|
-
const start = Number(/\d+/.exec(line)[0]);
|
|
210
|
-
const items = [];
|
|
211
|
-
while (i < lines.length && ORDERED_ITEM.test(lines[i])) {
|
|
212
|
-
items.push(lines[i].replace(ORDERED_ITEM, ''));
|
|
213
|
-
i = nextLooseListLine(lines, i + 1);
|
|
214
|
-
}
|
|
215
|
-
blocks.push({ kind: 'ol', start, items });
|
|
216
|
-
continue;
|
|
217
|
-
}
|
|
218
|
-
const paragraph = [];
|
|
219
|
-
while (i < lines.length && lines[i].trim() !== '' && !isBlockStart(lines, i)) {
|
|
220
|
-
paragraph.push(lines[i]);
|
|
221
|
-
i += 1;
|
|
222
|
-
}
|
|
223
|
-
blocks.push({ kind: 'p', text: paragraph.join('\n') });
|
|
232
|
+
const { block, next } = parseBlockAt(lines, i);
|
|
233
|
+
blocks.push(block);
|
|
234
|
+
i = next;
|
|
224
235
|
}
|
|
225
236
|
return blocks;
|
|
226
237
|
}
|
|
@@ -238,11 +249,11 @@ function nextLooseListLine(lines, i) {
|
|
|
238
249
|
}
|
|
239
250
|
function isBlockStart(lines, i) {
|
|
240
251
|
const line = lines[i];
|
|
241
|
-
return (
|
|
252
|
+
return (FENCE.test(line)
|
|
242
253
|
|| /^(#{1,3})\s+/.test(line)
|
|
243
|
-
||
|
|
244
|
-
||
|
|
245
|
-
||
|
|
254
|
+
|| RULE.test(line)
|
|
255
|
+
|| QUOTE_LINE.test(line)
|
|
256
|
+
|| BULLET_ITEM.test(line)
|
|
246
257
|
|| ORDERED_ITEM.test(line)
|
|
247
258
|
|| isTableStartAt(lines, i));
|
|
248
259
|
}
|
|
@@ -310,8 +321,9 @@ function useTableOverflow() {
|
|
|
310
321
|
const wrapRef = useRef(null);
|
|
311
322
|
const [isOverflowing, setIsOverflowing] = useState(false);
|
|
312
323
|
useEffect(() => {
|
|
324
|
+
// The wrap div renders unconditionally, so its ref is attached by the time this effect runs.
|
|
313
325
|
const el = wrapRef.current;
|
|
314
|
-
if (
|
|
326
|
+
if (typeof ResizeObserver !== 'function')
|
|
315
327
|
return;
|
|
316
328
|
const check = () => setIsOverflowing(el.scrollWidth > el.clientWidth + TABLE_OVERFLOW_SLACK_PX);
|
|
317
329
|
const observer = new ResizeObserver(check);
|
|
@@ -354,9 +366,8 @@ function TableBlock(props) {
|
|
|
354
366
|
const [expanded, setExpanded] = useState(false);
|
|
355
367
|
const dialogRef = useRef(null);
|
|
356
368
|
useEffect(() => {
|
|
369
|
+
// The `<dialog>` renders unconditionally, so its ref is attached by the time this effect runs.
|
|
357
370
|
const dialog = dialogRef.current;
|
|
358
|
-
if (!dialog)
|
|
359
|
-
return;
|
|
360
371
|
if (expanded)
|
|
361
372
|
openTableDialog(dialog);
|
|
362
373
|
else
|
|
@@ -22,7 +22,7 @@ export interface McpUiSurfaceCardProps extends ExtEventRenderProps {
|
|
|
22
22
|
* scrolling for content taller than its `preferredFrameSize` guess. A host embedding this card in
|
|
23
23
|
* a fixed-width sidebar should pass a cap sized to its own viewport.
|
|
24
24
|
*/
|
|
25
|
-
maxHeight?: number;
|
|
25
|
+
maxHeight?: number | undefined;
|
|
26
26
|
/**
|
|
27
27
|
* Time source for a card's answer deadline (`MCP_UI_EXPIRES_AT_META_KEY`). Omit for the wall clock;
|
|
28
28
|
* a test injects a fake one to step through the countdown.
|
|
@@ -30,7 +30,7 @@ export interface McpUiSurfaceCardProps extends ExtEventRenderProps {
|
|
|
30
30
|
expiryClock?: SurfaceExpiryClock;
|
|
31
31
|
}
|
|
32
32
|
/** Registered against `ext-event-renderer-registry.ts`'s `'mcp-ui'` name — see module doc. */
|
|
33
|
-
export declare function McpUiSurfaceCard({ events, sandboxProxyUrl, onToolCall, onOpenLink, maxHeight, call, runStreaming, expiryClock, answered, onAnswered }: McpUiSurfaceCardProps): import("react").JSX.Element;
|
|
33
|
+
export declare function McpUiSurfaceCard({ events, sandboxProxyUrl, onToolCall, onOpenLink, maxHeight, call, runStreaming, expiryClock, answered, onAnswered, onAnswerText, answerText, answerRecorded }: McpUiSurfaceCardProps): import("react").JSX.Element;
|
|
34
34
|
/**
|
|
35
35
|
* The ext-event slot key for MCP-UI: one slot per `ui://` URI. Each resource is its own card, so a
|
|
36
36
|
* run's second card renders where it arrived instead of inside the first card's slot, while a
|
|
@@ -61,6 +61,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
61
61
|
* `visible` field in per-element STATE when a caller opts into it — see the regression tests added
|
|
62
62
|
* against the real driver in `@jini-ai/agentic`.
|
|
63
63
|
*/
|
|
64
|
+
import { questionAnswerFromResult } from '../../core/question-answer.js';
|
|
65
|
+
import { MidRunUserMessage } from './MidRunUserMessage.js';
|
|
64
66
|
import { Fragment, useMemo } from 'react';
|
|
65
67
|
import { McpUiHost, parseUIResource, readActionPlan, readExpiresAt, readPreferredFrameSize, } from '@jini-ai/ui/mcp-ui';
|
|
66
68
|
import { defaultExtEventRendererRegistry } from '../ext-event-renderer-registry.js';
|
|
@@ -93,8 +95,10 @@ function latestResourcesByUri(events) {
|
|
|
93
95
|
* has teeth for a producer whose id scheme this package has never seen.
|
|
94
96
|
*/
|
|
95
97
|
function pendingMirrorHandle(uri) {
|
|
98
|
+
// Never empty: only resources whose URI starts with `ui://` reach a card (`@jini-ai/ui/mcp-ui`'s
|
|
99
|
+
// resource parser rejects every other scheme), so the sanitized handle always keeps at least `ui`.
|
|
96
100
|
const sanitized = uri.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
97
|
-
return `mcp-ui-pending-${sanitized
|
|
101
|
+
return `mcp-ui-pending-${sanitized}`;
|
|
98
102
|
}
|
|
99
103
|
/** The one label a mirror region carries — what is pending, and what an agent could report or wait on. */
|
|
100
104
|
function pendingMirrorLabel(t, plan) {
|
|
@@ -119,23 +123,40 @@ function ClosedSurface({ call, label }) {
|
|
|
119
123
|
const title = call?.input?.title;
|
|
120
124
|
return (_jsx("div", { className: "mcpui-surface-card mcpui-surface-card-closed", role: "status", children: typeof title === 'string' && title.trim() !== '' ? `${title}: ${label}` : label }));
|
|
121
125
|
}
|
|
126
|
+
/** The live card's answer lifecycle: its deadline and delivery notification, using their existing owners. */
|
|
127
|
+
function surfaceInteraction({ resource, answered, onToolCall, onAnswered, onAnswerText, call }, _options = {}) {
|
|
128
|
+
// An answered card has no deadline left: its countdown goes, and the deadline passing while the
|
|
129
|
+
// answer's work still runs must not relabel it "This question expired".
|
|
130
|
+
const expiresAtMs = resource === undefined || answered === true ? undefined : readExpiresAt(resource);
|
|
131
|
+
// Reports the person's delivered answer to the row, which stops the "Waiting for your answer" line.
|
|
132
|
+
const toolCall = onToolCall === undefined || onAnswered === undefined ? onToolCall : reportDeliveredAnswer({ onToolCall, onAnswered, onAnswerText, input: call?.input });
|
|
133
|
+
return { expiresAtMs, toolCall };
|
|
134
|
+
}
|
|
122
135
|
/** Registered against `ext-event-renderer-registry.ts`'s `'mcp-ui'` name — see module doc. */
|
|
123
|
-
export function McpUiSurfaceCard({ events, sandboxProxyUrl, onToolCall, onOpenLink, maxHeight, call, runStreaming, expiryClock, answered, onAnswered }) {
|
|
136
|
+
export function McpUiSurfaceCard({ events, sandboxProxyUrl, onToolCall, onOpenLink, maxHeight, call, runStreaming, expiryClock, answered, onAnswered, onAnswerText, answerText, answerRecorded }) {
|
|
124
137
|
const t = useT();
|
|
125
138
|
const resources = useMemo(() => latestResourcesByUri(events), [events]);
|
|
126
139
|
// One card per slot is the registered shape (`mcpUiSurfaceSlotKey`), so `call` belongs to exactly
|
|
127
140
|
// that card. A group holding several URIs has no single call to judge them by, and stays live.
|
|
128
141
|
const single = resources.length === 1 ? resources[0] : undefined;
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const expiry = useSurfaceExpiry({ expiresAtMs: single === undefined || answered === true ? undefined : readExpiresAt(single) }, expiryClock === undefined ? {} : { clock: expiryClock });
|
|
142
|
+
const { expiresAtMs, toolCall } = surfaceInteraction({ resource: single, answered, onToolCall, onAnswered, onAnswerText, call });
|
|
143
|
+
const expiry = useSurfaceExpiry({ expiresAtMs }, expiryClock === undefined ? {} : { clock: expiryClock });
|
|
132
144
|
const closed = single === undefined
|
|
133
145
|
? undefined
|
|
134
146
|
: closedSurfaceLabel({ t, call, runStreaming, documentsForUri: events.length, expired: expiry?.expired === true });
|
|
147
|
+
const answer = answerText ?? questionAnswerFromResult({ result: call?.result, input: call?.input });
|
|
148
|
+
return _jsx(SurfaceCardContent, { resources: resources, single: single, answerRecorded: answerRecorded, answer: answer, closed: closed, call: call, t: t, sandboxProxyUrl: sandboxProxyUrl, toolCall: toolCall, onOpenLink: onOpenLink, maxHeight: maxHeight, expiry: expiry });
|
|
149
|
+
}
|
|
150
|
+
/** Rendering priority stays receipt, answer, closure, then the live documents. */
|
|
151
|
+
function SurfaceCardContent({ resources, single, answerRecorded, answer, closed, call, t, sandboxProxyUrl, toolCall, onOpenLink, maxHeight, expiry }) {
|
|
152
|
+
// MOVE: The accepted-input receipt owns its bubble; rendering the closed card too duplicates typed answers.
|
|
153
|
+
if (single !== undefined && answerRecorded)
|
|
154
|
+
return null;
|
|
155
|
+
if (single !== undefined && answer !== undefined) {
|
|
156
|
+
return _jsx(MidRunUserMessage, { events: [{ text: answer === 'Declined' ? t('Declined') : answer }] });
|
|
157
|
+
}
|
|
135
158
|
if (closed !== undefined)
|
|
136
159
|
return _jsx(ClosedSurface, { call: call, label: closed });
|
|
137
|
-
// Reports the person's delivered answer to the row, which stops the "Waiting for your answer" line.
|
|
138
|
-
const toolCall = onToolCall === undefined || onAnswered === undefined ? onToolCall : reportDeliveredAnswer({ onToolCall, onAnswered });
|
|
139
160
|
if (resources.length === 0) {
|
|
140
161
|
// Visible rather than silent: an `ext` event named `mcp-ui` that carries no parseable resource
|
|
141
162
|
// is a producer bug, and a transcript that renders nothing for it hides the bug behind what
|
|
@@ -154,7 +175,7 @@ export function McpUiSurfaceCard({ events, sandboxProxyUrl, onToolCall, onOpenLi
|
|
|
154
175
|
// the previous iframe session, so the handshake never re-ran and the frame kept serving
|
|
155
176
|
// the OLD document's state. The URI stays in the key because it is real identity; the
|
|
156
177
|
// length was never a substitute for content.
|
|
157
|
-
sessionKey: `${resource.resource.uri}:${resource.resource.text}`, ...(toolCall === undefined ? {} : { onToolCall: toolCall }), ...(onOpenLink === undefined ? {} : { onOpenLink }), ...(height === undefined || Number.isNaN(height) ? {} : { initialHeight: height }),
|
|
178
|
+
sessionKey: `${resource.resource.uri}:${resource.resource.text}`, ...(toolCall === undefined ? {} : { onToolCall: toolCall }), ...(onOpenLink === undefined ? {} : { onOpenLink }), ...(height === undefined || Number.isNaN(height) ? {} : { initialHeight: height }), maxHeight: maxHeight }), plan !== undefined && _jsx(PendingSurfaceMirror, { uri: resource.resource.uri, plan: plan, t: t })] }, resource.resource.uri));
|
|
158
179
|
}), expiry?.expired === false && (_jsx("p", { className: "mcpui-surface-expiry", role: "timer", children: t('Expires in {time}', { time: expiry.remaining }) }))] }));
|
|
159
180
|
}
|
|
160
181
|
/**
|
|
@@ -185,6 +206,6 @@ export function mcpUiSurfaceSlotKey({ data }) {
|
|
|
185
206
|
* @returns An unregister handle, so a test or a hot reload can dispose cleanly.
|
|
186
207
|
*/
|
|
187
208
|
export function registerMcpUiSurfaceRenderer(options) {
|
|
188
|
-
return (options.registry ?? defaultExtEventRendererRegistry).register({ name: options.name ?? MCP_UI_EXT_EVENT_NAME, renderer: (props) => (_jsx(McpUiSurfaceCard, { ...props, sandboxProxyUrl: options.sandboxProxyUrl, ...(options.onToolCall === undefined ? {} : { onToolCall: options.onToolCall }), ...(options.onOpenLink === undefined ? {} : { onOpenLink: options.onOpenLink }),
|
|
209
|
+
return (options.registry ?? defaultExtEventRendererRegistry).register({ name: options.name ?? MCP_UI_EXT_EVENT_NAME, renderer: (props) => (_jsx(McpUiSurfaceCard, { ...props, sandboxProxyUrl: options.sandboxProxyUrl, ...(options.onToolCall === undefined ? {} : { onToolCall: options.onToolCall }), ...(options.onOpenLink === undefined ? {} : { onOpenLink: options.onOpenLink }), maxHeight: options.maxHeight })) }, { slotKey: mcpUiSurfaceSlotKey });
|
|
189
210
|
}
|
|
190
211
|
//# sourceMappingURL=McpUiSurfaceCard.js.map
|
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
import type { ChatMessage } from '../../core/index.js';
|
|
1
|
+
import type { ChatAttachment, ChatMessage } from '../../core/index.js';
|
|
2
2
|
import { type MessageRowProps } from './MessageRow.js';
|
|
3
|
+
interface PendingPrompt {
|
|
4
|
+
id?: string;
|
|
5
|
+
text: string;
|
|
6
|
+
attachments?: ChatAttachment[];
|
|
7
|
+
onCancel: () => void;
|
|
8
|
+
}
|
|
3
9
|
export interface MessageListProps extends Pick<MessageRowProps, 'projectFileNames' | 'onRequestOpenFile' | 'renderAttachment' | 'showUsageTotal' | 'showUsageLine' | 'toolCallDisplay' | 'showThinking'> {
|
|
4
10
|
messages: ChatMessage[];
|
|
5
11
|
isStreaming?: boolean;
|
|
@@ -11,24 +17,18 @@ export interface MessageListProps extends Pick<MessageRowProps, 'projectFileName
|
|
|
11
17
|
questionFormSubmittedAnswersByMessageId?: Record<string, Record<string, string | string[]>>;
|
|
12
18
|
onQuestionFormSubmit?: (messageId: string, text: string, answers: Record<string, string | string[]>) => void;
|
|
13
19
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* instead of a banner bolted above the composer. Deliberately NOT a `ChatMessage`: it has no
|
|
17
|
-
* persisted id (nothing here could mint one without risking collision with a real message once
|
|
18
|
-
* this prompt is actually sent) and is never written to `messages`/storage — purely a rendering
|
|
19
|
-
* of `useChatPane`'s own in-memory queue slot. `null`/omitted renders nothing, matching the old
|
|
20
|
-
* strip's "shown only once something is actually queued" behavior.
|
|
20
|
+
* MOVE: Pending inputs render as the newest transcript entries, never persisted ChatMessages.
|
|
21
|
+
* Their accepted ids identify cancellations; the singular prop remains a host compatibility seam.
|
|
21
22
|
*/
|
|
22
|
-
pendingPrompt?:
|
|
23
|
-
|
|
24
|
-
onCancel: () => void;
|
|
25
|
-
} | null;
|
|
23
|
+
pendingPrompt?: PendingPrompt | null;
|
|
24
|
+
pendingPrompts?: readonly PendingPrompt[];
|
|
26
25
|
}
|
|
27
26
|
/**
|
|
28
27
|
* Memoized: the pane re-renders on every composer keystroke, and re-rendering a long transcript
|
|
29
28
|
* (hundreds of tool rows, MCP-UI cards, markdown) per key made typing lag (owner report
|
|
30
|
-
* 2026-10-08). Callers must pass stable props — `ChatPane` memoizes `
|
|
29
|
+
* 2026-10-08). Callers must pass stable props — `ChatPane` memoizes `pendingPrompts` and
|
|
31
30
|
* `projectFileNames` for exactly this.
|
|
32
31
|
*/
|
|
33
32
|
export declare const MessageList: import("react").NamedExoticComponent<MessageListProps>;
|
|
33
|
+
export {};
|
|
34
34
|
//# sourceMappingURL=MessageList.d.ts.map
|
|
@@ -9,6 +9,10 @@ import type { ChatMessage } from '../../core/index.js';
|
|
|
9
9
|
export interface MessageListAutoScroll {
|
|
10
10
|
containerRef: RefObject<HTMLDivElement | null>;
|
|
11
11
|
onScroll: (event: UIEvent<HTMLElement>) => void;
|
|
12
|
+
/** True while the user has scrolled up away from the newest messages. */
|
|
13
|
+
awayFromLatest: boolean;
|
|
14
|
+
/** Scrolls to the newest message and resumes following new output. */
|
|
15
|
+
jumpToLatest: () => void;
|
|
12
16
|
}
|
|
13
17
|
/** Recompute costs once per immutable transcript snapshot, including saved history on reload. */
|
|
14
18
|
export declare function useConversationUsage({ messages }: {
|
|
@@ -17,7 +21,8 @@ export declare function useConversationUsage({ messages }: {
|
|
|
17
21
|
export declare function useMessageListAutoScroll({ messages, scrollIntent }: {
|
|
18
22
|
messages: ChatMessage[];
|
|
19
23
|
scrollIntent: boolean;
|
|
20
|
-
}, { onScrolled }?: {
|
|
24
|
+
}, { onScrolled, reducedMotion }?: {
|
|
21
25
|
onScrolled?: () => void;
|
|
26
|
+
reducedMotion?: () => boolean;
|
|
22
27
|
}): MessageListAutoScroll;
|
|
23
28
|
//# sourceMappingURL=MessageList.hooks.d.ts.map
|
|
@@ -4,14 +4,15 @@
|
|
|
4
4
|
* lives here (presentational), not in the headless `useConversation` hook, per that hook's own
|
|
5
5
|
* "transport-agnostic hooks never touch DOM" contract.
|
|
6
6
|
*/
|
|
7
|
-
import { useEffect, useMemo, useRef } from 'react';
|
|
7
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
8
8
|
import { conversationUsage } from '../../core/turn-usage.js';
|
|
9
9
|
import { nextStickToBottom, startsNewTurn } from './MessageList.rules.js';
|
|
10
|
+
const prefersReducedMotion = () => typeof window !== 'undefined' && (window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false);
|
|
10
11
|
/** Recompute costs once per immutable transcript snapshot, including saved history on reload. */
|
|
11
12
|
export function useConversationUsage({ messages }, _optional = {}) {
|
|
12
13
|
return useMemo(() => conversationUsage({ messages }), [messages]);
|
|
13
14
|
}
|
|
14
|
-
export function useMessageListAutoScroll({ messages, scrollIntent }, { onScrolled } = {}) {
|
|
15
|
+
export function useMessageListAutoScroll({ messages, scrollIntent }, { onScrolled, reducedMotion = prefersReducedMotion } = {}) {
|
|
15
16
|
const containerRef = useRef(null);
|
|
16
17
|
// Whether the transcript was scrolled to its bottom as of the last user scroll or programmatic
|
|
17
18
|
// scroll-to-bottom — read by the ResizeObserver effect below, which only ever sees the DOM AFTER a
|
|
@@ -23,6 +24,9 @@ export function useMessageListAutoScroll({ messages, scrollIntent }, { onScrolle
|
|
|
23
24
|
// Only scroll events write it (a browser fires one for programmatic scrolls too). Starts at
|
|
24
25
|
// Infinity, "no position seen yet", so a first event away from the bottom unsticks as it always did.
|
|
25
26
|
const lastScrollTopRef = useRef(Number.POSITIVE_INFINITY);
|
|
27
|
+
// State mirror of `!stickToBottomRef.current` for the "Jump to latest" button; the ref stays the
|
|
28
|
+
// source of truth for the scroll effects, which must not wait for a render.
|
|
29
|
+
const [awayFromLatest, setAwayFromLatest] = useState(false);
|
|
26
30
|
// Runs BEFORE the scroll effect below (effects run in declaration order), so a send re-sticks in
|
|
27
31
|
// time for that same commit's scroll. The user's own send (or retry) is an explicit "take me to
|
|
28
32
|
// the latest turn": without this, a user who had scrolled up to read history and then sent stayed
|
|
@@ -31,6 +35,7 @@ export function useMessageListAutoScroll({ messages, scrollIntent }, { onScrolle
|
|
|
31
35
|
useEffect(() => {
|
|
32
36
|
if (startsNewTurn({ previous: previousMessagesRef.current, next: messages })) {
|
|
33
37
|
stickToBottomRef.current = true;
|
|
38
|
+
setAwayFromLatest(false);
|
|
34
39
|
}
|
|
35
40
|
previousMessagesRef.current = messages;
|
|
36
41
|
}, [messages]);
|
|
@@ -81,7 +86,18 @@ export function useMessageListAutoScroll({ messages, scrollIntent }, { onScrolle
|
|
|
81
86
|
const el = event.currentTarget;
|
|
82
87
|
stickToBottomRef.current = nextStickToBottom(el, { wasSticking: stickToBottomRef.current, previousScrollTop: lastScrollTopRef.current });
|
|
83
88
|
lastScrollTopRef.current = el.scrollTop;
|
|
89
|
+
setAwayFromLatest(!stickToBottomRef.current);
|
|
84
90
|
};
|
|
85
|
-
|
|
91
|
+
const jumpToLatest = useCallback(() => {
|
|
92
|
+
const el = containerRef.current;
|
|
93
|
+
if (!el)
|
|
94
|
+
return;
|
|
95
|
+
// Re-stick first: a smooth scroll's own events move DOWN, which never unsticks, so output that
|
|
96
|
+
// streams in during the animation is followed too.
|
|
97
|
+
stickToBottomRef.current = true;
|
|
98
|
+
setAwayFromLatest(false);
|
|
99
|
+
el.scrollTo({ top: el.scrollHeight, behavior: reducedMotion() ? 'auto' : 'smooth' });
|
|
100
|
+
}, [reducedMotion]);
|
|
101
|
+
return { containerRef, onScroll, awayFromLatest, jumpToLatest };
|
|
86
102
|
}
|
|
87
103
|
//# sourceMappingURL=MessageList.hooks.js.map
|