@jini-ai/chat 0.6.11 → 0.6.12

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,10 @@
1
+ ## 0.6.12
2
+
3
+ ### Patch Changes
4
+
5
+ - chat: typing while a choice card waits queues the message instead of cancelling its tool call; answered choice cards keep their question and report option labels, not values; mid-run follow-ups read "Queued" until consumed; one steady thinking clock.
6
+ - @jini-ai/core@0.6.12
7
+
1
8
  ## 0.6.11
2
9
 
3
10
  ### Patch Changes
@@ -19,6 +19,7 @@
19
19
  */
20
20
  import { a2uiSurfaceIdOf, displayOnlySurfaceIdOf } from '@jini-ai/agentic/a2ui';
21
21
  import { isDelegatedWrapperToolName } from './tool-events.js';
22
+ import { MID_RUN_USER_MESSAGE_EVENT_NAME } from './turn-usage.js';
22
23
  /** Tool names whose only job is finding the right tool (MCP tool search / schema lookup). */
23
24
  const DISCOVERY_TOOL_NAMES = new Set([
24
25
  'ToolSearch',
@@ -88,6 +89,9 @@ function scanStatus({ scan, event, index }, _optional = {}) {
88
89
  }
89
90
  }
90
91
  function scanEvent(scan, event, index) {
92
+ // Queue acceptance/consumption is user input, not new agent activity. Neither restarts its clock.
93
+ if (event.kind === 'ext' && event.name === MID_RUN_USER_MESSAGE_EVENT_NAME)
94
+ return;
91
95
  if (isVisible(event)) {
92
96
  scan.lastVisible = { index, kind: event.kind };
93
97
  scan.visibleCount += 1;
@@ -222,7 +222,7 @@ function AssistantMessageRow({ message, usageSummary, showUsageTotal, showUsageL
222
222
  const renderExtGroup = (group) => {
223
223
  const renderer = registry.get({ name: group.name });
224
224
  if (!renderer && group.name === MID_RUN_USER_MESSAGE_EXT_EVENT_NAME)
225
- return _jsx(MidRunUserMessage, { events: group.events });
225
+ return _jsx(MidRunUserMessage, { events: group.events, runStreaming: answeredCards.runStreaming }, group.slot);
226
226
  if (!renderer)
227
227
  return null;
228
228
  const answerText = answeredCards.answerText(group.slot);
@@ -7,7 +7,7 @@ export declare function midRunUserMessageSlotKey({ data }: {
7
7
  data: unknown;
8
8
  }): string | undefined;
9
9
  /** The bubble for one mid-run message — the same classes as a sent user message, so it reads as one. */
10
- export declare function MidRunUserMessage({ events }: Pick<ExtEventRenderProps, 'events'>): import("react").JSX.Element | null;
10
+ export declare function MidRunUserMessage({ events, runStreaming }: Pick<ExtEventRenderProps, 'events'> & Partial<Pick<ExtEventRenderProps, 'runStreaming'>>): import("react").JSX.Element | null;
11
11
  /**
12
12
  * Registers {@link MidRunUserMessage} against the ext-event registry.
13
13
  *
@@ -14,6 +14,7 @@ import { acceptedInputEvent } from '../../core/accepted-input.js';
14
14
  import { SentAttachmentButton } from './SentAttachment.js';
15
15
  import { AttachmentPreviewModal } from './AttachmentPreviewModal.js';
16
16
  import { useAttachmentGallery } from './AttachmentGallery.hooks.js';
17
+ import { useT } from '../hooks/context.js';
17
18
  import { MID_RUN_USER_MESSAGE_EVENT_NAME } from '../../core/turn-usage.js';
18
19
  import { registerExtEventRenderer } from '../ext-event-renderer-registry.js';
19
20
  /** The ext-event name the daemon's mid-run `user_message` run event arrives under. One source with
@@ -28,7 +29,7 @@ export function midRunUserMessageSlotKey({ data }) {
28
29
  return field(data, 'id');
29
30
  }
30
31
  /** The bubble for one mid-run message — the same classes as a sent user message, so it reads as one. */
31
- export function MidRunUserMessage({ events }) {
32
+ export function MidRunUserMessage({ events, runStreaming = false }) {
32
33
  const data = events.at(-1);
33
34
  const text = field(data, 'text');
34
35
  if (text === undefined)
@@ -36,12 +37,14 @@ export function MidRunUserMessage({ events }) {
36
37
  const receipt = acceptedInputEvent({ event: { kind: 'ext', name: MID_RUN_USER_MESSAGE_EVENT_NAME, data } });
37
38
  // MOVE: A single live delivery may contain several queued inputs; display their original receipts.
38
39
  const inputs = receipt?.data.inputs ?? [{ id: field(data, 'id') ?? 'mid-run', text }];
39
- return _jsx(_Fragment, { children: inputs.map(input => _jsx(MidRunInput, { input: input }, input.id)) });
40
+ // A run that ended without reading the message hands it to the next turn; it is no longer queued here.
41
+ return _jsx(_Fragment, { children: inputs.map(input => _jsx(MidRunInput, { input: input, queued: runStreaming && (input.deliveryState ?? receipt?.data.deliveryState) === 'queued' }, input.id)) });
40
42
  }
41
- function MidRunInput({ input }) {
43
+ function MidRunInput({ input, queued }) {
44
+ const t = useT();
42
45
  const attachments = input.attachments ?? [];
43
46
  const gallery = useAttachmentGallery({ attachments });
44
- return (_jsxs("div", { className: "jini-message jini-message-user jini-message-user--mid-run", "data-agent-role": "region", "data-agent-label": "A message from the user, sent while the agent was working", children: [_jsx("div", { className: "jini-message-content", children: input.text }), attachments.length > 0 && _jsx("div", { className: "jini-message-attachments", children: attachments.map(attachment => _jsx(SentAttachmentButton, { attachment: attachment, onOpen: gallery.open }, attachment.path)) }), gallery.opened !== null && _jsx(AttachmentPreviewModal, { attachment: gallery.opened, onClose: gallery.close, navigation: gallery.navigation })] }));
47
+ return (_jsxs("div", { className: "jini-message jini-message-user jini-message-user--mid-run", "data-agent-role": "region", "data-agent-label": "A message from the user, sent while the agent was working", children: [queued && _jsx("div", { className: "jini-message-user__queued", role: "status", children: t('Queued — read after the current step') }), _jsx("div", { className: "jini-message-content", children: input.text }), attachments.length > 0 && _jsx("div", { className: "jini-message-attachments", children: attachments.map(attachment => _jsx(SentAttachmentButton, { attachment: attachment, onOpen: gallery.open }, attachment.path)) }), gallery.opened !== null && _jsx(AttachmentPreviewModal, { attachment: gallery.opened, onClose: gallery.close, navigation: gallery.navigation })] }));
45
48
  }
46
49
  /**
47
50
  * Registers {@link MidRunUserMessage} against the ext-event registry.
@@ -49,6 +52,6 @@ function MidRunInput({ input }) {
49
52
  * @returns An unregister handle, so a test or a hot reload can dispose cleanly.
50
53
  */
51
54
  export function registerMidRunUserMessageRenderer() {
52
- return registerExtEventRenderer({ name: MID_RUN_USER_MESSAGE_EXT_EVENT_NAME, renderer: (props) => _jsx(MidRunUserMessage, { events: props.events }) }, { slotKey: midRunUserMessageSlotKey });
55
+ return registerExtEventRenderer({ name: MID_RUN_USER_MESSAGE_EXT_EVENT_NAME, renderer: (props) => _jsx(MidRunUserMessage, { events: props.events, runStreaming: props.runStreaming }) }, { slotKey: midRunUserMessageSlotKey });
53
56
  }
54
57
  //# sourceMappingURL=MidRunUserMessage.js.map
@@ -1,5 +1,6 @@
1
1
  import { acceptedInputEvent, acceptedInputUserEvent, projectAcceptedInputEvents } from '../../../../core/accepted-input.js';
2
2
  import { useUserTextGuard } from '../../../hooks/useUserTextGuard.js';
3
+ import { isAwaitingAnswer } from '../../../../core/run-activity.js';
3
4
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
4
5
  import { appendQueuedTurn, describeChatPaneSendBlocker, findAwaitedTypedAnswerId, findChatPaneSendBlocker, findMidRunMessageRunId, isChatPaneQueueableBlocker, resolveChatPaneSelection, } from '../rules.js';
5
6
  import { definedProps } from '../../../util/defined-props.js';
@@ -108,11 +109,17 @@ function resolveChatPaneActivity(selectedAgent, conversation) {
108
109
  return 'streaming';
109
110
  return 'ready';
110
111
  }
112
+ /** Hold new input until its uploads or human gate settle; the existing queue owns the retry.
113
+ * @complexity O(e) in the newest message's events, with no I/O.
114
+ */
115
+ function waitingForLiveInput({ uploadPending, messages }, _options = {}) {
116
+ return uploadPending || isAwaitingAnswer({ events: messages.at(-1)?.events });
117
+ }
111
118
  /**
112
119
  * The host delivery and live run a send can go INTO instead of queueing behind, or `null` when the
113
120
  * host offers no mid-run delivery or no run can take it ({@link findMidRunMessageRunId}).
114
121
  *
115
- * @complexity O(1).
122
+ * @complexity O(e) in the newest message's events.
116
123
  */
117
124
  function findMidRunTarget({ deliver, blocker, attachmentCount, messages }) {
118
125
  if (deliver === undefined)
@@ -181,8 +188,8 @@ export function useChatPane(options) {
181
188
  const [storedQueuedInputs, setQueuedInputs] = useState([]);
182
189
  // MOVE: Uploads belong to the input that submitted them, even if a later message is queued first.
183
190
  const queuedUploadOwnersRef = useRef(new Map());
184
- // A send during upload resumes once pixels arrive; a rejected live delivery waits for run end.
185
- const queuedForUploadRef = useRef(false);
191
+ // A send held for uploads or a human card resumes live; a refused delivery waits for run end.
192
+ const queuedForLiveDeliveryRef = useRef(false);
186
193
  const submittedComposerRef = useRef(null);
187
194
  const startingPromptRef = useRef(false);
188
195
  const [typedAnswerNotice, setTypedAnswerNotice] = useState(null);
@@ -590,8 +597,10 @@ export function useChatPane(options) {
590
597
  }
591
598
  const turn = guard.redact({ text: prompt }, {});
592
599
  const uploadPending = activeUploadsRef.current.size > 0 || attachmentError !== null;
593
- if (uploadPending)
594
- queuedForUploadRef.current = queuedTurn === null || queuedForUploadRef.current;
600
+ // Retry live delivery after the human gate settles, using the same queue as pending uploads.
601
+ if (waitingForLiveInput({ uploadPending, messages: conversation.messages })) {
602
+ queuedForLiveDeliveryRef.current = queuedTurn === null || queuedForLiveDeliveryRef.current;
603
+ }
595
604
  // MOVE: The existing queue owns its files; a later steer must join it to preserve send order.
596
605
  const target = uploadPending || queuedInputs.some(input => input.liveMessageId === undefined) ? null : findMidRunTarget({
597
606
  deliver: options.deliverMidRunMessage,
@@ -679,7 +688,7 @@ export function useChatPane(options) {
679
688
  }, []);
680
689
  const cancelQueued = useCallback(() => {
681
690
  submittedComposerRef.current = null;
682
- queuedForUploadRef.current = false;
691
+ queuedForLiveDeliveryRef.current = false;
683
692
  queuedConversationIdRef.current = undefined;
684
693
  discardQueuedUploads({});
685
694
  setQueuedInputs([]);
@@ -693,7 +702,7 @@ export function useChatPane(options) {
693
702
  // clear blocker. Clear the slot before awaiting so re-renders cannot send it twice.
694
703
  useEffect(() => {
695
704
  if (queuedTurn === null) {
696
- queuedForUploadRef.current = false;
705
+ queuedForLiveDeliveryRef.current = false;
697
706
  return;
698
707
  }
699
708
  if (queuedConversationIdRef.current !== options.conversationId) {
@@ -705,13 +714,13 @@ export function useChatPane(options) {
705
714
  }
706
715
  if (activeUploadCount > 0 || attachmentError !== null)
707
716
  return;
708
- const target = queuedForUploadRef.current ? findMidRunTarget({
717
+ const target = queuedForLiveDeliveryRef.current ? findMidRunTarget({
709
718
  deliver: options.deliverMidRunMessage,
710
719
  blocker: sendBlocker, attachmentCount: queuedInputs.flatMap(input => input.attachments).length, messages: conversation.messages,
711
720
  }) : null;
712
721
  if (target === null && (sendBlocker !== null || awaitingRunReceipts({ inputs: queuedInputs, messages: conversation.messages })))
713
722
  return;
714
- queuedForUploadRef.current = false;
723
+ queuedForLiveDeliveryRef.current = false;
715
724
  const inputs = queuedInputs;
716
725
  const attachments = inputs.flatMap(input => input.attachments);
717
726
  setQueuedInputs([]);
@@ -740,7 +749,7 @@ export function useChatPane(options) {
740
749
  // `conversation.cancel()` above clears the streaming blocker — the flush effect fires it into
741
750
  // the just-reset conversation instead of discarding it like the rest of this turn's state.
742
751
  queuedConversationIdRef.current = undefined;
743
- queuedForUploadRef.current = false;
752
+ queuedForLiveDeliveryRef.current = false;
744
753
  queuedUploadOwnersRef.current.clear();
745
754
  setQueuedInputs([]);
746
755
  }, [composer, conversation, options.initialMessages]);
@@ -146,13 +146,16 @@ export declare function isTypedAnswerTurn({ blocker, attachmentCount, messages,
146
146
  * turn with a known `runId`.
147
147
  * Attachment support is decided by the host's delivery, never by dropping them here.
148
148
  *
149
- * @complexity O(1).
149
+ * A native live-input transport can reject a pending human gate when given another user turn.
150
+ * Keep ordinary text in the pane's queue until the card's tool settles; typed question answers
151
+ * have their own delivery path and never pass this guard.
152
+ * @complexity O(e) in the newest message's events.
150
153
  */
151
154
  export declare function findMidRunMessageRunId({ blocker, messages }: {
152
155
  blocker: ChatPaneSendBlocker | null;
153
156
  attachmentCount: number;
154
157
  messages: readonly ChatMessage[];
155
- }): string | null;
158
+ }, _options?: {}): string | null;
156
159
  /**
157
160
  * MOVE: Display is per-message; delivery is ONE queued turn. A second message behind the same run
158
161
  * is appended to that delivery (a blank line apart) rather than replacing it:
@@ -1,6 +1,7 @@
1
1
  import { configuredPickerModel, pickerModelOptions, isConcretePickerModel } from './model-options.js';
2
2
  import { isTerminalRunStatus } from '../../../core/messages.js';
3
3
  import { acceptedInputEvent } from '../../../core/accepted-input.js';
4
+ import { isAwaitingAnswer } from '../../../core/run-activity.js';
4
5
  function preferredOptionId(options) {
5
6
  return options?.find((option) => option.id === 'default')?.id ?? options?.[0]?.id;
6
7
  }
@@ -234,12 +235,17 @@ export function isTypedAnswerTurn({ blocker, attachmentCount, messages, toolName
234
235
  * turn with a known `runId`.
235
236
  * Attachment support is decided by the host's delivery, never by dropping them here.
236
237
  *
237
- * @complexity O(1).
238
+ * A native live-input transport can reject a pending human gate when given another user turn.
239
+ * Keep ordinary text in the pane's queue until the card's tool settles; typed question answers
240
+ * have their own delivery path and never pass this guard.
241
+ * @complexity O(e) in the newest message's events.
238
242
  */
239
- export function findMidRunMessageRunId({ blocker, messages }) {
243
+ export function findMidRunMessageRunId({ blocker, messages }, _options = {}) {
240
244
  if (!isChatPaneQueueableBlocker({ blocker }))
241
245
  return null;
242
246
  const last = messages.at(-1);
247
+ if (isAwaitingAnswer({ events: last?.events }))
248
+ return null;
243
249
  return last?.role === 'assistant' && last.runId ? last.runId : null;
244
250
  }
245
251
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jini-ai/chat",
3
- "version": "0.6.11",
3
+ "version": "0.6.12",
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": {
@@ -196,7 +196,7 @@
196
196
  "registry": "https://registry.npmjs.org"
197
197
  },
198
198
  "dependencies": {
199
- "@jini-ai/core": "^0.6.11"
199
+ "@jini-ai/core": "^0.6.12"
200
200
  },
201
201
  "peerDependencies": {
202
202
  "react": "^18.3.0 || ^19.0.0",
@@ -303,13 +303,13 @@
303
303
  "pg": "8.23.0",
304
304
  "@ag-ui/core": "0.0.58",
305
305
  "fast-check": "4.3.0",
306
- "@jini-ai/db": "0.6.11",
307
- "@jini-ai/agent-runtime": "0.6.11",
308
- "@jini-ai/protocol": "^0.6.11",
309
- "@jini-ai/core": "^0.6.11",
310
- "@jini-ai/diagnostics": "^0.6.11",
311
- "@jini-ai/ui": "^0.6.11",
312
- "@jini-ai/agentic": "^0.6.11"
306
+ "@jini-ai/db": "0.6.12",
307
+ "@jini-ai/agentic": "^0.6.12",
308
+ "@jini-ai/agent-runtime": "0.6.12",
309
+ "@jini-ai/diagnostics": "^0.6.12",
310
+ "@jini-ai/ui": "^0.6.12",
311
+ "@jini-ai/core": "^0.6.12",
312
+ "@jini-ai/protocol": "^0.6.12"
313
313
  },
314
314
  "typesVersions": {
315
315
  "*": {