@jini-ai/chat 0.6.16 → 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/dist/core/accepted-input.d.ts +12 -0
- package/dist/core/accepted-input.js +29 -1
- package/dist/core/assistant-client/transport.js +2 -0
- package/dist/core/messages.d.ts +2 -0
- package/dist/core/transport.d.ts +2 -0
- package/dist/core/user-text-redaction.js +30 -4
- 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 +1 -0
- package/dist/react/components/MessageList.d.ts +1 -0
- package/dist/react/components/MessageList.js +2 -1
- package/dist/react/components/MessageRow.js +2 -1
- package/dist/react/components/MidRunUserMessage.js +3 -2
- package/dist/react/features/chat-pane/components/ChatPane.js +1 -0
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +33 -16
- package/dist/react/features/chat-pane/styles.d.ts +1 -1
- package/dist/react/features/chat-pane/styles.js +20 -0
- package/dist/react/features/chat-pane/types.d.ts +4 -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/styles/reference.css +6 -0
- 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 +12 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
## 0.6.17
|
|
2
|
+
|
|
3
|
+
### Patch Changes
|
|
4
|
+
|
|
5
|
+
- Carry picked folders as per-message folder references, with draft and sent-message chips, persistence and replay.
|
|
6
|
+
- Preserve ordinary typed paths while redacting glued-on secrets and slash-containing keys.
|
|
7
|
+
- Handle npm 12 pack JSON through the shared parser in isolated-consumer tests.
|
|
8
|
+
- Treat Node builtin dependencies as builtins when copying isolated runtime fixtures.
|
|
9
|
+
|
|
1
10
|
## 0.6.13
|
|
2
11
|
|
|
3
12
|
### Patch Changes
|
|
@@ -9,6 +9,8 @@ export interface AcceptedChatInput {
|
|
|
9
9
|
answerText?: string;
|
|
10
10
|
/** Absent for legacy/ordinary inputs; consumed only after a CLI UUID receipt. */
|
|
11
11
|
deliveryState?: 'queued' | 'consumed';
|
|
12
|
+
/** Picked directory paths, kept byte-exact and separate from upload references. */
|
|
13
|
+
folders?: string[];
|
|
12
14
|
attachments?: ChatAttachment[];
|
|
13
15
|
secretRedaction?: ChatMessage['secretRedaction'];
|
|
14
16
|
/** Wire ids replaced by this receipt remain seen by recall. */
|
|
@@ -71,5 +73,15 @@ export declare function projectAcceptedInputEvents({ events }: {
|
|
|
71
73
|
export declare function acceptedInputsFromMessages({ messages }: {
|
|
72
74
|
messages: readonly ChatMessage[];
|
|
73
75
|
}, _options?: {}): AcceptedChatInput[];
|
|
76
|
+
/** Preserve folder metadata through existing event JSON storage; no attachment lifecycle or schema.
|
|
77
|
+
* Existing per-input receipts already own queued variants and must retain their identities.
|
|
78
|
+
*/
|
|
79
|
+
export declare function withFolderReceipt({ message }: {
|
|
80
|
+
message: ChatMessage;
|
|
81
|
+
}, _options?: {}): ChatMessage;
|
|
82
|
+
/** Hydrate the message's folder list from its accepted receipts after a database read. */
|
|
83
|
+
export declare function foldersFromReceipts({ events }: {
|
|
84
|
+
events: unknown;
|
|
85
|
+
}, _options?: {}): string[];
|
|
74
86
|
export {};
|
|
75
87
|
//# sourceMappingURL=accepted-input.d.ts.map
|
|
@@ -17,9 +17,11 @@ export function projectAcceptedInputMessages({ messages }, _options = {}) {
|
|
|
17
17
|
return [message];
|
|
18
18
|
const turn = { ...message };
|
|
19
19
|
delete turn.attachments;
|
|
20
|
+
delete turn.folders;
|
|
20
21
|
delete turn.secretRedaction;
|
|
21
22
|
// MOVE: Older receipts have no per-input files; retain those legacy files on the final bubble.
|
|
22
23
|
return inputs.map((input, index) => ({ ...turn, id: input.id, content: input.text,
|
|
24
|
+
folders: input.folders ?? (index === inputs.length - 1 ? message.folders ?? [] : []),
|
|
23
25
|
attachments: input.attachments ?? (index === inputs.length - 1 ? message.attachments ?? [] : []), ...(input.secretRedaction ? { secretRedaction: input.secretRedaction } : {}) }));
|
|
24
26
|
});
|
|
25
27
|
}
|
|
@@ -246,7 +248,7 @@ export function acceptedInputsFromMessages({ messages }, _options = {}) {
|
|
|
246
248
|
const input = acceptedInputEvent({ event });
|
|
247
249
|
return input ? [input.data] : [];
|
|
248
250
|
});
|
|
249
|
-
return receipts.length > 0 ? receipts : [{ id: message.id, text: message.content }];
|
|
251
|
+
return receipts.length > 0 ? receipts : [{ id: message.id, text: message.content, ...(message.folders ? { folders: message.folders } : {}) }];
|
|
250
252
|
}
|
|
251
253
|
return projectAcceptedInputEvents({ events: message.events ?? [] }).flatMap(event => {
|
|
252
254
|
const input = acceptedInputEvent({ event });
|
|
@@ -256,4 +258,30 @@ export function acceptedInputsFromMessages({ messages }, _options = {}) {
|
|
|
256
258
|
});
|
|
257
259
|
});
|
|
258
260
|
}
|
|
261
|
+
/** Preserve folder metadata through existing event JSON storage; no attachment lifecycle or schema.
|
|
262
|
+
* Existing per-input receipts already own queued variants and must retain their identities.
|
|
263
|
+
*/
|
|
264
|
+
export function withFolderReceipt({ message }, _options = {}) {
|
|
265
|
+
if (message.role !== 'user' || !message.folders?.length)
|
|
266
|
+
return message;
|
|
267
|
+
const events = message.events ?? [];
|
|
268
|
+
if (events.some(event => acceptedInputEvent({ event })))
|
|
269
|
+
return message;
|
|
270
|
+
return { ...message, events: [...events, acceptedInputUserEvent({ input: {
|
|
271
|
+
id: message.id, text: message.content, folders: message.folders,
|
|
272
|
+
...(message.secretRedaction ? { secretRedaction: message.secretRedaction } : {}),
|
|
273
|
+
...(message.attachments ? { attachments: message.attachments } : {}),
|
|
274
|
+
} }, { receipt: 'accepted' })] };
|
|
275
|
+
}
|
|
276
|
+
/** Hydrate the message's folder list from its accepted receipts after a database read. */
|
|
277
|
+
export function foldersFromReceipts({ events }, _options = {}) {
|
|
278
|
+
if (!Array.isArray(events))
|
|
279
|
+
return [];
|
|
280
|
+
return events.flatMap(event => {
|
|
281
|
+
if (event === null || typeof event !== 'object')
|
|
282
|
+
return [];
|
|
283
|
+
const folders = acceptedInputEvent({ event })?.data.folders;
|
|
284
|
+
return Array.isArray(folders) ? folders.filter((path) => typeof path === 'string') : [];
|
|
285
|
+
});
|
|
286
|
+
}
|
|
259
287
|
//# sourceMappingURL=accepted-input.js.map
|
|
@@ -375,6 +375,7 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
|
|
|
375
375
|
return {
|
|
376
376
|
messages: byokMessages({ history: input.history }, {}),
|
|
377
377
|
...attachmentIdsField(input),
|
|
378
|
+
...(input.folders?.length ? { folders: input.folders } : {}),
|
|
378
379
|
...conversationIdField(input),
|
|
379
380
|
agentPluginState: input.context?.["agentPluginState"],
|
|
380
381
|
userPrompt: input.context?.["userPrompt"],
|
|
@@ -584,6 +585,7 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
|
|
|
584
585
|
...modelField(input),
|
|
585
586
|
...reasoningField(input),
|
|
586
587
|
...attachmentIdsField(input),
|
|
588
|
+
...(input.folders?.length ? { folders: input.folders } : {}),
|
|
587
589
|
...pluginRefIdsField(input),
|
|
588
590
|
agentPluginState: input.context?.["agentPluginState"],
|
|
589
591
|
userPrompt: input.context?.["userPrompt"],
|
package/dist/core/messages.d.ts
CHANGED
|
@@ -58,6 +58,8 @@ export interface ChatMessage {
|
|
|
58
58
|
lastRunEventId?: string;
|
|
59
59
|
startedAt?: number;
|
|
60
60
|
endedAt?: number;
|
|
61
|
+
/** Picked directory paths, kept byte-exact and separate from upload references. */
|
|
62
|
+
folders?: string[];
|
|
61
63
|
attachments?: ChatAttachment[];
|
|
62
64
|
}
|
|
63
65
|
//# sourceMappingURL=messages.d.ts.map
|
package/dist/core/transport.d.ts
CHANGED
|
@@ -57,6 +57,8 @@ export interface StartRunInput {
|
|
|
57
57
|
history: ChatMessage[];
|
|
58
58
|
agentId?: string;
|
|
59
59
|
conversationId?: string | null;
|
|
60
|
+
/** Picked directory paths, kept byte-exact and separate from upload references. */
|
|
61
|
+
folders?: string[];
|
|
60
62
|
attachments?: ChatAttachment[];
|
|
61
63
|
context?: RunContext;
|
|
62
64
|
/** Stops the browser-side subscription; the run continues host-side. */
|
|
@@ -25,6 +25,24 @@ function entropy(text) {
|
|
|
25
25
|
}
|
|
26
26
|
return bits;
|
|
27
27
|
}
|
|
28
|
+
/** Recognize identifier paths, including the fragment after a space in a typed path.
|
|
29
|
+
* Require multiple segments and readable words: arbitrary base64 and long digit-mixed runs
|
|
30
|
+
* must still reach the entropy guard. Specific credential rules have already run. */
|
|
31
|
+
function isIdentifierPath({ text }, _options = {}) {
|
|
32
|
+
const segments = text.replace(/^(?:[a-z]:|~)?[\\/]*/i, '').split(/[\\/]/).filter(Boolean);
|
|
33
|
+
return segments.length >= 2 && segments.every(segment => /^[a-z][a-z0-9]*(?:[-_.][a-z0-9]+)*$/.test(segment)
|
|
34
|
+
|| segment.split(/[-_.]/).every(word => /^[A-Z]$/.test(word)
|
|
35
|
+
|| /^[A-Z]?[a-z]{2,}(?:[A-Z][a-z]{2,}|[0-9]{1,4}(?=[A-Z][a-z]{2,}|$))*$/.test(word)));
|
|
36
|
+
}
|
|
37
|
+
/** A piece of a random key: long, both letter cases, and a character-class change at most positions.
|
|
38
|
+
* Readable names (CamelCase, SCREAMING_CASE, dated file names) change class far less often, and
|
|
39
|
+
* every piece of a 40-character base64 key split by '/' is 12+ characters in most keys. */
|
|
40
|
+
function isOpaqueSegment(segment) {
|
|
41
|
+
if (segment.length < 12 || !/[A-Z]/.test(segment) || !/[a-z]/.test(segment))
|
|
42
|
+
return false;
|
|
43
|
+
const changes = segment.match(/[A-Z](?=[^A-Z])|[a-z](?=[^a-z])|\d(?=\D)/g)?.length ?? 0;
|
|
44
|
+
return changes / (segment.length - 1) >= 0.4;
|
|
45
|
+
}
|
|
28
46
|
/** Stricter than error redaction: also detects unlabelled long tokens, while retaining ordinary URLs,
|
|
29
47
|
* filesystem paths, hexadecimal hashes, identifiers and prose. Strong credential syntax inside
|
|
30
48
|
* URLs or paths is still caught. */
|
|
@@ -42,15 +60,23 @@ export function redactUserText({ text }, { secretField = false } = {}) {
|
|
|
42
60
|
const base = redactSecretShapes({ text: protectedText, policy: { additionalRules: CHAT_RULES } });
|
|
43
61
|
let count = base.redactions;
|
|
44
62
|
// Exempt locations only AFTER specific secret rules. Only quoted paths may contain whitespace;
|
|
45
|
-
// unquoted paths end there
|
|
63
|
+
// unquoted paths and URLs end there, and at , | ; and invisible separators, so an adjacent
|
|
64
|
+
// secret glued after them cannot inherit the exemption.
|
|
46
65
|
// A quoted path ends at its first matching quote, which must sit on a word boundary, so an
|
|
47
66
|
// apostrophe later in the line ("don't") cannot close an unterminated quote and exempt the gap.
|
|
48
67
|
// Require a root boundary so a slash inside an opaque token cannot exempt that token, and a
|
|
49
68
|
// folder after the root so a bare "/" + token is not mistaken for a path.
|
|
50
|
-
const locations = [
|
|
51
|
-
.
|
|
69
|
+
const locations = [
|
|
70
|
+
...[...base.text.matchAll(/\b[a-z][a-z0-9+.-]*:\/\/[^\s<>"'`,|;\u200B-\u200D\uFEFF]+|(?<!\w)(["'`])(?:~\/|[a-z]:[\\/]|\\\\|\/)[^\r\n"'`\\/]+[\\/](?:(?!\1)[^\r\n])*\1(?!\w)|(?<![\w+\/=\\~.-])(?:~\/|[a-z]:[\\/]|\\\\|\/)[^\s<>"'`,|;\u200B-\u200D\uFEFF\\/]+[\\/][^\s<>"'`,|;\u200B-\u200D\uFEFF]*/gi)]
|
|
71
|
+
.map(match => ({ match, url: /^[a-z][a-z0-9+.-]*:\/\//i.test(match[0]) })),
|
|
72
|
+
// The part of a hand-typed unquoted path after a space (Work/CustomerPortal…) has no root.
|
|
73
|
+
...[...base.text.matchAll(/[^\s<>"'`,|;\u200B-\u200D\uFEFF]+/g)]
|
|
74
|
+
.filter(match => isIdentifierPath({ text: match[0] })).map(match => ({ match, url: false })),
|
|
75
|
+
].map(({ match, url }) => ({ start: match.index, end: match.index + match[0].length, url }));
|
|
52
76
|
let cleaned = base.text.replace(/(?<![A-Za-z0-9_+\/=-])[A-Za-z0-9_+\/=-]{40,}(?![A-Za-z0-9_+\/=-])/g, (candidate, offset) => {
|
|
53
|
-
|
|
77
|
+
const location = locations.find(span => offset >= span.start && offset + candidate.length <= span.end);
|
|
78
|
+
// A key glued straight onto a path (/proj/KEY) is still checked; URLs keep their opaque ids.
|
|
79
|
+
if (location && (location.url || !candidate.split(/[\\/]/).some(isOpaqueSegment)))
|
|
54
80
|
return candidate;
|
|
55
81
|
if (/^[a-f0-9]+$/i.test(candidate) || !/[A-Za-z]/.test(candidate))
|
|
56
82
|
return candidate;
|
|
@@ -12,6 +12,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
12
12
|
* the slots a host supplies; it does not itself know what a "library
|
|
13
13
|
* picker" or "session mode" is.
|
|
14
14
|
*/
|
|
15
|
+
import { FolderChips } from './FolderChips.js';
|
|
15
16
|
import { useEffect, useLayoutEffect, useRef, useState, } from 'react';
|
|
16
17
|
import { RemixIcon } from '@jini-ai/ui';
|
|
17
18
|
import { useT } from '../hooks/context.js';
|
|
@@ -320,7 +321,7 @@ export function Composer({ composer, onSend, disabled = false, sendDisabled = fa
|
|
|
320
321
|
event.preventDefault();
|
|
321
322
|
runComposerHostEffect('attachmentPicker.onFiles', () => attachmentPicker.onFiles(files));
|
|
322
323
|
}
|
|
323
|
-
return (_jsxs("div", { className: "jini-composer", children: [_jsx(AccessorySlot, { className: "jini-composer-leading", children: leadingAccessories }), _jsx(AttachmentTray, { attachments: composer.attachments, onRemove: composer.removeAttachment }), _jsx("textarea", { ref: textareaRef, className: "jini-composer-input", value: composer.draft, placeholder: placeholder ?? t('Send a message…'), disabled: disabled, onChange: (e) => {
|
|
324
|
+
return (_jsxs("div", { className: "jini-composer", children: [_jsx(AccessorySlot, { className: "jini-composer-leading", children: leadingAccessories }), _jsxs("div", { className: "jini-composer-attachments", children: [_jsx(AttachmentTray, { attachments: composer.attachments, onRemove: composer.removeAttachment }), _jsx(FolderChips, { folders: composer.folders, onRemove: composer.removeFolder })] }), _jsx("textarea", { ref: textareaRef, className: "jini-composer-input", value: composer.draft, placeholder: placeholder ?? t('Send a message…'), disabled: disabled, onChange: (e) => {
|
|
324
325
|
composer.setDraft(e.target.value);
|
|
325
326
|
setSlashActiveIndex(0);
|
|
326
327
|
setDismissedSlashDraft(null);
|
|
@@ -3,6 +3,8 @@ interface CopyMessageButtonProps {
|
|
|
3
3
|
text: string;
|
|
4
4
|
/** Accessible name while idle; swaps to a "Copied" label once the copy lands. */
|
|
5
5
|
label: string;
|
|
6
|
+
/** Optional hover tooltip; icon-only buttons outside a message action row have no other visible label. */
|
|
7
|
+
title?: string | undefined;
|
|
6
8
|
}
|
|
7
9
|
/**
|
|
8
10
|
* Quiet, icon-only copy affordance shared by the user- and assistant-message
|
|
@@ -11,8 +13,9 @@ interface CopyMessageButtonProps {
|
|
|
11
13
|
* the icon swap and the accessible announcement of the transient state.
|
|
12
14
|
* @param props.text - Exact string to copy, verbatim.
|
|
13
15
|
* @param props.label - Idle accessible name (already translated by the caller).
|
|
16
|
+
* @param props.title - Optional hover tooltip (already translated); swaps to "Copied" like the label.
|
|
14
17
|
* @complexity O(1) — one child hook call, no branching beyond the copied-state read.
|
|
15
18
|
*/
|
|
16
|
-
export declare function CopyMessageButton({ text, label }: CopyMessageButtonProps): import("react").JSX.Element;
|
|
19
|
+
export declare function CopyMessageButton({ text, label, title }: CopyMessageButtonProps): import("react").JSX.Element;
|
|
17
20
|
export {};
|
|
18
21
|
//# sourceMappingURL=CopyMessageButton.d.ts.map
|
|
@@ -10,12 +10,13 @@ import { Icon } from './Icon.js';
|
|
|
10
10
|
* the icon swap and the accessible announcement of the transient state.
|
|
11
11
|
* @param props.text - Exact string to copy, verbatim.
|
|
12
12
|
* @param props.label - Idle accessible name (already translated by the caller).
|
|
13
|
+
* @param props.title - Optional hover tooltip (already translated); swaps to "Copied" like the label.
|
|
13
14
|
* @complexity O(1) — one child hook call, no branching beyond the copied-state read.
|
|
14
15
|
*/
|
|
15
|
-
export function CopyMessageButton({ text, label }) {
|
|
16
|
+
export function CopyMessageButton({ text, label, title }) {
|
|
16
17
|
const t = useT();
|
|
17
18
|
const { copied, copy } = useCopyToClipboard();
|
|
18
|
-
return (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "jini-message-action-btn", "aria-label": copied ? t('Copied') : label, onClick: () => {
|
|
19
|
+
return (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "jini-message-action-btn", "aria-label": copied ? t('Copied') : label, title: title && (copied ? t('Copied') : title), onClick: () => {
|
|
19
20
|
void copy(text);
|
|
20
21
|
}, children: _jsx(Icon, { name: copied ? 'check' : 'copy' }) }), _jsx("span", { className: "jini-message-copy-status", "aria-live": "polite", children: copied ? t('Copied to clipboard') : '' })] }));
|
|
21
22
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
type OnRemove = (input: {
|
|
2
|
+
path: string;
|
|
3
|
+
}, options?: {}) => void;
|
|
4
|
+
/**
|
|
5
|
+
* Shared composer/sent-message folder indicator; references never participate in uploads.
|
|
6
|
+
* One folder shows its chip directly; a "Folders (n)" toggle would only repeat its name.
|
|
7
|
+
*/
|
|
8
|
+
export declare function FolderChips({ folders, onRemove }: {
|
|
9
|
+
folders?: readonly string[] | undefined;
|
|
10
|
+
onRemove?: OnRemove | undefined;
|
|
11
|
+
}): import("react").JSX.Element | null;
|
|
12
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=FolderChips.d.ts.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useId, useState } from 'react';
|
|
3
|
+
import { useT } from '../hooks/context.js';
|
|
4
|
+
import { CopyMessageButton } from './CopyMessageButton.js';
|
|
5
|
+
import { Icon } from './Icon.js';
|
|
6
|
+
/** Display only: splitting both separators never changes the exact path carried or copied. */
|
|
7
|
+
function folderName({ path }, _options = {}) {
|
|
8
|
+
return path.split(/[\\/]/).filter(Boolean).at(-1) ?? path;
|
|
9
|
+
}
|
|
10
|
+
/** One folder: last name (full path on hover), copy, and × when removable. */
|
|
11
|
+
function FolderChip({ path, onRemove }) {
|
|
12
|
+
const t = useT();
|
|
13
|
+
const copyLabel = t('Copy folder path: {path}', { path });
|
|
14
|
+
const removeLabel = t('Remove folder: {path}', { path });
|
|
15
|
+
return _jsxs("li", { className: "jini-folder-chips__item", children: [_jsx(Icon, { name: "folder" }), _jsx("span", { className: "jini-folder-chips__name", title: path, "aria-label": path, children: folderName({ path }) }), _jsx(CopyMessageButton, { text: path, label: copyLabel, title: copyLabel }), onRemove && _jsx("button", { type: "button", className: "jini-folder-chips__remove", title: removeLabel, "aria-label": removeLabel, onClick: () => onRemove({ path }), children: _jsx(Icon, { name: "close" }) })] });
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Shared composer/sent-message folder indicator; references never participate in uploads.
|
|
19
|
+
* One folder shows its chip directly; a "Folders (n)" toggle would only repeat its name.
|
|
20
|
+
*/
|
|
21
|
+
export function FolderChips({ folders = [], onRemove }) {
|
|
22
|
+
const t = useT();
|
|
23
|
+
const [expanded, setExpanded] = useState(false);
|
|
24
|
+
const id = useId();
|
|
25
|
+
if (folders.length === 0)
|
|
26
|
+
return null;
|
|
27
|
+
const list = (listId) => _jsx("ul", { id: listId, className: "jini-folder-chips__list", children: folders.map(path => _jsx(FolderChip, { path: path, onRemove: onRemove }, path)) });
|
|
28
|
+
if (folders.length === 1)
|
|
29
|
+
return _jsx("div", { className: "jini-folder-chips", children: list() });
|
|
30
|
+
return _jsxs("div", { className: "jini-folder-chips", children: [_jsxs("button", { type: "button", className: "jini-folder-chips__toggle", "aria-expanded": expanded, "aria-controls": id, onClick: () => setExpanded(value => !value), children: [_jsx(Icon, { name: "folder" }), _jsx("span", { children: t('Folders ({count})', { count: folders.length }) }), _jsx(Icon, { name: expanded ? 'chevron-down' : 'chevron-right' })] }), expanded && list(id)] });
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=FolderChips.js.map
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* AgentIcon and WorkingDirPicker.
|
|
8
8
|
*/
|
|
9
9
|
import type { SVGProps } from 'react';
|
|
10
|
-
export type IconName = 'spinner' | 'check' | 'close' | 'chevron-down' | 'chevron-left' | 'chevron-right' | 'attach' | 'file' | 'image' | 'x' | 'comment' | 'search' | 'plus' | 'pencil' | 'copy' | 'retry';
|
|
10
|
+
export type IconName = 'spinner' | 'check' | 'close' | 'chevron-down' | 'chevron-left' | 'chevron-right' | 'attach' | 'folder' | 'file' | 'image' | 'x' | 'comment' | 'search' | 'plus' | 'pencil' | 'copy' | 'retry';
|
|
11
11
|
interface IconProps extends Omit<SVGProps<SVGSVGElement>, 'name'> {
|
|
12
12
|
name: IconName;
|
|
13
13
|
size?: number | string;
|
|
@@ -15,6 +15,7 @@ const ICON_GLYPHS = {
|
|
|
15
15
|
'chevron-left': _jsx("path", { d: "m15 18-6-6 6-6" }),
|
|
16
16
|
'chevron-right': _jsx("path", { d: "m9 18 6-6-6-6" }),
|
|
17
17
|
attach: _jsx("path", { d: "m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48" }),
|
|
18
|
+
folder: _jsx("path", { d: "M3 7V5a2 2 0 0 1 2-2h5l2 3h7a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7Z" }),
|
|
18
19
|
file: _jsxs(_Fragment, { children: [_jsx("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z" }), _jsx("path", { d: "M14 2v6h6" }), _jsx("path", { d: "M8 13h8M8 17h6" })] }),
|
|
19
20
|
image: _jsxs(_Fragment, { children: [_jsx("rect", { width: "18", height: "18", x: "3", y: "3", rx: "2" }), _jsx("circle", { cx: "9", cy: "9", r: "2" }), _jsx("path", { d: "m21 15-5-5L5 21" })] }),
|
|
20
21
|
pencil: _jsxs(_Fragment, { children: [_jsx("path", { d: "m16 3 5 5L8 21H3v-5L16 3Z" }), _jsx("path", { d: "m14 5 5 5" })] }),
|
|
@@ -10,6 +10,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
10
10
|
* scroll-anchoring state lives in `useMessageListAutoScroll`
|
|
11
11
|
* (`MessageList.hooks.ts`); see that file for why it is presentational.
|
|
12
12
|
*/
|
|
13
|
+
import { FolderChips } from './FolderChips.js';
|
|
13
14
|
import { memo, useMemo } from 'react';
|
|
14
15
|
import { projectAcceptedInputMessages } from '../../core/accepted-input.js';
|
|
15
16
|
import { isTerminalRunStatus } from '../../core/index.js';
|
|
@@ -55,6 +56,6 @@ function QueuedMessage({ prompt, renderAttachment }) {
|
|
|
55
56
|
const t = useT();
|
|
56
57
|
const attachments = prompt.attachments ?? [];
|
|
57
58
|
const gallery = useAttachmentGallery({ attachments });
|
|
58
|
-
return (_jsxs("div", { className: "jini-chat-pane__queued", role: "status", "aria-label": t('Message queued — not yet sent, will send once the current run finishes'), "data-testid": "chat-pane-queued", "data-agent-element": "chat-message-pending", "data-agent-role": "region", "data-agent-label": "A message queued to send once the current run finishes", children: [_jsx("div", { children: t('Queued — sends when this reply finishes') }), _jsx("div", { className: "jini-chat-pane__queued-text", children: prompt.text }), attachments.length > 0 && _jsx("div", { className: "jini-message-attachments", children: attachments.map(attachment => _jsx(SentAttachmentButton, { attachment: attachment, onOpen: gallery.open, renderAttachment: renderAttachment }, attachment.path)) }), gallery.opened !== null && _jsx(AttachmentPreviewModal, { attachment: gallery.opened, onClose: gallery.close, navigation: gallery.navigation }), _jsx("div", { className: "jini-chat-pane__queued-actions", children: _jsx("button", { type: "button", className: "jini-chat-pane__queued-cancel", onClick: () => prompt.onCancel(), title: t('Cancel queued message'), children: t('Cancel') }) })] }));
|
|
59
|
+
return (_jsxs("div", { className: "jini-chat-pane__queued", role: "status", "aria-label": t('Message queued — not yet sent, will send once the current run finishes'), "data-testid": "chat-pane-queued", "data-agent-element": "chat-message-pending", "data-agent-role": "region", "data-agent-label": "A message queued to send once the current run finishes", children: [_jsx("div", { children: t('Queued — sends when this reply finishes') }), prompt.folders?.length ? _jsx("div", { className: "jini-message-attachments", children: _jsx(FolderChips, { folders: prompt.folders }) }) : null, _jsx("div", { className: "jini-chat-pane__queued-text", children: prompt.text }), attachments.length > 0 && _jsx("div", { className: "jini-message-attachments", children: attachments.map(attachment => _jsx(SentAttachmentButton, { attachment: attachment, onOpen: gallery.open, renderAttachment: renderAttachment }, attachment.path)) }), gallery.opened !== null && _jsx(AttachmentPreviewModal, { attachment: gallery.opened, onClose: gallery.close, navigation: gallery.navigation }), _jsx("div", { className: "jini-chat-pane__queued-actions", children: _jsx("button", { type: "button", className: "jini-chat-pane__queued-cancel", onClick: () => prompt.onCancel(), title: t('Cancel queued message'), children: t('Cancel') }) })] }));
|
|
59
60
|
}
|
|
60
61
|
//# sourceMappingURL=MessageList.js.map
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { FolderChips } from './FolderChips.js';
|
|
2
3
|
import { projectAcceptedInputEvents } from '../../core/accepted-input.js';
|
|
3
4
|
import { MidRunUserMessage, MID_RUN_USER_MESSAGE_EXT_EVENT_NAME } from './MidRunUserMessage.js';
|
|
4
5
|
import { useSurfaceAnswerContexts } from './mcp-ui-question-input.js';
|
|
@@ -184,7 +185,7 @@ function UserMessageRow({ message, renderAttachment }) {
|
|
|
184
185
|
// different messages opening their own attachments independently is fine and expected.
|
|
185
186
|
// Previous/next steps through this message's images only.
|
|
186
187
|
const gallery = useAttachmentGallery({ attachments: message.attachments ?? NO_ATTACHMENTS });
|
|
187
|
-
return (_jsxs("div", { className: "jini-message jini-message-user", "data-message-id": message.id, "data-agent-element": `chat-message-${message.id}`, "data-agent-role": "region", "data-agent-label": "A message from the user", children: [message.attachments
|
|
188
|
+
return (_jsxs("div", { className: "jini-message jini-message-user", "data-message-id": message.id, "data-agent-element": `chat-message-${message.id}`, "data-agent-role": "region", "data-agent-label": "A message from the user", children: [(message.attachments?.length || message.folders?.length) ? (_jsxs("div", { className: "jini-message-attachments", children: [_jsx(FolderChips, { folders: message.folders }), (message.attachments ?? []).map((a) => (_jsx(SentAttachmentButton, { attachment: a, onOpen: gallery.open, renderAttachment: renderAttachment }, a.path)))] })) : null, _jsx("div", { className: "jini-message-content", children: message.content }), _jsx("div", { className: "jini-message-actions jini-message-actions--user", children: _jsx(CopyMessageButton, { text: message.content, label: t('Copy message') }) }), gallery.opened ? (_jsx(AttachmentPreviewModal, { attachment: gallery.opened, onClose: gallery.close, navigation: gallery.navigation })) : null] }));
|
|
188
189
|
}
|
|
189
190
|
function AssistantMessageRow({ message, usageSummary, showUsageTotal, showUsageLine, toolCallDisplay, showThinking, runStreaming, runSending, sendingLabel, runSucceeded, questionFormInteractive, questionFormSubmittedAnswers, onQuestionFormSubmit, projectFileNames, onRequestOpenFile, }) {
|
|
190
191
|
const registry = useExtEventRendererRegistry();
|
|
@@ -10,6 +10,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
10
10
|
* It is stored as an event of the assistant turn, not a `ChatMessage` of its own, because the agent folded it
|
|
11
11
|
* into the turn it was already running — there is no separate reply to hang off it.
|
|
12
12
|
*/
|
|
13
|
+
import { FolderChips } from './FolderChips.js';
|
|
13
14
|
import { acceptedInputEvent } from '../../core/accepted-input.js';
|
|
14
15
|
import { SentAttachmentButton } from './SentAttachment.js';
|
|
15
16
|
import { CopyMessageButton } from './CopyMessageButton.js';
|
|
@@ -37,7 +38,7 @@ export function MidRunUserMessage({ events, runStreaming = false, transcriptRow
|
|
|
37
38
|
return null;
|
|
38
39
|
const receipt = acceptedInputEvent({ event: { kind: 'ext', name: MID_RUN_USER_MESSAGE_EVENT_NAME, data } });
|
|
39
40
|
// MOVE: A single live delivery may contain several queued inputs; display their original receipts.
|
|
40
|
-
const inputs = receipt?.data.inputs ?? [{ id: field(data, 'id') ?? 'mid-run', text }];
|
|
41
|
+
const inputs = receipt?.data.inputs ?? [receipt?.data ?? { id: field(data, 'id') ?? 'mid-run', text }];
|
|
41
42
|
// A run that ended without reading the message hands it to the next turn; it is no longer queued here.
|
|
42
43
|
return _jsx(_Fragment, { children: inputs.map(input => _jsx(MidRunInput, { input: input, transcriptRow: transcriptRow, queued: runStreaming && (input.deliveryState ?? receipt?.data.deliveryState) === 'queued' }, input.id)) });
|
|
43
44
|
}
|
|
@@ -48,7 +49,7 @@ function MidRunInput({ input, queued, transcriptRow }) {
|
|
|
48
49
|
const t = useT();
|
|
49
50
|
const attachments = input.attachments ?? [];
|
|
50
51
|
const gallery = useAttachmentGallery({ attachments });
|
|
51
|
-
return (_jsxs("div", { className: `jini-message jini-message-user${transcriptRow ? '' : ' 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)) }), _jsx("div", { className: "jini-message-actions jini-message-actions--user", children: _jsx(CopyMessageButton, { text: input.text, label: t('Copy message') }) }), gallery.opened !== null && _jsx(AttachmentPreviewModal, { attachment: gallery.opened, onClose: gallery.close, navigation: gallery.navigation })] }));
|
|
52
|
+
return (_jsxs("div", { className: `jini-message jini-message-user${transcriptRow ? '' : ' 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') }), input.folders?.length ? _jsx("div", { className: "jini-message-attachments", children: _jsx(FolderChips, { folders: input.folders }) }) : null, _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)) }), _jsx("div", { className: "jini-message-actions jini-message-actions--user", children: _jsx(CopyMessageButton, { text: input.text, label: t('Copy message') }) }), gallery.opened !== null && _jsx(AttachmentPreviewModal, { attachment: gallery.opened, onClose: gallery.close, navigation: gallery.navigation })] }));
|
|
52
53
|
}
|
|
53
54
|
/**
|
|
54
55
|
* Registers {@link MidRunUserMessage} against the ext-event registry.
|
|
@@ -271,6 +271,7 @@ function useComposerHandlePublisher({ pane }, { composerHandle }) {
|
|
|
271
271
|
if (!composerHandle)
|
|
272
272
|
return undefined;
|
|
273
273
|
composerHandle.current = {
|
|
274
|
+
addFolders: pane.composer.addFolders,
|
|
274
275
|
insertText: (text) => {
|
|
275
276
|
pane.composer.setDraft(appendComposerDiscovery({ draft: composerDraftRef.current, insertText: text }));
|
|
276
277
|
},
|
|
@@ -50,12 +50,12 @@ const ATTACHMENTS_ONLY_PROMPT = 'Review the attached file(s).';
|
|
|
50
50
|
*/
|
|
51
51
|
function composerPrompt(composer) {
|
|
52
52
|
return composer.draft.trim()
|
|
53
|
-
|| (composer.attachments.length > 0 ? ATTACHMENTS_ONLY_PROMPT : '');
|
|
53
|
+
|| (composer.attachments.length > 0 || composer.folders?.length ? ATTACHMENTS_ONLY_PROMPT : '');
|
|
54
54
|
}
|
|
55
55
|
/** History hydration can replace a controller without an edit. Compare the accepted empty draft
|
|
56
56
|
* and staged files so holding Enter cannot requeue an attachment-only submission on that render. */
|
|
57
57
|
function isRepeatedComposerSubmission({ composer, submitted }, _options = {}) {
|
|
58
|
-
return submitted === composer || (submitted !== null && composer.draft.trim() === '' && submitted.attachments === composer.attachments);
|
|
58
|
+
return submitted === composer || (submitted !== null && composer.draft.trim() === '' && submitted.attachments === composer.attachments && submitted.folders === composer.folders);
|
|
59
59
|
}
|
|
60
60
|
function composerDeliveryBlocked({ delivering, starting, composer, submitted }, _options = {}) {
|
|
61
61
|
return delivering || starting || isRepeatedComposerSubmission({ composer, submitted });
|
|
@@ -331,6 +331,8 @@ export function useChatPane(options) {
|
|
|
331
331
|
latestComposerRef.current = composer;
|
|
332
332
|
const latestDraftRef = useRef(composer.draft);
|
|
333
333
|
latestDraftRef.current = composer.draft;
|
|
334
|
+
const latestFoldersRef = useRef(composer.folders ?? []);
|
|
335
|
+
latestFoldersRef.current = composer.folders ?? [];
|
|
334
336
|
const latestAttachmentsRef = useRef(composer.attachments);
|
|
335
337
|
latestAttachmentsRef.current = composer.attachments;
|
|
336
338
|
useEffect(() => {
|
|
@@ -440,6 +442,7 @@ export function useChatPane(options) {
|
|
|
440
442
|
const restoreQueuedDraft = useCallback(({ inputs, text, attachments }, _options = {}) => {
|
|
441
443
|
if (inputs === undefined)
|
|
442
444
|
return;
|
|
445
|
+
latestComposerRef.current.addFolders({ folders: inputs.flatMap(input => input.folders ?? []) });
|
|
443
446
|
latestComposerRef.current.setDraft([text, latestDraftRef.current].filter(Boolean).join('\n\n'));
|
|
444
447
|
const staged = new Set(latestAttachmentsRef.current.map(attachment => attachment.path));
|
|
445
448
|
for (const attachment of attachments) {
|
|
@@ -449,20 +452,21 @@ export function useChatPane(options) {
|
|
|
449
452
|
}, []);
|
|
450
453
|
/** A refused composer send is kept for Retry and goes back only into a composer that is still
|
|
451
454
|
* empty: text typed (or files staged) while it was sending are never overwritten. */
|
|
452
|
-
const restoreRefusedDraft = useCallback(({ redaction, draft, attachments }, _options = {}) => {
|
|
453
|
-
refusedSendRef.current = { redaction, draft, attachments };
|
|
455
|
+
const restoreRefusedDraft = useCallback(({ redaction, draft, attachments, folders = [] }, _options = {}) => {
|
|
456
|
+
refusedSendRef.current = { redaction, draft, attachments, folders };
|
|
457
|
+
latestComposerRef.current.addFolders({ folders });
|
|
454
458
|
if (latestDraftRef.current.trim() !== '' || latestAttachmentsRef.current.length > 0)
|
|
455
459
|
return;
|
|
456
460
|
latestComposerRef.current.setDraft(draft);
|
|
457
461
|
for (const attachment of attachments)
|
|
458
462
|
latestComposerRef.current.addAttachment(attachment);
|
|
459
463
|
}, []);
|
|
460
|
-
const finishPromptSend = useCallback(({ accepted, inputs, redaction, submittedDraft, attachments, composerDraft }, _options = {}) => {
|
|
464
|
+
const finishPromptSend = useCallback(({ accepted, inputs, redaction, submittedDraft, attachments, composerDraft, folders }, _options = {}) => {
|
|
461
465
|
if (!accepted) {
|
|
462
466
|
if (composerDraft === undefined)
|
|
463
467
|
restoreQueuedDraft({ inputs, text: redaction.text, attachments });
|
|
464
468
|
else
|
|
465
|
-
restoreRefusedDraft({ redaction, draft: composerDraft, attachments });
|
|
469
|
+
restoreRefusedDraft({ redaction, draft: composerDraft, attachments, folders });
|
|
466
470
|
return;
|
|
467
471
|
}
|
|
468
472
|
refusedSendRef.current = null;
|
|
@@ -471,11 +475,17 @@ export function useChatPane(options) {
|
|
|
471
475
|
latestComposerRef.current.setDraft('');
|
|
472
476
|
latestComposerRef.current.closeMention();
|
|
473
477
|
}
|
|
478
|
+
// Composer/queued sends already took these references. A later pick, even of the same path,
|
|
479
|
+
// belongs to the next draft and must survive this send's asynchronous acceptance.
|
|
480
|
+
if (composerDraft === undefined && inputs === undefined) {
|
|
481
|
+
for (const path of folders)
|
|
482
|
+
latestComposerRef.current.removeFolder({ path });
|
|
483
|
+
}
|
|
474
484
|
consumeDeliveredAttachments({ attachments });
|
|
475
485
|
}, [consumeDeliveredAttachments, restoreQueuedDraft, restoreRefusedDraft]);
|
|
476
486
|
/** `composerDraft`: the composer text this send already took out of the composer, so the
|
|
477
487
|
* composer is never cleared again on acceptance and a refusal hands the text back. */
|
|
478
|
-
const sendGuardedPrompt = useCallback(async ({ redaction, inputs, attachments = composer.attachments, composerDraft }, _options = {}) => {
|
|
488
|
+
const sendGuardedPrompt = useCallback(async ({ redaction, inputs, attachments = composer.attachments, composerDraft, folders = inputs ? inputs.flatMap(input => input.folders ?? []) : composer.folders ?? [] }, _options = {}) => {
|
|
479
489
|
const trimmed = redaction.text.trim();
|
|
480
490
|
assertPromptSendable({ trimmed, sendBlocker });
|
|
481
491
|
if (startingPromptRef.current)
|
|
@@ -494,6 +504,7 @@ export function useChatPane(options) {
|
|
|
494
504
|
// Empty attachment arrays remain absent at the transport boundary.
|
|
495
505
|
attachments: attachments.length === 0 ? undefined : attachments,
|
|
496
506
|
context,
|
|
507
|
+
folders: folders.length ? folders : undefined,
|
|
497
508
|
acceptedInputs: inputs,
|
|
498
509
|
onAccepted: ({ inputs: acceptedInputs }) => {
|
|
499
510
|
accepted = true;
|
|
@@ -503,7 +514,7 @@ export function useChatPane(options) {
|
|
|
503
514
|
} }));
|
|
504
515
|
if (!mountedRef.current || generation !== typedAnswerGenerationRef.current)
|
|
505
516
|
return;
|
|
506
|
-
finishPromptSend({ accepted, inputs, redaction, submittedDraft, attachments, composerDraft });
|
|
517
|
+
finishPromptSend({ accepted, inputs, redaction, submittedDraft, attachments, composerDraft, folders });
|
|
507
518
|
}
|
|
508
519
|
finally {
|
|
509
520
|
startingPromptRef.current = false;
|
|
@@ -520,24 +531,27 @@ export function useChatPane(options) {
|
|
|
520
531
|
return;
|
|
521
532
|
const draft = latestDraftRef.current;
|
|
522
533
|
const attachments = latestAttachmentsRef.current;
|
|
534
|
+
const folders = latestFoldersRef.current;
|
|
535
|
+
for (const path of folders)
|
|
536
|
+
latestComposerRef.current.removeFolder({ path });
|
|
523
537
|
latestComposerRef.current.setDraft('');
|
|
524
538
|
latestComposerRef.current.closeMention();
|
|
525
539
|
for (const attachment of attachments)
|
|
526
540
|
latestComposerRef.current.removeAttachment(attachment.path);
|
|
527
541
|
const redaction = guard.redact({ text: prompt.trim() }, {});
|
|
528
542
|
try {
|
|
529
|
-
await sendGuardedPrompt({ redaction, attachments, composerDraft: draft });
|
|
543
|
+
await sendGuardedPrompt({ redaction, attachments, folders, composerDraft: draft });
|
|
530
544
|
}
|
|
531
545
|
catch (error) {
|
|
532
546
|
// A host hook that throws (run context, activity) must not cost the typed text either.
|
|
533
|
-
restoreRefusedDraft({ redaction, draft, attachments });
|
|
547
|
+
restoreRefusedDraft({ redaction, draft, attachments, folders });
|
|
534
548
|
throw error;
|
|
535
549
|
}
|
|
536
550
|
}, [guard.redact, restoreRefusedDraft, sendBlocker, sendGuardedPrompt]);
|
|
537
551
|
// MOVE: Every queue path retains per-message receipts; appendQueuedTurn owns their one-turn delivery.
|
|
538
552
|
// A turn queued against another conversation is not joined — it would never be flushed there anyway.
|
|
539
553
|
const enqueueTurn = useCallback(({ turn, inputs, attachments = composer.attachments }, _options = {}) => {
|
|
540
|
-
const accepted = inputs ?? [{ id: createChatLocalId({}), text: turn.text, attachments,
|
|
554
|
+
const accepted = inputs ?? [{ id: createChatLocalId({}), text: turn.text, attachments, folders: composer.folders ?? [],
|
|
541
555
|
...(turn.secretRedacted ? { secretRedaction: { secretRedacted: true, count: turn.count } } : {}) }];
|
|
542
556
|
const sameConversation = queuedConversationIdRef.current === options.conversationId;
|
|
543
557
|
queuedConversationIdRef.current = options.conversationId;
|
|
@@ -550,7 +564,9 @@ export function useChatPane(options) {
|
|
|
550
564
|
// MOVE: Take files out of the composer without rotating their batch: every queued message ships together.
|
|
551
565
|
for (const attachment of attachments)
|
|
552
566
|
latestComposerRef.current.removeAttachment(attachment.path);
|
|
553
|
-
|
|
567
|
+
for (const path of composer.folders ?? [])
|
|
568
|
+
latestComposerRef.current.removeFolder({ path });
|
|
569
|
+
}, [composer.folders, composer.attachments, options.conversationId, recordAcceptedInput]);
|
|
554
570
|
const answerWaitingQuestion = useCallback(async ({ deliver, questionId, text }) => {
|
|
555
571
|
const safeText = guard.redactText(text);
|
|
556
572
|
const generation = typedAnswerGenerationRef.current;
|
|
@@ -647,7 +663,8 @@ export function useChatPane(options) {
|
|
|
647
663
|
queuedForLiveDeliveryRef.current = queuedTurn === null || queuedForLiveDeliveryRef.current;
|
|
648
664
|
}
|
|
649
665
|
// MOVE: The existing queue owns its files; a later steer must join it to preserve send order.
|
|
650
|
-
|
|
666
|
+
// Folder context is attached on ordinary run start; live messages have no such envelope.
|
|
667
|
+
const target = uploadPending || composer.folders?.length || queuedInputs.some(input => input.liveMessageId === undefined) ? null : findMidRunTarget({
|
|
651
668
|
deliver: options.deliverMidRunMessage,
|
|
652
669
|
blocker: sendBlocker,
|
|
653
670
|
attachmentCount: composer.attachments.length,
|
|
@@ -686,7 +703,7 @@ export function useChatPane(options) {
|
|
|
686
703
|
await sendOrdinaryTurn(prompt);
|
|
687
704
|
return;
|
|
688
705
|
}
|
|
689
|
-
const typedAnswer = findTypedAnswerTarget({
|
|
706
|
+
const typedAnswer = composer.folders?.length ? null : findTypedAnswerTarget({
|
|
690
707
|
deliver: options.deliverTypedAnswer,
|
|
691
708
|
blocker: sendBlocker,
|
|
692
709
|
attachmentCount: composer.attachments.length,
|
|
@@ -715,7 +732,7 @@ export function useChatPane(options) {
|
|
|
715
732
|
}
|
|
716
733
|
if (sendBlocker !== null || startingPromptRef.current)
|
|
717
734
|
return;
|
|
718
|
-
await sendGuardedPrompt({ redaction: refused.redaction, attachments: refused.attachments, composerDraft: refused.draft });
|
|
735
|
+
await sendGuardedPrompt({ redaction: refused.redaction, attachments: refused.attachments, composerDraft: refused.draft, folders: refused.folders });
|
|
719
736
|
}, [send, sendBlocker, sendGuardedPrompt]);
|
|
720
737
|
const sendAsNewMessage = useCallback(async () => {
|
|
721
738
|
const prompt = composerPrompt(composer);
|
|
@@ -773,7 +790,7 @@ export function useChatPane(options) {
|
|
|
773
790
|
}
|
|
774
791
|
if (activeUploadCount > 0 || attachmentError !== null)
|
|
775
792
|
return;
|
|
776
|
-
const target = queuedForLiveDeliveryRef.current ? findMidRunTarget({
|
|
793
|
+
const target = queuedForLiveDeliveryRef.current && !queuedInputs.some(input => input.folders?.length) ? findMidRunTarget({
|
|
777
794
|
deliver: options.deliverMidRunMessage,
|
|
778
795
|
blocker: sendBlocker, attachmentCount: queuedInputs.flatMap(input => input.attachments).length, messages: conversation.messages,
|
|
779
796
|
}) : null;
|