@jini-ai/chat 0.6.15-rc.2 → 0.6.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/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/assistant-client/ag-ui-transport.js +1 -1
- 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/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/transcript.js +13 -6
- package/dist/core/turn-usage.js +7 -2
- package/dist/core/user-text-redaction.d.ts +2 -5
- package/dist/core/user-text-redaction.js +3 -3
- 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/lazy-loader.js +16 -12
- package/dist/embed-widget/react/dialog.hooks.js +2 -2
- package/dist/embed-widget/react/embed.js +1 -1
- 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.js +19 -12
- package/dist/embed-widget/vanilla/state.js +4 -2
- 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/Markdown.js +29 -26
- package/dist/react/components/McpUiSurfaceCard.js +2 -1
- package/dist/react/components/MessageList.hooks.js +3 -2
- package/dist/react/components/MessageRow.js +23 -7
- 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.js +8 -7
- 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 +15 -5
- package/dist/react/features/chat-pane/components/ChatPane.js +4 -2
- 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.js +4 -2
- 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 +26 -3
- package/dist/react/features/chat-pane/types.d.ts +8 -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/message-blocks.js +4 -2
- package/dist/react/styles/reference.css +9 -0
- 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 +15 -15
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const ARIA_LABEL = 'aria-label';
|
|
1
2
|
import { CHAT_BUBBLE_STYLE } from '../../contracts/chat-bubble-style.js';
|
|
2
3
|
import { createVanillaChatController } from './state.js';
|
|
3
4
|
import { isQueuedPageAction } from '../../core/browser/page-actions.js';
|
|
@@ -37,7 +38,7 @@ export function mountVanillaChat(args, options = {}) {
|
|
|
37
38
|
const shell = node('div', 'jini-chat-vanilla');
|
|
38
39
|
const fab = button('');
|
|
39
40
|
fab.className = 'jini-chat-bubble';
|
|
40
|
-
fab.setAttribute(
|
|
41
|
+
fab.setAttribute(ARIA_LABEL, copy.fabLabel);
|
|
41
42
|
fab.setAttribute('aria-haspopup', 'dialog');
|
|
42
43
|
fab.append(svgIcon({ document, path: 'M4 4h16v12H9l-5 4V4z' }));
|
|
43
44
|
const panel = node('section', 'jini-chat-vanilla__panel');
|
|
@@ -45,12 +46,12 @@ export function mountVanillaChat(args, options = {}) {
|
|
|
45
46
|
fab.setAttribute('aria-controls', panel.id);
|
|
46
47
|
panel.setAttribute('role', 'dialog');
|
|
47
48
|
panel.setAttribute('aria-modal', 'false');
|
|
48
|
-
panel.setAttribute(
|
|
49
|
+
panel.setAttribute(ARIA_LABEL, copy.title);
|
|
49
50
|
const header = node('header', 'jini-chat-vanilla__header');
|
|
50
51
|
const title = node('strong', 'jini-chat-vanilla__title', copy.title);
|
|
51
52
|
const reset = button(copy.reset);
|
|
52
53
|
const close = button('');
|
|
53
|
-
close.setAttribute(
|
|
54
|
+
close.setAttribute(ARIA_LABEL, copy.close);
|
|
54
55
|
close.append(svgIcon({ document, path: 'M6 6l12 12M18 6L6 18' }));
|
|
55
56
|
header.append(title, reset, close);
|
|
56
57
|
const confirmation = node('div', 'jini-chat-vanilla__confirmation');
|
|
@@ -63,7 +64,7 @@ export function mountVanillaChat(args, options = {}) {
|
|
|
63
64
|
log.setAttribute('role', 'log');
|
|
64
65
|
log.setAttribute('aria-live', 'off');
|
|
65
66
|
log.setAttribute('aria-relevant', 'additions text');
|
|
66
|
-
log.setAttribute(
|
|
67
|
+
log.setAttribute(ARIA_LABEL, copy.title);
|
|
67
68
|
log.tabIndex = 0;
|
|
68
69
|
const status = node('div', 'jini-chat-vanilla__status');
|
|
69
70
|
status.setAttribute('role', 'status');
|
|
@@ -81,20 +82,20 @@ export function mountVanillaChat(args, options = {}) {
|
|
|
81
82
|
const form = node('form', 'jini-chat-vanilla__composer');
|
|
82
83
|
const input = node('textarea', 'jini-chat-vanilla__input');
|
|
83
84
|
input.placeholder = copy.placeholder;
|
|
84
|
-
input.setAttribute(
|
|
85
|
+
input.setAttribute(ARIA_LABEL, copy.placeholder);
|
|
85
86
|
input.maxLength = 8000;
|
|
86
87
|
input.rows = 2;
|
|
87
88
|
const send = button(copy.send);
|
|
88
89
|
send.type = 'submit';
|
|
89
90
|
send.classList.add('jini-chat-vanilla__send');
|
|
90
|
-
send.setAttribute(
|
|
91
|
+
send.setAttribute(ARIA_LABEL, copy.send);
|
|
91
92
|
send.replaceChildren(svgIcon({ document, path: 'M5 4l15 8-15 8v-6l9-2-9-2V4z' }));
|
|
92
93
|
const stop = button(copy.stop);
|
|
93
94
|
form.append(input, send, stop);
|
|
94
95
|
const greeting = node('p', 'jini-chat-vanilla__greeting', copy.greeting);
|
|
95
96
|
const suggestions = node('div', 'jini-chat-vanilla__suggestions');
|
|
96
97
|
if (copy.suggestionsLabel)
|
|
97
|
-
suggestions.setAttribute(
|
|
98
|
+
suggestions.setAttribute(ARIA_LABEL, copy.suggestionsLabel);
|
|
98
99
|
const suggestionEntries = (args.suggestions ?? []).map(text => ({ text, button: button(text) }));
|
|
99
100
|
const suggestionButtons = suggestionEntries.map(entry => entry.button);
|
|
100
101
|
suggestions.append(...suggestionButtons);
|
|
@@ -180,11 +181,15 @@ export function mountVanillaChat(args, options = {}) {
|
|
|
180
181
|
const moveFocus = ({ state, composerHadFocus, stopHadFocus }) => {
|
|
181
182
|
if (!state.open)
|
|
182
183
|
return;
|
|
183
|
-
if (!wasOpen && initialized)
|
|
184
|
+
if (!wasOpen && initialized) {
|
|
184
185
|
focusEntry({ busy: state.busy });
|
|
185
|
-
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (state.busy && composerHadFocus) {
|
|
186
189
|
stop.focus();
|
|
187
|
-
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (!state.busy && stopHadFocus)
|
|
188
193
|
input.focus();
|
|
189
194
|
};
|
|
190
195
|
const renderFocus = ({ state, composerHadFocus, stopHadFocus, panelHadFocus }) => {
|
|
@@ -295,9 +300,11 @@ export function mountVanillaChat(args, options = {}) {
|
|
|
295
300
|
if (!controller.getState({}).open || !panel.contains(document.activeElement))
|
|
296
301
|
return;
|
|
297
302
|
const key = event;
|
|
298
|
-
if (key.key === 'Escape')
|
|
303
|
+
if (key.key === 'Escape') {
|
|
299
304
|
onEscape({ key });
|
|
300
|
-
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
if (key.key === 'Tab')
|
|
301
308
|
trapTab({ key });
|
|
302
309
|
});
|
|
303
310
|
listen(element, 'jini-chat-open', () => {
|
|
@@ -51,7 +51,8 @@ export function createVanillaChatController(args, _options = {}) {
|
|
|
51
51
|
const content = text.trim();
|
|
52
52
|
if (!content || state.busy)
|
|
53
53
|
return false;
|
|
54
|
-
|
|
54
|
+
generation += 1;
|
|
55
|
+
const token = generation;
|
|
55
56
|
const abort = args.createAbortController({});
|
|
56
57
|
active = abort;
|
|
57
58
|
const history = [...state.messages, { id: args.ids({}), role: 'user', content }];
|
|
@@ -80,8 +81,9 @@ export function createVanillaChatController(args, _options = {}) {
|
|
|
80
81
|
active = null;
|
|
81
82
|
events = final;
|
|
82
83
|
state = { ...state, busy: false };
|
|
84
|
+
const completedStatus = abort.signal.aborted ? 'canceled' : 'succeeded';
|
|
83
85
|
updateReply({ ...reply, content: assistantContentFromEvents({ events }), events: [...events],
|
|
84
|
-
runStatus: failed ? 'failed' :
|
|
86
|
+
runStatus: failed ? 'failed' : completedStatus }, { persist: true });
|
|
85
87
|
if (!failed && !abort.signal.aborted)
|
|
86
88
|
settleActions(events);
|
|
87
89
|
};
|
|
@@ -154,7 +154,8 @@ export function useAttachmentPreviewModal({ attachment, onClose, navigation }, {
|
|
|
154
154
|
const cachedFile = getAttachmentPreviewSource({ path: attachment.path });
|
|
155
155
|
const attemptImage = !imageFailed && cachedFile !== undefined && looksLikeImageAttachment({ attachment: attachment }) && canCreateObjectUrl();
|
|
156
156
|
const attemptText = !attemptImage && cachedFile !== undefined && looksLikeTextAttachment(attachment.name);
|
|
157
|
-
const
|
|
157
|
+
const fallbackStatus = attemptText ? 'text' : 'unsupported';
|
|
158
|
+
const status = attemptImage ? 'image' : fallbackStatus;
|
|
158
159
|
// Object URL: created/revoked in an effect (not `useMemo`) so a React 18 Strict Mode double-render
|
|
159
160
|
// can never create two URLs and revoke only one — the cleanup below always pairs with the create
|
|
160
161
|
// that produced the URL currently in state.
|
|
@@ -24,7 +24,7 @@ export function ChatSettingsDialog(required, _optional = {}) {
|
|
|
24
24
|
useLayoutEffect(() => {
|
|
25
25
|
const dialog = dialogRef.current;
|
|
26
26
|
if (!dialog || !required.open)
|
|
27
|
-
return;
|
|
27
|
+
return undefined;
|
|
28
28
|
openerRef.current = document.activeElement;
|
|
29
29
|
if (typeof dialog.showModal === 'function') {
|
|
30
30
|
if (!dialog.open)
|
|
@@ -47,7 +47,7 @@ export function ChatSettingsDialog(required, _optional = {}) {
|
|
|
47
47
|
// layout cleanups and would pull focus straight back to the closing dialog's × button.
|
|
48
48
|
useEffect(() => {
|
|
49
49
|
if (!required.open)
|
|
50
|
-
return;
|
|
50
|
+
return undefined;
|
|
51
51
|
return () => {
|
|
52
52
|
const opener = openerRef.current;
|
|
53
53
|
if (opener instanceof HTMLElement && opener.isConnected)
|
|
@@ -60,13 +60,22 @@ export function ChatSettingsDialog(required, _optional = {}) {
|
|
|
60
60
|
if (event.target !== event.currentTarget)
|
|
61
61
|
return;
|
|
62
62
|
const { clientX: x, clientY: y } = event, box = event.currentTarget.getBoundingClientRect();
|
|
63
|
-
|
|
63
|
+
const horizontalOutside = x < box.left || x > box.right;
|
|
64
|
+
const verticalOutside = y < box.top || y > box.bottom;
|
|
65
|
+
if ((x === 0 && y === 0) || horizontalOutside || verticalOutside)
|
|
64
66
|
required.onClose();
|
|
65
|
-
}, children: [_jsxs("header", { className: "jini-chat-settings-header", children: [_jsx("h2", { id: titleId, className: "jini-dialog-title", children: vm.t('Chat settings') }), _jsx("button", { ref: closeRef, type: "button", className: "jini-dialog-close jini-chat-settings-close", "aria-label": vm.t('Close'), onClick: required.onClose, children: _jsx("span", { "aria-hidden": "true", children: "\u00D7" }) })] }), _jsx(TabBar, { tabs: vm.tabs, activeId: vm.tab, onChange: vm.setTab, ariaLabel: vm.t('Chat settings') }), _jsx("div", { className: "jini-chat-settings-body", role: "tabpanel", "aria-label": vm.tab === 'display' ? vm.t('Display') : vm.t('Agent permissions'), children: vm.tab === 'display' ? vm.display.map(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
67
|
+
}, children: [_jsxs("header", { className: "jini-chat-settings-header", children: [_jsx("h2", { id: titleId, className: "jini-dialog-title", children: vm.t('Chat settings') }), _jsx("button", { ref: closeRef, type: "button", className: "jini-dialog-close jini-chat-settings-close", "aria-label": vm.t('Close'), onClick: required.onClose, children: _jsx("span", { "aria-hidden": "true", children: "\u00D7" }) })] }), _jsx(TabBar, { tabs: vm.tabs, activeId: vm.tab, onChange: vm.setTab, ariaLabel: vm.t('Chat settings') }), _jsx("div", { className: "jini-chat-settings-body", role: "tabpanel", "aria-label": vm.tab === 'display' ? vm.t('Display') : vm.t('Agent permissions'), children: vm.tab === 'display' ? vm.display.map(renderDisplaySection)
|
|
68
|
+
: renderPermissions(vm) })] });
|
|
69
|
+
}
|
|
70
|
+
function renderDisplaySection(section) {
|
|
71
|
+
if (section.radio)
|
|
72
|
+
return (
|
|
73
|
+
// One ⓘ for the whole choice, beside the legend (owner, 2026-10-18), not one per option. The
|
|
74
|
+
// fieldset is named by the label span alone, so the tip's text never joins the group's name.
|
|
75
|
+
_jsxs("fieldset", { className: "jini-chat-settings-group", "aria-labelledby": section.label ? section.labelId : undefined, "aria-describedby": [section.hint ? section.hintId : '', section.tooltip ? section.tooltipId : ''].filter(Boolean).join(' ') || undefined, children: [section.label && _jsxs("legend", { className: "jini-chat-settings-legend", children: [_jsx("span", { id: section.labelId, children: section.label }), section.tooltip && _jsx(InfoTip, { label: section.tooltip, descriptionId: section.tooltipId })] }), section.hint && _jsx("p", { id: section.hintId, className: "jini-chat-settings-description jini-chat-settings-description--group", children: section.hint }), _jsx(ChatSettingsSegmented, { name: section.name, items: section.items })] }, section.key));
|
|
76
|
+
return _jsxs("fieldset", { className: "jini-chat-settings-group", children: [section.label && _jsx("legend", { className: "jini-chat-settings-legend", children: section.label }), section.items.map(item => _jsx(SettingSwitch, { label: item.label, checked: !!item.checked, disabled: item.disabled, onChange: item.change, description: item.hint, descriptionId: item.hintId, tooltip: item.tooltip }, item.key))] }, section.key);
|
|
77
|
+
}
|
|
78
|
+
function renderPermissions(vm) {
|
|
79
|
+
return _jsxs(_Fragment, { children: [_jsx("p", { className: "jini-chat-settings-note", children: vm.t('Permission changes apply to the next run.') }), vm.permissions.unavailableReason && _jsx("p", { className: "jini-chat-settings-note", children: vm.t(vm.permissions.unavailableReason) }), _jsx(SettingSwitch, { className: "jini-chat-settings-row--master", label: vm.master.label, checked: vm.master.checked, disabled: vm.master.disabled, onChange: checked => vm.master.onCheckedChange({ checked }), description: vm.master.description, descriptionId: vm.master.descriptionId }), _jsx("div", { className: "jini-chat-settings-capabilities", children: vm.capabilities.map(item => _jsx(SettingSwitch, { label: item.control.label, checked: item.control.checked, disabled: item.control.disabled, onChange: checked => item.control.onCheckedChange({ checked }), description: item.description, descriptionId: item.descriptionId }, item.key)) }), vm.permissions.busy && _jsx("p", { role: "status", className: "jini-chat-settings-note", children: vm.t('Saving…') }), vm.permissions.error && _jsx("p", { role: "alert", className: "jini-chat-settings-error", children: vm.permissions.error })] });
|
|
71
80
|
}
|
|
72
81
|
//# sourceMappingURL=ChatSettingsDialog.js.map
|
|
@@ -114,11 +114,11 @@ export function Composer({ composer, onSend, disabled = false, sendDisabled = fa
|
|
|
114
114
|
*/
|
|
115
115
|
useEffect(() => {
|
|
116
116
|
if (!anyPopoverOpen(popovers))
|
|
117
|
-
return;
|
|
117
|
+
return undefined;
|
|
118
118
|
const textarea = textareaRef.current;
|
|
119
119
|
const composerRoot = textarea?.closest('.jini-composer');
|
|
120
120
|
if (!composerRoot)
|
|
121
|
-
return;
|
|
121
|
+
return undefined;
|
|
122
122
|
function handleOutsideMouseDown(event) {
|
|
123
123
|
const target = event.target;
|
|
124
124
|
if (!target)
|
|
@@ -327,7 +327,7 @@ export function Composer({ composer, onSend, disabled = false, sendDisabled = fa
|
|
|
327
327
|
}, onKeyDown: handleKeyDown,
|
|
328
328
|
// Draft persistence is debounced (see `DRAFT_PERSIST_DEBOUNCE_MS`); leaving the field is a
|
|
329
329
|
// natural point to make it durable now.
|
|
330
|
-
onBlur: () => composer.flushDraft?.(), onPaste: handleAttachmentPaste, ...slashTextareaAria(slashOpen, slashActiveOption), rows: 3 }), slashOpen ? (_jsx(ComposerSlashMenu, { matches: slash.matches, activeIndex: slashActiveOption, onSelect: selectSlashItem, showFilterHint: slash.showFilterHint, style: slashMenuPosition, t: t })) : null, _jsxs("div", { className: "jini-composer-footer", children: [_jsx(ComposerAttachButton, { attachmentPicker: attachmentPicker, hidden: hasDiscoveryItems, disabled: disabled, inputRef: attachmentInputRef, onChange: handleAttachmentChange, t: t }), _jsx(ComposerDiscoveryMenu, { groups: discoveryGroups, open: discoveryMenuOpen, disabled: disabled, ...(attachmentPicker ? { attachmentPicker } : {}), attachmentInputRef: attachmentInputRef, onToggle: () => setDiscoveryMenuOpen((open) => !open), onClose: () => setDiscoveryMenuOpen(false), onSelect: selectPlusItem, onAttachmentChange: handleAttachmentChange, style: discoveryMenuPosition, t: t }), _jsx(AccessorySlot, { className: "jini-composer-footer-leading", children: footerLeadingAccessory }), _jsx(ComposerWorkdirControl, { workingDirectory: workingDirectory, onChangeWorkingDirectory: onChangeWorkingDirectory, onPickWorkingDirectory: onPickWorkingDirectory, disabled: disabled, open: workdirOpen, draft: workdirDraft, position: workdirPosition, onToggle: toggleWorkdir, onDraftChange: setWorkdirDraft, onConfirm: confirmWorkdir, onClose: () => setWorkdirOpen(false), t: t }), _jsx(AccessorySlot, { className: "jini-composer-footer-accessories", children: footerAccessories }), _jsx(ComposerPlusMenu, { items: plusMenuItems, t: t }), _jsx(ComposerSendGroup, { running: running, disabled: disabled, sendDisabled: sendDisabled, canSubmit: composer.canSubmit, onSend: onSend, onCancel: onCancel, sendDisabledReason: sendDisabledReason, sendDisabledReasonId: sendDisabledReasonId, t: t })] })] }));
|
|
330
|
+
onBlur: () => composer.flushDraft?.(), onPaste: handleAttachmentPaste, ...slashTextareaAria(slashOpen, slashActiveOption), rows: 3 }), slashOpen ? (_jsx(ComposerSlashMenu, { matches: slash.matches, activeIndex: slashActiveOption, onSelect: selectSlashItem, showFilterHint: slash.showFilterHint, style: slashMenuPosition, t: t })) : null, _jsxs("div", { className: "jini-composer-footer", children: [_jsx(ComposerAttachButton, { attachmentPicker: attachmentPicker, hidden: hasDiscoveryItems, disabled: disabled, inputRef: attachmentInputRef, onChange: handleAttachmentChange, t: t }), _jsx(ComposerDiscoveryMenu, { groups: discoveryGroups, open: discoveryMenuOpen, disabled: disabled, ...(attachmentPicker ? { attachmentPicker } : {}), attachmentInputRef: attachmentInputRef, onToggle: () => setDiscoveryMenuOpen((open) => !open), onClose: () => setDiscoveryMenuOpen(false), onSelect: selectPlusItem, onAttachmentChange: handleAttachmentChange, style: discoveryMenuPosition, t: t }), _jsx(AccessorySlot, { className: "jini-composer-footer-leading", children: footerLeadingAccessory }), _jsx(ComposerWorkdirControl, { workingDirectory: workingDirectory, onChangeWorkingDirectory: onChangeWorkingDirectory, onPickWorkingDirectory: onPickWorkingDirectory, disabled: disabled, open: workdirOpen, draft: workdirDraft, position: workdirPosition, onToggle: toggleWorkdir, onDraftChange: setWorkdirDraft, onConfirm: confirmWorkdir, onClose: () => setWorkdirOpen(false), t: t }), _jsxs("div", { className: "jini-composer-footer-trailing", children: [_jsx(AccessorySlot, { className: "jini-composer-footer-accessories", children: footerAccessories }), _jsx(ComposerPlusMenu, { items: plusMenuItems, t: t }), _jsx(ComposerSendGroup, { running: running, disabled: disabled, sendDisabled: sendDisabled, canSubmit: composer.canSubmit, onSend: onSend, onCancel: onCancel, sendDisabledReason: sendDisabledReason, sendDisabledReasonId: sendDisabledReasonId, t: t })] })] })] }));
|
|
331
331
|
}
|
|
332
332
|
function anyPopoverOpen({ discoveryMenuOpen, slashOpen, workdirOpen }) {
|
|
333
333
|
return discoveryMenuOpen || slashOpen || workdirOpen;
|
|
@@ -380,8 +380,9 @@ function ComposerWorkdirControl({ workingDirectory, onChangeWorkingDirectory, on
|
|
|
380
380
|
if (e.key === 'Enter') {
|
|
381
381
|
e.preventDefault();
|
|
382
382
|
onConfirm();
|
|
383
|
+
return;
|
|
383
384
|
}
|
|
384
|
-
|
|
385
|
+
if (e.key === 'Escape') {
|
|
385
386
|
e.preventDefault();
|
|
386
387
|
onClose();
|
|
387
388
|
}
|
|
@@ -105,7 +105,7 @@ export function ConversationList({ conversations: savedConversations, activeConv
|
|
|
105
105
|
// nothing on every document event.
|
|
106
106
|
useEffect(() => {
|
|
107
107
|
if (!open)
|
|
108
|
-
return;
|
|
108
|
+
return undefined;
|
|
109
109
|
// The root div is committed before any effect runs, so it is never null here. Capturing it
|
|
110
110
|
// (rather than reading the ref per event) keeps a late event during unmount, after React has
|
|
111
111
|
// detached the ref but before this cleanup runs, measured against the real node.
|
|
@@ -135,10 +135,10 @@ export function ConversationList({ conversations: savedConversations, activeConv
|
|
|
135
135
|
}, [open]);
|
|
136
136
|
useEffect(() => {
|
|
137
137
|
if (!onSearch)
|
|
138
|
-
return;
|
|
138
|
+
return undefined;
|
|
139
139
|
if (!deferredQuery) {
|
|
140
140
|
setServerResults(null);
|
|
141
|
-
return;
|
|
141
|
+
return undefined;
|
|
142
142
|
}
|
|
143
143
|
// `cancelled` guards the classic out-of-order response: a slow query for "a" resolving after
|
|
144
144
|
// a fast one for "abc" would otherwise overwrite the newer, correct results.
|
|
@@ -190,6 +190,7 @@ export function ConversationList({ conversations: savedConversations, activeConv
|
|
|
190
190
|
});
|
|
191
191
|
}, [confirmDelete, onDelete, t]);
|
|
192
192
|
const active = conversations.find((c) => c.id === activeConversationId) ?? null;
|
|
193
|
+
const emptyLabel = () => conversations.length === 0 ? emptyState ?? t('No conversations yet') : t('No conversations match');
|
|
193
194
|
return (_jsxs("div", { className: "jini-conv", ref: rootRef, children: [_jsx("button", { type: "button", className: `jini-conv-trigger${open ? ' jini-conv-trigger-open' : ''}`, "aria-haspopup": "menu", "aria-expanded": open, ...conversationTriggerAttributes({ activeTitle: active?.title, t }, { label: triggerLabel, nativeTitle }), "data-testid": "conversation-trigger", onClick: () => setOpen((v) => !v), children: _jsx(Icon, { name: "comment", size: 16 }) }), open ? (_jsxs("div", { className: "jini-conv-menu", role: "menu", "data-testid": "conversation-menu", children: [_jsxs("div", { className: "jini-conv-head", children: [_jsxs("span", { className: "jini-conv-heading", children: [t('Conversations'), _jsx("span", { className: "jini-conv-count", "data-testid": "conversation-count", children: conversations.length })] }), _jsxs("button", { type: "button", className: "jini-conv-new", disabled: createDisabled, "data-testid": "conversation-new",
|
|
194
195
|
// `disabled` alone guards this: a disabled button never dispatches a click.
|
|
195
196
|
onClick: () => {
|
|
@@ -199,7 +200,7 @@ export function ConversationList({ conversations: savedConversations, activeConv
|
|
|
199
200
|
// the remaining gap between "switcher closed" and "the host actually knows".
|
|
200
201
|
runConversationListHostEffect('onCreate', () => onCreate());
|
|
201
202
|
setOpen(false);
|
|
202
|
-
}, children: [_jsx(Icon, { name: "plus", size: 12 }), _jsx("span", { children: t('New') })] })] }), _jsxs("label", { className: "jini-conv-search", children: [_jsx(Icon, { name: "search", size: 13 }), _jsx("input", { type: "search", value: query, placeholder: t('Search conversations'), "data-testid": "conversation-search", onChange: (event) => setQuery(event.currentTarget.value) })] }), _jsx("div", { className: "jini-conv-list", "data-testid": "conversation-list", children: conversations.length === 0
|
|
203
|
+
}, children: [_jsx(Icon, { name: "plus", size: 12 }), _jsx("span", { children: t('New') })] })] }), _jsxs("label", { className: "jini-conv-search", children: [_jsx(Icon, { name: "search", size: 13 }), _jsx("input", { type: "search", value: query, placeholder: t('Search conversations'), "data-testid": "conversation-search", onChange: (event) => setQuery(event.currentTarget.value) })] }), _jsx("div", { className: "jini-conv-list", "data-testid": "conversation-list", children: conversations.length === 0 || visible.length === 0 ? (_jsx("div", { className: "jini-conv-empty", children: emptyLabel() })) : (visible.map((item) => (_jsxs("div", { className: `jini-conv-item${item.id === activeConversationId ? ' jini-conv-item-active' : ''}`, "data-testid": `conversation-item-${item.id}`, children: [editingId === item.id ? (_jsx(ConversationTitleEditor, { title: rawTitles.get(item.id) ?? null, "data-testid": `conversation-rename-${item.id}`, onCommit: (next) => commitRename(item.id, next), onCancel: () => setEditingId(null) })) : (_jsxs("button", { type: "button", className: "jini-conv-item-select", "data-testid": `conversation-select-${item.id}`, title: t('Double-click to rename'), onClick: () => {
|
|
203
204
|
cancelPendingSelect();
|
|
204
205
|
pendingSelectRef.current = setTimeout(() => {
|
|
205
206
|
pendingSelectRef.current = null;
|
|
@@ -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);
|
|
@@ -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. */
|
|
@@ -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;
|
|
@@ -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())
|
|
@@ -235,7 +243,11 @@ function AssistantMessageRow({ message, usageSummary, showUsageTotal, showUsageL
|
|
|
235
243
|
// arrives for it, instead of staying tombstoned for the message's whole lifetime.
|
|
236
244
|
_jsx(ExtEventErrorBoundary, { name: group.name, children: transcriptRow ? node : _jsx("div", { children: node }) }, `${group.slot}:${group.events.length}`));
|
|
237
245
|
};
|
|
238
|
-
const renderTextSegments = (text, keyPrefix) => splitOnQuestionForms({ input: text }).map((segment, i) =>
|
|
246
|
+
const renderTextSegments = (text, keyPrefix) => splitOnQuestionForms({ input: text }).map((segment, i) => {
|
|
247
|
+
if (segment.kind === 'text')
|
|
248
|
+
return segment.text.trim() ? (_jsx("div", { className: "jini-message-content", children: _jsx(Markdown, { children: segment.text }) }, `${keyPrefix}-${i}`)) : null;
|
|
249
|
+
return (_jsx(QuestionForm, { form: segment.form, interactive: questionFormInteractive, ...(questionFormSubmittedAnswers !== undefined ? { submittedAnswers: questionFormSubmittedAnswers } : {}), ...(onQuestionFormSubmit !== undefined ? { onSubmit: onQuestionFormSubmit } : {}) }, `${keyPrefix}-${i}`));
|
|
250
|
+
});
|
|
239
251
|
const renderBlock = (block) => {
|
|
240
252
|
if (block.kind === 'text') {
|
|
241
253
|
return _jsx(React.Fragment, { children: renderTextSegments(block.text, block.key) }, block.key);
|
|
@@ -243,7 +255,9 @@ function AssistantMessageRow({ message, usageSummary, showUsageTotal, showUsageL
|
|
|
243
255
|
if (block.kind === 'reply-end') {
|
|
244
256
|
const reply = usage.replies[block.index];
|
|
245
257
|
// A trailing input may have no reply: its predecessor's usage belongs above that input.
|
|
246
|
-
|
|
258
|
+
if (!reply || !showUsageLine)
|
|
259
|
+
return null;
|
|
260
|
+
return _jsx(UsageSummary, { usage: reply, showTotal: showUsageTotal, ...(block.index === lastFinishedReply ? { message } : {}) }, block.key);
|
|
247
261
|
}
|
|
248
262
|
if (block.kind === 'tools')
|
|
249
263
|
return renderTools(block.rows, block.key);
|
|
@@ -260,21 +274,23 @@ function AssistantMessageRow({ message, usageSummary, showUsageTotal, showUsageL
|
|
|
260
274
|
return _jsx(React.Fragment, { children: renderExtGroup(group) }, block.key);
|
|
261
275
|
return (_jsx("div", { className: "jini-message-ext-events", children: renderExtGroup(group) }, block.key));
|
|
262
276
|
};
|
|
277
|
+
// The live footer follows the run as transcript sections split; its clock must not remount.
|
|
263
278
|
const renderAssistantSection = ({ section, key, footer }) => (_jsxs("div", { className: usageLineMeetsActions({ footer: footerUsage, terminalNotice, pending, runInProgress }, { showTotal: showUsageTotal })
|
|
264
279
|
? 'jini-message jini-message-assistant jini-message-assistant--usage-actions'
|
|
265
280
|
: '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
281
|
? 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));
|
|
282
|
+
: (_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 })] }, footer ? message.id : key));
|
|
268
283
|
// The lossless fallback keeps the host's text intact; user receipts still own separate rows,
|
|
269
284
|
// in their original receipt order, outside the assistant's actions and usage footer.
|
|
285
|
+
// Keep the same child-array nesting as the interleaved path so React preserves the footer.
|
|
270
286
|
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)] });
|
|
287
|
+
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
288
|
// A reply boundary without reported usage renders nothing; it must not create an empty row's gap.
|
|
273
289
|
const visibleBlocks = blocks.filter(block => block.kind !== 'reply-end' || (showUsageLine && usage.replies[block.index] !== undefined));
|
|
274
290
|
const { sections, footerIndex } = transcriptSections({ blocks: visibleBlocks, runInProgress });
|
|
275
|
-
return
|
|
276
|
-
|
|
277
|
-
|
|
291
|
+
return _jsxs(_Fragment, { children: [_jsx(RunAccessNotice, { events: message.events }), sections.map((section, index) => isUserSection({ section })
|
|
292
|
+
? _jsx(React.Fragment, { children: section.map(renderBlock) }, section[0].key)
|
|
293
|
+
: renderAssistantSection({ section, key: section[0]?.key ?? message.id, footer: index === footerIndex }))] });
|
|
278
294
|
}
|
|
279
295
|
/** Preserve optional render-prop presence for custom extension renderers. */
|
|
280
296
|
function answerRenderProps({ answeredCards, slot, answerText }, _options = {}) {
|
|
@@ -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
|