@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 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: "note", children: t(notice.detail) });
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
- const terminalNotice = terminalMessageNotice({ message }, {});
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.21",
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.21",
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/ui": "^0.6.19"
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
  "*": {