@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 +7 -0
- package/dist/core/run-activity.js +4 -0
- package/dist/react/components/MessageRow.js +1 -1
- package/dist/react/components/MidRunUserMessage.d.ts +1 -1
- package/dist/react/components/MidRunUserMessage.js +8 -5
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +19 -10
- package/dist/react/features/chat-pane/rules.d.ts +5 -2
- package/dist/react/features/chat-pane/rules.js +8 -2
- package/package.json +9 -9
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
|
-
|
|
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(
|
|
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
|
|
185
|
-
const
|
|
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
|
-
|
|
594
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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.
|
|
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.
|
|
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.
|
|
307
|
-
"@jini-ai/
|
|
308
|
-
"@jini-ai/
|
|
309
|
-
"@jini-ai/
|
|
310
|
-
"@jini-ai/
|
|
311
|
-
"@jini-ai/
|
|
312
|
-
"@jini-ai/
|
|
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
|
"*": {
|