@jini-ai/chat 0.6.21 → 0.6.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -0
- package/dist/react/components/MessageList.js +6 -2
- package/dist/react/components/MessageRow.d.ts +2 -0
- package/dist/react/components/MessageRow.js +8 -5
- package/dist/react/ext-event-renderer-registry.d.ts +8 -0
- package/dist/react/ext-event-renderer-registry.js +13 -2
- package/package.json +8 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
## 0.6.22
|
|
2
|
+
|
|
3
|
+
- The full CLI access notice shows once per conversation, also after replay or remount.
|
|
4
|
+
- A validated extension renderer can declare that it shows a failed turn's explanation (`terminalNotice`); the generic notice stays when the renderer is absent or refuses the payload.
|
|
5
|
+
|
|
1
6
|
## 0.6.21
|
|
2
7
|
|
|
3
8
|
- Per-chat memory churn stopped: run checkpoints are size-scaled (`checkpointIntervalMs({ bytes })`: 1 s, 3 s from 1 MB, 10 s from 5 MB) and written as guarded SQL deltas (append or text-tail splice under a length+tail receipt), falling back to a full checkpoint on mismatch. A completed checkpoint is never retried unchanged.
|
|
@@ -20,9 +20,9 @@ import { MessageRow } from './MessageRow.js';
|
|
|
20
20
|
import { SentAttachmentButton } from './SentAttachment.js';
|
|
21
21
|
import { AttachmentPreviewModal } from './AttachmentPreviewModal.js';
|
|
22
22
|
import { useAttachmentGallery } from './AttachmentGallery.hooks.js';
|
|
23
|
-
function renderMessageRow({ message, isLast, isStreaming, usageSummary, sending }, { activeQuestionFormMessageId, questionFormSubmittedAnswersByMessageId, onQuestionFormSubmit, ...rowProps } = {}) {
|
|
23
|
+
function renderMessageRow({ message, isLast, isStreaming, usageSummary, sending, showAccessNotice }, { activeQuestionFormMessageId, questionFormSubmittedAnswersByMessageId, onQuestionFormSubmit, ...rowProps } = {}) {
|
|
24
24
|
const runStreaming = isLast && isStreaming && !isTerminalRunStatus({ status: message.runStatus });
|
|
25
|
-
return (_jsx(MessageRow, { message: message, ...rowProps, ...(usageSummary === undefined ? {} : { usageSummary }), runStreaming: runStreaming, runSending: sending, 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) } : {}) }, message.id));
|
|
25
|
+
return (_jsx(MessageRow, { message: message, ...rowProps, showAccessNotice: showAccessNotice, ...(usageSummary === undefined ? {} : { usageSummary }), runStreaming: runStreaming, runSending: sending, 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) } : {}) }, message.id));
|
|
26
26
|
}
|
|
27
27
|
/**
|
|
28
28
|
* Memoized: the pane re-renders on every composer keystroke, and re-rendering a long transcript
|
|
@@ -33,9 +33,13 @@ function renderMessageRow({ message, isLast, isStreaming, usageSummary, sending
|
|
|
33
33
|
export const MessageList = memo(function MessageList({ messages: sourceMessages, isStreaming = false, scrollIntent = false, onScrolled, pendingPrompt, pendingPrompts, ...rowProps }) {
|
|
34
34
|
const messages = useMemo(() => projectAcceptedInputMessages({ messages: sourceMessages }), [sourceMessages]);
|
|
35
35
|
const usageByMessage = useConversationUsage({ messages });
|
|
36
|
+
// Derive from the conversation's saved events, so replay/remount does not repeat the notice.
|
|
37
|
+
const accessNoticeId = useMemo(() => messages.find(message => message.role === 'assistant'
|
|
38
|
+
&& message.events?.some(event => event.kind === 'status' && event.label === 'full_cli_access' && event.detail))?.id, [messages]);
|
|
36
39
|
const { containerRef, onScroll, awayFromLatest, jumpToLatest } = useMessageListAutoScroll({ messages, scrollIntent }, onScrolled !== undefined ? { onScrolled } : {});
|
|
37
40
|
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, index) => renderMessageRow({
|
|
38
41
|
message,
|
|
42
|
+
showAccessNotice: message.id === accessNoticeId,
|
|
39
43
|
isLast: message.id === messages[messages.length - 1]?.id,
|
|
40
44
|
// A reply's start is accepted together with its user turn (`inputPending` clears then).
|
|
41
45
|
// A run id also proves acceptance after reload, when a legacy pending flag can survive.
|
|
@@ -54,6 +54,8 @@ import type { ChatAttachment, ChatMessage } from '../../core/index.js';
|
|
|
54
54
|
import { type ToolCallDisplay } from '../tool-call-display.js';
|
|
55
55
|
export interface MessageRowProps {
|
|
56
56
|
message: ChatMessage;
|
|
57
|
+
/** Conversation hosts show the first access status only; standalone rows retain it by default. */
|
|
58
|
+
showAccessNotice?: boolean;
|
|
57
59
|
/** Per-turn charge and prefix total derived from the complete conversation's persisted events. */
|
|
58
60
|
usageSummary?: ChatUsageSummary;
|
|
59
61
|
/** Append the chat's running total as of each reply ("· total $3.95") to its Done line. On by default
|
|
@@ -147,7 +147,7 @@ function RunAccessNotice({ events }) {
|
|
|
147
147
|
const notice = events?.find(event => event.kind === 'status' && event.label === 'full_cli_access');
|
|
148
148
|
if (notice?.kind !== 'status' || !notice.detail)
|
|
149
149
|
return null;
|
|
150
|
-
return _jsx("div", { className: "jini-chat-pane__notice", role: "
|
|
150
|
+
return _jsx("div", { className: "jini-chat-pane__notice", role: "status", children: t(notice.detail) });
|
|
151
151
|
}
|
|
152
152
|
function ThinkingBlock({ text }) {
|
|
153
153
|
const t = useT();
|
|
@@ -187,13 +187,16 @@ function UserMessageRow({ message, renderAttachment }) {
|
|
|
187
187
|
const gallery = useAttachmentGallery({ attachments: message.attachments ?? NO_ATTACHMENTS });
|
|
188
188
|
return (_jsxs("div", { className: "jini-message jini-message-user", "data-message-id": message.id, "data-agent-element": `chat-message-${message.id}`, "data-agent-role": "region", "data-agent-label": "A message from the user", children: [(message.attachments?.length || message.folders?.length) ? (_jsxs("div", { className: "jini-message-attachments", children: [_jsx(FolderChips, { folders: message.folders }), (message.attachments ?? []).map((a) => (_jsx(SentAttachmentButton, { attachment: a, onOpen: gallery.open, renderAttachment: renderAttachment }, a.path)))] })) : null, _jsx("div", { className: "jini-message-content", children: message.content }), _jsx("div", { className: "jini-message-actions jini-message-actions--user", children: _jsx(CopyMessageButton, { text: message.content, label: t('Copy message') }) }), gallery.opened ? (_jsx(AttachmentPreviewModal, { attachment: gallery.opened, onClose: gallery.close, navigation: gallery.navigation })) : null] }));
|
|
189
189
|
}
|
|
190
|
-
function AssistantMessageRow({ message, usageSummary, showUsageTotal, showUsageLine, toolCallDisplay, showThinking, runStreaming, runSending, sendingLabel, runSucceeded, questionFormInteractive, questionFormSubmittedAnswers, onQuestionFormSubmit, projectFileNames, onRequestOpenFile, }) {
|
|
190
|
+
function AssistantMessageRow({ showAccessNotice = true, message, usageSummary, showUsageTotal, showUsageLine, toolCallDisplay, showThinking, runStreaming, runSending, sendingLabel, runSucceeded, questionFormInteractive, questionFormSubmittedAnswers, onQuestionFormSubmit, projectFileNames, onRequestOpenFile, }) {
|
|
191
191
|
const registry = useExtEventRendererRegistry();
|
|
192
192
|
const messageUsage = useMessageUsage({ message, ...(usageSummary === undefined ? {} : { summary: usageSummary }) });
|
|
193
193
|
const timeline = useToolTimeline({ events: message.events }, { runStreaming, runSucceeded });
|
|
194
194
|
const extGroups = useExtEventGroups({ events: message.events }, { slotOf: (name, data) => registry.slot({ name, data }) });
|
|
195
195
|
const answeredCards = useAnsweredCards({ slotOf: registry.slot, events: message.events ?? [], groups: extGroups, runStreaming, runStatus: message.runStatus });
|
|
196
|
-
|
|
196
|
+
// A validated extension may already show the explanation. Keep the generic fallback when its
|
|
197
|
+
// renderer is absent or refuses the payload, so malformed history never hides a failed turn.
|
|
198
|
+
const extensionExplainsFailure = !runStreaming && !runSucceeded && extGroups.some(group => registry.hasTerminalNotice(group));
|
|
199
|
+
const terminalNotice = extensionExplainsFailure ? null : terminalMessageNotice({ message }, {});
|
|
197
200
|
const visibleContent = stripArtifact({ content: message.content });
|
|
198
201
|
const displayBlocks = messageBlocks({ message, rows: timeline.rows, registry }, { toolCallDisplay, includeThinking: showThinking });
|
|
199
202
|
// Artifact stripping trims/replaces text, invalidating the event offsets for rendering. Keep the
|
|
@@ -286,11 +289,11 @@ function AssistantMessageRow({ message, usageSummary, showUsageTotal, showUsageL
|
|
|
286
289
|
// in their original receipt order, outside the assistant's actions and usage footer.
|
|
287
290
|
// Keep the same child-array nesting as the interleaved path so React preserves the footer.
|
|
288
291
|
if (blocks === null)
|
|
289
|
-
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)] });
|
|
292
|
+
return _jsxs(_Fragment, { children: [showAccessNotice && _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)] });
|
|
290
293
|
// A reply boundary without reported usage renders nothing; it must not create an empty row's gap.
|
|
291
294
|
const visibleBlocks = blocks.filter(block => block.kind !== 'reply-end' || (showUsageLine && usage.replies[block.index] !== undefined));
|
|
292
295
|
const { sections, footerIndex } = transcriptSections({ blocks: visibleBlocks, runInProgress });
|
|
293
|
-
return _jsxs(_Fragment, { children: [_jsx(RunAccessNotice, { events: message.events }), sections.map((section, index) => isUserSection({ section })
|
|
296
|
+
return _jsxs(_Fragment, { children: [showAccessNotice && _jsx(RunAccessNotice, { events: message.events }), sections.map((section, index) => isUserSection({ section })
|
|
294
297
|
? _jsx(React.Fragment, { children: section.map(renderBlock) }, section[0].key)
|
|
295
298
|
: renderAssistantSection({ section, key: section[0]?.key ?? message.id, footer: index === footerIndex }))] });
|
|
296
299
|
}
|
|
@@ -97,6 +97,10 @@ export type ExtEventSlotKey = (required: {
|
|
|
97
97
|
export interface ExtEventRendererOptions {
|
|
98
98
|
/** See {@link ExtEventSlotKey}. Omit for one shared slot per `name`. */
|
|
99
99
|
slotKey?: ExtEventSlotKey;
|
|
100
|
+
/** True only when the renderer will show the terminal explanation for these validated events. */
|
|
101
|
+
terminalNotice?: (required: {
|
|
102
|
+
events: readonly unknown[];
|
|
103
|
+
}) => boolean;
|
|
100
104
|
}
|
|
101
105
|
/** Owned renderer and slot-key state for one chat tree. */
|
|
102
106
|
export interface ExtEventRendererRegistry {
|
|
@@ -111,6 +115,10 @@ export interface ExtEventRendererRegistry {
|
|
|
111
115
|
name: string;
|
|
112
116
|
data: unknown;
|
|
113
117
|
}): string;
|
|
118
|
+
hasTerminalNotice(required: {
|
|
119
|
+
name: string;
|
|
120
|
+
events: readonly unknown[];
|
|
121
|
+
}): boolean;
|
|
114
122
|
clear(): void;
|
|
115
123
|
}
|
|
116
124
|
/** Create independent extension renderer state, including each protocol's slot grouping.
|
|
@@ -10,7 +10,7 @@ export function createExtEventRendererRegistry(_required = {}) {
|
|
|
10
10
|
* can dispose cleanly. Re-registering the same name overwrites — last writer wins.
|
|
11
11
|
*/
|
|
12
12
|
function register({ name, renderer }, options = {}) {
|
|
13
|
-
const entry = { renderer, slotKey: options.slotKey };
|
|
13
|
+
const entry = { renderer, slotKey: options.slotKey, terminalNotice: options.terminalNotice };
|
|
14
14
|
renderers.set(name, entry);
|
|
15
15
|
return () => {
|
|
16
16
|
if (renderers.get(name) === entry)
|
|
@@ -20,6 +20,17 @@ export function createExtEventRendererRegistry(_required = {}) {
|
|
|
20
20
|
function get({ name }) {
|
|
21
21
|
return renderers.get(name)?.renderer;
|
|
22
22
|
}
|
|
23
|
+
/** Let a registered, validated explanation own the terminal surface; failures retain the fallback.
|
|
24
|
+
* @complexity O(1) registry lookup plus the host's validation; no effects.
|
|
25
|
+
*/
|
|
26
|
+
function hasTerminalNotice({ name, events }) {
|
|
27
|
+
try {
|
|
28
|
+
return renderers.get(name)?.terminalNotice?.({ events }) === true;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
23
34
|
/**
|
|
24
35
|
* The render slot an ext event belongs to: its `name`, or `name:key` when the name's renderer was
|
|
25
36
|
* registered with a {@link ExtEventSlotKey} that recognizes this event. A throwing key function
|
|
@@ -42,7 +53,7 @@ export function createExtEventRendererRegistry(_required = {}) {
|
|
|
42
53
|
}
|
|
43
54
|
/** Visible mainly for tests. */
|
|
44
55
|
function clear() { renderers.clear(); }
|
|
45
|
-
return { register, get, slot, clear };
|
|
56
|
+
return { register, get, slot, hasTerminalNotice, clear };
|
|
46
57
|
}
|
|
47
58
|
/** Compatibility default for hosts that have not supplied owned state. */
|
|
48
59
|
export const defaultExtEventRendererRegistry = createExtEventRendererRegistry({});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jini-ai/chat",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.22",
|
|
4
4
|
"description": "Jini's chat/artifact frontend, consolidated into one package with two subpaths: framework-free chat vocabulary and pure parsers in `/core`, headless hooks + presentational components + slots in `/react`. Import only the layer you need.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -214,9 +214,9 @@
|
|
|
214
214
|
},
|
|
215
215
|
"dependencies": {
|
|
216
216
|
"kysely": "^0.29.6",
|
|
217
|
-
"@jini-ai/core": "^0.6.
|
|
218
|
-
"@jini-ai/diagnostics": "^0.6.19",
|
|
217
|
+
"@jini-ai/core": "^0.6.22",
|
|
219
218
|
"@jini-ai/agentic": "^0.6.19",
|
|
219
|
+
"@jini-ai/diagnostics": "^0.6.22",
|
|
220
220
|
"@jini-ai/db": "^0.6.19"
|
|
221
221
|
},
|
|
222
222
|
"peerDependencies": {
|
|
@@ -261,13 +261,13 @@
|
|
|
261
261
|
"pg": "8.23.0",
|
|
262
262
|
"@ag-ui/core": "0.0.58",
|
|
263
263
|
"fast-check": "4.3.0",
|
|
264
|
+
"@jini-ai/agent-runtime": "0.6.22",
|
|
264
265
|
"@jini-ai/agentic": "^0.6.19",
|
|
265
|
-
"@jini-ai/core": "^0.6.21",
|
|
266
|
-
"@jini-ai/agent-runtime": "0.6.19",
|
|
267
|
-
"@jini-ai/diagnostics": "^0.6.19",
|
|
268
|
-
"@jini-ai/db": "0.6.19",
|
|
269
266
|
"@jini-ai/protocol": "^0.6.19",
|
|
270
|
-
"@jini-ai/
|
|
267
|
+
"@jini-ai/core": "^0.6.22",
|
|
268
|
+
"@jini-ai/diagnostics": "^0.6.22",
|
|
269
|
+
"@jini-ai/ui": "^0.6.22",
|
|
270
|
+
"@jini-ai/db": "0.6.19"
|
|
271
271
|
},
|
|
272
272
|
"typesVersions": {
|
|
273
273
|
"*": {
|