@jini-ai/chat 0.6.15 → 0.6.17
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 +9 -0
- package/README.md +6 -1
- package/dist/core/accepted-input.d.ts +12 -0
- package/dist/core/accepted-input.js +29 -1
- package/dist/core/antigravity/tool-presentation.d.ts +13 -0
- package/dist/core/antigravity/tool-presentation.js +20 -0
- package/dist/core/assistant-client/ag-ui-transport.js +13 -1
- package/dist/core/assistant-client/transport.js +2 -0
- package/dist/core/events.d.ts +5 -1
- package/dist/core/messages.d.ts +2 -0
- package/dist/core/tool-events.d.ts +18 -8
- package/dist/core/tool-events.js +72 -16
- package/dist/core/tool-presentation.d.ts +14 -0
- package/dist/core/tool-presentation.js +8 -0
- package/dist/core/transport.d.ts +2 -0
- package/dist/core/user-text-redaction.d.ts +2 -1
- package/dist/core/user-text-redaction.js +37 -4
- package/dist/embed-widget/browser/frontend-session-bridge.d.ts +99 -0
- package/dist/embed-widget/browser/frontend-session-bridge.js +384 -0
- package/dist/embed-widget/browser/session-store.d.ts +3 -9
- package/dist/embed-widget/browser/session-store.js +6 -38
- package/dist/embed-widget/react/embed.d.ts +3 -10
- package/dist/embed-widget/react/embed.js +8 -52
- package/dist/embed-widget/vanilla/index.d.ts +0 -5
- package/dist/embed-widget/vanilla/index.js +4 -14
- package/dist/embed-widget/vanilla/state.d.ts +3 -11
- package/dist/embed-widget/vanilla/state.js +6 -37
- package/dist/embed-widget/vanilla/style.css +1 -1
- package/dist/react/agent-bridge/frontend-session-bridge.d.ts +2 -56
- package/dist/react/agent-bridge/frontend-session-bridge.js +2 -248
- package/dist/react/components/Composer.js +2 -1
- package/dist/react/components/CopyMessageButton.d.ts +4 -1
- package/dist/react/components/CopyMessageButton.js +3 -2
- package/dist/react/components/FolderChips.d.ts +13 -0
- package/dist/react/components/FolderChips.js +32 -0
- package/dist/react/components/Icon.d.ts +1 -1
- package/dist/react/components/Icon.js +2 -0
- package/dist/react/components/Markdown.d.ts +1 -0
- package/dist/react/components/Markdown.js +1 -1
- package/dist/react/components/MessageList.d.ts +2 -1
- package/dist/react/components/MessageList.js +2 -1
- package/dist/react/components/MessageRow.d.ts +3 -0
- package/dist/react/components/MessageRow.js +12 -9
- package/dist/react/components/MidRunUserMessage.js +3 -2
- package/dist/react/components/ToolCard.d.ts +3 -1
- package/dist/react/components/ToolCard.js +77 -58
- package/dist/react/components/ToolCard.rules.d.ts +20 -0
- package/dist/react/components/ToolCard.rules.js +92 -0
- package/dist/react/features/chat-pane/components/AgentRuntimePicker.js +18 -6
- package/dist/react/features/chat-pane/components/ChatPane.js +4 -3
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +3 -1
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +42 -19
- package/dist/react/features/chat-pane/styles.d.ts +1 -1
- package/dist/react/features/chat-pane/styles.js +96 -9
- package/dist/react/features/chat-pane/types.d.ts +7 -0
- package/dist/react/hooks/composer-draft-cache.d.ts +9 -6
- package/dist/react/hooks/composer-draft-cache.js +24 -0
- package/dist/react/hooks/useComposer.d.ts +7 -0
- package/dist/react/hooks/useComposer.js +31 -4
- package/dist/react/hooks/useConversation.d.ts +2 -0
- package/dist/react/hooks/useConversation.js +4 -1
- package/dist/react/hooks/useUserTextGuard.d.ts +1 -0
- package/dist/react/hooks/useUserTextGuard.js +3 -1
- package/dist/react/styles/reference.css +6 -0
- package/dist/react/tool-call-display.d.ts +11 -0
- package/dist/react/tool-call-display.js +59 -2
- package/dist/store/legacy/sqlite/messages.d.ts +12 -10
- package/dist/store/legacy/sqlite/messages.js +4 -1
- package/dist/store/runtime/durable-run-store.js +35 -3
- package/dist/store/sql/store.js +5 -1
- package/package.json +91 -86
- package/dist/core/browser/page-actions.d.ts +0 -121
- package/dist/core/browser/page-actions.js +0 -115
- package/dist/embed-widget/browser/page-actions.d.ts +0 -2
- package/dist/embed-widget/browser/page-actions.js +0 -3
|
@@ -59,6 +59,8 @@ export const COMPOSER_DRAFT_STORAGE_PREFIX = 'jini.chat.composer-draft.v1.';
|
|
|
59
59
|
* so a corrupt or unwritable attachment entry can never cost the operator their words, and vice
|
|
60
60
|
* versa. See {@link readCachedAttachments} for what these references are and are not.
|
|
61
61
|
*/
|
|
62
|
+
const COMPOSER_FOLDERS_STORAGE_PREFIX = 'jini.chat.composer-folders.v1.';
|
|
63
|
+
const folderDrafts = new Map();
|
|
62
64
|
export const COMPOSER_ATTACHMENTS_STORAGE_PREFIX = 'jini.chat.composer-attachments.v1.';
|
|
63
65
|
const drafts = new Map();
|
|
64
66
|
const stagedAttachments = new Map();
|
|
@@ -465,11 +467,33 @@ export function writeCachedAttachments({ conversationId, attachments }) {
|
|
|
465
467
|
*/
|
|
466
468
|
export function __resetComposerDraftCacheForTests(options) {
|
|
467
469
|
drafts.clear();
|
|
470
|
+
folderDrafts.clear();
|
|
468
471
|
stagedAttachments.clear();
|
|
469
472
|
stagedBatchIds.clear();
|
|
470
473
|
if (options?.keepStorage === true)
|
|
471
474
|
return;
|
|
475
|
+
pruneStored(COMPOSER_FOLDERS_STORAGE_PREFIX, 0);
|
|
472
476
|
pruneStored(COMPOSER_DRAFT_STORAGE_PREFIX, 0);
|
|
473
477
|
pruneStored(COMPOSER_ATTACHMENTS_STORAGE_PREFIX, 0);
|
|
474
478
|
}
|
|
479
|
+
/** Folder drafts share the bounded cache owner, but never upload liveness or TTL checks. */
|
|
480
|
+
export function readCachedFolders({ conversationId }, _options = {}) {
|
|
481
|
+
if (!conversationId)
|
|
482
|
+
return [];
|
|
483
|
+
const cached = folderDrafts.get(conversationId);
|
|
484
|
+
if (cached)
|
|
485
|
+
return [...cached];
|
|
486
|
+
const stored = readStoredEnvelope(COMPOSER_FOLDERS_STORAGE_PREFIX, { conversationId, decode: parsed => {
|
|
487
|
+
const value = parsed;
|
|
488
|
+
return value?.v === 1 && Array.isArray(value.folders) && value.folders.every(path => typeof path === 'string') ? value.folders : null;
|
|
489
|
+
} });
|
|
490
|
+
return stored ?? [];
|
|
491
|
+
}
|
|
492
|
+
/** Persist exact strings independently of text and attachment validation. */
|
|
493
|
+
export function writeCachedFolders({ conversationId, folders }, _options = {}) {
|
|
494
|
+
if (!conversationId)
|
|
495
|
+
return;
|
|
496
|
+
setBounded(folderDrafts, { key: conversationId, value: [...folders] });
|
|
497
|
+
writeStoredEnvelope(COMPOSER_FOLDERS_STORAGE_PREFIX, { conversationId, envelope: folders.length ? { v: 1, t: Date.now(), folders } : null });
|
|
498
|
+
}
|
|
475
499
|
//# sourceMappingURL=composer-draft-cache.js.map
|
|
@@ -72,6 +72,13 @@ export interface UseComposerResult {
|
|
|
72
72
|
/** Optional for compatibility with host-created composer controllers. */
|
|
73
73
|
history?: ComposerHistoryController;
|
|
74
74
|
setDraft: (next: string) => void;
|
|
75
|
+
folders?: string[];
|
|
76
|
+
addFolders: (input: {
|
|
77
|
+
folders: readonly string[];
|
|
78
|
+
}, options?: {}) => void;
|
|
79
|
+
removeFolder: (input: {
|
|
80
|
+
path: string;
|
|
81
|
+
}, options?: {}) => void;
|
|
75
82
|
attachments: ChatAttachment[];
|
|
76
83
|
/** Uploads via `project.uploadFiles` (when supplied) and stages the results; no-ops (with a rejected promise) when no upload port is wired. */
|
|
77
84
|
addAttachments: (files: File[]) => Promise<void>;
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
*/
|
|
30
30
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
31
31
|
import { useComposerHistory } from './useComposerHistory.hooks.js';
|
|
32
|
-
import { readCachedAttachments, readCachedDraft, rememberCachedDraft, writeCachedAttachments, writeCachedDraft, } from './composer-draft-cache.js';
|
|
32
|
+
import { readCachedFolders, writeCachedFolders, readCachedAttachments, readCachedDraft, rememberCachedDraft, writeCachedAttachments, writeCachedDraft, } from './composer-draft-cache.js';
|
|
33
33
|
import { cacheAttachmentPreviewSource } from './attachment-preview-cache.js';
|
|
34
34
|
/**
|
|
35
35
|
* The real window/document pair, or `null` when this runtime has no `window` — a non-DOM React
|
|
@@ -104,6 +104,23 @@ function useDeferredDraftWrite(pageLifecycle) {
|
|
|
104
104
|
export function useComposer(options = {}) {
|
|
105
105
|
const { project, composerSlots, persistence, conversationId, validateAttachments } = options;
|
|
106
106
|
const [draft, setDraftState] = useState(() => options.initialDraft ?? persistence?.read() ?? readCachedDraft({ conversationId: conversationId }) ?? '');
|
|
107
|
+
const [folders, setFolders] = useState(() => readCachedFolders({ conversationId }));
|
|
108
|
+
// Persist inside the state transition with its conversation key: switching cannot write an old
|
|
109
|
+
// render's folders over the newly selected conversation, and a pick survives immediate unmount.
|
|
110
|
+
const addFolders = useCallback(({ folders: picked }, _options = {}) => {
|
|
111
|
+
setFolders(previous => {
|
|
112
|
+
const next = [...new Set([...previous, ...picked])];
|
|
113
|
+
writeCachedFolders({ conversationId, folders: next });
|
|
114
|
+
return next;
|
|
115
|
+
});
|
|
116
|
+
}, [conversationId]);
|
|
117
|
+
const removeFolder = useCallback(({ path }, _options = {}) => {
|
|
118
|
+
setFolders(previous => {
|
|
119
|
+
const next = previous.filter(folder => folder !== path);
|
|
120
|
+
writeCachedFolders({ conversationId, folders: next });
|
|
121
|
+
return next;
|
|
122
|
+
});
|
|
123
|
+
}, [conversationId]);
|
|
107
124
|
const [attachments, setAttachments] = useState([]);
|
|
108
125
|
const [agent, setAgent] = useState(options.initialAgent);
|
|
109
126
|
const [mention, setMention] = useState(EMPTY_MENTION);
|
|
@@ -164,8 +181,13 @@ export function useComposer(options = {}) {
|
|
|
164
181
|
// reason — nothing was switched away from.
|
|
165
182
|
if (previousConversationId == null && conversationId != null && restored === null && draftRef.current.trim() !== '') {
|
|
166
183
|
writeCachedDraft({ conversationId: conversationId, draft: draftRef.current });
|
|
184
|
+
writeCachedFolders({ conversationId, folders });
|
|
167
185
|
return;
|
|
168
186
|
}
|
|
187
|
+
if (previousConversationId == null && conversationId != null)
|
|
188
|
+
writeCachedFolders({ conversationId, folders });
|
|
189
|
+
else
|
|
190
|
+
setFolders(readCachedFolders({ conversationId }));
|
|
169
191
|
setDraftState(restored ?? '');
|
|
170
192
|
setAttachments([]);
|
|
171
193
|
setMention(EMPTY_MENTION);
|
|
@@ -284,14 +306,19 @@ export function useComposer(options = {}) {
|
|
|
284
306
|
}, [draft, setDraft]);
|
|
285
307
|
const reset = useCallback(() => {
|
|
286
308
|
setDraft('');
|
|
309
|
+
setFolders([]);
|
|
310
|
+
writeCachedFolders({ conversationId, folders: [] });
|
|
287
311
|
clearAttachments();
|
|
288
312
|
closeMention();
|
|
289
|
-
}, [clearAttachments, closeMention, setDraft]);
|
|
290
|
-
const canSubmit = draft.trim().length > 0 || attachments.length > 0;
|
|
313
|
+
}, [clearAttachments, closeMention, setDraft, conversationId]);
|
|
314
|
+
const canSubmit = draft.trim().length > 0 || attachments.length > 0 || folders.length > 0;
|
|
291
315
|
return useMemo(() => ({
|
|
292
316
|
draft,
|
|
293
317
|
history,
|
|
294
318
|
setDraft,
|
|
319
|
+
folders,
|
|
320
|
+
addFolders,
|
|
321
|
+
removeFolder,
|
|
295
322
|
attachments,
|
|
296
323
|
addAttachments,
|
|
297
324
|
addAttachment,
|
|
@@ -306,6 +333,6 @@ export function useComposer(options = {}) {
|
|
|
306
333
|
canSubmit,
|
|
307
334
|
reset,
|
|
308
335
|
flushDraft: deferredWrite.flush,
|
|
309
|
-
}), [draft, history, setDraft, attachments, addAttachments, addAttachment, removeAttachment, clearAttachments, agent, mention, openMention, closeMention, selectMention, canSubmit, reset, deferredWrite.flush]);
|
|
336
|
+
}), [draft, history, setDraft, folders, addFolders, removeFolder, attachments, addAttachments, addAttachment, removeAttachment, clearAttachments, agent, mention, openMention, closeMention, selectMention, canSubmit, reset, deferredWrite.flush]);
|
|
310
337
|
}
|
|
311
338
|
//# sourceMappingURL=useComposer.js.map
|
|
@@ -27,6 +27,8 @@ export interface SendMessageOptions {
|
|
|
27
27
|
}) => void;
|
|
28
28
|
/** Non-secret signal from a composer that already sanitized the content before this boundary. */
|
|
29
29
|
secretRedaction?: ChatMessage['secretRedaction'];
|
|
30
|
+
/** Picked directory paths, kept byte-exact and separate from upload references. */
|
|
31
|
+
folders?: string[];
|
|
30
32
|
attachments?: ChatAttachment[];
|
|
31
33
|
context?: RunContext;
|
|
32
34
|
agentId?: string;
|
|
@@ -32,6 +32,7 @@ function pendingUserMessage({ content, sendOptions, createMessageId }, options =
|
|
|
32
32
|
content,
|
|
33
33
|
createdAt: Date.now(),
|
|
34
34
|
...(sendOptions.secretRedaction ? { secretRedaction: sendOptions.secretRedaction } : {}),
|
|
35
|
+
...(sendOptions.folders ? { folders: sendOptions.folders } : {}),
|
|
35
36
|
...(sendOptions.attachments ? { attachments: sendOptions.attachments } : {}),
|
|
36
37
|
} }, options).message;
|
|
37
38
|
}
|
|
@@ -213,6 +214,7 @@ export function useConversation(options) {
|
|
|
213
214
|
history,
|
|
214
215
|
...(resolvedAgentId !== undefined ? { agentId: resolvedAgentId } : {}),
|
|
215
216
|
conversationId,
|
|
217
|
+
...(sendOptions.folders !== undefined ? { folders: sendOptions.folders } : {}),
|
|
216
218
|
...(sendOptions.attachments !== undefined ? { attachments: sendOptions.attachments } : {}),
|
|
217
219
|
...(sendOptions.context !== undefined ? { context: sendOptions.context } : {}),
|
|
218
220
|
});
|
|
@@ -222,7 +224,7 @@ export function useConversation(options) {
|
|
|
222
224
|
setSendRefused(true);
|
|
223
225
|
return;
|
|
224
226
|
}
|
|
225
|
-
const inputs = sendOptions.acceptedInputs ?? [{ id: userMessage.id, text: userMessage.content }];
|
|
227
|
+
const inputs = sendOptions.acceptedInputs ?? [{ id: userMessage.id, text: userMessage.content, ...(userMessage.folders ? { folders: userMessage.folders } : {}) }];
|
|
226
228
|
setMessagesState(prev => prev.map(message => message.id === userMessage.id ? { ...message, inputPending: false } : message));
|
|
227
229
|
sendOptions.onAccepted?.({ inputs });
|
|
228
230
|
}, [agentId, conversationId, createMessageId, run, options.redactUserText, options.onSecretRedacted]);
|
|
@@ -254,6 +256,7 @@ export function useConversation(options) {
|
|
|
254
256
|
history,
|
|
255
257
|
...(resolvedAgentId !== undefined ? { agentId: resolvedAgentId } : {}),
|
|
256
258
|
conversationId,
|
|
259
|
+
...(priorUser.folders !== undefined ? { folders: priorUser.folders } : {}),
|
|
257
260
|
...(priorUser.attachments !== undefined ? { attachments: priorUser.attachments } : {}),
|
|
258
261
|
});
|
|
259
262
|
}, [agentId, conversationId, run, options.redactUserText, options.onSecretRedacted]);
|
|
@@ -7,5 +7,6 @@ export declare function useUserTextGuard(options: UserTextRedactionOptions, _opt
|
|
|
7
7
|
}, _options?: {}) => import("../index.js").UserTextRedaction;
|
|
8
8
|
redactText: (text: string) => string;
|
|
9
9
|
notifyRedaction: (signal: SecretRedactionSignal) => void;
|
|
10
|
+
clearNotice: () => void;
|
|
10
11
|
};
|
|
11
12
|
//# sourceMappingURL=useUserTextGuard.d.ts.map
|
|
@@ -14,6 +14,8 @@ export function useUserTextGuard(options, _optional = {}) {
|
|
|
14
14
|
return result;
|
|
15
15
|
}, [options.redactUserText, notifyRedaction]);
|
|
16
16
|
const redactText = useCallback((text) => redact({ text }, {}).text, [redact]);
|
|
17
|
-
|
|
17
|
+
/** The notice is about one message: the pane clears it when the person sends the next one (a key in that one sets it again) or dismisses it. */
|
|
18
|
+
const clearNotice = useCallback(() => setSecretRedacted(false), []);
|
|
19
|
+
return { secretRedacted, redact, redactText, notifyRedaction, clearNotice };
|
|
18
20
|
}
|
|
19
21
|
//# sourceMappingURL=useUserTextGuard.js.map
|
|
@@ -328,3 +328,9 @@
|
|
|
328
328
|
* deleted silently) so a future reader who remembers seeing "queued" in this file finds the pointer
|
|
329
329
|
* instead of concluding the removal was accidental.
|
|
330
330
|
*/
|
|
331
|
+
|
|
332
|
+
.jini-composer-attachments, .jini-folder-chips__list { display: flex; flex-wrap: wrap; }
|
|
333
|
+
.jini-folder-chips, .jini-folder-chips__name { min-width: 0; max-width: 100%; overflow-wrap: anywhere; }
|
|
334
|
+
.jini-folder-chips__toggle, .jini-folder-chips__item { display: flex; align-items: center; }
|
|
335
|
+
.jini-folder-chips__list { list-style: none; }
|
|
336
|
+
.jini-folder-chips svg, .jini-folder-chips button { flex-shrink: 0; }
|
|
@@ -32,6 +32,17 @@ export interface ToolDisplayRow {
|
|
|
32
32
|
export declare function isFailedToolRow({ row }: {
|
|
33
33
|
row: Pick<ToolDisplayRow, 'status'>;
|
|
34
34
|
}): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Ids of failed rows that a LATER successful call of the same tool and target retried (owner, 2026-10-31: a
|
|
37
|
+
* red "HTTP 400 … page was NOT written" row and "· 2 failed" stayed on screen after the immediate
|
|
38
|
+
* retry wrote the page). Those rows render as a neutral "Retried" and are not counted as failed;
|
|
39
|
+
* a failure nothing later succeeded at stays red. Inputs without target coordinates match by
|
|
40
|
+
* tool alone; a targetless success cannot clear a failure naming a target. Pass one turn's rows.
|
|
41
|
+
* @complexity O(n + target bytes) time and space.
|
|
42
|
+
*/
|
|
43
|
+
export declare function retriedToolRowIds({ rows }: {
|
|
44
|
+
rows: readonly ToolDisplayRow[];
|
|
45
|
+
}, _options?: {}): ReadonlySet<string>;
|
|
35
46
|
/**
|
|
36
47
|
* Applies a display mode to an interleaved message.
|
|
37
48
|
*
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { humanizeToolName } from '../core/index.js';
|
|
1
|
+
import { humanizeToolName, isDelegatedWrapperToolName } from '../core/index.js';
|
|
2
2
|
/**
|
|
3
3
|
* Whether a row failed: the tool returned an error, or the run
|
|
4
4
|
* ended without the tool ever answering (`deriveToolStatus` maps both to `'error'`).
|
|
@@ -7,6 +7,62 @@ import { humanizeToolName } from '../core/index.js';
|
|
|
7
7
|
export function isFailedToolRow({ row }) {
|
|
8
8
|
return row.status === 'error';
|
|
9
9
|
}
|
|
10
|
+
/** The tool a row called: a delegated wrapper's `input.toolId`, else its own name (wrappers share one name). */
|
|
11
|
+
function toolIdentity(row) {
|
|
12
|
+
if (!isDelegatedWrapperToolName({ name: row.name }))
|
|
13
|
+
return row.name;
|
|
14
|
+
const input = row.use?.input;
|
|
15
|
+
const toolId = typeof input === 'object' && input !== null ? input.toolId : undefined;
|
|
16
|
+
return typeof toolId === 'string' && toolId ? toolId : row.name;
|
|
17
|
+
}
|
|
18
|
+
/** Prefer a stable id over editable slugs/paths; normalize input aliases within each kind.
|
|
19
|
+
* @complexity O(target bytes), fixed field count; pure, absent coordinates return undefined.
|
|
20
|
+
*/
|
|
21
|
+
function targetCoordinate({ input }, _options = {}) {
|
|
22
|
+
const kinds = [
|
|
23
|
+
['id', ['postId', 'post_id', 'pageId', 'page_id', 'entryId', 'entry_id', 'id']],
|
|
24
|
+
['slug', ['slug']],
|
|
25
|
+
['path', ['path', 'filePath', 'file_path']],
|
|
26
|
+
];
|
|
27
|
+
for (const [kind, fields] of kinds) {
|
|
28
|
+
const field = fields.find(key => typeof input[key] === 'number' || (typeof input[key] === 'string' && input[key] !== ''));
|
|
29
|
+
if (field !== undefined)
|
|
30
|
+
return [kind, input[field]];
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
/** Target coordinates only: corrected content must not prevent a retry from matching.
|
|
35
|
+
* Wrapper inputs belong to the delegated tool, whose identity toolIdentity already resolves.
|
|
36
|
+
* @complexity O(target bytes); the coordinate list is fixed. Pure, with no input mutation.
|
|
37
|
+
*/
|
|
38
|
+
function retryIdentity({ row }, _options = {}) {
|
|
39
|
+
const input = row.use?.input;
|
|
40
|
+
const record = typeof input === 'object' && input !== null ? input : {};
|
|
41
|
+
const target = isDelegatedWrapperToolName({ name: row.name }) ? record.input : record;
|
|
42
|
+
const coordinates = typeof target === 'object' && target !== null ? target : {};
|
|
43
|
+
return JSON.stringify([toolIdentity(row), targetCoordinate({ input: coordinates })]);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Ids of failed rows that a LATER successful call of the same tool and target retried (owner, 2026-10-31: a
|
|
47
|
+
* red "HTTP 400 … page was NOT written" row and "· 2 failed" stayed on screen after the immediate
|
|
48
|
+
* retry wrote the page). Those rows render as a neutral "Retried" and are not counted as failed;
|
|
49
|
+
* a failure nothing later succeeded at stays red. Inputs without target coordinates match by
|
|
50
|
+
* tool alone; a targetless success cannot clear a failure naming a target. Pass one turn's rows.
|
|
51
|
+
* @complexity O(n + target bytes) time and space.
|
|
52
|
+
*/
|
|
53
|
+
export function retriedToolRowIds({ rows }, _options = {}) {
|
|
54
|
+
const succeededLater = new Set();
|
|
55
|
+
const retried = new Set();
|
|
56
|
+
for (let i = rows.length - 1; i >= 0; i -= 1) {
|
|
57
|
+
const row = rows[i];
|
|
58
|
+
const tool = retryIdentity({ row });
|
|
59
|
+
if (row.status === 'complete')
|
|
60
|
+
succeededLater.add(tool);
|
|
61
|
+
else if (isFailedToolRow({ row }) && succeededLater.has(tool))
|
|
62
|
+
retried.add(row.id);
|
|
63
|
+
}
|
|
64
|
+
return retried;
|
|
65
|
+
}
|
|
10
66
|
/** Running = no result yet while the run streams (`'executing'`), or not started (`'inProgress'`). */
|
|
11
67
|
function isRunningToolRow(row) {
|
|
12
68
|
return row.status === 'executing' || row.status === 'inProgress';
|
|
@@ -109,7 +165,8 @@ export function summarizeToolGroup({ rows }) {
|
|
|
109
165
|
if (isRunningToolRow(row))
|
|
110
166
|
return { kind: 'running', name: humanizeToolName({ name: row.name }, { input: row.use?.input }) };
|
|
111
167
|
}
|
|
112
|
-
|
|
168
|
+
const retried = retriedToolRowIds({ rows });
|
|
169
|
+
return { kind: 'done', count: rows.length, failed: rows.filter((row) => isFailedToolRow({ row }) && !retried.has(row.id)).length };
|
|
113
170
|
}
|
|
114
171
|
/**
|
|
115
172
|
* The collapsed line's text. Each phrase is its own translation key (the English string is the key,
|
|
@@ -25,6 +25,12 @@ export declare function listMessages({ db, conversationId }: {
|
|
|
25
25
|
db: SqliteDb;
|
|
26
26
|
conversationId: string;
|
|
27
27
|
}): {
|
|
28
|
+
sessionMode: ChatSessionMode | undefined;
|
|
29
|
+
runContext: any;
|
|
30
|
+
createdAt: any;
|
|
31
|
+
startedAt: any;
|
|
32
|
+
endedAt: any;
|
|
33
|
+
folders?: string[];
|
|
28
34
|
id: any;
|
|
29
35
|
role: any;
|
|
30
36
|
content: any;
|
|
@@ -35,11 +41,6 @@ export declare function listMessages({ db, conversationId }: {
|
|
|
35
41
|
lastRunEventId: any;
|
|
36
42
|
events: any;
|
|
37
43
|
attachments: any;
|
|
38
|
-
sessionMode: ChatSessionMode | undefined;
|
|
39
|
-
runContext: any;
|
|
40
|
-
createdAt: any;
|
|
41
|
-
startedAt: any;
|
|
42
|
-
endedAt: any;
|
|
43
44
|
}[];
|
|
44
45
|
/**
|
|
45
46
|
* Thrown by `upsertMessage` when the caller presents a message `id` together with a
|
|
@@ -150,6 +151,12 @@ export declare function deleteMessage({ db, id }: {
|
|
|
150
151
|
}): void;
|
|
151
152
|
/** @internal Converts a raw SQLite row to a plain object with JSON columns parsed and null coerced to undefined. */
|
|
152
153
|
declare function normalizeMessage(row: DbRow): {
|
|
154
|
+
sessionMode: ChatSessionMode | undefined;
|
|
155
|
+
runContext: any;
|
|
156
|
+
createdAt: any;
|
|
157
|
+
startedAt: any;
|
|
158
|
+
endedAt: any;
|
|
159
|
+
folders?: string[];
|
|
153
160
|
id: any;
|
|
154
161
|
role: any;
|
|
155
162
|
content: any;
|
|
@@ -160,11 +167,6 @@ declare function normalizeMessage(row: DbRow): {
|
|
|
160
167
|
lastRunEventId: any;
|
|
161
168
|
events: any;
|
|
162
169
|
attachments: any;
|
|
163
|
-
sessionMode: ChatSessionMode | undefined;
|
|
164
|
-
runContext: any;
|
|
165
|
-
createdAt: any;
|
|
166
|
-
startedAt: any;
|
|
167
|
-
endedAt: any;
|
|
168
170
|
};
|
|
169
171
|
export {};
|
|
170
172
|
//# sourceMappingURL=messages.d.ts.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withFolderReceipt, foldersFromReceipts } from "../../../core/accepted-input.js";
|
|
1
2
|
import { redactUserText } from '../../../core/user-text-redaction.js';
|
|
2
3
|
import { parseJsonOrUndef } from '@jini-ai/db/core';
|
|
3
4
|
/** @internal Column alias list used by both `listMessages` and `upsertMessage`'s re-fetch. */
|
|
@@ -117,7 +118,7 @@ function messageFieldValues(m) {
|
|
|
117
118
|
m.role,
|
|
118
119
|
m.content,
|
|
119
120
|
...[m.agentId, m.agentName, m.runId, m.runStatus, m.lastRunEventId].map(orNull),
|
|
120
|
-
jsonOrNull(m.events),
|
|
121
|
+
jsonOrNull(withFolderReceipt({ message: m }).events),
|
|
121
122
|
jsonOrNull(m.attachments),
|
|
122
123
|
normalizeMessageSessionMode(m.sessionMode) ?? null,
|
|
123
124
|
jsonOrNull(m.runContext),
|
|
@@ -273,6 +274,7 @@ export function deleteMessage({ db, id }) {
|
|
|
273
274
|
}
|
|
274
275
|
/** @internal Converts a raw SQLite row to a plain object with JSON columns parsed and null coerced to undefined. */
|
|
275
276
|
function normalizeMessage(row) {
|
|
277
|
+
const folders = foldersFromReceipts({ events: parseJsonOrUndef(row.eventsJson) });
|
|
276
278
|
return {
|
|
277
279
|
id: row.id,
|
|
278
280
|
role: row.role,
|
|
@@ -284,6 +286,7 @@ function normalizeMessage(row) {
|
|
|
284
286
|
lastRunEventId: row.lastRunEventId ?? undefined,
|
|
285
287
|
events: parseJsonOrUndef(row.eventsJson),
|
|
286
288
|
attachments: parseJsonOrUndef(row.attachmentsJson),
|
|
289
|
+
...(folders.length ? { folders } : {}),
|
|
287
290
|
sessionMode: normalizeMessageSessionMode(row.sessionMode),
|
|
288
291
|
runContext: parseJsonOrUndef(row.runContextJson),
|
|
289
292
|
createdAt: row.createdAt ?? undefined,
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { acceptedInputEvent, acceptedInputUserEvent } from "../../core/accepted-input.js";
|
|
2
|
+
import { redactUserMessage } from "../../core/user-text-redaction.js";
|
|
1
3
|
import { recoveredRunEvents } from "../../core/index.js";
|
|
2
4
|
import { chatKernelTables } from "../sql/boundary.js";
|
|
3
5
|
import { sql } from "kysely";
|
|
@@ -18,6 +20,10 @@ function isAcceptedUserShape({ message }, _optional = {}) {
|
|
|
18
20
|
return false;
|
|
19
21
|
return message.createdAt === undefined || Number.isFinite(message.createdAt);
|
|
20
22
|
}
|
|
23
|
+
/** Directory existence is host policy; the neutral store validates only its message contract. */
|
|
24
|
+
function validAcceptedFolders({ folders }, _options = {}) {
|
|
25
|
+
return Array.isArray(folders) && folders.length <= 10 && folders.every(path => typeof path === 'string' && path.length > 0);
|
|
26
|
+
}
|
|
21
27
|
function areAcceptedAttachments({ attachments }, _optional = {}) {
|
|
22
28
|
return Array.isArray(attachments) && attachments.every((attachment) => {
|
|
23
29
|
if (!attachment || typeof attachment.path !== "string" || typeof attachment.name !== "string")
|
|
@@ -25,6 +31,31 @@ function areAcceptedAttachments({ attachments }, _optional = {}) {
|
|
|
25
31
|
return attachment.kind === "file" || attachment.kind === "image";
|
|
26
32
|
});
|
|
27
33
|
}
|
|
34
|
+
/** Both top-level and queued user inputs obey the same reference contract. */
|
|
35
|
+
function validateAcceptedMetadata({ message }, _options = {}) {
|
|
36
|
+
if (message.folders !== undefined && !validAcceptedFolders({ folders: message.folders }))
|
|
37
|
+
throw new Error("Invalid accepted user folders");
|
|
38
|
+
if (message.attachments !== undefined && !areAcceptedAttachments({ attachments: message.attachments }, {}))
|
|
39
|
+
throw new Error("Invalid accepted user attachments");
|
|
40
|
+
}
|
|
41
|
+
/** Preserve only accepted user receipts; a browser cannot supply assistant lifecycle events.
|
|
42
|
+
* Receipt text uses the same guard as ordinary user text; folder data remains byte-exact.
|
|
43
|
+
*/
|
|
44
|
+
function acceptedReceiptFields({ message }, _options = {}) {
|
|
45
|
+
if (!Array.isArray(message.events))
|
|
46
|
+
return {};
|
|
47
|
+
const events = message.events.flatMap(event => {
|
|
48
|
+
if (!event || typeof event !== 'object')
|
|
49
|
+
return [];
|
|
50
|
+
const input = acceptedInputEvent({ event })?.data;
|
|
51
|
+
if (!input)
|
|
52
|
+
return [];
|
|
53
|
+
validateAcceptedMetadata({ message: input });
|
|
54
|
+
const safe = redactUserMessage({ message: { role: 'user', content: input.text } }).message;
|
|
55
|
+
return [acceptedInputUserEvent({ input: { ...input, text: safe.content } }, { receipt: 'accepted' })];
|
|
56
|
+
});
|
|
57
|
+
return events.length ? { events } : {};
|
|
58
|
+
}
|
|
28
59
|
function acceptedUserMessage({ request, now }, _optional = {}) {
|
|
29
60
|
const context = JSON.parse(request.contextRef);
|
|
30
61
|
// Older clients supplied only a prompt; never invent a user identity from that transcript.
|
|
@@ -33,11 +64,12 @@ function acceptedUserMessage({ request, now }, _optional = {}) {
|
|
|
33
64
|
return null;
|
|
34
65
|
if (!isAcceptedUserShape({ message }, {}))
|
|
35
66
|
throw new Error("Invalid accepted user message");
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
// User input cannot claim an assistant run's status/events. Keep the same identity and payload
|
|
67
|
+
validateAcceptedMetadata({ message });
|
|
68
|
+
// User input cannot claim assistant lifecycle events. Keep the same identity and payload
|
|
39
69
|
// as its browser PUT, including the original clock and attachment order.
|
|
40
70
|
return { id: message.id, role: "user", content: message.content, createdAt: message.createdAt ?? now,
|
|
71
|
+
...acceptedReceiptFields({ message }),
|
|
72
|
+
...(message.folders ? { folders: message.folders } : {}),
|
|
41
73
|
...(message.attachments ? { attachments: message.attachments } : {}) };
|
|
42
74
|
}
|
|
43
75
|
function recoveryFields({ row, metadata }, _optional = {}) {
|
package/dist/store/sql/store.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withFolderReceipt, foldersFromReceipts } from '../../core/accepted-input.js';
|
|
1
2
|
const MESSAGE_POSITION = 'm.position';
|
|
2
3
|
const CONVERSATION_UPDATED_AT = 'c.updated_at';
|
|
3
4
|
/** One SQL query body upstreamed from the existing host adapter; no resource/schema ownership. */
|
|
@@ -84,8 +85,10 @@ function agentFields(row) {
|
|
|
84
85
|
function jsonFields(row) {
|
|
85
86
|
const events = parseJsonColumn(row.events_json);
|
|
86
87
|
const attachments = parseJsonColumn(row.attachments_json);
|
|
88
|
+
const folders = foldersFromReceipts({ events });
|
|
87
89
|
return {
|
|
88
90
|
...(events === undefined ? {} : { events }),
|
|
91
|
+
...(folders.length ? { folders } : {}),
|
|
89
92
|
...(attachments === undefined ? {} : { attachments }),
|
|
90
93
|
};
|
|
91
94
|
}
|
|
@@ -190,7 +193,8 @@ export function createSqlChatStore({ kernel, scope: inputScope }, { clock = crea
|
|
|
190
193
|
return rows.map(toMessage);
|
|
191
194
|
}
|
|
192
195
|
/** Writes and projects one message inside the borrowed transaction; null for absent/foreign IDs. */
|
|
193
|
-
function writeMessage(conversationId,
|
|
196
|
+
function writeMessage(conversationId, message) {
|
|
197
|
+
const m = withFolderReceipt({ message });
|
|
194
198
|
return kernel.transaction(async () => {
|
|
195
199
|
await kernel.lockKey(conversationLockKey(conversationId));
|
|
196
200
|
const owned = await run((db) => db
|