@jini-ai/chat 0.5.6 → 0.5.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/ag-ui/entry.js +2 -0
- package/dist/core/assistant-client/durable-run-subscription.d.ts +4 -1
- package/dist/core/assistant-client/durable-run-subscription.js +13 -4
- package/dist/core/durable-projection.d.ts +3 -1
- package/dist/core/durable-projection.js +40 -7
- package/dist/core/events.d.ts +3 -0
- package/dist/core/history-tool-metadata.d.ts +23 -0
- package/dist/core/history-tool-metadata.js +13 -0
- package/dist/core/index.d.ts +4 -0
- package/dist/core/index.js +4 -0
- package/dist/core/persistence/title.d.ts +24 -0
- package/dist/core/persistence/title.js +60 -9
- package/dist/core/run-activity.d.ts +15 -2
- package/dist/core/run-activity.js +6 -4
- package/dist/core/run-events/entry.d.ts +12 -1
- package/dist/core/run-events/entry.js +19 -6
- package/dist/core/tool-call-detail.d.ts +51 -0
- package/dist/core/tool-call-detail.js +67 -0
- package/dist/core/turn-usage.d.ts +50 -0
- package/dist/core/turn-usage.js +114 -0
- package/dist/core/usage-format.d.ts +10 -0
- package/dist/core/usage-format.js +15 -0
- package/dist/embed-widget/browser/lazy-loader.d.ts +34 -0
- package/dist/embed-widget/browser/lazy-loader.js +116 -0
- package/dist/embed-widget/react/dialog.hooks.d.ts +28 -0
- package/dist/embed-widget/react/dialog.hooks.js +54 -0
- package/dist/embed-widget/react/embed.d.ts +1 -0
- package/dist/embed-widget/react/embed.js +5 -3
- package/dist/embed-widget/react/header.d.ts +3 -1
- package/dist/embed-widget/react/header.hooks.d.ts +13 -0
- package/dist/embed-widget/react/header.hooks.js +32 -0
- package/dist/embed-widget/react/header.js +4 -14
- package/dist/embed-widget/vanilla/index.d.ts +63 -0
- package/dist/embed-widget/vanilla/index.js +319 -0
- package/dist/embed-widget/vanilla/state.d.ts +48 -0
- package/dist/embed-widget/vanilla/state.js +120 -0
- package/dist/embed-widget/vanilla/style.css +29 -0
- package/dist/react/chat-pane.d.ts +2 -2
- package/dist/react/chat-pane.js +1 -1
- package/dist/react/components/A2uiSurfaceCard.d.ts +5 -1
- package/dist/react/components/A2uiSurfaceCard.js +9 -9
- package/dist/react/components/AttachmentPreviewModal.hooks.d.ts +7 -0
- package/dist/react/components/AttachmentPreviewModal.hooks.js +1 -1
- package/dist/react/components/ChatFab.d.ts +4 -1
- package/dist/react/components/ChatFab.js +2 -2
- package/dist/react/components/Composer.d.ts +2 -0
- package/dist/react/components/Composer.js +5 -2
- package/dist/react/components/ConversationList.d.ts +8 -1
- package/dist/react/components/ConversationList.hooks.d.ts +18 -0
- package/dist/react/components/ConversationList.hooks.js +25 -0
- package/dist/react/components/ConversationList.js +12 -5
- package/dist/react/components/ConversationList.rules.d.ts +25 -0
- package/dist/react/components/ConversationList.rules.js +19 -0
- package/dist/react/components/Markdown.js +6 -6
- package/dist/react/components/McpUiSurfaceCard.d.ts +1 -1
- package/dist/react/components/McpUiSurfaceCard.js +8 -4
- package/dist/react/components/MessageList.d.ts +8 -13
- package/dist/react/components/MessageList.hooks.d.ts +4 -0
- package/dist/react/components/MessageList.hooks.js +6 -1
- package/dist/react/components/MessageList.js +23 -4
- package/dist/react/components/MessageRow.d.ts +23 -1
- package/dist/react/components/MessageRow.js +61 -36
- package/dist/react/components/MessageRow.usage.d.ts +48 -0
- package/dist/react/components/MessageRow.usage.js +47 -0
- package/dist/react/components/MidRunUserMessage.d.ts +17 -0
- package/dist/react/components/MidRunUserMessage.js +41 -0
- package/dist/react/components/SentAttachment.d.ts +19 -0
- package/dist/react/components/SentAttachment.hooks.d.ts +44 -0
- package/dist/react/components/SentAttachment.hooks.js +56 -0
- package/dist/react/components/SentAttachment.js +14 -0
- package/dist/react/components/ToolCallGroup.d.ts +12 -0
- package/dist/react/components/ToolCallGroup.hooks.d.ts +20 -0
- package/dist/react/components/ToolCallGroup.hooks.js +23 -0
- package/dist/react/components/ToolCallGroup.js +13 -0
- package/dist/react/components/ToolCard.js +41 -22
- package/dist/react/components/mcp-ui-surface-state.d.ts +21 -0
- package/dist/react/components/mcp-ui-surface-state.js +26 -0
- package/dist/react/ext-event-renderer-registry.d.ts +7 -0
- package/dist/react/features/chat-pane/components/ChatPane.d.ts +1 -1
- package/dist/react/features/chat-pane/components/ChatPane.js +20 -7
- package/dist/react/features/chat-pane/create-mid-run-message-poster.d.ts +44 -0
- package/dist/react/features/chat-pane/create-mid-run-message-poster.js +69 -0
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +4 -1
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +65 -10
- package/dist/react/features/chat-pane/hooks/useChatPaneNewTabLinks.hooks.d.ts +25 -0
- package/dist/react/features/chat-pane/hooks/useChatPaneNewTabLinks.hooks.js +70 -0
- package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.js +4 -0
- package/dist/react/features/chat-pane/hooks/useChatPaneSuggestion.hooks.d.ts +9 -0
- package/dist/react/features/chat-pane/hooks/useChatPaneSuggestion.hooks.js +11 -0
- package/dist/react/features/chat-pane/hooks/useChatPaneSurface.hooks.d.ts +17 -0
- package/dist/react/features/chat-pane/hooks/useChatPaneSurface.hooks.js +15 -0
- package/dist/react/features/chat-pane/index.d.ts +2 -1
- package/dist/react/features/chat-pane/index.js +1 -0
- package/dist/react/features/chat-pane/rules.d.ts +27 -0
- package/dist/react/features/chat-pane/rules.js +32 -0
- package/dist/react/features/chat-pane/styles.d.ts +1 -1
- package/dist/react/features/chat-pane/styles.js +92 -1
- package/dist/react/features/chat-pane/types.d.ts +39 -1
- package/dist/react/hooks/composer-draft-cache.d.ts +15 -0
- package/dist/react/hooks/composer-draft-cache.js +20 -0
- package/dist/react/hooks/useAnsweredCards.d.ts +20 -0
- package/dist/react/hooks/useAnsweredCards.js +23 -0
- package/dist/react/hooks/useComposer.d.ts +14 -0
- package/dist/react/hooks/useComposer.js +78 -6
- package/dist/react/hooks/useConversation.d.ts +8 -0
- package/dist/react/hooks/useConversation.js +34 -0
- package/dist/react/hooks/useRunActivity.d.ts +5 -3
- package/dist/react/hooks/useRunActivity.js +4 -3
- package/dist/react/index.d.ts +8 -2
- package/dist/react/index.js +6 -1
- package/dist/react/message-blocks.d.ts +22 -3
- package/dist/react/message-blocks.js +25 -3
- package/dist/react/tool-call-display.d.ts +90 -0
- package/dist/react/tool-call-display.js +97 -0
- package/dist/server/durable-runs/recover.js +19 -11
- package/dist/server/run-finalizer/entry.js +2 -1
- package/dist/store/ports.d.ts +25 -2
- package/dist/store/runtime/durable-run-store.js +5 -2
- package/dist/store/runtime/entry.d.ts +1 -0
- package/dist/store/runtime/entry.js +1 -0
- package/dist/store/runtime/history-tools.d.ts +27 -0
- package/dist/store/runtime/history-tools.js +169 -0
- package/dist/store/runtime/run-ledger.js +5 -2
- package/dist/store/sql/store.js +52 -0
- package/dist/testing/index.d.ts +7 -0
- package/dist/testing/index.js +22 -0
- package/package.json +48 -18
|
@@ -54,7 +54,7 @@ function extractSurfaceId(message) {
|
|
|
54
54
|
return undefined;
|
|
55
55
|
}
|
|
56
56
|
/** Renders one component and its children by walking the surface's component map from `componentId` — identical degrade-sanely behavior to `A2uiLab.tsx`'s `RenderComponent`. */
|
|
57
|
-
function RenderComponent({ interpreter, surfaceId, componentId, ancestors, onAction, }) {
|
|
57
|
+
function RenderComponent({ interpreter, surfaceId, componentId, ancestors, onAction, registry, }) {
|
|
58
58
|
if (ancestors.has(componentId)) {
|
|
59
59
|
return (_jsxs("span", { className: "a2ui-placeholder a2ui-placeholder-cycle", "data-a2ui-status": "cycle", "data-a2ui-component-id": componentId, children: ["\u27F2 circular reference at \u201C", componentId, "\u201D"] }));
|
|
60
60
|
}
|
|
@@ -65,7 +65,7 @@ function RenderComponent({ interpreter, surfaceId, componentId, ancestors, onAct
|
|
|
65
65
|
}
|
|
66
66
|
const nextAncestors = new Set(ancestors);
|
|
67
67
|
nextAncestors.add(componentId);
|
|
68
|
-
const childProps = { interpreter, surfaceId, ancestors: nextAncestors, onAction };
|
|
68
|
+
const childProps = { interpreter, surfaceId, ancestors: nextAncestors, onAction, registry };
|
|
69
69
|
function resolveText(value) {
|
|
70
70
|
// Text props already passed the catalog schema. Parsing preserves that wire type at this
|
|
71
71
|
// Record<string, unknown> boundary without asserting a shape the compiler cannot verify.
|
|
@@ -108,9 +108,9 @@ function RenderComponent({ interpreter, surfaceId, componentId, ancestors, onAct
|
|
|
108
108
|
// one of the 13 basic types this switch has no case for, or a registry-sourced component
|
|
109
109
|
// (`shadcn.*`/`recharts.*`) merged into the catalog by `A2uiSurfaceCard`'s own
|
|
110
110
|
// `buildA2uiCatalogFromRegistry` call below. Resolved against the SAME registry
|
|
111
|
-
//
|
|
111
|
+
// the catalog below uses, so a type addable to the catalog and
|
|
112
112
|
// renderable there is addable and renderable here too.
|
|
113
|
-
const entry =
|
|
113
|
+
const entry = registry.resolveById({ id: component.component });
|
|
114
114
|
if (!entry) {
|
|
115
115
|
return (_jsxs("span", { className: "a2ui-placeholder", "data-a2ui-status": "unrenderable-type", children: ["\u26A0 no renderer yet for component type \u201C", component.component, "\u201D"] }));
|
|
116
116
|
}
|
|
@@ -139,16 +139,16 @@ function isThenable(value) {
|
|
|
139
139
|
return typeof value === 'object' && value !== null && typeof value.then === 'function';
|
|
140
140
|
}
|
|
141
141
|
/** Registered against `ext-event-renderer-registry.ts`'s `'a2ui'` name — see module doc. */
|
|
142
|
-
export function A2uiSurfaceCard({ events, runId, onAgentAction }) {
|
|
142
|
+
export function A2uiSurfaceCard({ events, runId, onAgentAction, registry = DEFAULT_INTERACTIVE_UI_REGISTRY }) {
|
|
143
143
|
const t = useT();
|
|
144
|
-
// Merges
|
|
144
|
+
// Merges the host registry (shadcn/recharts by default) into the lab catalog, keyed under
|
|
145
145
|
// the SAME `catalogId` the lab catalog already uses — so a tool built against
|
|
146
146
|
// `createLabCatalog({}).catalogId` (e.g. `demo-a2ui-tool.ts`) still validates unchanged, and now
|
|
147
147
|
// ALSO gains every registry component as a real, addable/removable catalog entry.
|
|
148
148
|
const catalog = useMemo(() => {
|
|
149
149
|
const base = createLabCatalog({});
|
|
150
|
-
return buildA2uiCatalogFromRegistry({ registry
|
|
151
|
-
}, []);
|
|
150
|
+
return buildA2uiCatalogFromRegistry({ registry, catalogId: base.catalogId }, { base });
|
|
151
|
+
}, [registry]);
|
|
152
152
|
const interpreterRef = useRef(null);
|
|
153
153
|
if (!interpreterRef.current) {
|
|
154
154
|
const clock = createSystemClock();
|
|
@@ -283,7 +283,7 @@ export function A2uiSurfaceCard({ events, runId, onAgentAction }) {
|
|
|
283
283
|
if (!root) {
|
|
284
284
|
return refusalNotice ? (_jsx("div", { className: "a2ui-surface-card a2ui-surface-refused", role: "status", children: t('A2UI surface refused: {reason}', { reason: refusalNotice }) })) : (_jsx("div", { className: "a2ui-surface-card a2ui-surface-waiting", role: "status", children: t('Waiting for the agent to send a root component…') }));
|
|
285
285
|
}
|
|
286
|
-
return (_jsxs("div", { className: "a2ui-surface-card", "data-a2ui-surface-id": surfaceId, children: [_jsx(RenderComponent, { interpreter: interpreter, surfaceId: surfaceId, componentId: "root", ancestors: new Set(), onAction: handleAction }), refusalNotice ? (_jsx("div", { className: "a2ui-surface-notice a2ui-surface-notice-refused", role: "status", children: t('Action refused: {reason}', { reason: refusalNotice }) })) : null, pendingAgentAction ? (_jsx("div", { className: "a2ui-surface-notice a2ui-surface-notice-unwired", role: "status", children: t('This action is meant for the agent, but this host has not wired up a live agent-action relay yet.') })) : null, deliveryFailureNotice ? (
|
|
286
|
+
return (_jsxs("div", { className: "a2ui-surface-card", "data-a2ui-surface-id": surfaceId, children: [_jsx(RenderComponent, { interpreter: interpreter, surfaceId: surfaceId, componentId: "root", ancestors: new Set(), onAction: handleAction, registry: registry }), refusalNotice ? (_jsx("div", { className: "a2ui-surface-notice a2ui-surface-notice-refused", role: "status", children: t('Action refused: {reason}', { reason: refusalNotice }) })) : null, pendingAgentAction ? (_jsx("div", { className: "a2ui-surface-notice a2ui-surface-notice-unwired", role: "status", children: t('This action is meant for the agent, but this host has not wired up a live agent-action relay yet.') })) : null, deliveryFailureNotice ? (
|
|
287
287
|
// Deliberately its own notice, not folded into `refusalNotice` above: that one fires
|
|
288
288
|
// *before* anything is sent (the interpreter said no); this one fires *after* — the message
|
|
289
289
|
// left this component but the host reports it never arrived. Conflating them would tell the
|
|
@@ -58,6 +58,13 @@ export declare function readFileAsText({ file }: {
|
|
|
58
58
|
}, { createReader }?: {
|
|
59
59
|
createReader?: () => FileReader;
|
|
60
60
|
}): Promise<string>;
|
|
61
|
+
/**
|
|
62
|
+
* `false` in an environment with no Blob-URL support — real browsers have shipped this for well
|
|
63
|
+
* over a decade, but jsdom (this package's own test environment) never implemented it, and this
|
|
64
|
+
* package cannot assume every embedding host is a browser tab. Feature-detected the same way this
|
|
65
|
+
* file already feature-detects `<dialog>`'s `showModal`/`close`, rather than assumed.
|
|
66
|
+
*/
|
|
67
|
+
export declare function canCreateObjectUrl(): boolean;
|
|
61
68
|
export interface AttachmentTextPreview {
|
|
62
69
|
content: string;
|
|
63
70
|
truncated: boolean;
|
|
@@ -78,7 +78,7 @@ export function readFileAsText({ file }, { createReader = () => new FileReader()
|
|
|
78
78
|
* package cannot assume every embedding host is a browser tab. Feature-detected the same way this
|
|
79
79
|
* file already feature-detects `<dialog>`'s `showModal`/`close`, rather than assumed.
|
|
80
80
|
*/
|
|
81
|
-
function canCreateObjectUrl() {
|
|
81
|
+
export function canCreateObjectUrl() {
|
|
82
82
|
return typeof URL !== 'undefined' && typeof URL.createObjectURL === 'function';
|
|
83
83
|
}
|
|
84
84
|
/**
|
|
@@ -3,6 +3,9 @@ export interface ChatFabProps {
|
|
|
3
3
|
readonly onToggle: () => void;
|
|
4
4
|
/** Rendered as the accessible name, and announced as a state change to assistive tech. */
|
|
5
5
|
readonly label?: string;
|
|
6
|
+
readonly 'aria-label'?: string;
|
|
7
|
+
readonly 'aria-controls'?: string;
|
|
8
|
+
readonly 'aria-haspopup'?: 'dialog';
|
|
6
9
|
}
|
|
7
|
-
export declare function ChatFab({ open, onToggle, label }: ChatFabProps): import("react").JSX.Element;
|
|
10
|
+
export declare function ChatFab({ open, onToggle, label, 'aria-label': accessibleLabel, 'aria-controls': controls, 'aria-haspopup': popup }: ChatFabProps): import("react").JSX.Element;
|
|
8
11
|
//# sourceMappingURL=ChatFab.d.ts.map
|
|
@@ -7,7 +7,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
7
7
|
* for the `chat-fab`/`chat-fab-open` classes.
|
|
8
8
|
*/
|
|
9
9
|
import { useChatFabDrag } from '../hooks/useChatFabDrag.js';
|
|
10
|
-
export function ChatFab({ open, onToggle, label = 'chat' }) {
|
|
10
|
+
export function ChatFab({ open, onToggle, label = 'chat', 'aria-label': accessibleLabel, 'aria-controls': controls, 'aria-haspopup': popup }) {
|
|
11
11
|
const { buttonRef, position, onPointerDown, onPointerMove, onPointerUp, onPointerCancel } = useChatFabDrag({ onClick: onToggle });
|
|
12
12
|
return (_jsx("button", { ref: buttonRef, type: "button", className: `chat-fab${open ? ' chat-fab-open' : ''}`,
|
|
13
13
|
// Not `onClick`: the pointer gesture already decides click-versus-drag, and keeping both
|
|
@@ -19,6 +19,6 @@ export function ChatFab({ open, onToggle, label = 'chat' }) {
|
|
|
19
19
|
event.preventDefault();
|
|
20
20
|
onToggle();
|
|
21
21
|
}
|
|
22
|
-
}, style: position === null ? undefined : { left: position.x, top: position.y, right: 'auto', bottom: 'auto' }, "aria-expanded": open, "aria-label": open ? `Hide ${label}` : `Show ${label}
|
|
22
|
+
}, style: position === null ? undefined : { left: position.x, top: position.y, right: 'auto', bottom: 'auto' }, "aria-expanded": open, "aria-label": accessibleLabel ?? (open ? `Hide ${label}` : `Show ${label}`), "aria-controls": controls, "aria-haspopup": popup, children: _jsx("span", { "aria-hidden": "true", children: open ? '×' : '💬' }) }));
|
|
23
23
|
}
|
|
24
24
|
//# sourceMappingURL=ChatFab.js.map
|
|
@@ -19,6 +19,8 @@ export interface ComposerProps {
|
|
|
19
19
|
* separately placed cancel affordance elsewhere in the pane. Always clickable when true,
|
|
20
20
|
* regardless of `disabled`/`sendDisabled`/`composer.canSubmit`: those all gate *submitting a new
|
|
21
21
|
* draft*, which has nothing to do with whether a caller may cancel the run already in flight.
|
|
22
|
+
* Once the draft is submittable (and neither gate refuses it), Send comes back beside Stop, so a
|
|
23
|
+
* message can go out mid-run without stopping anything.
|
|
22
24
|
*/
|
|
23
25
|
running?: boolean;
|
|
24
26
|
/** Cancels the in-flight run. Required when `running` is true; ignored otherwise. */
|
|
@@ -338,7 +338,10 @@ export function Composer({ composer, onSend, disabled = false, sendDisabled = fa
|
|
|
338
338
|
composer.setDraft(e.target.value);
|
|
339
339
|
setSlashActiveIndex(0);
|
|
340
340
|
setDismissedSlashDraft(null);
|
|
341
|
-
}, onKeyDown: handleKeyDown,
|
|
341
|
+
}, onKeyDown: handleKeyDown,
|
|
342
|
+
// Draft persistence is debounced (see `DRAFT_PERSIST_DEBOUNCE_MS`); leaving the field is a
|
|
343
|
+
// natural point to make it durable now.
|
|
344
|
+
onBlur: () => composer.flushDraft?.(), onPaste: handleAttachmentPaste, "aria-controls": slashOpen ? 'jini-composer-slash-menu' : undefined, "aria-expanded": slashOpen, "aria-activedescendant": slashOpen
|
|
342
345
|
? `jini-composer-slash-option-${Math.min(slashActiveIndex, slashMatches.length - 1)}`
|
|
343
346
|
: undefined, rows: 3 }), slashOpen ? (_jsx(ComposerSlashMenu, { matches: slashMatches, activeIndex: Math.min(slashActiveIndex, slashMatches.length - 1), onSelect: (item) => selectSlashItem(slashMatches.findIndex((match) => match.item === item)), showFilterHint: showSlashFilterHint, style: slashMenuPosition, t: t })) : null, _jsxs("div", { className: "jini-composer-footer", children: [attachmentPicker && !hasDiscoveryItems ? (_jsxs("div", { className: "jini-composer-attachment-picker", children: [_jsx("input", { ref: attachmentInputRef, className: "jini-composer-file-input", "data-testid": "composer-attachment-input", type: "file", multiple: true, "aria-label": t('Attach files'), accept: attachmentPicker.accept, disabled: disabled || attachmentPicker.uploading, onChange: handleAttachmentChange }), _jsx("button", { type: "button", className: "jini-composer-attach", disabled: disabled || attachmentPicker.uploading, onClick: () => attachmentInputRef.current?.click(), title: t(attachmentPicker.uploading ? 'Attaching files…' : 'Attach files'), "aria-label": t(attachmentPicker.uploading ? 'Attaching files…' : 'Attach files'), children: _jsx(RemixIcon, { name: attachmentPicker.uploading ? 'refresh-line' : 'add-line', size: 20, ...(attachmentPicker.uploading ? { className: 'jini-composer-spinner' } : {}) }) })] })) : null, hasDiscoveryItems ? (_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 })) : null, slots?.footerLeadingAccessory ? (_jsx("div", { className: "jini-composer-footer-leading", children: slots.footerLeadingAccessory })) : null, onChangeWorkingDirectory || onPickWorkingDirectory ? (_jsxs("div", { className: "jini-composer-workdir", "data-testid": "composer-workdir", children: [_jsx("button", { type: "button", className: "jini-composer-attach", "data-testid": "composer-workdir-trigger", disabled: disabled, onClick: onPickWorkingDirectory ? () => onPickWorkingDirectory() : toggleWorkdir, ...(onPickWorkingDirectory ? {} : { 'aria-haspopup': 'dialog', 'aria-expanded': workdirOpen }), title: workingDirectory ?? t('Set working directory'), "aria-label": workingDirectory
|
|
344
347
|
? t('Working directory: {dir}', { dir: workingDirectory })
|
|
@@ -351,6 +354,6 @@ export function Composer({ composer, onSend, disabled = false, sendDisabled = fa
|
|
|
351
354
|
e.preventDefault();
|
|
352
355
|
setWorkdirOpen(false);
|
|
353
356
|
}
|
|
354
|
-
}, placeholder: t('Working directory path'), "aria-label": t('Working directory path'), autoFocus: true }), _jsx("button", { type: "button", className: "jini-composer-workdir-confirm", "data-testid": "composer-workdir-confirm", onClick: confirmWorkdir, children: t('Set') })] })) : null] })) : null, slots?.footerAccessories ? (_jsx("div", { className: "jini-composer-footer-accessories", children: slots.footerAccessories })) : null, slots?.plusMenuItems && slots.plusMenuItems.length > 0 ? (_jsx("div", { className: "jini-composer-plus-menu", children: slots.plusMenuItems.map((item) => (_jsxs("button", { type: "button", className: "jini-composer-plus-item", onClick: () => runComposerHostEffect('plusMenuItems.onSelect', () => item.onSelect()), title: t(item.label), children: [item.icon ?? null, _jsx("span", { children: t(item.label) })] }, item.id))) })) : null, running ? (_jsx("button", { type: "button", className: "jini-composer-send
|
|
357
|
+
}, placeholder: t('Working directory path'), "aria-label": t('Working directory path'), autoFocus: true }), _jsx("button", { type: "button", className: "jini-composer-workdir-confirm", "data-testid": "composer-workdir-confirm", onClick: confirmWorkdir, children: t('Set') })] })) : null] })) : null, slots?.footerAccessories ? (_jsx("div", { className: "jini-composer-footer-accessories", children: slots.footerAccessories })) : null, slots?.plusMenuItems && slots.plusMenuItems.length > 0 ? (_jsx("div", { className: "jini-composer-plus-menu", children: slots.plusMenuItems.map((item) => (_jsxs("button", { type: "button", className: "jini-composer-plus-item", onClick: () => runComposerHostEffect('plusMenuItems.onSelect', () => item.onSelect()), title: t(item.label), children: [item.icon ?? null, _jsx("span", { children: t(item.label) })] }, item.id))) })) : null, _jsxs("span", { className: "jini-composer-send-group", children: [!running || (!disabled && !sendDisabled && composer.canSubmit) ? (_jsx("button", { type: "button", className: "jini-composer-send", disabled: disabled || sendDisabled || !composer.canSubmit, onClick: onSend, title: t('Send'), "aria-label": t('Send'), children: _jsx(RemixIcon, { name: "send-plane-2-line", size: 16 }) })) : null, running ? (_jsx("button", { type: "button", className: "jini-composer-send jini-composer-send--stop", onClick: onCancel, title: t('Stop run'), "aria-label": t('Stop run'), children: _jsx(RemixIcon, { name: "stop-fill", size: 14 }) })) : null] })] })] }));
|
|
355
358
|
}
|
|
356
359
|
//# sourceMappingURL=Composer.js.map
|
|
@@ -26,6 +26,13 @@ export interface ConversationListProps {
|
|
|
26
26
|
/** Disables the "New" action — e.g. while the current conversation is still empty. */
|
|
27
27
|
createDisabled?: boolean;
|
|
28
28
|
emptyState?: React.ReactNode;
|
|
29
|
+
/** Replaces the trigger's accessible name (default "Conversations — {active title}"). */
|
|
30
|
+
triggerLabel?: string;
|
|
31
|
+
/**
|
|
32
|
+
* `false` drops the trigger's native `title` tooltip, for a host that draws its own tooltip over
|
|
33
|
+
* the icon (the browser's dark tip would otherwise show on top of it). Defaults to true.
|
|
34
|
+
*/
|
|
35
|
+
nativeTitle?: boolean;
|
|
29
36
|
}
|
|
30
|
-
export declare function ConversationList({ conversations, activeConversationId, onSelect, onCreate, onDelete, onRename, onSearch, confirmDelete, createDisabled, emptyState, }: ConversationListProps): import("react").JSX.Element;
|
|
37
|
+
export declare function ConversationList({ conversations: savedConversations, activeConversationId, onSelect, onCreate, onDelete, onRename, onSearch, confirmDelete, createDisabled, emptyState, triggerLabel, nativeTitle, }: ConversationListProps): import("react").JSX.Element;
|
|
31
38
|
//# sourceMappingURL=ConversationList.d.ts.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ConversationListItem } from './ConversationList.js';
|
|
2
|
+
/** Project saved/search titles for presentation without renaming durable conversations.
|
|
3
|
+
* The switcher has no prompt or title provenance, so keep wording and casing while stripping
|
|
4
|
+
* markup through the same owner as the panel title. Caller arrays and objects stay untouched.
|
|
5
|
+
* @param input.conversations - The loaded conversations.
|
|
6
|
+
* @param input.serverResults - Optional server search results, or null before a response.
|
|
7
|
+
* @returns Plain-text display lists and raw titles for lossless editing. Hosts ellipsize rows in CSS.
|
|
8
|
+
* @complexity O(n + sum(t²)) worst-case cleanup time; O(n + sum(t)) space for n items of length t.
|
|
9
|
+
*/
|
|
10
|
+
export declare function useConversationListTitles({ conversations, serverResults }: {
|
|
11
|
+
conversations: ConversationListItem[];
|
|
12
|
+
serverResults: ConversationListItem[] | null;
|
|
13
|
+
}, _options?: {}): {
|
|
14
|
+
conversations: ConversationListItem[];
|
|
15
|
+
serverResults: ConversationListItem[] | null;
|
|
16
|
+
rawTitles: ReadonlyMap<string, string | null>;
|
|
17
|
+
};
|
|
18
|
+
//# sourceMappingURL=ConversationList.hooks.d.ts.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { stripConversationTitleMarkdown } from '../../core/persistence/title.js';
|
|
3
|
+
/** Project saved/search titles for presentation without renaming durable conversations.
|
|
4
|
+
* The switcher has no prompt or title provenance, so keep wording and casing while stripping
|
|
5
|
+
* markup through the same owner as the panel title. Caller arrays and objects stay untouched.
|
|
6
|
+
* @param input.conversations - The loaded conversations.
|
|
7
|
+
* @param input.serverResults - Optional server search results, or null before a response.
|
|
8
|
+
* @returns Plain-text display lists and raw titles for lossless editing. Hosts ellipsize rows in CSS.
|
|
9
|
+
* @complexity O(n + sum(t²)) worst-case cleanup time; O(n + sum(t)) space for n items of length t.
|
|
10
|
+
*/
|
|
11
|
+
export function useConversationListTitles({ conversations, serverResults }, _options = {}) {
|
|
12
|
+
return useMemo(() => {
|
|
13
|
+
const rawTitles = new Map();
|
|
14
|
+
const display = (items) => items.map(item => ({
|
|
15
|
+
...item, title: item.title === null ? null : stripConversationTitleMarkdown({ text: item.title }) || null,
|
|
16
|
+
}));
|
|
17
|
+
// Search results can contain unloaded rows or newer titles for loaded ids; visible results win.
|
|
18
|
+
for (const item of conversations)
|
|
19
|
+
rawTitles.set(item.id, item.title);
|
|
20
|
+
for (const item of serverResults ?? [])
|
|
21
|
+
rawTitles.set(item.id, item.title);
|
|
22
|
+
return { conversations: display(conversations), serverResults: serverResults === null ? null : display(serverResults), rawTitles };
|
|
23
|
+
}, [conversations, serverResults]);
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=ConversationList.hooks.js.map
|
|
@@ -24,6 +24,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
24
24
|
import { useCallback, useDeferredValue, useEffect, useMemo, useRef, useState } from 'react';
|
|
25
25
|
import { useT } from '../hooks/context.js';
|
|
26
26
|
import { Icon } from './Icon.js';
|
|
27
|
+
import { useConversationListTitles } from './ConversationList.hooks.js';
|
|
28
|
+
import { conversationTriggerAttributes } from './ConversationList.rules.js';
|
|
27
29
|
/**
|
|
28
30
|
* How long a click waits to see whether it is really the first half of a double-click.
|
|
29
31
|
*
|
|
@@ -63,13 +65,15 @@ function relativeTime(updatedAt, now) {
|
|
|
63
65
|
return `${hours}h ago`;
|
|
64
66
|
return `${Math.round(hours / 24)}d ago`;
|
|
65
67
|
}
|
|
66
|
-
export function ConversationList({ conversations, activeConversationId, onSelect, onCreate, onDelete, onRename, onSearch, confirmDelete, createDisabled = false, emptyState, }) {
|
|
68
|
+
export function ConversationList({ conversations: savedConversations, activeConversationId, onSelect, onCreate, onDelete, onRename, onSearch, confirmDelete, createDisabled = false, emptyState, triggerLabel, nativeTitle = true, }) {
|
|
67
69
|
const t = useT();
|
|
68
70
|
const [open, setOpen] = useState(false);
|
|
69
71
|
const [query, setQuery] = useState('');
|
|
70
|
-
const [
|
|
72
|
+
const [savedServerResults, setServerResults] = useState(null);
|
|
73
|
+
const { conversations, serverResults, rawTitles } = useConversationListTitles({ conversations: savedConversations, serverResults: savedServerResults });
|
|
71
74
|
const [editingId, setEditingId] = useState(null);
|
|
72
75
|
const [draft, setDraft] = useState('');
|
|
76
|
+
const originalDraftRef = useRef('');
|
|
73
77
|
const rootRef = useRef(null);
|
|
74
78
|
const deferredQuery = useDeferredValue(query);
|
|
75
79
|
/**
|
|
@@ -169,7 +173,8 @@ export function ConversationList({ conversations, activeConversationId, onSelect
|
|
|
169
173
|
// No optimistic title update happens here — the row keeps rendering `item.title` from
|
|
170
174
|
// props until the host's own state updates, so a rejection cannot leave the UI showing a
|
|
171
175
|
// title the server didn't accept. Reporting the failure is what closes the remaining gap.
|
|
172
|
-
|
|
176
|
+
// Merely opening and leaving the editor must not normalize a deliberately saved title.
|
|
177
|
+
if (next && draft !== originalDraftRef.current)
|
|
173
178
|
runConversationListHostEffect('onRename', () => onRename(id, next));
|
|
174
179
|
}, [draft, onRename]);
|
|
175
180
|
const requestDelete = useCallback((item) => {
|
|
@@ -188,7 +193,7 @@ export function ConversationList({ conversations, activeConversationId, onSelect
|
|
|
188
193
|
});
|
|
189
194
|
}, [confirmDelete, onDelete, t]);
|
|
190
195
|
const active = conversations.find((c) => c.id === activeConversationId) ?? null;
|
|
191
|
-
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,
|
|
196
|
+
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", onClick: () => {
|
|
192
197
|
if (createDisabled)
|
|
193
198
|
return;
|
|
194
199
|
// No optimistic row is inserted — the list still comes straight from props — so a
|
|
@@ -211,8 +216,10 @@ export function ConversationList({ conversations, activeConversationId, onSelect
|
|
|
211
216
|
}, DOUBLE_CLICK_GRACE_MS);
|
|
212
217
|
}, onDoubleClick: () => {
|
|
213
218
|
cancelPendingSelect();
|
|
219
|
+
// Snapshot the raw seed: a host refresh during editing is not a user edit.
|
|
220
|
+
originalDraftRef.current = rawTitles.get(item.id) ?? '';
|
|
214
221
|
setEditingId(item.id);
|
|
215
|
-
setDraft(
|
|
222
|
+
setDraft(originalDraftRef.current);
|
|
216
223
|
}, children: [_jsx("span", { className: "jini-conv-item-title", children: item.title ?? t('Untitled') }), _jsxs("span", { className: "jini-conv-item-meta", children: [item.messageCount === undefined ? '' : `${item.messageCount} msg · `, relativeTime(item.updatedAt, now)] })] })), _jsx("button", { type: "button", className: "jini-conv-item-delete", "aria-label": t('Delete conversation'), "data-testid": `conversation-delete-${item.id}`, onClick: (event) => {
|
|
217
224
|
event.stopPropagation();
|
|
218
225
|
requestDelete(item);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Accessible name and optional native tooltip for the conversation switcher's icon-only trigger. */
|
|
2
|
+
export interface ConversationTriggerAttributes {
|
|
3
|
+
'aria-label': string;
|
|
4
|
+
title?: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Resolves the switcher trigger's `aria-label` and native `title`. By default both name the active
|
|
8
|
+
* conversation. A host that draws its own tooltip over the icon passes `nativeTitle: false`, so the
|
|
9
|
+
* browser's dark native tip does not show on top of it, and `label` to rename the control (for
|
|
10
|
+
* example "Chats") instead of stripping the attribute at runtime.
|
|
11
|
+
* @param input.activeTitle - The active conversation's display title, or null/undefined when none.
|
|
12
|
+
* @param input.t - The package translator.
|
|
13
|
+
* @param options.label - Replaces the default accessible name (and the native title, when kept).
|
|
14
|
+
* @param options.nativeTitle - `false` omits the native `title` attribute. Defaults to true.
|
|
15
|
+
* @returns Props to spread onto the trigger button.
|
|
16
|
+
* @complexity O(1).
|
|
17
|
+
*/
|
|
18
|
+
export declare function conversationTriggerAttributes({ activeTitle, t }: {
|
|
19
|
+
activeTitle: string | null | undefined;
|
|
20
|
+
t: (key: string) => string;
|
|
21
|
+
}, { label, nativeTitle }?: {
|
|
22
|
+
label?: string | undefined;
|
|
23
|
+
nativeTitle?: boolean | undefined;
|
|
24
|
+
}): ConversationTriggerAttributes;
|
|
25
|
+
//# sourceMappingURL=ConversationList.rules.d.ts.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves the switcher trigger's `aria-label` and native `title`. By default both name the active
|
|
3
|
+
* conversation. A host that draws its own tooltip over the icon passes `nativeTitle: false`, so the
|
|
4
|
+
* browser's dark native tip does not show on top of it, and `label` to rename the control (for
|
|
5
|
+
* example "Chats") instead of stripping the attribute at runtime.
|
|
6
|
+
* @param input.activeTitle - The active conversation's display title, or null/undefined when none.
|
|
7
|
+
* @param input.t - The package translator.
|
|
8
|
+
* @param options.label - Replaces the default accessible name (and the native title, when kept).
|
|
9
|
+
* @param options.nativeTitle - `false` omits the native `title` attribute. Defaults to true.
|
|
10
|
+
* @returns Props to spread onto the trigger button.
|
|
11
|
+
* @complexity O(1).
|
|
12
|
+
*/
|
|
13
|
+
export function conversationTriggerAttributes({ activeTitle, t }, { label, nativeTitle = true } = {}) {
|
|
14
|
+
const ariaLabel = label ?? (activeTitle ? `${t('Conversations')} — ${activeTitle}` : t('Conversations'));
|
|
15
|
+
if (!nativeTitle)
|
|
16
|
+
return { 'aria-label': ariaLabel };
|
|
17
|
+
return { 'aria-label': ariaLabel, title: label ?? activeTitle ?? t('Conversations') };
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=ConversationList.rules.js.map
|
|
@@ -389,16 +389,16 @@ const INLINE_RE = new RegExp(String.raw `(\x60[^\x60]+\x60)|(${MD_LINK})|(\*\*[^
|
|
|
389
389
|
// Only these targets become anchors; anything else (`javascript:`, `data:`, protocol-relative
|
|
390
390
|
// `//host`) stays literal text, since the text is untrusted model output.
|
|
391
391
|
const SAFE_LINK_HREF = /^(?:https?:\/\/|mailto:|\/(?!\/)|#)/iu;
|
|
392
|
-
//
|
|
393
|
-
//
|
|
394
|
-
|
|
392
|
+
// Every link opens in a new tab — an in-app path too, so following one never navigates the tab the
|
|
393
|
+
// chat lives in (owner 2026-10-08). Only a `#` same-document anchor stays in place. ChatPane's
|
|
394
|
+
// click guard (`useChatPaneNewTabLinks`) covers links this renderer does not draw.
|
|
395
|
+
const NEW_TAB = { target: '_blank', rel: 'noopener noreferrer' };
|
|
395
396
|
function renderMarkdownLink(source, key) {
|
|
396
397
|
// `MD_LINK` already matched `source`, so these two groups always capture.
|
|
397
398
|
const [, label, href] = MD_LINK_PARTS.exec(source);
|
|
398
399
|
if (!SAFE_LINK_HREF.test(href))
|
|
399
400
|
return source;
|
|
400
|
-
|
|
401
|
-
return (_jsx("a", { href: href, ...(offsite ? { target: '_blank', rel: 'noreferrer' } : {}), children: renderInline(label) }, key));
|
|
401
|
+
return (_jsx("a", { href: href, ...(href.startsWith('#') ? {} : NEW_TAB), children: renderInline(label) }, key));
|
|
402
402
|
}
|
|
403
403
|
function renderInline(text) {
|
|
404
404
|
const nodes = [];
|
|
@@ -421,7 +421,7 @@ function renderInline(text) {
|
|
|
421
421
|
else if (italic)
|
|
422
422
|
nodes.push(_jsx("em", { children: italic.slice(1, -1) }, key++));
|
|
423
423
|
else if (link)
|
|
424
|
-
nodes.push(_jsx("a", { href: link,
|
|
424
|
+
nodes.push(_jsx("a", { href: link, ...NEW_TAB, children: link }, key++));
|
|
425
425
|
// `whole` is `match[0]` — a successful `RegExpExecArray` always has a
|
|
426
426
|
// defined (if possibly empty) full-match string at index 0, so this
|
|
427
427
|
// fallback is unreachable (same dead-fallback reasoning as above).
|
|
@@ -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 }: McpUiSurfaceCardProps): import("react").JSX.Element;
|
|
33
|
+
export declare function McpUiSurfaceCard({ events, sandboxProxyUrl, onToolCall, onOpenLink, maxHeight, call, runStreaming, expiryClock, answered, onAnswered }: 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
|
|
@@ -66,7 +66,7 @@ import { McpUiHost, parseUIResource, readActionPlan, readExpiresAt, readPreferre
|
|
|
66
66
|
import { registerExtEventRenderer } from '../ext-event-renderer-registry.js';
|
|
67
67
|
import { useT } from '../hooks/context.js';
|
|
68
68
|
import { useSurfaceExpiry } from '../hooks/useSurfaceExpiry.js';
|
|
69
|
-
import { closedSurfaceLabel } from './mcp-ui-surface-state.js';
|
|
69
|
+
import { closedSurfaceLabel, reportDeliveredAnswer } from './mcp-ui-surface-state.js';
|
|
70
70
|
/** The `kind: 'ext'` event name this renderer claims. */
|
|
71
71
|
export const MCP_UI_EXT_EVENT_NAME = 'mcp-ui';
|
|
72
72
|
/** Collapses the event stream to the newest resource per `ui://` URI, in first-appearance order. */
|
|
@@ -120,18 +120,22 @@ function ClosedSurface({ call, label }) {
|
|
|
120
120
|
return (_jsx("div", { className: "mcpui-surface-card mcpui-surface-card-closed", role: "status", children: typeof title === 'string' && title.trim() !== '' ? `${title}: ${label}` : label }));
|
|
121
121
|
}
|
|
122
122
|
/** 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 }) {
|
|
123
|
+
export function McpUiSurfaceCard({ events, sandboxProxyUrl, onToolCall, onOpenLink, maxHeight, call, runStreaming, expiryClock, answered, onAnswered }) {
|
|
124
124
|
const t = useT();
|
|
125
125
|
const resources = useMemo(() => latestResourcesByUri(events), [events]);
|
|
126
126
|
// One card per slot is the registered shape (`mcpUiSurfaceSlotKey`), so `call` belongs to exactly
|
|
127
127
|
// that card. A group holding several URIs has no single call to judge them by, and stays live.
|
|
128
128
|
const single = resources.length === 1 ? resources[0] : undefined;
|
|
129
|
-
|
|
129
|
+
// An answered card has no deadline left: its countdown goes, and the deadline passing while the
|
|
130
|
+
// answer's work still runs must not relabel it "This question expired".
|
|
131
|
+
const expiry = useSurfaceExpiry({ expiresAtMs: single === undefined || answered === true ? undefined : readExpiresAt(single) }, expiryClock === undefined ? {} : { clock: expiryClock });
|
|
130
132
|
const closed = single === undefined
|
|
131
133
|
? undefined
|
|
132
134
|
: closedSurfaceLabel({ t, call, runStreaming, documentsForUri: events.length, expired: expiry?.expired === true });
|
|
133
135
|
if (closed !== undefined)
|
|
134
136
|
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 });
|
|
135
139
|
if (resources.length === 0) {
|
|
136
140
|
// Visible rather than silent: an `ext` event named `mcp-ui` that carries no parseable resource
|
|
137
141
|
// is a producer bug, and a transcript that renders nothing for it hides the bug behind what
|
|
@@ -150,7 +154,7 @@ export function McpUiSurfaceCard({ events, sandboxProxyUrl, onToolCall, onOpenLi
|
|
|
150
154
|
// the previous iframe session, so the handshake never re-ran and the frame kept serving
|
|
151
155
|
// the OLD document's state. The URI stays in the key because it is real identity; the
|
|
152
156
|
// length was never a substitute for content.
|
|
153
|
-
sessionKey: `${resource.resource.uri}:${resource.resource.text}`, ...(
|
|
157
|
+
sessionKey: `${resource.resource.uri}:${resource.resource.text}`, ...(toolCall === undefined ? {} : { onToolCall: toolCall }), ...(onOpenLink === undefined ? {} : { onOpenLink }), ...(height === undefined || Number.isNaN(height) ? {} : { initialHeight: height }), ...(maxHeight === undefined ? {} : { maxHeight }) }), plan !== undefined && _jsx(PendingSurfaceMirror, { uri: resource.resource.uri, plan: plan, t: t })] }, resource.resource.uri));
|
|
154
158
|
}), expiry?.expired === false && (_jsx("p", { className: "mcpui-surface-expiry", role: "timer", children: t('Expires in {time}', { time: expiry.remaining }) }))] }));
|
|
155
159
|
}
|
|
156
160
|
/**
|
|
@@ -1,17 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module MessageList
|
|
3
|
-
*
|
|
4
|
-
* Renders a conversation's messages via `<MessageRow>`, and — matching
|
|
5
|
-
* `useConversation`'s `scrollIntent` flag (see
|
|
6
|
-
* `ADS-memory/reports/jini-port/recon/r4b-webui-design.md` §4: "message array, optimistic
|
|
7
|
-
* user message, scroll-intent flag") — auto-scrolls to the newest content
|
|
8
|
-
* whenever `scrollIntent` is true, calling `onScrolled` once it has. The
|
|
9
|
-
* scroll-anchoring state lives in `useMessageListAutoScroll`
|
|
10
|
-
* (`MessageList.hooks.ts`); see that file for why it is presentational.
|
|
11
|
-
*/
|
|
12
1
|
import type { ChatMessage } from '../../core/index.js';
|
|
13
2
|
import { type MessageRowProps } from './MessageRow.js';
|
|
14
|
-
export interface MessageListProps extends Pick<MessageRowProps, 'projectFileNames' | 'onRequestOpenFile' | 'renderAttachment'> {
|
|
3
|
+
export interface MessageListProps extends Pick<MessageRowProps, 'projectFileNames' | 'onRequestOpenFile' | 'renderAttachment' | 'showUsageTotal' | 'showUsageLine' | 'toolCallDisplay' | 'showThinking'> {
|
|
15
4
|
messages: ChatMessage[];
|
|
16
5
|
isStreaming?: boolean;
|
|
17
6
|
/** Mirrors `useConversation().scrollIntent` — when `true`, this component scrolls to the bottom on mount/update. */
|
|
@@ -35,5 +24,11 @@ export interface MessageListProps extends Pick<MessageRowProps, 'projectFileName
|
|
|
35
24
|
onCancel: () => void;
|
|
36
25
|
} | null;
|
|
37
26
|
}
|
|
38
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Memoized: the pane re-renders on every composer keystroke, and re-rendering a long transcript
|
|
29
|
+
* (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 `pendingPrompt` and
|
|
31
|
+
* `projectFileNames` for exactly this.
|
|
32
|
+
*/
|
|
33
|
+
export declare const MessageList: import("react").NamedExoticComponent<MessageListProps>;
|
|
39
34
|
//# sourceMappingURL=MessageList.d.ts.map
|
|
@@ -10,6 +10,10 @@ export interface MessageListAutoScroll {
|
|
|
10
10
|
containerRef: RefObject<HTMLDivElement | null>;
|
|
11
11
|
onScroll: (event: UIEvent<HTMLElement>) => void;
|
|
12
12
|
}
|
|
13
|
+
/** Recompute costs once per immutable transcript snapshot, including saved history on reload. */
|
|
14
|
+
export declare function useConversationUsage({ messages }: {
|
|
15
|
+
messages: readonly ChatMessage[];
|
|
16
|
+
}, _optional?: {}): Map<string, import("../../core/turn-usage.js").ChatUsageSummary>;
|
|
13
17
|
export declare function useMessageListAutoScroll({ messages, scrollIntent }: {
|
|
14
18
|
messages: ChatMessage[];
|
|
15
19
|
scrollIntent: boolean;
|
|
@@ -4,8 +4,13 @@
|
|
|
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, useRef } from 'react';
|
|
7
|
+
import { useEffect, useMemo, useRef } from 'react';
|
|
8
|
+
import { conversationUsage } from '../../core/turn-usage.js';
|
|
8
9
|
import { nextStickToBottom, startsNewTurn } from './MessageList.rules.js';
|
|
10
|
+
/** Recompute costs once per immutable transcript snapshot, including saved history on reload. */
|
|
11
|
+
export function useConversationUsage({ messages }, _optional = {}) {
|
|
12
|
+
return useMemo(() => conversationUsage({ messages }), [messages]);
|
|
13
|
+
}
|
|
9
14
|
export function useMessageListAutoScroll({ messages, scrollIntent }, { onScrolled } = {}) {
|
|
10
15
|
const containerRef = useRef(null);
|
|
11
16
|
// Whether the transcript was scrolled to its bottom as of the last user scroll or programmatic
|
|
@@ -1,15 +1,34 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* @module MessageList
|
|
4
|
+
*
|
|
5
|
+
* Renders a conversation's messages via `<MessageRow>`, and — matching
|
|
6
|
+
* `useConversation`'s `scrollIntent` flag (see
|
|
7
|
+
* `ADS-memory/reports/jini-port/recon/r4b-webui-design.md` §4: "message array, optimistic
|
|
8
|
+
* user message, scroll-intent flag") — auto-scrolls to the newest content
|
|
9
|
+
* whenever `scrollIntent` is true, calling `onScrolled` once it has. The
|
|
10
|
+
* scroll-anchoring state lives in `useMessageListAutoScroll`
|
|
11
|
+
* (`MessageList.hooks.ts`); see that file for why it is presentational.
|
|
12
|
+
*/
|
|
13
|
+
import { memo } from 'react';
|
|
2
14
|
import { isTerminalRunStatus } from '../../core/index.js';
|
|
3
15
|
import { useT } from '../hooks/context.js';
|
|
4
|
-
import { useMessageListAutoScroll } from './MessageList.hooks.js';
|
|
16
|
+
import { useMessageListAutoScroll, useConversationUsage } from './MessageList.hooks.js';
|
|
5
17
|
import { MessageRow } from './MessageRow.js';
|
|
6
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Memoized: the pane re-renders on every composer keystroke, and re-rendering a long transcript
|
|
20
|
+
* (hundreds of tool rows, MCP-UI cards, markdown) per key made typing lag (owner report
|
|
21
|
+
* 2026-10-08). Callers must pass stable props — `ChatPane` memoizes `pendingPrompt` and
|
|
22
|
+
* `projectFileNames` for exactly this.
|
|
23
|
+
*/
|
|
24
|
+
export const MessageList = memo(function MessageList({ messages, isStreaming = false, scrollIntent = false, onScrolled, activeQuestionFormMessageId = null, questionFormSubmittedAnswersByMessageId, onQuestionFormSubmit, projectFileNames, onRequestOpenFile, renderAttachment, showUsageTotal = true, showUsageLine = true, toolCallDisplay = 'show', showThinking = false, pendingPrompt = null, }) {
|
|
7
25
|
const t = useT();
|
|
26
|
+
const usageByMessage = useConversationUsage({ messages });
|
|
8
27
|
const { containerRef, onScroll } = useMessageListAutoScroll({ messages, scrollIntent }, onScrolled !== undefined ? { onScrolled } : {});
|
|
9
28
|
return (_jsxs("div", { className: "jini-message-list", ref: containerRef, onScroll: onScroll, "data-agent-element": "chat-transcript", "data-agent-role": "list", "data-agent-label": "The conversation transcript, oldest message first", children: [messages.map((message) => {
|
|
10
29
|
const isLast = message.id === messages[messages.length - 1]?.id;
|
|
11
30
|
const runStreaming = isLast && isStreaming && !isTerminalRunStatus({ status: message.runStatus });
|
|
12
|
-
return (_jsx(MessageRow, { message: message, runStreaming: runStreaming, runSucceeded: message.runStatus === 'succeeded', questionFormInteractive: message.id === activeQuestionFormMessageId, ...(questionFormSubmittedAnswersByMessageId?.[message.id] !== undefined ? { questionFormSubmittedAnswers: questionFormSubmittedAnswersByMessageId[message.id] } : {}), ...(onQuestionFormSubmit !== undefined ? { onQuestionFormSubmit: (text, answers) => onQuestionFormSubmit(message.id, text, answers) } : {}), ...(projectFileNames !== undefined ? { projectFileNames } : {}), ...(onRequestOpenFile !== undefined ? { onRequestOpenFile } : {}), ...(renderAttachment !== undefined ? { renderAttachment } : {}) }, message.id));
|
|
31
|
+
return (_jsx(MessageRow, { message: message, ...(usageByMessage.has(message.id) ? { usageSummary: usageByMessage.get(message.id) } : {}), showUsageTotal: showUsageTotal, showUsageLine: showUsageLine, toolCallDisplay: toolCallDisplay, showThinking: showThinking, runStreaming: runStreaming, runSucceeded: message.runStatus === 'succeeded', questionFormInteractive: message.id === activeQuestionFormMessageId, ...(questionFormSubmittedAnswersByMessageId?.[message.id] !== undefined ? { questionFormSubmittedAnswers: questionFormSubmittedAnswersByMessageId[message.id] } : {}), ...(onQuestionFormSubmit !== undefined ? { onQuestionFormSubmit: (text, answers) => onQuestionFormSubmit(message.id, text, answers) } : {}), ...(projectFileNames !== undefined ? { projectFileNames } : {}), ...(onRequestOpenFile !== undefined ? { onRequestOpenFile } : {}), ...(renderAttachment !== undefined ? { renderAttachment } : {}) }, message.id));
|
|
13
32
|
}), pendingPrompt ? (_jsxs("div", { className: "jini-chat-pane__queued", role: "status", "aria-label": t('Message queued — not yet sent, will send once the current run finishes'), "data-testid": "chat-pane-queued", "data-agent-element": "chat-message-pending", "data-agent-role": "region", "data-agent-label": "A message queued to send once the current run finishes", children: [_jsx("div", { className: "jini-chat-pane__queued-text", children: pendingPrompt.text }), _jsx("div", { className: "jini-chat-pane__queued-actions", children: _jsx("button", { type: "button", className: "jini-chat-pane__queued-cancel", onClick: pendingPrompt.onCancel, title: t('Cancel queued message'), children: t('Cancel') }) })] })) : null] }));
|
|
14
|
-
}
|
|
33
|
+
});
|
|
15
34
|
//# sourceMappingURL=MessageList.js.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ChatUsageSummary } from '../../core/turn-usage.js';
|
|
1
2
|
/**
|
|
2
3
|
* @module MessageRow
|
|
3
4
|
*
|
|
@@ -50,8 +51,24 @@
|
|
|
50
51
|
*/
|
|
51
52
|
import React, { type ReactNode } from 'react';
|
|
52
53
|
import type { ChatAttachment, ChatMessage } from '../../core/index.js';
|
|
54
|
+
import { type ToolCallDisplay } from '../tool-call-display.js';
|
|
53
55
|
export interface MessageRowProps {
|
|
54
56
|
message: ChatMessage;
|
|
57
|
+
/** Per-turn charge and prefix total derived from the complete conversation's persisted events. */
|
|
58
|
+
usageSummary?: ChatUsageSummary;
|
|
59
|
+
/** Append the chat's running total as of each reply ("· total $3.95") to its Done line. On by default
|
|
60
|
+
* (owner, 2026-10-10: keep the total); pass `false` to show only each turn's own cost. */
|
|
61
|
+
showUsageTotal?: boolean;
|
|
62
|
+
/** Show the "Done · time · cost" line(s) at all. On by default; `false` hides every one, total
|
|
63
|
+
* included (a host's "Done line" display choice, 2026-10-11). */
|
|
64
|
+
showUsageLine?: boolean;
|
|
65
|
+
/** How tool calls show (`../tool-call-display.ts`): every card (`'show'`, the default), one
|
|
66
|
+
* fold-up line per turn (`'collapsed'`), or only failed ones (`'hidden'`). */
|
|
67
|
+
toolCallDisplay?: ToolCallDisplay;
|
|
68
|
+
/** Show the model's reasoning (`thinking` events) as a fold-up block where it happened. Off by
|
|
69
|
+
* default — the transcript never showed it before this option. Needs the interleaved layout; a
|
|
70
|
+
* message that falls back to the flat layout shows none. */
|
|
71
|
+
showThinking?: boolean;
|
|
55
72
|
/** Whether this message's own run is still streaming. */
|
|
56
73
|
runStreaming?: boolean;
|
|
57
74
|
runSucceeded?: boolean;
|
|
@@ -64,5 +81,10 @@ export interface MessageRowProps {
|
|
|
64
81
|
/** Host-supplied renderer for a `ChatAttachment` chip. Falls back to a plain filename chip. */
|
|
65
82
|
renderAttachment?: (attachment: ChatAttachment) => ReactNode;
|
|
66
83
|
}
|
|
67
|
-
|
|
84
|
+
/**
|
|
85
|
+
* Memoized so a transcript re-render (a streaming update to the newest message, or a parent that
|
|
86
|
+
* re-renders per composer keystroke) only re-renders rows whose props actually changed. Messages
|
|
87
|
+
* are replaced, never mutated, so identity is a sound change signal.
|
|
88
|
+
*/
|
|
89
|
+
export declare const MessageRow: React.NamedExoticComponent<MessageRowProps>;
|
|
68
90
|
//# sourceMappingURL=MessageRow.d.ts.map
|