@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
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { pickerModelValue, pickerCatalogNote, pickerModelOptions, shortModelLabel } from '../model-options.js';
|
|
3
|
+
import { useId } from 'react';
|
|
3
4
|
import { createPortal } from 'react-dom';
|
|
4
5
|
import { AgentIcon, RemixIcon, SearchableModelSelect } from '@jini-ai/ui';
|
|
6
|
+
import { InfoTip } from '@jini-ai/ui/admin-widgets';
|
|
5
7
|
import { assessNativeToolPolicy } from '@jini-ai/agent-runtime/native-tool-policy';
|
|
6
8
|
import { useT } from '../../../hooks/context.js';
|
|
7
9
|
import { definedProps } from '../../../util/defined-props.js';
|
|
@@ -18,27 +20,37 @@ export function runtimeAgentStatus({ agent }) {
|
|
|
18
20
|
return 'Installed · sign-in required';
|
|
19
21
|
return agent.version ?? 'Installed';
|
|
20
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* An agent's `fullCliAccessNote` as a warning icon whose tip (hover, focus, tap) holds the text.
|
|
25
|
+
* It was a paragraph beside the model picker that squeezed the composer row (owner 2026-10-30).
|
|
26
|
+
* `descriptionId` lets the agent's own radio in the picker list read the same text.
|
|
27
|
+
*/
|
|
28
|
+
function FullAccessNoteTip({ note, descriptionId, t }) {
|
|
29
|
+
return (_jsx("span", { className: "jini-runtime-access-note", children: _jsx(InfoTip, { label: t(note), triggerLabel: t('Full computer access'), descriptionId: descriptionId, placement: "top", icon: _jsx(RemixIcon, { name: "alert-line", size: 16 }) }) }));
|
|
30
|
+
}
|
|
21
31
|
/** Selection preflight uses the launch assessor; it never substitutes for enforcement. */
|
|
22
32
|
function NativeToolPolicyWarning({ agent, policy, isApi }) {
|
|
23
33
|
const t = useT();
|
|
24
34
|
if (!agent || isApi)
|
|
25
35
|
return null;
|
|
26
36
|
const warning = policy ? assessNativeToolPolicy({ def: agent, policy }) : null;
|
|
27
|
-
return _jsxs(_Fragment, { children: [agent.fullCliAccessNote ? _jsx(
|
|
37
|
+
return _jsxs(_Fragment, { children: [agent.fullCliAccessNote ? _jsx(FullAccessNoteTip, { note: agent.fullCliAccessNote, t: t }) : null, warning ? _jsx("p", { role: "alert", children: t(warning) }) : null] });
|
|
28
38
|
}
|
|
29
39
|
function RuntimeModeButtons({ executionMode, daemonOnline, apiModeAvailable, onExecutionModeChange, t, }) {
|
|
30
40
|
const localModeLabel = executionMode === 'local' ? t('active') : '';
|
|
31
41
|
return (_jsxs(_Fragment, { children: [_jsxs("button", { type: "button", className: `jini-runtime-mode${executionMode === 'local' ? ' is-active' : ''}`, disabled: !daemonOnline && executionMode !== 'local', onClick: () => onExecutionModeChange?.('local'), children: [_jsx(RemixIcon, { name: "terminal-box-line", size: 18 }), _jsx("span", { children: t('Use Local CLI') }), _jsx("span", { className: "jini-runtime-mode__meta", children: !daemonOnline ? t('offline') : localModeLabel }), executionMode === 'local' ? (_jsx(RemixIcon, { name: "check-line", size: 19, className: "jini-runtime-check" })) : null] }), _jsxs("button", { type: "button", className: `jini-runtime-mode${executionMode === 'api' ? ' is-active' : ''}`, disabled: !apiModeAvailable, onClick: () => onExecutionModeChange?.('api'), children: [_jsx(RemixIcon, { name: "link", size: 18 }), _jsx("span", { children: t('Use API · BYOK') }), !apiModeAvailable ? _jsx("span", { className: "jini-runtime-mode__meta", children: t('not configured') }) : null, executionMode === 'api' ? (_jsx(RemixIcon, { name: "check-line", size: 19, className: "jini-runtime-check" })) : null] })] }));
|
|
32
42
|
}
|
|
33
43
|
function RuntimeAgentList({ orderedAgents, value, onChange, agentIconBasePath, t, }) {
|
|
44
|
+
const noteIdPrefix = useId();
|
|
34
45
|
if (orderedAgents.length === 0) {
|
|
35
46
|
return _jsx("div", { className: "jini-runtime-empty", children: t('No available code agents') });
|
|
36
47
|
}
|
|
37
|
-
return (_jsx(_Fragment, { children: orderedAgents.map((agent) => {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
48
|
+
return (_jsx(_Fragment, { children: orderedAgents.map((agent) => (_jsx(RuntimeAgentRow, { agent: agent, active: agent.id === value.agentId, noteId: agent.fullCliAccessNote ? `${noteIdPrefix}-${agent.id}-note` : undefined, onChange: onChange, agentIconBasePath: agentIconBasePath, t: t }, agent.id))) }));
|
|
49
|
+
}
|
|
50
|
+
function RuntimeAgentRow({ agent, active, noteId, onChange, agentIconBasePath, t }) {
|
|
51
|
+
const status = runtimeAgentStatus({ agent: agent });
|
|
52
|
+
// The note's icon is a control of its own, so it sits beside the radio, never inside it.
|
|
53
|
+
return (_jsxs("div", { className: "jini-runtime-agent-row", children: [_jsxs("button", { type: "button", role: "radio", "aria-checked": active, className: `jini-runtime-agent${active ? ' is-active' : ''}`, "aria-current": active ? 'true' : undefined, "aria-label": `${agent.name} · ${status}`, ...(noteId ? { 'aria-describedby': noteId } : {}), onClick: () => onChange(defaultChatPaneSelection({ agent: agent })), children: [_jsx(AgentIcon, { id: agent.id, size: 20, className: "jini-runtime-agent-icon", ...definedProps({ source: { basePath: agentIconBasePath } }) }), _jsx("span", { className: "jini-runtime-agent__copy", children: _jsx("strong", { children: agent.name }) }), agent.supportsTools === false ? (_jsx("span", { className: "jini-runtime-agent__badge", title: t("This CLI has no MCP support, so it cannot use this app's tools."), children: t('No tools') })) : null, _jsx("span", { className: "jini-runtime-agent__status", children: active ? t('selected') : status }), active ? (_jsx(RemixIcon, { name: "check-line", size: 19, className: "jini-runtime-check" })) : null] }), agent.fullCliAccessNote ? _jsx(FullAccessNoteTip, { note: agent.fullCliAccessNote, descriptionId: noteId, t: t }) : null] }));
|
|
42
54
|
}
|
|
43
55
|
/**
|
|
44
56
|
* The composer bar's "Model ⌄" and "Effort ⌄" drop-ups for the selected CLI. These replace the
|
|
@@ -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
|
},
|
|
@@ -302,11 +303,11 @@ function buildChatPaneSlots({ surfaceSlots, embed, renderRuntimePicker }, { lead
|
|
|
302
303
|
footerAccessories: embed ? undefined : renderRuntimePicker(),
|
|
303
304
|
} });
|
|
304
305
|
}
|
|
305
|
-
function ChatPaneTranscript({ pane, emptyState, embed, suggestions, onSelectSuggestion, pendingPrompts, projectFileNameSet, toolCallDisplay, showUsageLine, showThinking, t, }) {
|
|
306
|
+
function ChatPaneTranscript({ pane, sendingLabel, emptyState, embed, suggestions, onSelectSuggestion, pendingPrompts, projectFileNameSet, toolCallDisplay, showUsageLine, showThinking, t, }) {
|
|
306
307
|
if (pane.conversation.messages.length === 0 && emptyState) {
|
|
307
308
|
return (_jsxs("div", { className: "jini-message-list jini-message-list--empty", children: [emptyState, embed && _jsx(ChatPaneSuggestionsRow, { suggestions: suggestions, onSelect: onSelectSuggestion, t: t })] }));
|
|
308
309
|
}
|
|
309
|
-
return (_jsx(MessageList, { messages: pane.conversation.messages, isStreaming: pane.conversation.isStreaming, scrollIntent: pane.conversation.scrollIntent, onScrolled: pane.conversation.acknowledgeScroll, pendingPrompts: pendingPrompts, toolCallDisplay: toolCallDisplay, showUsageLine: showUsageLine, showThinking: showThinking, ...(projectFileNameSet === undefined ? {} : { projectFileNames: projectFileNameSet }) }));
|
|
310
|
+
return (_jsx(MessageList, { messages: pane.conversation.messages, ...definedProps({ source: { sendingLabel } }), isStreaming: pane.conversation.isStreaming, scrollIntent: pane.conversation.scrollIntent, onScrolled: pane.conversation.acknowledgeScroll, pendingPrompts: pendingPrompts, toolCallDisplay: toolCallDisplay, showUsageLine: showUsageLine, showThinking: showThinking, ...(projectFileNameSet === undefined ? {} : { projectFileNames: projectFileNameSet }) }));
|
|
310
311
|
}
|
|
311
312
|
/** The controls-area suggestions row shows on every workspace surface, and on the embed surface
|
|
312
313
|
* only when the empty transcript has no `emptyState` to host it instead. */
|
|
@@ -404,6 +405,6 @@ export function ChatPane(props) {
|
|
|
404
405
|
const selectSuggestion = useChatPaneSuggestion({ rootRef, setDraft: pane.composer.setDraft });
|
|
405
406
|
const openLinkInNewTab = useChatPaneNewTabLinks();
|
|
406
407
|
const { pendingPrompts, projectFileNameSet } = useChatPaneTranscriptProps({ pane }, definedProps({ source: { projectFileNames: p.projectFileNames } }));
|
|
407
|
-
return (_jsxs("section", { className: chatPaneClassName(p.variant, p.className), style: p.style, "data-activity": pane.activity, ref: rootRef, onClickCapture: openLinkInNewTab, children: [_jsx("style", { "data-jini-chat-pane-styles": "true", children: CHAT_PANE_STYLES }), p.header ?? defaultHeader(p.title, pane.reset, t), p.agentPluginsMode !== 'off' && _jsx(AgentPluginUsage, { conversationId: pane.conversation.conversationId, messages: pane.conversation.messages }), _jsxs("div", { className: "jini-chat-pane__body", children: [_jsx(ChatPaneTranscript, { pane: pane, emptyState: p.emptyState, embed: surface.embed, suggestions: p.suggestions, onSelectSuggestion: selectSuggestion, pendingPrompts: pendingPrompts, projectFileNameSet: projectFileNameSet, toolCallDisplay: p.toolCallDisplay, showUsageLine: p.showUsageLine, showThinking: p.showThinking, t: t }), _jsxs("div", { className: "jini-chat-pane__controls", ref: controlsRef, children: [showsControlsSuggestions(surface.embed, pane.conversation.messages.length, p.emptyState) && (_jsx(ChatPaneSuggestionsRow, { suggestions: p.suggestions, onSelect: selectSuggestion, t: t })), pane.secretRedacted ?
|
|
408
|
+
return (_jsxs("section", { className: chatPaneClassName(p.variant, p.className), style: p.style, "data-activity": pane.activity, ref: rootRef, onClickCapture: openLinkInNewTab, children: [_jsx("style", { "data-jini-chat-pane-styles": "true", children: CHAT_PANE_STYLES }), p.header ?? defaultHeader(p.title, pane.reset, t), p.agentPluginsMode !== 'off' && _jsx(AgentPluginUsage, { conversationId: pane.conversation.conversationId, messages: pane.conversation.messages }), _jsxs("div", { className: "jini-chat-pane__body", children: [_jsx(ChatPaneTranscript, { pane: pane, sendingLabel: p.sendingLabel, emptyState: p.emptyState, embed: surface.embed, suggestions: p.suggestions, onSelectSuggestion: selectSuggestion, pendingPrompts: pendingPrompts, projectFileNameSet: projectFileNameSet, toolCallDisplay: p.toolCallDisplay, showUsageLine: p.showUsageLine, showThinking: p.showThinking, t: t }), _jsxs("div", { className: "jini-chat-pane__controls", ref: controlsRef, children: [showsControlsSuggestions(surface.embed, pane.conversation.messages.length, p.emptyState) && (_jsx(ChatPaneSuggestionsRow, { suggestions: p.suggestions, onSelect: selectSuggestion, t: t })), pane.secretRedacted ? (_jsxs("div", { className: "jini-chat-pane__status jini-chat-pane__status--dismissible", role: "status", children: [_jsx("span", { children: t(SECRET_REDACTED_NOTICE) }), _jsx("button", { type: "button", className: "jini-chat-pane__error-action", onClick: pane.dismissSecretRedacted, children: t('Dismiss') })] })) : null, _jsx(ChatPaneStatusMessages, { unavailable: unavailable, scanningAgents: runtimeView.scanningAgents, connectingAgents: runtimeView.connectingAgents, executionMode: p.executionMode, apiModeAvailable: p.apiModeAvailable, onExecutionModeChange: p.onExecutionModeChange, sendError: pane.conversation.sendError, onRetrySend: () => void pane.retrySend(), attachmentError: pane.attachmentError, dropReadError: fileDrop.dropReadError, workingDirectoryError: pane.workingDirectoryError, workingDirectoryPending: pane.workingDirectoryPending, workingDirectoryInvalid: pane.workingDirectoryInvalid, runtimeInventoryError: inventory.runtimeInventoryError, typedAnswerNotice: pane.typedAnswerNotice, t: t }), _jsx(ChatPaneComposerArea, { fileDrop: fileDrop, uploadAttachments: surface.uploadAttachments, pane: pane, disabled: p.disabled, unavailable: unavailable, placeholder: composerPlaceholder, slots: slots, attachmentAccept: p.attachmentAccept, workingDirectoryAccess: p.workingDirectoryAccess, workingDirectoryControlPlacement: surface.workingDirectoryControlPlacement, t: t })] })] }), p.footer] }));
|
|
408
409
|
}
|
|
409
410
|
//# sourceMappingURL=ChatPane.js.map
|
|
@@ -44,8 +44,10 @@ export interface UseChatPaneOptions extends UserTextRedactionOptions {
|
|
|
44
44
|
deliverMidRunMessage?: DeliverMidRunMessage;
|
|
45
45
|
}
|
|
46
46
|
export interface UseChatPaneResult extends UseChatPaneWorkingDirectoryResult {
|
|
47
|
-
/** Show a localized non-secret notice when a paste was removed. */
|
|
47
|
+
/** Show a localized non-secret notice when a paste was removed. Cleared by the next send or {@link dismissSecretRedacted}. */
|
|
48
48
|
secretRedacted: boolean;
|
|
49
|
+
/** Hides the redaction notice. */
|
|
50
|
+
dismissSecretRedacted: () => void;
|
|
49
51
|
conversation: UseConversationResult;
|
|
50
52
|
composer: UseComposerResult;
|
|
51
53
|
selection: ChatPaneAgentSelection;
|
|
@@ -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,
|
|
@@ -680,12 +697,13 @@ export function useChatPane(options) {
|
|
|
680
697
|
if (!prompt || composerDeliveryBlocked({ delivering: deliveringTypedAnswerRef.current, starting: startingPromptRef.current, composer, submitted: submittedComposerRef.current }))
|
|
681
698
|
return;
|
|
682
699
|
setTypedAnswerNotice(null);
|
|
700
|
+
guard.clearNotice();
|
|
683
701
|
// Until upload completes, an empty attachment list is not evidence of a text-only answer.
|
|
684
702
|
if (uploadingWhileStreaming) {
|
|
685
703
|
await sendOrdinaryTurn(prompt);
|
|
686
704
|
return;
|
|
687
705
|
}
|
|
688
|
-
const typedAnswer = findTypedAnswerTarget({
|
|
706
|
+
const typedAnswer = composer.folders?.length ? null : findTypedAnswerTarget({
|
|
689
707
|
deliver: options.deliverTypedAnswer,
|
|
690
708
|
blocker: sendBlocker,
|
|
691
709
|
attachmentCount: composer.attachments.length,
|
|
@@ -701,6 +719,7 @@ export function useChatPane(options) {
|
|
|
701
719
|
composer,
|
|
702
720
|
conversation.isStreaming,
|
|
703
721
|
conversation.messages,
|
|
722
|
+
guard.clearNotice,
|
|
704
723
|
options.deliverTypedAnswer,
|
|
705
724
|
sendBlocker,
|
|
706
725
|
sendOrdinaryTurn,
|
|
@@ -713,23 +732,25 @@ export function useChatPane(options) {
|
|
|
713
732
|
}
|
|
714
733
|
if (sendBlocker !== null || startingPromptRef.current)
|
|
715
734
|
return;
|
|
716
|
-
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 });
|
|
717
736
|
}, [send, sendBlocker, sendGuardedPrompt]);
|
|
718
737
|
const sendAsNewMessage = useCallback(async () => {
|
|
719
738
|
const prompt = composerPrompt(composer);
|
|
720
739
|
if (!prompt || composerDeliveryBlocked({ delivering: deliveringTypedAnswerRef.current, starting: startingPromptRef.current, composer, submitted: submittedComposerRef.current }))
|
|
721
740
|
return;
|
|
722
741
|
setTypedAnswerNotice(null);
|
|
742
|
+
guard.clearNotice();
|
|
723
743
|
await sendOrdinaryTurn(prompt);
|
|
724
|
-
}, [composer, sendOrdinaryTurn]);
|
|
744
|
+
}, [composer, guard.clearNotice, sendOrdinaryTurn]);
|
|
725
745
|
const interruptSend = useCallback(() => {
|
|
726
746
|
const prompt = composerPrompt(composer);
|
|
727
747
|
if (!prompt || composerDeliveryBlocked({ delivering: deliveringTypedAnswerRef.current, starting: startingPromptRef.current, composer, submitted: submittedComposerRef.current }))
|
|
728
748
|
return;
|
|
749
|
+
guard.clearNotice();
|
|
729
750
|
enqueueTurn({ turn: guard.redact({ text: prompt }, {}) });
|
|
730
751
|
composer.setDraft('');
|
|
731
752
|
conversation.cancel();
|
|
732
|
-
}, [composer, conversation, enqueueTurn, guard.redact]);
|
|
753
|
+
}, [composer, conversation, enqueueTurn, guard.clearNotice, guard.redact]);
|
|
733
754
|
/** Abort only uploads owned by the removed inputs; late completions must not refill the composer. */
|
|
734
755
|
const discardQueuedUploads = useCallback(({ id }, _options = {}) => {
|
|
735
756
|
for (const [controller, owner] of queuedUploadOwnersRef.current) {
|
|
@@ -769,7 +790,7 @@ export function useChatPane(options) {
|
|
|
769
790
|
}
|
|
770
791
|
if (activeUploadCount > 0 || attachmentError !== null)
|
|
771
792
|
return;
|
|
772
|
-
const target = queuedForLiveDeliveryRef.current ? findMidRunTarget({
|
|
793
|
+
const target = queuedForLiveDeliveryRef.current && !queuedInputs.some(input => input.folders?.length) ? findMidRunTarget({
|
|
773
794
|
deliver: options.deliverMidRunMessage,
|
|
774
795
|
blocker: sendBlocker, attachmentCount: queuedInputs.flatMap(input => input.attachments).length, messages: conversation.messages,
|
|
775
796
|
}) : null;
|
|
@@ -799,6 +820,7 @@ export function useChatPane(options) {
|
|
|
799
820
|
composer.reset();
|
|
800
821
|
setAttachmentError(null);
|
|
801
822
|
setTypedAnswerNotice(null);
|
|
823
|
+
guard.clearNotice();
|
|
802
824
|
typedAnswerGenerationRef.current += 1;
|
|
803
825
|
// Without this, a prompt queued behind a streaming run survives `reset()` and — once
|
|
804
826
|
// `conversation.cancel()` above clears the streaming blocker — the flush effect fires it into
|
|
@@ -808,9 +830,10 @@ export function useChatPane(options) {
|
|
|
808
830
|
queuedUploadOwnersRef.current.clear();
|
|
809
831
|
setQueuedInputs([]);
|
|
810
832
|
refusedSendRef.current = null;
|
|
811
|
-
}, [composer, conversation, options.initialMessages]);
|
|
833
|
+
}, [composer, conversation, guard.clearNotice, options.initialMessages]);
|
|
812
834
|
return {
|
|
813
835
|
secretRedacted: guard.secretRedacted,
|
|
836
|
+
dismissSecretRedacted: guard.clearNotice,
|
|
814
837
|
conversation,
|
|
815
838
|
composer,
|
|
816
839
|
selection,
|