@jini-ai/chat 0.5.3 → 0.5.5
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 +11 -0
- package/dist/core/ag-ui/entry.d.ts +33 -0
- package/dist/core/ag-ui/entry.js +80 -0
- package/dist/core/agentic/chat-capabilities.d.ts +2 -4
- package/dist/core/agentic/index.d.ts +4 -17
- package/dist/core/agentic/index.js +4 -17
- package/dist/core/assistant-client/a2ui-action-poster.d.ts +46 -0
- package/dist/core/assistant-client/a2ui-action-poster.js +113 -0
- package/dist/core/assistant-client/ag-ui-transport.d.ts +68 -0
- package/dist/core/assistant-client/ag-ui-transport.js +147 -0
- package/dist/core/assistant-client/attachment-liveness.d.ts +37 -0
- package/dist/core/assistant-client/attachment-liveness.js +93 -0
- package/dist/core/assistant-client/durable-run-subscription.d.ts +39 -0
- package/dist/core/assistant-client/durable-run-subscription.js +96 -0
- package/dist/core/assistant-client/entry.d.ts +13 -0
- package/dist/core/assistant-client/entry.js +7 -0
- package/dist/core/assistant-client/messages.d.ts +73 -0
- package/dist/core/assistant-client/messages.js +68 -0
- package/dist/core/assistant-client/transport.d.ts +164 -0
- package/dist/core/assistant-client/transport.js +734 -0
- package/dist/core/browser/page-actions.d.ts +121 -0
- package/dist/core/browser/page-actions.js +113 -0
- package/dist/core/composer-history.d.ts +54 -0
- package/dist/core/composer-history.js +57 -0
- package/dist/core/durable-projection.d.ts +28 -0
- package/dist/core/durable-projection.js +71 -0
- package/dist/core/durable-runs/continuation.d.ts +18 -0
- package/dist/core/durable-runs/continuation.js +61 -0
- package/dist/core/durable-runs/ledger-contracts.d.ts +53 -0
- package/dist/core/durable-runs/ledger-contracts.js +2 -0
- package/dist/core/durable-runs/policies.d.ts +6 -0
- package/dist/core/durable-runs/policies.js +6 -0
- package/dist/core/durable-runs/ports.d.ts +123 -0
- package/dist/core/durable-runs/ports.js +2 -0
- package/dist/core/durable-runs/run-contracts.d.ts +17 -0
- package/dist/core/durable-runs/run-contracts.js +2 -0
- package/dist/core/durable-runs/run-lock.d.ts +4 -0
- package/dist/core/durable-runs/run-lock.js +4 -0
- package/dist/core/index.d.ts +9 -19
- package/dist/core/index.js +8 -19
- package/dist/core/messages.d.ts +2 -0
- package/dist/core/question-form/format.d.ts +0 -9
- package/dist/core/question-form/format.js +16 -2
- package/dist/core/question-form/normalize.js +1 -0
- package/dist/core/question-form/secret-answers.d.ts +38 -0
- package/dist/core/question-form/secret-answers.js +45 -0
- package/dist/core/question-form/types.d.ts +2 -0
- package/dist/core/question-form.d.ts +1 -0
- package/dist/core/question-form.js +1 -0
- package/dist/core/run-events/entry.d.ts +208 -16
- package/dist/core/run-events/entry.js +273 -39
- package/dist/core/transcript.d.ts +3 -2
- package/dist/core/transcript.js +19 -3
- package/dist/core/transport.d.ts +4 -0
- package/dist/core/user-text-redaction.d.ts +56 -0
- package/dist/core/user-text-redaction.js +89 -0
- package/dist/embed-widget/browser/page-actions.d.ts +1 -120
- package/dist/embed-widget/browser/page-actions.js +2 -112
- package/dist/embed-widget/react/embed.d.ts +1 -1
- package/dist/embed-widget/react/embed.js +1 -1
- package/dist/react/artifact-types.d.ts +9 -15
- package/dist/react/components/Composer.js +6 -0
- package/dist/react/components/MessageRow.js +24 -11
- package/dist/react/components/QuestionForm.d.ts +2 -1
- package/dist/react/components/QuestionForm.js +18 -12
- package/dist/react/components/QuestionsPanel.d.ts +3 -2
- package/dist/react/components/QuestionsPanel.js +2 -2
- package/dist/react/features/chat-pane/agent-tools.d.ts +1 -1
- package/dist/react/features/chat-pane/agent-tools.js +0 -1
- package/dist/react/features/chat-pane/components/AgentRuntimePicker.js +5 -2
- package/dist/react/features/chat-pane/components/ChatPane.d.ts +1 -1
- package/dist/react/features/chat-pane/components/ChatPane.js +9 -4
- package/dist/react/features/chat-pane/create-typed-answer-poster.d.ts +2 -1
- package/dist/react/features/chat-pane/create-typed-answer-poster.js +33 -1
- package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.js +3 -2
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +7 -1
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +38 -23
- package/dist/react/features/chat-pane/model-options.d.ts +10 -0
- package/dist/react/features/chat-pane/model-options.js +51 -0
- package/dist/react/features/chat-pane/rules.d.ts +3 -2
- package/dist/react/features/chat-pane/rules.js +12 -4
- package/dist/react/features/chat-pane/types.d.ts +30 -1
- package/dist/react/hooks/assistant-chats-contracts.d.ts +164 -0
- package/dist/react/hooks/assistant-chats-contracts.js +2 -0
- package/dist/react/hooks/assistant-chats.d.ts +7 -0
- package/dist/react/hooks/assistant-chats.js +960 -0
- package/dist/react/hooks/attachment-preview-cache.d.ts +1 -1
- package/dist/react/hooks/attachment-preview-cache.js +1 -1
- package/dist/react/hooks/composer-draft-cache.d.ts +2 -2
- package/dist/react/hooks/composer-draft-cache.js +2 -2
- package/dist/react/hooks/composer-history-storage.d.ts +8 -0
- package/dist/react/hooks/composer-history-storage.js +40 -0
- package/dist/react/hooks/fab-position.d.ts +90 -0
- package/dist/react/hooks/fab-position.js +271 -0
- package/dist/react/hooks/useComposer.d.ts +10 -1
- package/dist/react/hooks/useComposer.js +14 -2
- package/dist/react/hooks/useComposerHistory.hooks.d.ts +15 -0
- package/dist/react/hooks/useComposerHistory.hooks.js +90 -0
- package/dist/react/hooks/useComposerHistoryKeys.hooks.d.ts +9 -0
- package/dist/react/hooks/useComposerHistoryKeys.hooks.js +90 -0
- package/dist/react/hooks/useConversation.d.ts +4 -1
- package/dist/react/hooks/useConversation.js +20 -14
- package/dist/react/hooks/useRunStream.js +12 -2
- package/dist/react/hooks/useUserTextGuard.d.ts +11 -0
- package/dist/react/hooks/useUserTextGuard.js +19 -0
- package/dist/react/index.d.ts +14 -21
- package/dist/react/index.js +8 -0
- package/dist/server/durable-runs/accept.d.ts +25 -0
- package/dist/server/durable-runs/accept.js +21 -0
- package/dist/server/durable-runs/entry.d.ts +5 -0
- package/dist/server/durable-runs/entry.js +4 -0
- package/dist/server/durable-runs/recover.d.ts +5 -0
- package/dist/server/durable-runs/recover.js +147 -0
- package/dist/server/run-finalizer/ports.d.ts +2 -16
- package/dist/store/legacy/sqlite/messages.d.ts +24 -18
- package/dist/store/legacy/sqlite/messages.js +12 -2
- package/dist/store/pglite/index.d.ts +2 -1
- package/dist/store/postgres/index.d.ts +2 -1
- package/dist/store/runtime/chat-expiry-sweep.d.ts +40 -0
- package/dist/store/runtime/chat-expiry-sweep.js +70 -0
- package/dist/store/runtime/chat-history-store.d.ts +20 -0
- package/dist/store/runtime/chat-history-store.js +50 -0
- package/dist/store/runtime/durable-run-store.d.ts +21 -0
- package/dist/store/runtime/durable-run-store.js +342 -0
- package/dist/store/runtime/entry.d.ts +7 -0
- package/dist/store/runtime/entry.js +6 -0
- package/dist/store/runtime/kernel.d.ts +29 -0
- package/dist/store/runtime/kernel.js +2 -0
- package/dist/store/runtime/run-guarded-chat-store.d.ts +10 -0
- package/dist/store/runtime/run-guarded-chat-store.js +42 -0
- package/dist/store/runtime/run-ledger.d.ts +15 -0
- package/dist/store/runtime/run-ledger.js +206 -0
- package/dist/store/sql/boundary.d.ts +6 -0
- package/dist/store/sql/boundary.js +7 -1
- package/dist/store/sql/store.d.ts +3 -2
- package/dist/store/sql/store.js +9 -2
- package/dist/store/sqlite/index.d.ts +3 -2
- package/package.json +75 -10
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { composerHistoryKeyAction, createComposerHistoryState, mergeComposerHistory, normalizeComposerHistory, transitionComposerHistory, } from '../../core/composer-history.js';
|
|
3
|
+
import { browserComposerHistoryStorage } from './composer-history-storage.js';
|
|
4
|
+
const EMPTY_MESSAGES = [];
|
|
5
|
+
export function useComposerHistory({ setRecalledDraft }, { messages = EMPTY_MESSAGES, scope = 'default', storage = browserComposerHistoryStorage, conversationId } = {}) {
|
|
6
|
+
const browseRef = useRef(createComposerHistoryState({}));
|
|
7
|
+
const [recent, setRecent] = useState([]);
|
|
8
|
+
const hydratedPortRef = useRef(null);
|
|
9
|
+
const [hydratedScope, setHydratedScope] = useState(null);
|
|
10
|
+
const pendingRef = useRef([]);
|
|
11
|
+
const conversationRef = useRef(conversationId);
|
|
12
|
+
const scopeRef = useRef(scope);
|
|
13
|
+
const seenRef = useRef(new Set(messages.map((message) => message.id)));
|
|
14
|
+
const writeQueueRef = useRef(Promise.resolve());
|
|
15
|
+
const current = useMemo(() => messages.filter((message) => message.role === 'user').map((message) => message.content), [messages]);
|
|
16
|
+
const entries = mergeComposerHistory({ recent, current });
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
let cancelled = false;
|
|
19
|
+
pendingRef.current = [];
|
|
20
|
+
hydratedPortRef.current = null;
|
|
21
|
+
setRecent([]);
|
|
22
|
+
setHydratedScope(null);
|
|
23
|
+
browseRef.current = createComposerHistoryState({});
|
|
24
|
+
// Read after this hook's outstanding writes so an injected async store cannot return stale
|
|
25
|
+
// entries when a scope is revisited. A remounted hook's default storage writes synchronously.
|
|
26
|
+
void writeQueueRef.current.then(() => storage.read({ scope }, {})).catch(() => []).then((loaded) => {
|
|
27
|
+
if (cancelled)
|
|
28
|
+
return;
|
|
29
|
+
// Bootstrap an empty recent store from the already-sent current transcript. This
|
|
30
|
+
// makes opening a new chat useful immediately after installing recall; opening an
|
|
31
|
+
// older transcript later does not reorder an established recent list.
|
|
32
|
+
const seed = loaded.length > 0 ? loaded : current;
|
|
33
|
+
setRecent(normalizeComposerHistory({ entries: [...seed, ...pendingRef.current] }));
|
|
34
|
+
hydratedPortRef.current = storage;
|
|
35
|
+
setHydratedScope(scope);
|
|
36
|
+
});
|
|
37
|
+
return () => { cancelled = true; };
|
|
38
|
+
}, [scope, storage]);
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
const adopted = conversationRef.current == null && conversationId != null && scopeRef.current === scope;
|
|
41
|
+
conversationRef.current = conversationId;
|
|
42
|
+
scopeRef.current = scope;
|
|
43
|
+
if (adopted)
|
|
44
|
+
return;
|
|
45
|
+
browseRef.current = createComposerHistoryState({});
|
|
46
|
+
// A conversation's existing turns are available for navigation, but merely opening old
|
|
47
|
+
// conversations must not reorder the cross-conversation recent list.
|
|
48
|
+
seenRef.current = new Set(messages.map((message) => message.id));
|
|
49
|
+
// messages is deliberately absent: newly appended turns are handled below, not reseeded.
|
|
50
|
+
}, [conversationId, scope]);
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
const added = [];
|
|
53
|
+
for (const message of messages) {
|
|
54
|
+
if (!seenRef.current.has(message.id) && message.role === 'user')
|
|
55
|
+
added.push(message.content);
|
|
56
|
+
seenRef.current.add(message.id);
|
|
57
|
+
}
|
|
58
|
+
if (added.length === 0)
|
|
59
|
+
return;
|
|
60
|
+
pendingRef.current.push(...added);
|
|
61
|
+
pendingRef.current = normalizeComposerHistory({ entries: pendingRef.current });
|
|
62
|
+
setRecent((previous) => normalizeComposerHistory({ entries: [...previous, ...added] }));
|
|
63
|
+
}, [messages]);
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (hydratedScope !== scope || hydratedPortRef.current !== storage)
|
|
66
|
+
return;
|
|
67
|
+
// Serialize writes; a slow earlier write must not overwrite a later prompt.
|
|
68
|
+
writeQueueRef.current = writeQueueRef.current.then(() => storage.write({ scope, entries: recent }, {})).catch(() => { });
|
|
69
|
+
}, [hydratedScope, recent, scope, storage]);
|
|
70
|
+
const navigate = useCallback((input, _optional = {}) => {
|
|
71
|
+
const action = composerHistoryKeyAction(input);
|
|
72
|
+
if (action === null)
|
|
73
|
+
return false;
|
|
74
|
+
const transition = transitionComposerHistory({ state: browseRef.current, action, text: input.text, entries });
|
|
75
|
+
if (!transition.handled)
|
|
76
|
+
return false;
|
|
77
|
+
browseRef.current = transition.state;
|
|
78
|
+
// Recall is a view, not an edit to the cached working draft. Reload while copying a
|
|
79
|
+
// recalled entry still restores what the operator was writing before they browsed.
|
|
80
|
+
setRecalledDraft(transition.text);
|
|
81
|
+
return true;
|
|
82
|
+
}, [entries, setRecalledDraft]);
|
|
83
|
+
const edit = useCallback((_required, _optional = {}) => {
|
|
84
|
+
// The first edit leaves history. Its text is now the working draft; stored entries and
|
|
85
|
+
// the original browsing snapshot are never edited in place.
|
|
86
|
+
browseRef.current = createComposerHistoryState({});
|
|
87
|
+
}, []);
|
|
88
|
+
return useMemo(() => ({ navigate, edit }), [navigate, edit]);
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=useComposerHistory.hooks.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type KeyboardEvent, type RefObject } from 'react';
|
|
2
|
+
import type { UseComposerResult } from './useComposer.js';
|
|
3
|
+
export declare function useComposerHistoryKeys({ composer, textareaRef }: {
|
|
4
|
+
composer: UseComposerResult;
|
|
5
|
+
textareaRef: RefObject<HTMLTextAreaElement | null>;
|
|
6
|
+
}, { popupOpen }?: {
|
|
7
|
+
popupOpen?: boolean;
|
|
8
|
+
}): (event: KeyboardEvent<HTMLTextAreaElement>) => boolean;
|
|
9
|
+
//# sourceMappingURL=useComposerHistoryKeys.hooks.d.ts.map
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { useLayoutEffect, useReducer, useRef } from 'react';
|
|
2
|
+
/** Measures visual lines as well as newlines, so a wrapped paragraph keeps native arrow motion. */
|
|
3
|
+
function visualLineBounds({ textarea }) {
|
|
4
|
+
if (textarea.clientWidth === 0)
|
|
5
|
+
return {};
|
|
6
|
+
const computed = getComputedStyle(textarea);
|
|
7
|
+
const mirror = document.createElement('div');
|
|
8
|
+
for (const property of ['font', 'letter-spacing', 'word-spacing', 'line-height', 'text-indent', 'padding', 'tab-size', 'word-break', 'overflow-wrap', 'direction']) {
|
|
9
|
+
mirror.style.setProperty(property, computed.getPropertyValue(property));
|
|
10
|
+
}
|
|
11
|
+
mirror.style.position = 'fixed';
|
|
12
|
+
mirror.style.visibility = 'hidden';
|
|
13
|
+
mirror.style.pointerEvents = 'none';
|
|
14
|
+
mirror.style.whiteSpace = 'pre-wrap';
|
|
15
|
+
mirror.style.overflowWrap = 'break-word';
|
|
16
|
+
mirror.style.boxSizing = 'border-box';
|
|
17
|
+
mirror.style.width = `${textarea.clientWidth}px`;
|
|
18
|
+
const text = document.createTextNode(textarea.value);
|
|
19
|
+
mirror.append(text, document.createTextNode('\u200b'));
|
|
20
|
+
document.body.append(mirror);
|
|
21
|
+
try {
|
|
22
|
+
function lineAt(offset) {
|
|
23
|
+
const range = document.createRange();
|
|
24
|
+
range.setStart(text, offset);
|
|
25
|
+
range.setEnd(text, offset);
|
|
26
|
+
return range.getBoundingClientRect().top;
|
|
27
|
+
}
|
|
28
|
+
const first = lineAt(0);
|
|
29
|
+
const last = lineAt(text.length);
|
|
30
|
+
// Both selection endpoints must be on the boundary visual line. Selecting across soft
|
|
31
|
+
// wraps should stay native just like selecting across explicit newline characters.
|
|
32
|
+
const start = lineAt(textarea.selectionStart);
|
|
33
|
+
const end = lineAt(textarea.selectionEnd);
|
|
34
|
+
return { firstLine: Math.abs(start - first) < 1 && Math.abs(end - first) < 1, lastLine: Math.abs(start - last) < 1 && Math.abs(end - last) < 1 };
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
mirror.remove();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export function useComposerHistoryKeys({ composer, textareaRef }, { popupOpen = false } = {}) {
|
|
41
|
+
const pendingCaretRef = useRef(null);
|
|
42
|
+
// A duplicate history entry can equal the current value, so setDraft may bail out.
|
|
43
|
+
// Still commit the caret request now rather than carrying it into the next real edit.
|
|
44
|
+
const [, requestCaretCommit] = useReducer((revision) => revision + 1, 0);
|
|
45
|
+
useLayoutEffect(() => {
|
|
46
|
+
if (!pendingCaretRef.current)
|
|
47
|
+
return;
|
|
48
|
+
const placement = pendingCaretRef.current;
|
|
49
|
+
pendingCaretRef.current = null;
|
|
50
|
+
const textarea = textareaRef.current;
|
|
51
|
+
if (textarea) {
|
|
52
|
+
const offset = placement === 'start' ? 0 : textarea.value.length;
|
|
53
|
+
textarea.setSelectionRange(offset, offset);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
return (event) => {
|
|
57
|
+
if (event.shiftKey || event.ctrlKey || event.altKey || event.metaKey)
|
|
58
|
+
return false;
|
|
59
|
+
if (event.defaultPrevented || popupOpen || event.nativeEvent.isComposing || event.nativeEvent.keyCode === 229)
|
|
60
|
+
return false;
|
|
61
|
+
if (!['ArrowUp', 'ArrowDown', 'Escape'].includes(event.key))
|
|
62
|
+
return false;
|
|
63
|
+
const textarea = event.currentTarget;
|
|
64
|
+
const handled = composer.history?.navigate({
|
|
65
|
+
key: event.key,
|
|
66
|
+
text: textarea.value,
|
|
67
|
+
selectionStart: textarea.selectionStart,
|
|
68
|
+
selectionEnd: textarea.selectionEnd,
|
|
69
|
+
shiftKey: event.shiftKey,
|
|
70
|
+
ctrlKey: event.ctrlKey,
|
|
71
|
+
altKey: event.altKey,
|
|
72
|
+
metaKey: event.metaKey,
|
|
73
|
+
...visualLineBounds({ textarea }),
|
|
74
|
+
}) ?? false;
|
|
75
|
+
if (handled) {
|
|
76
|
+
event.preventDefault();
|
|
77
|
+
// React's controlled value lands on the following commit. Placing the caret before
|
|
78
|
+
// that commit clamps it to the OLD draft's length and breaks multiline Down gating.
|
|
79
|
+
// Up stays on the first line of even a multiline recall so repeated Up can browse
|
|
80
|
+
// older entries. Down/Escape go to the end, including when restoring the draft.
|
|
81
|
+
pendingCaretRef.current = event.key === 'ArrowUp' ? 'start' : 'end';
|
|
82
|
+
requestCaretCommit();
|
|
83
|
+
// Recalling an identical string may not cause a render; the existing value is safe.
|
|
84
|
+
const offset = pendingCaretRef.current === 'start' ? 0 : textarea.value.length;
|
|
85
|
+
textarea.setSelectionRange(offset, offset);
|
|
86
|
+
}
|
|
87
|
+
return handled;
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=useComposerHistoryKeys.hooks.js.map
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { type UserTextRedactionOptions } from '../../core/user-text-redaction.js';
|
|
1
2
|
import type { ChatAttachment, ChatMessage } from '../../core/index.js';
|
|
2
3
|
import type { ChatTransport, RunContext } from '../../core/index.js';
|
|
3
|
-
export interface UseConversationOptions {
|
|
4
|
+
export interface UseConversationOptions extends UserTextRedactionOptions {
|
|
4
5
|
transport: ChatTransport;
|
|
5
6
|
initialMessages?: ChatMessage[];
|
|
6
7
|
conversationId?: string | null;
|
|
@@ -9,6 +10,8 @@ export interface UseConversationOptions {
|
|
|
9
10
|
createMessageId?: () => string;
|
|
10
11
|
}
|
|
11
12
|
export interface SendMessageOptions {
|
|
13
|
+
/** Non-secret signal from a composer that already sanitized the content before this boundary. */
|
|
14
|
+
secretRedaction?: ChatMessage['secretRedaction'];
|
|
12
15
|
attachments?: ChatAttachment[];
|
|
13
16
|
context?: RunContext;
|
|
14
17
|
agentId?: string;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { redactUserHistory, redactUserMessage } from '../../core/user-text-redaction.js';
|
|
2
|
+
import { preserveAssistantContent, mergeRunEvents } from '../../core/durable-projection.js';
|
|
1
3
|
/**
|
|
2
4
|
* @module useConversation
|
|
3
5
|
*
|
|
@@ -22,7 +24,7 @@ function defaultCreateMessageId() {
|
|
|
22
24
|
}
|
|
23
25
|
export function useConversation(options) {
|
|
24
26
|
const { transport, conversationId = null, agentId, createMessageId = defaultCreateMessageId } = options;
|
|
25
|
-
const [messages, setMessagesState] = useState(options.initialMessages ?? []);
|
|
27
|
+
const [messages, setMessagesState] = useState(() => redactUserHistory({ history: options.initialMessages ?? [] }, { ...(options.redactUserText ? { redactUserText: options.redactUserText } : {}) }));
|
|
26
28
|
const [scrollIntent, setScrollIntent] = useState(false);
|
|
27
29
|
const run = useRunStream({ transport });
|
|
28
30
|
// The assistant message id the currently-active run is writing into.
|
|
@@ -113,8 +115,9 @@ export function useConversation(options) {
|
|
|
113
115
|
return {
|
|
114
116
|
...m,
|
|
115
117
|
...(nextRunId !== undefined ? { runId: nextRunId } : {}),
|
|
116
|
-
events: run.events,
|
|
117
|
-
|
|
118
|
+
events: mergeRunEvents({ saved: m.events, incoming: run.events }, {}),
|
|
119
|
+
// Replayed completion prefixes cannot erase a saved interrupted segment.
|
|
120
|
+
content: preserveAssistantContent({ saved: m.content, incoming: assistantContentFromEvents({ events: run.events }) }, {}),
|
|
118
121
|
runStatus,
|
|
119
122
|
...(isTerminalRunStatus({ status: runStatus }) ? { endedAt: Date.now() } : {}),
|
|
120
123
|
};
|
|
@@ -132,13 +135,14 @@ export function useConversation(options) {
|
|
|
132
135
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
133
136
|
}, [run.events, run.status, run.runId]);
|
|
134
137
|
const sendMessage = useCallback(async (content, sendOptions = {}) => {
|
|
135
|
-
const userMessage = {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
138
|
+
const userMessage = redactUserMessage({ message: {
|
|
139
|
+
id: createMessageId(),
|
|
140
|
+
role: 'user',
|
|
141
|
+
content,
|
|
142
|
+
createdAt: Date.now(),
|
|
143
|
+
...(sendOptions.secretRedaction ? { secretRedaction: sendOptions.secretRedaction } : {}),
|
|
144
|
+
...(sendOptions.attachments ? { attachments: sendOptions.attachments } : {}),
|
|
145
|
+
} }, options).message;
|
|
142
146
|
const resolvedAgentId = sendOptions.agentId ?? agentId;
|
|
143
147
|
const assistantMessage = {
|
|
144
148
|
id: createMessageId(),
|
|
@@ -150,7 +154,7 @@ export function useConversation(options) {
|
|
|
150
154
|
startedAt: Date.now(),
|
|
151
155
|
};
|
|
152
156
|
activeAssistantIdRef.current = assistantMessage.id;
|
|
153
|
-
const history = [...messagesRef.current, userMessage];
|
|
157
|
+
const history = redactUserHistory({ history: [...messagesRef.current, userMessage] }, options);
|
|
154
158
|
// Functional, never `[...history, assistantMessage]`: `messagesRef` is the last RENDER's
|
|
155
159
|
// snapshot, and a host that sends from an effect (`useChatPane`'s queued-prompt flush) runs in
|
|
156
160
|
// the same commit as the reconciliation effect that just marked the previous turn terminal.
|
|
@@ -160,13 +164,14 @@ export function useConversation(options) {
|
|
|
160
164
|
setMessagesState((prev) => [...prev, userMessage, assistantMessage]);
|
|
161
165
|
setScrollIntent(true);
|
|
162
166
|
await run.start({
|
|
167
|
+
assistantMessageId: assistantMessage.id,
|
|
163
168
|
history,
|
|
164
169
|
...(resolvedAgentId !== undefined ? { agentId: resolvedAgentId } : {}),
|
|
165
170
|
conversationId,
|
|
166
171
|
...(sendOptions.attachments !== undefined ? { attachments: sendOptions.attachments } : {}),
|
|
167
172
|
...(sendOptions.context !== undefined ? { context: sendOptions.context } : {}),
|
|
168
173
|
});
|
|
169
|
-
}, [agentId, conversationId, createMessageId, run]);
|
|
174
|
+
}, [agentId, conversationId, createMessageId, run, options.redactUserText, options.onSecretRedacted]);
|
|
170
175
|
const retry = useCallback(async (assistantMessageId) => {
|
|
171
176
|
const idx = messagesRef.current.findIndex((m) => m.id === assistantMessageId);
|
|
172
177
|
if (idx <= 0)
|
|
@@ -174,7 +179,7 @@ export function useConversation(options) {
|
|
|
174
179
|
const priorUser = messagesRef.current[idx - 1];
|
|
175
180
|
if (!priorUser || priorUser.role !== 'user')
|
|
176
181
|
return;
|
|
177
|
-
const history = messagesRef.current.slice(0, idx);
|
|
182
|
+
const history = redactUserHistory({ history: messagesRef.current.slice(0, idx) }, options);
|
|
178
183
|
const resetAssistant = { ...messagesRef.current[idx], content: '', events: [], runStatus: 'queued' };
|
|
179
184
|
activeAssistantIdRef.current = resetAssistant.id;
|
|
180
185
|
// Functional for the same reason as `sendMessage`'s: keep a just-queued terminal update.
|
|
@@ -191,12 +196,13 @@ export function useConversation(options) {
|
|
|
191
196
|
// attachment beside the user turn, the failure reads as the model ignoring it rather than as
|
|
192
197
|
// the client never having sent it.
|
|
193
198
|
await run.start({
|
|
199
|
+
assistantMessageId: resetAssistant.id,
|
|
194
200
|
history,
|
|
195
201
|
...(resolvedAgentId !== undefined ? { agentId: resolvedAgentId } : {}),
|
|
196
202
|
conversationId,
|
|
197
203
|
...(priorUser.attachments !== undefined ? { attachments: priorUser.attachments } : {}),
|
|
198
204
|
});
|
|
199
|
-
}, [agentId, conversationId, run]);
|
|
205
|
+
}, [agentId, conversationId, run, options.redactUserText, options.onSecretRedacted]);
|
|
200
206
|
const cancel = useCallback(() => run.cancel(), [run]);
|
|
201
207
|
const acknowledgeScroll = useCallback(() => setScrollIntent(false), []);
|
|
202
208
|
const suppressScroll = useCallback(() => setScrollIntent(false), []);
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* comparison can't do this — `start()`'s handlers must exist before the
|
|
18
18
|
* transport's promise resolves with the real id).
|
|
19
19
|
*/
|
|
20
|
+
import { mergeRunEvents } from '../../core/durable-projection.js';
|
|
20
21
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
21
22
|
function reportRunStreamHostEffectFailure(effectName, error) {
|
|
22
23
|
console.error(`[@jini-ai/chat] useRunStream ${effectName} host effect failed:`, error);
|
|
@@ -77,6 +78,15 @@ export function useRunStream({ transport }) {
|
|
|
77
78
|
const makeHandlers = useCallback((generation) => {
|
|
78
79
|
const isStale = () => generationRef.current !== generation;
|
|
79
80
|
return {
|
|
81
|
+
onCheckpoint: (message) => {
|
|
82
|
+
if (isStale())
|
|
83
|
+
return;
|
|
84
|
+
const statuses = { queued: 'streaming', running: 'streaming', succeeded: 'done', failed: 'error', canceled: 'canceled' };
|
|
85
|
+
setState((prev) => ({ ...prev, runId: message.runId ?? prev.runId,
|
|
86
|
+
status: message.runStatus ? statuses[message.runStatus] : prev.status,
|
|
87
|
+
events: mergeRunEvents({ saved: prev.events, incoming: message.events ?? [] }, {}),
|
|
88
|
+
}));
|
|
89
|
+
},
|
|
80
90
|
onEvent: (ev) => {
|
|
81
91
|
if (isStale())
|
|
82
92
|
return;
|
|
@@ -93,12 +103,12 @@ export function useRunStream({ transport }) {
|
|
|
93
103
|
onError: (err) => {
|
|
94
104
|
if (isStale())
|
|
95
105
|
return;
|
|
96
|
-
setState((prev) => ({ ...prev, status: 'error', error: err }));
|
|
106
|
+
setState((prev) => ({ ...prev, status: 'error', error: err, events: [...prev.events, { kind: 'status', code: 'run_terminal', label: err.message }] }));
|
|
97
107
|
},
|
|
98
108
|
onDone: (finalEvents) => {
|
|
99
109
|
if (isStale())
|
|
100
110
|
return;
|
|
101
|
-
setState((prev) => ({ ...prev, status: prev.status === 'error' ? prev.status : 'done', events: finalEvents }));
|
|
111
|
+
setState((prev) => ({ ...prev, status: prev.status === 'error' || prev.status === 'canceled' ? prev.status : 'done', events: mergeRunEvents({ saved: prev.events, incoming: finalEvents }, {}) }));
|
|
102
112
|
},
|
|
103
113
|
};
|
|
104
114
|
}, []);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type SecretRedactionSignal, type UserTextRedactionOptions } from '../../core/user-text-redaction.js';
|
|
2
|
+
/** Shared notice state for composer and question answers. Effects see only a count, never text. */
|
|
3
|
+
export declare function useUserTextGuard(options: UserTextRedactionOptions, _optional?: Record<string, never>): {
|
|
4
|
+
secretRedacted: boolean;
|
|
5
|
+
redact: ({ text }: {
|
|
6
|
+
text: string;
|
|
7
|
+
}, _options?: {}) => import("../../core/user-text-redaction.js").UserTextRedaction;
|
|
8
|
+
redactText: (text: string) => string;
|
|
9
|
+
notifyRedaction: (signal: SecretRedactionSignal) => void;
|
|
10
|
+
};
|
|
11
|
+
//# sourceMappingURL=useUserTextGuard.d.ts.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useCallback, useState } from 'react';
|
|
2
|
+
import { redactUserText } from '../../core/user-text-redaction.js';
|
|
3
|
+
/** Shared notice state for composer and question answers. Effects see only a count, never text. */
|
|
4
|
+
export function useUserTextGuard(options, _optional = {}) {
|
|
5
|
+
const [secretRedacted, setSecretRedacted] = useState(false);
|
|
6
|
+
const notifyRedaction = useCallback((signal) => {
|
|
7
|
+
setSecretRedacted(true);
|
|
8
|
+
options.onSecretRedacted?.(signal);
|
|
9
|
+
}, [options.onSecretRedacted]);
|
|
10
|
+
const redact = useCallback(({ text }, _options = {}) => {
|
|
11
|
+
const result = (options.redactUserText ?? redactUserText)({ text }, {});
|
|
12
|
+
if (result.secretRedacted)
|
|
13
|
+
notifyRedaction({ secretRedacted: true, count: result.count });
|
|
14
|
+
return result;
|
|
15
|
+
}, [options.redactUserText, notifyRedaction]);
|
|
16
|
+
const redactText = useCallback((text) => redact({ text }, {}).text, [redact]);
|
|
17
|
+
return { secretRedacted, redact, redactText, notifyRedaction };
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=useUserTextGuard.js.map
|
package/dist/react/index.d.ts
CHANGED
|
@@ -1,25 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @jini-ai/chat
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* spec this package implements, and archived provenance ledger for provenance.
|
|
6
|
-
*
|
|
7
|
-
* This barrel is filled in incrementally as each layer lands (hooks first,
|
|
8
|
-
* then presentational components, then the `<JiniChatProvider>` composition
|
|
9
|
-
* root) — see archived provenance ledger's "Status" section for what's shipped so far.
|
|
10
|
-
*
|
|
11
|
-
* ## Every export here is explicit. Do not reintroduce `export *`.
|
|
12
|
-
*
|
|
13
|
-
* This file used 15 `export * from` statements until 2026-08-05. That made the package's public
|
|
14
|
-
* API *implicit*: whatever any of those modules happened to export was public, so renaming an
|
|
15
|
-
* internal helper shipped a breaking change nobody reviewed, and no one could read this file and
|
|
16
|
-
* say what the package exports. It is also the likeliest reason consumers reach past this barrel
|
|
17
|
-
* into `@jini-ai/chat/core` deep paths — a grab-bag invites bypassing.
|
|
18
|
-
*
|
|
19
|
-
* The expansion was mechanical and provably surface-preserving: 193 symbols before and after,
|
|
20
|
-
* identical in name AND in value-vs-type kind. `features/chat-pane/index.ts` is the style model.
|
|
21
|
-
*
|
|
22
|
-
* When adding an export, add the name here deliberately. That edit IS the API review.
|
|
2
|
+
* @jini-ai/chat/react — headless hooks, presentational components and slots over framework-free
|
|
3
|
+
* chat/core vocabulary. Every export is explicit: adding or renaming a name here is a public API
|
|
4
|
+
* review, so internal module exports cannot silently become public. Do not use export *.
|
|
23
5
|
*/
|
|
24
6
|
export type { ChatTransport, FeedbackChange, OnFeedback, ReattachRunOptions, RunContext, RunHandlers, StartRunInput, } from '../core/index.js';
|
|
25
7
|
export { RendererRegistry, } from './artifact-types.js';
|
|
@@ -163,4 +145,15 @@ export type { JiniChatProviderProps, JiniChatSlots } from './components/JiniChat
|
|
|
163
145
|
export { createFrontendSessionBridge } from './agent-bridge/frontend-session-bridge.js';
|
|
164
146
|
export type { FrontendSessionBridge, FrontendSessionBridgeArgs, FrontendSessionBridgeOptions, } from './agent-bridge/frontend-session-bridge.js';
|
|
165
147
|
export { createDomPageDriver, currentAgentPage, type DomPageDriverOptions } from '@jini-ai/agentic/dom';
|
|
148
|
+
export { createBrowserComposerHistoryStorage } from './hooks/composer-history-storage.js';
|
|
149
|
+
export type { ComposerHistoryStoragePort } from '../core/composer-history.js';
|
|
150
|
+
export { SECRET_REDACTED_NOTICE, redactUserText } from '../core/user-text-redaction.js';
|
|
151
|
+
export type { UserTextRedactionOptions } from '../core/user-text-redaction.js';
|
|
152
|
+
export { useUserTextGuard } from './hooks/useUserTextGuard.js';
|
|
153
|
+
export { cacheAttachmentPreviewSource } from './hooks/attachment-preview-cache.js';
|
|
154
|
+
export { readCachedAttachmentBatchId, writeCachedAttachmentBatchId } from './hooks/composer-draft-cache.js';
|
|
155
|
+
export { createAssistantChatsClient } from "./hooks/assistant-chats.js";
|
|
156
|
+
export type { AssistantChatsPort, AssistantChatsClientPorts, AssistantChatsClient, UseAssistantChats } from "./hooks/assistant-chats-contracts.js";
|
|
157
|
+
export { useFabPosition } from "./hooks/fab-position.js";
|
|
158
|
+
export type { FabPositionInput, FabPositionResult } from "./hooks/fab-position.js";
|
|
166
159
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/react/index.js
CHANGED
|
@@ -121,4 +121,12 @@ export { JiniChatProvider, useJiniChatSlots, useOnFeedback } from './components/
|
|
|
121
121
|
*/
|
|
122
122
|
export { createFrontendSessionBridge } from './agent-bridge/frontend-session-bridge.js';
|
|
123
123
|
export { createDomPageDriver, currentAgentPage } from '@jini-ai/agentic/dom';
|
|
124
|
+
export { createBrowserComposerHistoryStorage } from './hooks/composer-history-storage.js';
|
|
125
|
+
// ChatPane consumers share the same redaction and draft/preview state owners.
|
|
126
|
+
export { SECRET_REDACTED_NOTICE, redactUserText } from '../core/user-text-redaction.js';
|
|
127
|
+
export { useUserTextGuard } from './hooks/useUserTextGuard.js';
|
|
128
|
+
export { cacheAttachmentPreviewSource } from './hooks/attachment-preview-cache.js';
|
|
129
|
+
export { readCachedAttachmentBatchId, writeCachedAttachmentBatchId } from './hooks/composer-draft-cache.js';
|
|
130
|
+
export { createAssistantChatsClient } from "./hooks/assistant-chats.js";
|
|
131
|
+
export { useFabPosition } from "./hooks/fab-position.js";
|
|
124
132
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { AcceptedRunRequest, DurableRun, DurableRunStore } from "../../core/durable-runs/ports.js";
|
|
2
|
+
export interface RunAcceptancePorts {
|
|
3
|
+
readonly store: DurableRunStore;
|
|
4
|
+
/** Sanitize the host envelope before it is durably accepted. */
|
|
5
|
+
readonly sanitizeContextRef: (required: {
|
|
6
|
+
contextRef: string;
|
|
7
|
+
}, optional: {}) => string;
|
|
8
|
+
readonly now: () => number;
|
|
9
|
+
readonly mintRunId: () => string;
|
|
10
|
+
readonly launch: (required: {
|
|
11
|
+
run: DurableRun;
|
|
12
|
+
request: AcceptedRunRequest;
|
|
13
|
+
}, optional: {}) => Promise<void>;
|
|
14
|
+
readonly attach: (required: DurableRun, optional: {}) => void;
|
|
15
|
+
}
|
|
16
|
+
export declare function createRunAcceptance(ports: RunAcceptancePorts, _optional?: {}): {
|
|
17
|
+
accept(required: {
|
|
18
|
+
principalId: string;
|
|
19
|
+
workspaceId: string;
|
|
20
|
+
conversationId: string;
|
|
21
|
+
messageId: string;
|
|
22
|
+
request: AcceptedRunRequest;
|
|
23
|
+
}, _options?: {}): Promise<DurableRun | null>;
|
|
24
|
+
};
|
|
25
|
+
//# sourceMappingURL=accept.d.ts.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function createRunAcceptance(ports, _optional = {}) {
|
|
2
|
+
return {
|
|
3
|
+
async accept(required, _options = {}) {
|
|
4
|
+
const request = { ...required.request, contextRef: ports.sanitizeContextRef({ contextRef: required.request.contextRef }, {}) };
|
|
5
|
+
const run = await ports.store.accept({ ...required, request, runId: ports.mintRunId(), now: ports.now() }, {});
|
|
6
|
+
if (!run)
|
|
7
|
+
return null;
|
|
8
|
+
// The stub and accepted input are committed BEFORE dispatch. The daemon uses the stored
|
|
9
|
+
// attempt id as its idempotency key, so duplicate POSTs cannot start two executors.
|
|
10
|
+
if (run.message.runStatus === "queued" && run.recoveryCount === 0 && !run.cancelReason) {
|
|
11
|
+
try {
|
|
12
|
+
await ports.launch({ run, request: run.request }, {});
|
|
13
|
+
}
|
|
14
|
+
catch { /* The accepted attempt is recoverable even if its response was lost. */ }
|
|
15
|
+
}
|
|
16
|
+
ports.attach(run, {});
|
|
17
|
+
return run;
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=accept.js.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { DurableRecovery, RecoveryPorts } from "../../core/durable-runs/ports.js";
|
|
2
|
+
export { RECOVERY_LIMIT, RECOVERY_WINDOW_MS, RUN_STALENESS_MS, SAVED_WORK_NOTICE, EXHAUSTED_NOTICE } from "../../core/durable-runs/policies.js";
|
|
3
|
+
/** Recovery coordinator behind explicit host ports. One CAS owns both the attempt and its conversation slot. */
|
|
4
|
+
export declare function createDurableRecovery(ports: RecoveryPorts, _optional?: {}): DurableRecovery;
|
|
5
|
+
//# sourceMappingURL=recover.d.ts.map
|