@jini-ai/chat 0.6.5 → 0.6.7
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 +12 -0
- package/dist/core/accepted-input.d.ts +54 -0
- package/dist/core/accepted-input.js +165 -0
- package/dist/core/agent-plugin-chat-state.d.ts +43 -0
- package/dist/core/agent-plugin-chat-state.js +48 -0
- package/dist/core/assistant-client/a2ui-action-poster.js +32 -28
- package/dist/core/assistant-client/messages.d.ts +1 -1
- package/dist/core/assistant-client/messages.js +2 -2
- package/dist/core/assistant-client/transport.d.ts +2 -2
- package/dist/core/assistant-client/transport.js +184 -97
- package/dist/core/assistant-content.d.ts +2 -1
- package/dist/core/assistant-content.js +9 -11
- package/dist/core/browser/page-actions.js +4 -2
- package/dist/core/composer-history.d.ts +16 -9
- package/dist/core/composer-history.js +28 -16
- package/dist/core/conversation-title.d.ts +3 -0
- package/dist/core/conversation-title.js +3 -0
- package/dist/core/durable-projection.js +20 -13
- package/dist/core/durable-runs/continuation.d.ts +4 -0
- package/dist/core/durable-runs/continuation.js +35 -6
- package/dist/core/durable-runs/ledger-contracts.d.ts +2 -1
- package/dist/core/durable-runs/policies.d.ts +1 -0
- package/dist/core/durable-runs/policies.js +1 -0
- package/dist/core/durable-runs/ports.d.ts +6 -2
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +2 -0
- package/dist/core/messages.d.ts +2 -0
- package/dist/core/partial-json.d.ts +6 -7
- package/dist/core/partial-json.js +81 -47
- package/dist/core/persistence/title.js +4 -2
- package/dist/core/question-answer.d.ts +23 -0
- package/dist/core/question-answer.js +66 -0
- package/dist/core/question-form/format.d.ts +6 -6
- package/dist/core/question-form/format.js +16 -17
- package/dist/core/question-form/normalize.d.ts +18 -0
- package/dist/core/question-form/normalize.js +112 -122
- package/dist/core/question-form/scan.js +141 -203
- package/dist/core/run-activity.js +62 -49
- package/dist/core/run-events/entry.d.ts +10 -1
- package/dist/core/run-events/entry.js +117 -87
- package/dist/core/todos.d.ts +1 -1
- package/dist/core/todos.js +60 -85
- package/dist/core/tool-events.js +14 -12
- package/dist/core/transcript.js +62 -16
- package/dist/core/turn-usage.d.ts +2 -1
- package/dist/core/turn-usage.js +56 -27
- package/dist/core/user-text-redaction.js +3 -0
- package/dist/core/util/manifest.js +77 -82
- package/dist/core/util/markdown-context.js +51 -53
- package/dist/core/util/parser.js +136 -144
- package/dist/core/util/pointer.js +16 -16
- package/dist/core/util/strip.js +57 -67
- package/dist/core/util/validate.js +15 -17
- package/dist/embed-widget/browser/session-store.js +21 -12
- package/dist/embed-widget/transports/fetch-sse.js +108 -95
- package/dist/embed-widget/vanilla/index.js +81 -70
- package/dist/react/agent-bridge/frontend-session-bridge.js +80 -52
- package/dist/react/components/AttachmentPreviewModal.js +25 -1
- package/dist/react/components/ChatSettingsDialog.d.ts +13 -0
- package/dist/react/components/ChatSettingsDialog.hooks.d.ts +115 -0
- package/dist/react/components/ChatSettingsDialog.hooks.js +40 -0
- package/dist/react/components/ChatSettingsDialog.js +72 -0
- package/dist/react/components/ChatSettingsSegmented.d.ts +18 -0
- package/dist/react/components/ChatSettingsSegmented.js +11 -0
- package/dist/react/components/Composer.d.ts +5 -1
- package/dist/react/components/Composer.js +126 -82
- package/dist/react/components/ConversationList.js +25 -39
- package/dist/react/components/ConversationTitleEditor.d.ts +27 -0
- package/dist/react/components/ConversationTitleEditor.js +54 -0
- package/dist/react/components/Icon.d.ts +1 -1
- package/dist/react/components/Icon.js +25 -45
- package/dist/react/components/Markdown.js +134 -123
- package/dist/react/components/McpUiSurfaceCard.d.ts +2 -2
- package/dist/react/components/McpUiSurfaceCard.js +30 -9
- package/dist/react/components/MessageList.d.ts +13 -13
- package/dist/react/components/MessageList.hooks.d.ts +6 -1
- package/dist/react/components/MessageList.hooks.js +19 -3
- package/dist/react/components/MessageList.js +34 -10
- package/dist/react/components/MessageRow.js +48 -32
- package/dist/react/components/MessageRow.usage.d.ts +5 -2
- package/dist/react/components/MessageRow.usage.js +20 -11
- package/dist/react/components/MidRunUserMessage.js +16 -3
- package/dist/react/components/QuestionForm.js +148 -76
- package/dist/react/components/QuestionsPanel.js +1 -1
- package/dist/react/components/TodoCard.js +12 -6
- package/dist/react/components/ToolCard.js +47 -40
- package/dist/react/components/composer-discovery.js +6 -1
- package/dist/react/components/mcp-ui-surface-state.d.ts +6 -11
- package/dist/react/components/mcp-ui-surface-state.js +23 -8
- package/dist/react/ext-event-renderer-registry.d.ts +8 -0
- package/dist/react/ext-event-renderer-registry.js +5 -2
- package/dist/react/features/chat-pane/components/AgentPluginUsage.d.ts +7 -0
- package/dist/react/features/chat-pane/components/AgentPluginUsage.js +45 -0
- package/dist/react/features/chat-pane/components/AgentRuntimePicker.js +55 -19
- package/dist/react/features/chat-pane/components/ChatPane.d.ts +1 -1
- package/dist/react/features/chat-pane/components/ChatPane.js +192 -74
- package/dist/react/features/chat-pane/components/RuntimeDropUp.d.ts +29 -0
- package/dist/react/features/chat-pane/components/RuntimeDropUp.js +45 -0
- package/dist/react/features/chat-pane/create-daemon-attachment-uploader.js +80 -47
- package/dist/react/features/chat-pane/create-mcp-ui-tool-caller.js +44 -40
- package/dist/react/features/chat-pane/create-mid-run-message-poster.d.ts +6 -6
- package/dist/react/features/chat-pane/create-mid-run-message-poster.js +8 -8
- package/dist/react/features/chat-pane/create-typed-answer-poster.d.ts +3 -3
- package/dist/react/features/chat-pane/create-typed-answer-poster.js +3 -3
- package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.d.ts +6 -1
- package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.js +11 -5
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +12 -7
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +340 -149
- package/dist/react/features/chat-pane/hooks/useChatPaneControlsHeight.hooks.js +7 -6
- package/dist/react/features/chat-pane/hooks/useChatPaneNewTabLinks.hooks.js +28 -10
- package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.d.ts +3 -3
- package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.js +60 -12
- package/dist/react/features/chat-pane/hooks/useRuntimeDropUp.hooks.d.ts +49 -0
- package/dist/react/features/chat-pane/hooks/useRuntimeDropUp.hooks.js +143 -0
- package/dist/react/features/chat-pane/model-options.d.ts +18 -1
- package/dist/react/features/chat-pane/model-options.js +49 -20
- package/dist/react/features/chat-pane/rules.d.ts +8 -9
- package/dist/react/features/chat-pane/rules.js +84 -46
- package/dist/react/features/chat-pane/styles.d.ts +1 -1
- package/dist/react/features/chat-pane/styles.js +156 -37
- package/dist/react/features/chat-pane/types.d.ts +17 -8
- package/dist/react/hooks/assistant-chats.js +12 -5
- package/dist/react/hooks/composer-draft-cache.js +116 -118
- package/dist/react/hooks/composer-history-storage.js +5 -5
- package/dist/react/hooks/fab-position.js +7 -4
- package/dist/react/hooks/useAnsweredCards.d.ts +21 -4
- package/dist/react/hooks/useAnsweredCards.js +38 -11
- package/dist/react/hooks/useArtifactStream.js +28 -22
- package/dist/react/hooks/useComposer.d.ts +20 -0
- package/dist/react/hooks/useComposer.js +22 -9
- package/dist/react/hooks/useComposerHistory.hooks.d.ts +6 -1
- package/dist/react/hooks/useComposerHistory.hooks.js +39 -19
- package/dist/react/hooks/useComposerHistoryKeys.hooks.js +24 -14
- package/dist/react/hooks/useConversation.d.ts +7 -0
- package/dist/react/hooks/useConversation.js +23 -10
- package/dist/react/hooks/useExtEventGroups.js +39 -39
- package/dist/react/hooks/useQuestionForms.js +21 -26
- package/dist/react/hooks/useRunStream.js +10 -2
- package/dist/react/index.d.ts +7 -1
- package/dist/react/index.js +9 -0
- package/dist/react/message-blocks.d.ts +6 -4
- package/dist/react/message-blocks.js +94 -72
- package/dist/react/slots.d.ts +2 -2
- package/dist/react/tool-call-display.js +19 -17
- package/dist/server/durable-runs/recover.d.ts +1 -1
- package/dist/server/durable-runs/recover.js +139 -76
- package/dist/server/run-finalizer/entry.js +25 -15
- package/dist/store/legacy/sqlite/conversations.d.ts +11 -11
- package/dist/store/legacy/sqlite/conversations.js +69 -98
- package/dist/store/legacy/sqlite/messages.d.ts +10 -2
- package/dist/store/legacy/sqlite/messages.js +113 -71
- package/dist/store/runtime/chat-history-store.js +9 -3
- package/dist/store/runtime/durable-run-store.d.ts +2 -2
- package/dist/store/runtime/durable-run-store.js +165 -117
- package/dist/store/runtime/history-tools.js +118 -82
- package/dist/store/runtime/run-ledger.d.ts +4 -1
- package/dist/store/runtime/run-ledger.js +56 -16
- package/dist/store/sql/cursor.js +38 -18
- package/dist/store/sql/store.d.ts +18 -0
- package/dist/store/sql/store.js +46 -15
- package/package.json +12 -10
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { AgentPluginUsage } from './AgentPluginUsage.js';
|
|
2
3
|
import { SECRET_REDACTED_NOTICE } from '../../../../core/user-text-redaction.js';
|
|
3
|
-
import { useEffect, useMemo, useRef } from 'react';
|
|
4
|
+
import { useEffect, useId, useMemo, useRef } from 'react';
|
|
4
5
|
import { WorkingDirPicker } from '@jini-ai/ui';
|
|
5
6
|
import { Composer } from '../../../components/Composer.js';
|
|
6
7
|
import { appendComposerDiscovery } from '../../../components/composer-discovery.js';
|
|
7
8
|
import { MessageList } from '../../../components/MessageList.js';
|
|
8
9
|
import { useT } from '../../../hooks/context.js';
|
|
9
10
|
import { definedProps } from '../../../util/defined-props.js';
|
|
10
|
-
import { describeTypedAnswerNotice, isChatPaneApiModeConfigured, isChatPaneFileDropEnabled } from '../rules.js';
|
|
11
|
+
import { describeChatPaneSendBlocker, describeTypedAnswerNotice, isChatPaneApiModeConfigured, isChatPaneFileDropEnabled } from '../rules.js';
|
|
11
12
|
import { useChatPane } from '../hooks/useChatPane.hooks.js';
|
|
12
13
|
import { useChatPaneAgentControl } from '../hooks/useChatPaneAgentControl.hooks.js';
|
|
13
14
|
import { useChatPaneComposerPlaceholder } from '../hooks/useChatPaneComposerPlaceholder.hooks.js';
|
|
@@ -75,10 +76,54 @@ function ChatPaneNoUsableCliMessage({ executionMode, apiModeAvailable, onExecuti
|
|
|
75
76
|
}
|
|
76
77
|
return (_jsxs("div", { className: "jini-chat-pane__error", role: "alert", children: [t('No usable CLI is selected.'), ' ', _jsx("button", { type: "button", className: "jini-chat-pane__error-action", onClick: () => onExecutionModeChange('api'), children: t('Switch to BYOK') })] }));
|
|
77
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* `unavailable` alone conflates "inventory still loading" with "inventory loaded, nothing
|
|
81
|
+
* usable" — both present as `selectedAgent === undefined`. `scanningAgents` is the signal
|
|
82
|
+
* that tells them apart, mirroring the `workingDirectoryPending`/`workingDirectoryInvalid`
|
|
83
|
+
* pair below: a pending detection is a `status`, only a *finished* detection that found
|
|
84
|
+
* nothing is an `alert`. `connectingAgents` is a third state: the inventory could not be
|
|
85
|
+
* read at all (the host is unreachable, typically a daemon restart) and a retry is
|
|
86
|
+
* pending — a plain status too, never "No usable CLI", which only a SUCCESSFUL answer can
|
|
87
|
+
* establish.
|
|
88
|
+
*/
|
|
89
|
+
function ChatPaneAvailabilityStatus({ unavailable, scanningAgents, connectingAgents, executionMode, apiModeAvailable, onExecutionModeChange, t, }) {
|
|
90
|
+
if (!unavailable)
|
|
91
|
+
return null;
|
|
92
|
+
if (connectingAgents) {
|
|
93
|
+
return (_jsx("div", { className: "jini-chat-pane__status", role: "status", children: t('Connecting to the assistant…') }));
|
|
94
|
+
}
|
|
95
|
+
if (scanningAgents) {
|
|
96
|
+
return (_jsx("div", { className: "jini-chat-pane__status", role: "status", children: t('Loading available CLIs') }));
|
|
97
|
+
}
|
|
98
|
+
return (_jsx(ChatPaneNoUsableCliMessage, { executionMode: executionMode, apiModeAvailable: apiModeAvailable, onExecutionModeChange: onExecutionModeChange, t: t }));
|
|
99
|
+
}
|
|
100
|
+
function ChatPaneWorkingDirectoryStatus({ workingDirectoryPending, workingDirectoryInvalid, t, }) {
|
|
101
|
+
if (workingDirectoryPending) {
|
|
102
|
+
return (_jsx("div", { className: "jini-chat-pane__status", role: "status", children: t('Checking working directory…') }));
|
|
103
|
+
}
|
|
104
|
+
if (!workingDirectoryInvalid)
|
|
105
|
+
return null;
|
|
106
|
+
return (_jsx("div", { className: "jini-chat-pane__error", role: "alert", children: t('Working directory is unavailable.') }));
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* The typed-answer delivery notice. Only `'failed'` ever reaches it: `useChatPane` queues a
|
|
110
|
+
* `'not-pending'` refusal as one ordinary message itself (`answerWaitingQuestion`), without a notice.
|
|
111
|
+
*
|
|
112
|
+
* MOVE (from the retired "Send as a new message" button this notice once rendered for
|
|
113
|
+
* `'not-pending'`): the held answer's only way into a new run besides clearing the draft — an
|
|
114
|
+
* explicit choice, so a closed question never turns an answer into a paid run on a second Enter.
|
|
115
|
+
* The hook's automatic one-time fallback now owns that guarantee, which left the button unreachable.
|
|
116
|
+
*/
|
|
117
|
+
function ChatPaneTypedAnswerNotice({ typedAnswerNotice, t, }) {
|
|
118
|
+
if (!typedAnswerNotice)
|
|
119
|
+
return null;
|
|
120
|
+
return (_jsx("div", { className: "jini-chat-pane__error", role: "alert", children: t(describeTypedAnswerNotice({ notice: typedAnswerNotice })) }));
|
|
121
|
+
}
|
|
78
122
|
/** The pane's stack of mutually-independent error/status banners — each condition is its own
|
|
79
123
|
* source of truth, so they are rendered as siblings rather than folded into one derived state. */
|
|
80
|
-
function ChatPaneStatusMessages({
|
|
81
|
-
|
|
124
|
+
function ChatPaneStatusMessages({ attachmentError, dropReadError, workingDirectoryError, workingDirectoryPending, workingDirectoryInvalid, runtimeInventoryError, typedAnswerNotice, ...availability }) {
|
|
125
|
+
const { t } = availability;
|
|
126
|
+
return (_jsxs(_Fragment, { children: [_jsx(ChatPaneAvailabilityStatus, { ...availability }), attachmentError ? (_jsx("div", { className: "jini-chat-pane__error", role: "alert", children: attachmentError.message })) : null, dropReadError ? (_jsx("div", { className: "jini-chat-pane__error", role: "alert", children: dropReadError })) : null, workingDirectoryError ? (_jsx("div", { className: "jini-chat-pane__error", role: "alert", children: workingDirectoryError.message })) : null, _jsx(ChatPaneWorkingDirectoryStatus, { workingDirectoryPending: workingDirectoryPending, workingDirectoryInvalid: workingDirectoryInvalid, t: t }), runtimeInventoryError && !availability.connectingAgents ? (_jsx("div", { className: "jini-chat-pane__error", role: "alert", children: runtimeInventoryError.message })) : null, _jsx(ChatPaneTypedAnswerNotice, { typedAnswerNotice: typedAnswerNotice, t: t })] }));
|
|
82
127
|
}
|
|
83
128
|
function ChatPaneWorkingDirectoryBlock({ workingDirectoryAccess, workingDirectoryControlPlacement, pane, t, }) {
|
|
84
129
|
if (workingDirectoryAccess && workingDirectoryControlPlacement === 'below') {
|
|
@@ -166,63 +211,53 @@ function resolveComposerWorkingDirectory(workingDirectoryAccess, workingDirector
|
|
|
166
211
|
*
|
|
167
212
|
* The pending-turn strip previously lived here (a banner bolted above the composer). It now
|
|
168
213
|
* renders inside `<MessageList>` itself, as the newest transcript entry — see that component's
|
|
169
|
-
* `
|
|
214
|
+
* `pendingPrompts` prop and this component's own call site below. */
|
|
170
215
|
function ChatPaneComposerArea({ fileDrop, uploadAttachments, pane, disabled, unavailable, placeholder, slots, attachmentAccept, workingDirectoryAccess, workingDirectoryControlPlacement, t, }) {
|
|
216
|
+
const sendDisabledReasonId = useId();
|
|
217
|
+
const sendDisabledReason = pane.sendBlocker && pane.sendBlocker !== 'streaming'
|
|
218
|
+
? t(describeChatPaneSendBlocker(pane.sendBlocker))
|
|
219
|
+
: undefined;
|
|
171
220
|
return (_jsxs("div", { className: `jini-chat-pane__drop-target${fileDrop.draggingFiles ? ' is-dragging-files' : ''}`, "data-testid": "chat-pane-file-drop-target", "data-dragging-files": fileDrop.draggingFiles ? 'true' : 'false', ...resolveDropTargetProps(uploadAttachments, fileDrop), children: [fileDrop.draggingFiles ? (_jsx("span", { className: "jini-chat-pane__drop-announcement", role: "status", children: t('Drop files to attach') })) : null, _jsx(Composer, { composer: pane.composer, onSend: () => void pane.send(),
|
|
172
221
|
// Streaming no longer locks the textarea: `pane.send()` queues a turn typed during a run
|
|
173
222
|
// rather than refusing it, so the operator can keep writing while the agent thinks.
|
|
174
223
|
disabled: disabled || unavailable,
|
|
175
224
|
// The one blocker `send()` handles itself. Every other refusal still greys out the button.
|
|
176
|
-
sendDisabled: !pane.canSend && pane.sendBlocker !== 'streaming', running: pane.conversation.isStreaming, onCancel: pane.conversation.cancel, onInterrupt: pane.interruptSend, ...definedProps({ source: { placeholder } }), slots: slots, ...resolveComposerAttachmentPicker(uploadAttachments, pane, attachmentAccept), ...resolveComposerWorkingDirectory(workingDirectoryAccess, workingDirectoryControlPlacement, pane) }), _jsx(ChatPaneWorkingDirectoryBlock, { workingDirectoryAccess: workingDirectoryAccess, workingDirectoryControlPlacement: workingDirectoryControlPlacement, pane: pane, t: t })] }));
|
|
225
|
+
sendDisabled: !pane.canSend && pane.sendBlocker !== 'streaming', ...definedProps({ source: { sendDisabledReason, sendDisabledReasonId: sendDisabledReason ? sendDisabledReasonId : undefined } }), running: pane.conversation.isStreaming, onCancel: pane.conversation.cancel, onInterrupt: pane.interruptSend, ...definedProps({ source: { placeholder } }), slots: { ...slots, footerAccessories: _jsxs(_Fragment, { children: [sendDisabledReason ? _jsx("small", { id: sendDisabledReasonId, className: "jini-composer-send-blocker", children: sendDisabledReason }) : null, slots.footerAccessories] }) }, ...resolveComposerAttachmentPicker(uploadAttachments, pane, attachmentAccept), ...resolveComposerWorkingDirectory(workingDirectoryAccess, workingDirectoryControlPlacement, pane) }), _jsx(ChatPaneWorkingDirectoryBlock, { workingDirectoryAccess: workingDirectoryAccess, workingDirectoryControlPlacement: workingDirectoryControlPlacement, pane: pane, t: t })] }));
|
|
177
226
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
validateAttachments,
|
|
213
|
-
uploadAttachments,
|
|
214
|
-
onActivityChange,
|
|
215
|
-
onMessagesChange,
|
|
216
|
-
redactUserText,
|
|
217
|
-
onSecretRedacted,
|
|
218
|
-
workingDirectory,
|
|
219
|
-
initialWorkingDirectory,
|
|
220
|
-
onChangeWorkingDirectory,
|
|
221
|
-
workingDirectoryAccess,
|
|
222
|
-
apiModeConfigured,
|
|
223
|
-
deliverTypedAnswer,
|
|
224
|
-
deliverMidRunMessage,
|
|
225
|
-
} }));
|
|
227
|
+
const EMPTY_SUGGESTIONS = [];
|
|
228
|
+
const CHAT_PANE_DEFAULTS = {
|
|
229
|
+
agents: EMPTY_AGENTS,
|
|
230
|
+
runtimeStatusPollMs: 5_000,
|
|
231
|
+
variant: 'workspace',
|
|
232
|
+
scanningAgents: false,
|
|
233
|
+
daemonOnline: true,
|
|
234
|
+
executionMode: 'local',
|
|
235
|
+
apiModeAvailable: false,
|
|
236
|
+
suggestions: EMPTY_SUGGESTIONS,
|
|
237
|
+
workingDirectoryControlPlacement: 'below',
|
|
238
|
+
toolCallDisplay: 'show',
|
|
239
|
+
showUsageLine: true,
|
|
240
|
+
showThinking: false,
|
|
241
|
+
agentPluginsMode: 'auto',
|
|
242
|
+
disabled: false,
|
|
243
|
+
runtimePickerPlacement: 'up',
|
|
244
|
+
};
|
|
245
|
+
function applyChatPaneDefaults(props) {
|
|
246
|
+
const resolved = { ...props, ...CHAT_PANE_DEFAULTS };
|
|
247
|
+
for (const key of Object.keys(CHAT_PANE_DEFAULTS)) {
|
|
248
|
+
if (props[key] !== undefined)
|
|
249
|
+
Object.assign(resolved, { [key]: props[key] });
|
|
250
|
+
}
|
|
251
|
+
return resolved;
|
|
252
|
+
}
|
|
253
|
+
function resolveAgentControlOptions(agentControl) {
|
|
254
|
+
return definedProps({ source: {
|
|
255
|
+
enabled: agentControl?.enabled ?? false,
|
|
256
|
+
bridgeAccess: agentControl?.bridgeAccess,
|
|
257
|
+
webmcp: agentControl?.webmcp,
|
|
258
|
+
} });
|
|
259
|
+
}
|
|
260
|
+
function useComposerHandlePublisher({ pane }, { composerHandle }) {
|
|
226
261
|
// Mirrors `Composer.tsx`'s own `draftRef`: `composerHandle.insertText` (below) is called from
|
|
227
262
|
// OUTSIDE any render, so it cannot close over `pane.composer.draft` directly — that would freeze
|
|
228
263
|
// it at whatever the draft was on the render that captured it. Reassigned unconditionally every
|
|
@@ -245,14 +280,100 @@ export function ChatPane({ transport, agents: injectedAgents = EMPTY_AGENTS, run
|
|
|
245
280
|
// closure on every render rather than genuinely skipping renders — cheap, and it keeps
|
|
246
281
|
// `pane.composer.setDraft` from ever going stale if that reference ever changes.
|
|
247
282
|
}, [composerHandle, pane.composer]);
|
|
283
|
+
}
|
|
284
|
+
function useChatPaneTranscriptProps({ pane }, { projectFileNames }) {
|
|
285
|
+
// `MessageList` is memoized so a composer keystroke does not re-render the transcript; these two
|
|
286
|
+
// props would otherwise be fresh objects on every render and defeat it.
|
|
287
|
+
const { queuedMessages, cancelQueuedMessage } = pane;
|
|
288
|
+
const pendingPrompts = useMemo(() => queuedMessages.map(input => ({ ...input, onCancel: () => cancelQueuedMessage({ id: input.id }) })), [queuedMessages, cancelQueuedMessage]);
|
|
289
|
+
const projectFileNameSet = useMemo(() => (projectFileNames === undefined ? undefined : new Set(projectFileNames)), [projectFileNames]);
|
|
290
|
+
return { pendingPrompts, projectFileNameSet };
|
|
291
|
+
}
|
|
292
|
+
/** `composerSlots`'s type excludes both `leadingAccessories` and `footerAccessories` (see
|
|
293
|
+
* `ChatPaneProps.composerSlots`'s doc), so the two explicit keys below are never overwriting a
|
|
294
|
+
* caller-supplied value — they are the ONLY source for both. */
|
|
295
|
+
function buildChatPaneSlots({ surfaceSlots, embed, renderRuntimePicker }, { leadingAccessory }) {
|
|
296
|
+
return definedProps({ source: {
|
|
297
|
+
...surfaceSlots,
|
|
298
|
+
leadingAccessories: embed ? undefined : leadingAccessory,
|
|
299
|
+
footerAccessories: embed ? undefined : renderRuntimePicker(),
|
|
300
|
+
} });
|
|
301
|
+
}
|
|
302
|
+
function ChatPaneTranscript({ pane, emptyState, embed, suggestions, onSelectSuggestion, pendingPrompts, projectFileNameSet, toolCallDisplay, showUsageLine, showThinking, t, }) {
|
|
303
|
+
if (pane.conversation.messages.length === 0 && emptyState) {
|
|
304
|
+
return (_jsxs("div", { className: "jini-message-list jini-message-list--empty", children: [emptyState, embed && _jsx(ChatPaneSuggestionsRow, { suggestions: suggestions, onSelect: onSelectSuggestion, t: t })] }));
|
|
305
|
+
}
|
|
306
|
+
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 }) }));
|
|
307
|
+
}
|
|
308
|
+
/** The controls-area suggestions row shows on every workspace surface, and on the embed surface
|
|
309
|
+
* only when the empty transcript has no `emptyState` to host it instead. */
|
|
310
|
+
function showsControlsSuggestions(embed, messageCount, emptyState) {
|
|
311
|
+
return !embed || (messageCount === 0 && !emptyState);
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Renders the self-contained chat-pane composition, including runtime and
|
|
315
|
+
* working-directory orchestration owned by `@jini-ai/chat-react`.
|
|
316
|
+
*
|
|
317
|
+
* @complexity Time/space: O(n) in rendered messages, agents, and suggestions.
|
|
318
|
+
* @overallScore 100/100
|
|
319
|
+
*/
|
|
320
|
+
export function ChatPane(props) {
|
|
321
|
+
const p = applyChatPaneDefaults(props);
|
|
322
|
+
const t = useT();
|
|
323
|
+
const surface = useChatPaneSurface({
|
|
324
|
+
variant: p.variant,
|
|
325
|
+
composerSlots: p.composerSlots,
|
|
326
|
+
workingDirectoryControlPlacement: p.workingDirectoryControlPlacement,
|
|
327
|
+
uploadAttachments: p.uploadAttachments,
|
|
328
|
+
});
|
|
329
|
+
const composerPlaceholder = useChatPaneComposerPlaceholder({ placeholder: p.placeholder, placeholders: p.placeholders });
|
|
330
|
+
const inventory = useChatPaneRuntimeInventory(definedProps({ source: {
|
|
331
|
+
access: p.runtimeAccess,
|
|
332
|
+
initialAgents: p.agents,
|
|
333
|
+
pollIntervalMs: p.runtimeStatusPollMs,
|
|
334
|
+
} }));
|
|
335
|
+
const runtimeView = resolveRuntimeAccessView(p.runtimeAccess, p.agents, p.scanningAgents, p.daemonOnline, p.onRescanAgents, inventory);
|
|
336
|
+
// Computed once and threaded into both `useChatPane` (so `sendBlocker` agrees) and this
|
|
337
|
+
// component's own `unavailable` below (so the status banner and composer agree) — see
|
|
338
|
+
// `isChatPaneApiModeConfigured` for why API mode alone isn't enough.
|
|
339
|
+
const apiModeConfigured = isChatPaneApiModeConfigured({
|
|
340
|
+
executionMode: p.executionMode,
|
|
341
|
+
apiModeAvailable: p.apiModeAvailable,
|
|
342
|
+
byokRuntime: p.byokRuntime,
|
|
343
|
+
});
|
|
344
|
+
const pane = useChatPane(definedProps({ source: {
|
|
345
|
+
transport: p.transport,
|
|
346
|
+
agents: runtimeView.agents,
|
|
347
|
+
initialMessages: p.initialMessages,
|
|
348
|
+
refreshSignal: p.refreshSignal,
|
|
349
|
+
conversationId: p.conversationId,
|
|
350
|
+
initialSelection: p.initialSelection,
|
|
351
|
+
selection: p.selection,
|
|
352
|
+
onSelectionChange: p.onSelectionChange,
|
|
353
|
+
runContext: p.runContext,
|
|
354
|
+
initialDraft: p.initialDraft,
|
|
355
|
+
composerHistoryScope: p.composerHistoryScope,
|
|
356
|
+
composerHistoryStorage: p.composerHistoryStorage,
|
|
357
|
+
composerHistoryCap: p.composerHistoryCap,
|
|
358
|
+
validateAttachments: p.validateAttachments,
|
|
359
|
+
uploadAttachments: p.uploadAttachments,
|
|
360
|
+
onActivityChange: p.onActivityChange,
|
|
361
|
+
onMessagesChange: p.onMessagesChange,
|
|
362
|
+
redactUserText: p.redactUserText,
|
|
363
|
+
onSecretRedacted: p.onSecretRedacted,
|
|
364
|
+
workingDirectory: p.workingDirectory,
|
|
365
|
+
initialWorkingDirectory: p.initialWorkingDirectory,
|
|
366
|
+
onChangeWorkingDirectory: p.onChangeWorkingDirectory,
|
|
367
|
+
workingDirectoryAccess: p.workingDirectoryAccess,
|
|
368
|
+
apiModeConfigured,
|
|
369
|
+
deliverTypedAnswer: p.deliverTypedAnswer,
|
|
370
|
+
deliverMidRunMessage: p.deliverMidRunMessage,
|
|
371
|
+
} }));
|
|
372
|
+
useComposerHandlePublisher({ pane }, definedProps({ source: { composerHandle: p.composerHandle } }));
|
|
248
373
|
// No `runContext` here on purpose: agent-driven sends go through `pane.sendPrompt`, which builds
|
|
249
374
|
// the context from the SAME `runContext` already handed to `useChatPane` above. A second copy
|
|
250
375
|
// would be a second source of truth that could silently drift from the composer's.
|
|
251
|
-
useChatPaneAgentControl({ pane: pane },
|
|
252
|
-
enabled: agentControl?.enabled ?? false,
|
|
253
|
-
bridgeAccess: agentControl?.bridgeAccess,
|
|
254
|
-
webmcp: agentControl?.webmcp,
|
|
255
|
-
} }));
|
|
376
|
+
useChatPaneAgentControl({ pane: pane }, resolveAgentControlOptions(p.agentControl));
|
|
256
377
|
// Selection resolution only returns available agents, so absence normally means nothing usable
|
|
257
378
|
// is selected — except a configured BYOK turn (`apiModeConfigured`, computed above), which calls
|
|
258
379
|
// the provider directly over HTTP and never touches the CLI inventory.
|
|
@@ -260,28 +381,25 @@ export function ChatPane({ transport, agents: injectedAgents = EMPTY_AGENTS, run
|
|
|
260
381
|
const fileDrop = useChatPaneFileDrop({
|
|
261
382
|
enabled: isChatPaneFileDropEnabled({
|
|
262
383
|
canUpload: surface.uploadAttachments !== undefined,
|
|
263
|
-
disabled,
|
|
384
|
+
disabled: p.disabled,
|
|
264
385
|
unavailable,
|
|
265
386
|
uploading: pane.isUploadingAttachments,
|
|
266
387
|
}),
|
|
267
388
|
onFiles: pane.addAttachments,
|
|
268
389
|
});
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
390
|
+
const slots = buildChatPaneSlots({
|
|
391
|
+
surfaceSlots: surface.composerSlots,
|
|
392
|
+
embed: surface.embed,
|
|
393
|
+
renderRuntimePicker: () => (_jsx(AgentRuntimePicker, { agents: runtimeView.agents, value: pane.selection, onChange: pane.setSelection, ...definedProps({ source: { onRescan: runtimeView.rescanAgents } }), scanning: runtimeView.scanningAgents, daemonOnline: runtimeView.daemonOnline, placement: p.runtimePickerPlacement, executionMode: p.executionMode, apiModeAvailable: p.apiModeAvailable, ...definedProps({ source: {
|
|
394
|
+
onExecutionModeChange: p.onExecutionModeChange,
|
|
395
|
+
byokRuntime: p.byokRuntime,
|
|
396
|
+
onByokModelChange: p.onByokModelChange,
|
|
397
|
+
} }) })),
|
|
398
|
+
}, { leadingAccessory: p.leadingAccessory });
|
|
277
399
|
const { rootRef, controlsRef } = useChatPaneControlsHeight();
|
|
278
400
|
const selectSuggestion = useChatPaneSuggestion({ rootRef, setDraft: pane.composer.setDraft });
|
|
279
401
|
const openLinkInNewTab = useChatPaneNewTabLinks();
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
const { queuedPrompt, cancelQueued } = pane;
|
|
283
|
-
const pendingPrompt = useMemo(() => (queuedPrompt === null ? null : { text: queuedPrompt, onCancel: cancelQueued }), [queuedPrompt, cancelQueued]);
|
|
284
|
-
const projectFileNameSet = useMemo(() => (projectFileNames === undefined ? undefined : new Set(projectFileNames)), [projectFileNames]);
|
|
285
|
-
return (_jsxs("section", { className: chatPaneClassName(variant, className), style: style, "data-activity": pane.activity, ref: rootRef, onClickCapture: openLinkInNewTab, children: [_jsx("style", { "data-jini-chat-pane-styles": "true", children: CHAT_PANE_STYLES }), resolveChatPaneHeader(header, title, pane.reset, t), _jsxs("div", { className: "jini-chat-pane__body", children: [pane.conversation.messages.length === 0 && emptyState ? (_jsxs("div", { className: "jini-message-list jini-message-list--empty", children: [emptyState, surface.embed && _jsx(ChatPaneSuggestionsRow, { suggestions: suggestions, onSelect: selectSuggestion, t: t })] })) : (_jsx(MessageList, { messages: pane.conversation.messages, isStreaming: pane.conversation.isStreaming, scrollIntent: pane.conversation.scrollIntent, onScrolled: pane.conversation.acknowledgeScroll, pendingPrompt: pendingPrompt, toolCallDisplay: toolCallDisplay, showUsageLine: showUsageLine, showThinking: showThinking, ...(projectFileNameSet === undefined ? {} : { projectFileNames: projectFileNameSet }) })), _jsxs("div", { className: "jini-chat-pane__controls", ref: controlsRef, children: [(!surface.embed || (pane.conversation.messages.length === 0 && !emptyState)) && (_jsx(ChatPaneSuggestionsRow, { suggestions: suggestions, onSelect: selectSuggestion, t: t })), pane.secretRedacted ? _jsx("div", { className: "jini-chat-pane__notice", role: "status", children: t(SECRET_REDACTED_NOTICE) }) : null, _jsx(ChatPaneStatusMessages, { unavailable: unavailable, scanningAgents: runtimeView.scanningAgents, connectingAgents: runtimeView.connectingAgents, executionMode: executionMode, apiModeAvailable: apiModeAvailable, onExecutionModeChange: onExecutionModeChange, conversationError: pane.conversation.error, attachmentError: pane.attachmentError, dropReadError: fileDrop.dropReadError, workingDirectoryError: pane.workingDirectoryError, workingDirectoryPending: pane.workingDirectoryPending, workingDirectoryInvalid: pane.workingDirectoryInvalid, runtimeInventoryError: inventory.runtimeInventoryError, typedAnswerNotice: pane.typedAnswerNotice, onSendAsNewMessage: () => void pane.sendAsNewMessage(), t: t }), _jsx(ChatPaneComposerArea, { fileDrop: fileDrop, uploadAttachments: surface.uploadAttachments, pane: pane, disabled: disabled, unavailable: unavailable, placeholder: composerPlaceholder, slots: slots, attachmentAccept: attachmentAccept, workingDirectoryAccess: workingDirectoryAccess, workingDirectoryControlPlacement: surface.workingDirectoryControlPlacement, t: t })] })] }), footer] }));
|
|
402
|
+
const { pendingPrompts, projectFileNameSet } = useChatPaneTranscriptProps({ pane }, definedProps({ source: { projectFileNames: p.projectFileNames } }));
|
|
403
|
+
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 }), resolveChatPaneHeader(p.header, 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 ? _jsx("div", { className: "jini-chat-pane__notice", role: "status", children: t(SECRET_REDACTED_NOTICE) }) : null, _jsx(ChatPaneStatusMessages, { unavailable: unavailable, scanningAgents: runtimeView.scanningAgents, connectingAgents: runtimeView.connectingAgents, executionMode: p.executionMode, apiModeAvailable: p.apiModeAvailable, onExecutionModeChange: p.onExecutionModeChange, 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] }));
|
|
286
404
|
}
|
|
287
405
|
//# sourceMappingURL=ChatPane.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
export interface RuntimeDropUpProps {
|
|
3
|
+
/** Names both the trigger (with the current value appended) and the menu. */
|
|
4
|
+
label: string;
|
|
5
|
+
/** What the trigger shows: the current choice's display label. */
|
|
6
|
+
valueLabel: string;
|
|
7
|
+
/** A shorter form the trigger shows instead (e.g. "Opus 5.5"); the full `valueLabel` then
|
|
8
|
+
* appears in a hover/focus tooltip and stays the trigger's accessible name. */
|
|
9
|
+
triggerLabel?: string | undefined;
|
|
10
|
+
value: string;
|
|
11
|
+
options: readonly {
|
|
12
|
+
id: string;
|
|
13
|
+
label: string;
|
|
14
|
+
}[];
|
|
15
|
+
onSelect: (id: string) => void;
|
|
16
|
+
/** Small muted line above the options (e.g. the model catalog's "Cached · …" note). */
|
|
17
|
+
note?: string | undefined;
|
|
18
|
+
/** Menu width cap in px; the menu is also clamped to the viewport. */
|
|
19
|
+
width: number;
|
|
20
|
+
className?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* A compact, borderless "Label ⌄" trigger in the composer bar that opens a menu of choices above
|
|
24
|
+
* itself. Used for the model and the reasoning effort; the selection itself stays with the caller.
|
|
25
|
+
*
|
|
26
|
+
* @complexity Time/space: O(n) in the option count.
|
|
27
|
+
*/
|
|
28
|
+
export declare function RuntimeDropUp({ label, valueLabel, triggerLabel, value, options, onSelect, note, width, className, }: RuntimeDropUpProps): ReactNode;
|
|
29
|
+
//# sourceMappingURL=RuntimeDropUp.d.ts.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
3
|
+
import { RemixIcon, infoTipBubbleStyle, useInfoTip } from '@jini-ai/ui';
|
|
4
|
+
import { useRuntimeDropUp } from '../hooks/useRuntimeDropUp.hooks.js';
|
|
5
|
+
/** The full `valueLabel` in the same white bubble `InfoTip` uses, portalled to `document.body`. */
|
|
6
|
+
function RuntimeDropUpTipBubble({ tip, valueLabel }) {
|
|
7
|
+
return createPortal(_jsx("span", { ref: tip.bubbleRef, className: tip.placement === 'below' ? 'info-tip-bubble info-tip-bubble-below' : 'info-tip-bubble', role: "presentation", "aria-hidden": "true", style: infoTipBubbleStyle(tip.coords), children: valueLabel }), document.body);
|
|
8
|
+
}
|
|
9
|
+
function RuntimeDropUpMenu({ dropUp, label, value, options, onSelect, note }) {
|
|
10
|
+
if (!dropUp.open || !dropUp.position)
|
|
11
|
+
return null;
|
|
12
|
+
return (_jsxs("div", { ref: dropUp.menuRef, id: dropUp.menuId, role: "menu", "aria-label": label, className: "jini-runtime-dropup", style: dropUp.position, onKeyDown: dropUp.handleMenuKeyDown, children: [note ? _jsx("div", { className: "jini-runtime-dropup__note", children: note }) : null, options.map((option) => {
|
|
13
|
+
const active = option.id === value;
|
|
14
|
+
return (_jsxs("button", { type: "button", role: "menuitemradio", "aria-checked": active, className: `jini-runtime-dropup__item${active ? ' is-active' : ''}`, onClick: () => dropUp.choose(() => onSelect(option.id)), children: [_jsx("span", { className: "jini-runtime-dropup__item-label", children: option.label }), active ? _jsx(RemixIcon, { name: "check-line", size: 16, className: "jini-runtime-check" }) : null] }, option.id));
|
|
15
|
+
})] }));
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* A compact, borderless "Label ⌄" trigger in the composer bar that opens a menu of choices above
|
|
19
|
+
* itself. Used for the model and the reasoning effort; the selection itself stays with the caller.
|
|
20
|
+
*
|
|
21
|
+
* @complexity Time/space: O(n) in the option count.
|
|
22
|
+
*/
|
|
23
|
+
export function RuntimeDropUp({ label, valueLabel, triggerLabel, value, options, onSelect, note, width, className, }) {
|
|
24
|
+
const dropUp = useRuntimeDropUp({ width });
|
|
25
|
+
const { menuId, open, triggerRef, toggleOpen, handleTriggerKeyDown } = dropUp;
|
|
26
|
+
// The same white bubble as `InfoTip` (its hook places it), rather than the browser's own title
|
|
27
|
+
// tooltip; only when the trigger shows a shortened label, so there is something to reveal.
|
|
28
|
+
const tip = useInfoTip({}, { placement: 'top' });
|
|
29
|
+
const shortened = triggerLabel !== undefined && triggerLabel !== valueLabel;
|
|
30
|
+
// No tip over the open menu, which already lists the full name.
|
|
31
|
+
const tipVisible = shortened && !open;
|
|
32
|
+
const showTip = () => {
|
|
33
|
+
if (tipVisible)
|
|
34
|
+
tip.show();
|
|
35
|
+
};
|
|
36
|
+
return (_jsxs(_Fragment, { children: [_jsxs("button", { ref: triggerRef, type: "button", className: `jini-runtime-dropup-trigger${className ? ` ${className}` : ''}`, "aria-label": `${label}: ${valueLabel}`, "aria-haspopup": "menu", "aria-expanded": open, "aria-controls": menuId, ...(shortened ? {} : { title: valueLabel }), onClick: () => {
|
|
37
|
+
tip.hide();
|
|
38
|
+
toggleOpen();
|
|
39
|
+
}, onKeyDown: (event) => {
|
|
40
|
+
if (event.key === 'Escape')
|
|
41
|
+
tip.hide();
|
|
42
|
+
handleTriggerKeyDown(event);
|
|
43
|
+
}, onMouseEnter: showTip, onMouseLeave: tip.leave, onFocus: showTip, onBlur: tip.hide, children: [_jsx("span", { ref: tip.iconRef, className: "jini-runtime-dropup-trigger__label", children: triggerLabel ?? valueLabel }), _jsx(RemixIcon, { name: "arrow-down-s-line", size: 15, className: "jini-runtime-trigger__chevron" })] }), tip.open && tipVisible ? _jsx(RuntimeDropUpTipBubble, { tip: tip, valueLabel: valueLabel }) : null, _jsx(RuntimeDropUpMenu, { dropUp: dropUp, label: label, value: value, options: options, onSelect: onSelect, note: note })] }));
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=RuntimeDropUp.js.map
|
|
@@ -43,6 +43,66 @@ async function messageForFailedUpload(response, fallback) {
|
|
|
43
43
|
return fallback;
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
|
+
/** Refuses a selection that would break a per-file, per-turn count, or per-turn byte quota. */
|
|
47
|
+
function assertWithinQuota({ files, current, incomingBytes, quota }) {
|
|
48
|
+
if (files.some((file) => file.size > quota.maxAttachmentBytes)) {
|
|
49
|
+
throw new Error(`Each attachment must be ${formatByteLimit(quota.maxAttachmentBytes)} or smaller.`);
|
|
50
|
+
}
|
|
51
|
+
if (current.count + files.length > quota.maxAttachmentCount) {
|
|
52
|
+
throw new Error(`You can attach at most ${quota.maxAttachmentCount} files to one message.`);
|
|
53
|
+
}
|
|
54
|
+
if (current.bytes + incomingBytes > quota.maxBatchBytes) {
|
|
55
|
+
throw new Error(`Attachments for one message must total ${formatByteLimit(quota.maxBatchBytes)} or less.`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** Forwards the composer's abort (including one that already happened) into `controller`; returns the unlink. */
|
|
59
|
+
function linkParentAbort({ controller, parent }) {
|
|
60
|
+
if (parent === undefined)
|
|
61
|
+
return () => { };
|
|
62
|
+
const abortFromParent = () => controller.abort(parent.reason);
|
|
63
|
+
parent.addEventListener('abort', abortFromParent, { once: true });
|
|
64
|
+
if (parent.aborted)
|
|
65
|
+
abortFromParent();
|
|
66
|
+
return () => parent.removeEventListener('abort', abortFromParent);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Runs `concurrency` workers over a shared index, writing each result at its input position in
|
|
70
|
+
* `uploaded`. Never rejects: the first failure is returned (and aborts the rest) instead.
|
|
71
|
+
*/
|
|
72
|
+
async function uploadInOrder({ files, concurrency, controller, uploaded, uploadOne }) {
|
|
73
|
+
let nextIndex = 0;
|
|
74
|
+
let firstError;
|
|
75
|
+
const worker = async () => {
|
|
76
|
+
while (!controller.signal.aborted) {
|
|
77
|
+
const index = nextIndex;
|
|
78
|
+
nextIndex += 1;
|
|
79
|
+
const file = files[index];
|
|
80
|
+
if (file === undefined)
|
|
81
|
+
return;
|
|
82
|
+
try {
|
|
83
|
+
uploaded[index] = await uploadOne(file);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
// Kept, not thrown: aborting stops the sibling worker, and the *first* failure is the one
|
|
87
|
+
// worth reporting — the aborts it causes are noise.
|
|
88
|
+
firstError ??= error;
|
|
89
|
+
controller.abort();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
await Promise.all(Array.from({ length: concurrency }, worker));
|
|
94
|
+
return firstError;
|
|
95
|
+
}
|
|
96
|
+
/** Throws the turn's first upload failure, else the abort that stopped it; returns when neither happened. */
|
|
97
|
+
function throwIfUploadFailed({ firstError, signal }) {
|
|
98
|
+
if (firstError !== undefined)
|
|
99
|
+
throw firstError;
|
|
100
|
+
if (signal.aborted) {
|
|
101
|
+
throw signal.reason instanceof Error
|
|
102
|
+
? signal.reason
|
|
103
|
+
: new Error('Attachment upload was canceled.');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
46
106
|
/**
|
|
47
107
|
* Builds a `ChatPaneProps['uploadAttachments']` bound to one daemon.
|
|
48
108
|
*
|
|
@@ -50,17 +110,21 @@ async function messageForFailedUpload(response, fallback) {
|
|
|
50
110
|
* when the page is served through a proxy that already forwards `/api`.
|
|
51
111
|
*/
|
|
52
112
|
export function createDaemonAttachmentUploader({ baseUrl, fetch }, options = {}) {
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
113
|
+
const quota = {
|
|
114
|
+
maxAttachmentBytes: options.maxAttachmentBytes ?? DEFAULTS.maxAttachmentBytes,
|
|
115
|
+
maxAttachmentCount: options.maxAttachmentCount ?? DEFAULTS.maxAttachmentCount,
|
|
116
|
+
maxBatchBytes: options.maxBatchBytes ?? DEFAULTS.maxBatchBytes,
|
|
117
|
+
};
|
|
56
118
|
const timeoutMs = options.timeoutMs ?? DEFAULTS.timeoutMs;
|
|
57
119
|
const concurrency = options.concurrency ?? DEFAULTS.concurrency;
|
|
58
120
|
const endpoint = `${baseUrl.replace(/\/$/u, '')}/api/attachments`;
|
|
121
|
+
// Read per request rather than captured once, so a global installed after construction is used.
|
|
122
|
+
const send = (input, init) => (fetch ?? globalThis.fetch)(input, init);
|
|
59
123
|
// Per-uploader, not module-level: two panes pointed at two daemons must not share a quota.
|
|
60
124
|
const batchUsage = new Map();
|
|
61
125
|
async function uploadOne(file, batchId, signal) {
|
|
62
126
|
const query = new URLSearchParams({ batch: batchId, name: file.name });
|
|
63
|
-
const response = await (
|
|
127
|
+
const response = await send(`${endpoint}?${query}`, {
|
|
64
128
|
method: 'POST',
|
|
65
129
|
// Fixed, not `file.type` — see this module's doc.
|
|
66
130
|
headers: { 'content-type': 'application/octet-stream' },
|
|
@@ -84,7 +148,7 @@ export function createDaemonAttachmentUploader({ baseUrl, fetch }, options = {})
|
|
|
84
148
|
// caller-supplied signal to compose with and no large body — it is a small best-effort JSON
|
|
85
149
|
// DELETE, and an unprotected `fetch` here would hang `uploadAttachments`'s rejection (the
|
|
86
150
|
// `await` below) on a stalled daemon instead of surfacing `firstError` promptly.
|
|
87
|
-
await (
|
|
151
|
+
await send(endpoint, {
|
|
88
152
|
method: 'DELETE',
|
|
89
153
|
signal: AbortSignal.timeout(15_000),
|
|
90
154
|
headers: { 'content-type': 'application/json' },
|
|
@@ -104,15 +168,7 @@ export function createDaemonAttachmentUploader({ baseUrl, fetch }, options = {})
|
|
|
104
168
|
const batchId = uploadOptions?.batchId ?? crypto.randomUUID();
|
|
105
169
|
const incomingBytes = files.reduce((total, file) => total + file.size, 0);
|
|
106
170
|
const current = batchUsage.get(batchId) ?? { count: 0, bytes: 0, touchedAt: now };
|
|
107
|
-
|
|
108
|
-
throw new Error(`Each attachment must be ${formatByteLimit(maxAttachmentBytes)} or smaller.`);
|
|
109
|
-
}
|
|
110
|
-
if (current.count + files.length > maxAttachmentCount) {
|
|
111
|
-
throw new Error(`You can attach at most ${maxAttachmentCount} files to one message.`);
|
|
112
|
-
}
|
|
113
|
-
if (current.bytes + incomingBytes > maxBatchBytes) {
|
|
114
|
-
throw new Error(`Attachments for one message must total ${formatByteLimit(maxBatchBytes)} or less.`);
|
|
115
|
-
}
|
|
171
|
+
assertWithinQuota({ files, current, incomingBytes, quota });
|
|
116
172
|
// Reserved up front and rolled back on failure, so two overlapping calls for the same turn
|
|
117
173
|
// cannot each see the same free headroom.
|
|
118
174
|
batchUsage.set(batchId, {
|
|
@@ -121,32 +177,9 @@ export function createDaemonAttachmentUploader({ baseUrl, fetch }, options = {})
|
|
|
121
177
|
touchedAt: now,
|
|
122
178
|
});
|
|
123
179
|
const controller = new AbortController();
|
|
124
|
-
const
|
|
125
|
-
uploadOptions?.signal.addEventListener('abort', abortFromParent, { once: true });
|
|
126
|
-
if (uploadOptions?.signal.aborted)
|
|
127
|
-
abortFromParent();
|
|
180
|
+
const unlinkParentAbort = linkParentAbort({ controller, parent: uploadOptions?.signal });
|
|
128
181
|
const timeout = globalThis.setTimeout(() => controller.abort(new DOMException('Attachment upload timed out', 'TimeoutError')), timeoutMs);
|
|
129
182
|
const uploaded = new Array(files.length);
|
|
130
|
-
let nextIndex = 0;
|
|
131
|
-
let firstError;
|
|
132
|
-
const worker = async () => {
|
|
133
|
-
while (!controller.signal.aborted) {
|
|
134
|
-
const index = nextIndex;
|
|
135
|
-
nextIndex += 1;
|
|
136
|
-
const file = files[index];
|
|
137
|
-
if (file === undefined)
|
|
138
|
-
return;
|
|
139
|
-
try {
|
|
140
|
-
uploaded[index] = await uploadOne(file, batchId, controller.signal);
|
|
141
|
-
}
|
|
142
|
-
catch (error) {
|
|
143
|
-
// Kept, not thrown: aborting stops the sibling worker, and the *first* failure is the one
|
|
144
|
-
// worth reporting — the aborts it causes are noise.
|
|
145
|
-
firstError ??= error;
|
|
146
|
-
controller.abort();
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
};
|
|
150
183
|
const settled = () => uploaded.filter((attachment) => attachment !== undefined);
|
|
151
184
|
// The failure is captured rather than rethrown from inside the `try`, so that the timer/listener
|
|
152
185
|
// teardown in `finally` has exactly two reachable paths (the turn succeeded, or it did not)
|
|
@@ -154,14 +187,14 @@ export function createDaemonAttachmentUploader({ baseUrl, fetch }, options = {})
|
|
|
154
187
|
let failure;
|
|
155
188
|
let succeeded = false;
|
|
156
189
|
try {
|
|
157
|
-
await
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
190
|
+
const firstError = await uploadInOrder({
|
|
191
|
+
files,
|
|
192
|
+
concurrency,
|
|
193
|
+
controller,
|
|
194
|
+
uploaded,
|
|
195
|
+
uploadOne: (file) => uploadOne(file, batchId, controller.signal),
|
|
196
|
+
});
|
|
197
|
+
throwIfUploadFailed({ firstError, signal: controller.signal });
|
|
165
198
|
succeeded = true;
|
|
166
199
|
}
|
|
167
200
|
catch (error) {
|
|
@@ -169,7 +202,7 @@ export function createDaemonAttachmentUploader({ baseUrl, fetch }, options = {})
|
|
|
169
202
|
}
|
|
170
203
|
finally {
|
|
171
204
|
globalThis.clearTimeout(timeout);
|
|
172
|
-
|
|
205
|
+
unlinkParentAbort();
|
|
173
206
|
}
|
|
174
207
|
if (succeeded)
|
|
175
208
|
return settled();
|