@jini-ai/chat 0.5.6 → 0.5.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/dist/core/durable-projection.d.ts +3 -1
- package/dist/core/durable-projection.js +40 -7
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +1 -0
- package/dist/core/run-events/entry.d.ts +12 -1
- package/dist/core/run-events/entry.js +17 -6
- package/dist/core/tool-call-detail.d.ts +36 -0
- package/dist/core/tool-call-detail.js +49 -0
- package/dist/core/turn-usage.d.ts +11 -0
- package/dist/core/turn-usage.js +17 -0
- package/dist/react/chat-pane.d.ts +2 -2
- package/dist/react/chat-pane.js +1 -1
- package/dist/react/components/Composer.d.ts +2 -0
- package/dist/react/components/Composer.js +5 -2
- package/dist/react/components/Markdown.js +6 -6
- package/dist/react/components/MessageList.d.ts +7 -12
- package/dist/react/components/MessageList.js +20 -2
- package/dist/react/components/MessageRow.d.ts +6 -1
- package/dist/react/components/MessageRow.js +10 -4
- package/dist/react/components/MidRunUserMessage.d.ts +27 -0
- package/dist/react/components/MidRunUserMessage.js +39 -0
- package/dist/react/components/ToolCard.js +33 -22
- package/dist/react/features/chat-pane/components/ChatPane.d.ts +1 -1
- package/dist/react/features/chat-pane/components/ChatPane.js +11 -3
- package/dist/react/features/chat-pane/create-mid-run-message-poster.d.ts +44 -0
- package/dist/react/features/chat-pane/create-mid-run-message-poster.js +69 -0
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +3 -1
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +55 -6
- package/dist/react/features/chat-pane/hooks/useChatPaneNewTabLinks.hooks.d.ts +25 -0
- package/dist/react/features/chat-pane/hooks/useChatPaneNewTabLinks.hooks.js +70 -0
- package/dist/react/features/chat-pane/index.d.ts +2 -1
- package/dist/react/features/chat-pane/index.js +1 -0
- package/dist/react/features/chat-pane/rules.d.ts +13 -0
- package/dist/react/features/chat-pane/rules.js +14 -0
- package/dist/react/features/chat-pane/styles.d.ts +1 -1
- package/dist/react/features/chat-pane/styles.js +7 -0
- package/dist/react/features/chat-pane/types.d.ts +23 -0
- package/dist/react/hooks/composer-draft-cache.d.ts +15 -0
- package/dist/react/hooks/composer-draft-cache.js +20 -0
- package/dist/react/hooks/useComposer.d.ts +14 -0
- package/dist/react/hooks/useComposer.js +78 -6
- package/dist/react/index.d.ts +4 -2
- package/dist/react/index.js +3 -1
- package/dist/server/durable-runs/recover.js +19 -11
- package/dist/server/run-finalizer/entry.js +2 -1
- package/dist/store/runtime/durable-run-store.js +5 -2
- package/dist/store/runtime/run-ledger.js +5 -2
- package/package.json +8 -8
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
3
|
* @module ToolCard
|
|
4
4
|
*
|
|
@@ -20,8 +20,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
20
20
|
* `AskUserQuestion`) falls through to `GenericCard`, which is a correct,
|
|
21
21
|
* generic rendering for it.
|
|
22
22
|
*/
|
|
23
|
-
import { useState } from 'react';
|
|
24
|
-
import { formatToolOutputForDisplay, isDelegatedWrapperToolName, isTodoWriteToolName, parseTodoWriteInput, toRenderProps } from '../../core/index.js';
|
|
23
|
+
import { useId, useState } from 'react';
|
|
24
|
+
import { describeToolCallDetail, formatToolOutputForDisplay, isDelegatedWrapperToolName, isTodoWriteToolName, parseTodoWriteInput, toRenderProps } from '../../core/index.js';
|
|
25
25
|
import { useT } from '../hooks/context.js';
|
|
26
26
|
import { getToolRenderer } from '../tool-renderer-registry.js';
|
|
27
27
|
import { Icon } from './Icon.js';
|
|
@@ -132,7 +132,6 @@ function primaryTarget(input) {
|
|
|
132
132
|
*/
|
|
133
133
|
function DelegatedToolCard({ name, input, result, runStreaming, runSucceeded }) {
|
|
134
134
|
const t = useT();
|
|
135
|
-
const [open, setOpen] = useState(false);
|
|
136
135
|
// Wrapper form (only reached on a failed wrapper call, per ToolCard's suppression comment):
|
|
137
136
|
// the real id and args are nested under {toolId, input}, not `input` itself.
|
|
138
137
|
const wrapper = (input ?? {});
|
|
@@ -140,21 +139,19 @@ function DelegatedToolCard({ name, input, result, runStreaming, runSucceeded })
|
|
|
140
139
|
const args = isDelegatedWrapperToolName({ name: name }) ? wrapper.input : input;
|
|
141
140
|
const target = primaryTarget(args);
|
|
142
141
|
const isRunning = runStreaming && !result;
|
|
143
|
-
return (
|
|
142
|
+
return (_jsx(ExpandableCard, { className: "op-jini-tool", head: _jsxs(_Fragment, { children: [_jsx(ResultBadge, { result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }), _jsxs("span", { className: `op-title${isRunning ? ' shimmer-text' : ''}`, children: [t('Tool call'), " \u00B7 ", humanizeToolId(toolId)] }), target ? _jsx("span", { className: "op-meta", children: target }) : null] }), after: _jsx(FileErrorDetail, { result: result }), children: _jsx(ToolCallDetail, { input: args ?? {}, result: result, runStreaming: runStreaming, runSucceeded: runSucceeded, children: _jsx(ToolResultMedia, { result: result }) }) }));
|
|
144
143
|
}
|
|
145
144
|
function SearchToolsCard({ input, result, runStreaming, runSucceeded }) {
|
|
146
145
|
const t = useT();
|
|
147
|
-
const [open, setOpen] = useState(false);
|
|
148
146
|
const obj = (input ?? {});
|
|
149
147
|
const isRunning = runStreaming && !result;
|
|
150
|
-
return (
|
|
148
|
+
return (_jsx(ExpandableCard, { className: "op-jini-tool", head: _jsxs(_Fragment, { children: [_jsx(ResultBadge, { result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }), _jsx("span", { className: `op-title${isRunning ? ' shimmer-text' : ''}`, children: t('Search tools') }), obj.query ? _jsx("span", { className: "op-meta", children: obj.query }) : null] }), after: _jsx(FileErrorDetail, { result: result }), children: _jsx(ToolCallDetail, { input: input, result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }) }));
|
|
151
149
|
}
|
|
152
150
|
function DescribeToolCard({ input, result, runStreaming, runSucceeded }) {
|
|
153
151
|
const t = useT();
|
|
154
|
-
const [open, setOpen] = useState(false);
|
|
155
152
|
const obj = (input ?? {});
|
|
156
153
|
const isRunning = runStreaming && !result;
|
|
157
|
-
return (
|
|
154
|
+
return (_jsx(ExpandableCard, { className: "op-jini-tool", head: _jsxs(_Fragment, { children: [_jsx(ResultBadge, { result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }), _jsx("span", { className: `op-title${isRunning ? ' shimmer-text' : ''}`, children: t('Describe tool') }), obj.id ? _jsx("span", { className: "op-meta", children: obj.id }) : null] }), after: _jsx(FileErrorDetail, { result: result }), children: _jsx(ToolCallDetail, { input: input, result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }) }));
|
|
158
155
|
}
|
|
159
156
|
function OpenInTabButton({ filePath, ctx }) {
|
|
160
157
|
const t = useT();
|
|
@@ -174,68 +171,82 @@ function OpenInTabButton({ filePath, ctx }) {
|
|
|
174
171
|
}
|
|
175
172
|
function FileWriteCard({ input, result, runStreaming, runSucceeded, ctx }) {
|
|
176
173
|
const t = useT();
|
|
177
|
-
const [open, setOpen] = useState(false);
|
|
178
174
|
const obj = (input ?? {});
|
|
179
175
|
const file = obj.file_path ?? obj.filePath ?? obj.path ?? '(unnamed)';
|
|
180
176
|
// See OpenInTabButton's baseName comment: `.split('/').pop()` is never undefined.
|
|
181
177
|
const baseName = file.split('/').pop();
|
|
182
178
|
const lines = typeof obj.content === 'string' ? obj.content.split('\n').length : null;
|
|
183
179
|
const isRunning = runStreaming && !result;
|
|
184
|
-
return (_jsxs(
|
|
180
|
+
return (_jsxs(ExpandableCard, { className: "op-file", head: _jsxs(_Fragment, { children: [_jsx(ResultBadge, { result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }), _jsx("span", { className: `op-title${isRunning ? ' shimmer-text' : ''}`, children: t('Write') }), _jsxs("span", { className: "op-meta", children: [baseName, lines !== null ? ` · ${t('{n} lines', { n: lines })}` : ''] })] }), after: _jsx(FileErrorDetail, { result: result }), children: [_jsxs("div", { className: "op-card-detail op-card-file-detail", children: [_jsx("code", { className: "op-path", children: file }), _jsx(OpenInTabButton, { filePath: file, ctx: ctx })] }), _jsx(ToolCallDetail, { input: input, result: result, runStreaming: runStreaming, runSucceeded: runSucceeded })] }));
|
|
185
181
|
}
|
|
186
182
|
function FileEditCard({ input, result, runStreaming, runSucceeded, ctx }) {
|
|
187
183
|
const t = useT();
|
|
188
|
-
const [open, setOpen] = useState(false);
|
|
189
184
|
const obj = (input ?? {});
|
|
190
185
|
const file = obj.file_path ?? obj.filePath ?? obj.path ?? '(unnamed)';
|
|
191
186
|
// See OpenInTabButton's baseName comment: `.split('/').pop()` is never undefined.
|
|
192
187
|
const baseName = file.split('/').pop();
|
|
193
188
|
const editCount = Array.isArray(obj.edits) ? obj.edits.length : 1;
|
|
194
189
|
const isRunning = runStreaming && !result;
|
|
195
|
-
return (_jsxs(
|
|
190
|
+
return (_jsxs(ExpandableCard, { className: "op-file", head: _jsxs(_Fragment, { children: [_jsx(ResultBadge, { result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }), _jsx("span", { className: `op-title${isRunning ? ' shimmer-text' : ''}`, children: t('Edit') }), _jsxs("span", { className: "op-meta", children: [baseName, " \u00B7 ", editCount, " ", editCount === 1 ? t('change') : t('changes')] })] }), after: _jsx(FileErrorDetail, { result: result }), children: [_jsxs("div", { className: "op-card-detail op-card-file-detail", children: [_jsx("code", { className: "op-path", children: file }), _jsx(OpenInTabButton, { filePath: file, ctx: ctx })] }), _jsx(ToolCallDetail, { input: input, result: result, runStreaming: runStreaming, runSucceeded: runSucceeded })] }));
|
|
196
191
|
}
|
|
197
192
|
function FileReadCard({ input, result, runStreaming, runSucceeded, ctx }) {
|
|
198
193
|
const t = useT();
|
|
199
|
-
const [open, setOpen] = useState(false);
|
|
200
194
|
const obj = (input ?? {});
|
|
201
195
|
const file = obj.file_path ?? obj.filePath ?? obj.path ?? '(unnamed)';
|
|
202
196
|
// See OpenInTabButton's baseName comment: `.split('/').pop()` is never undefined.
|
|
203
197
|
const baseName = file.split('/').pop();
|
|
204
198
|
const isRunning = runStreaming && !result;
|
|
205
|
-
return (_jsxs(
|
|
199
|
+
return (_jsxs(ExpandableCard, { className: "op-file", head: _jsxs(_Fragment, { children: [_jsx(ResultBadge, { result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }), _jsx("span", { className: `op-title${isRunning ? ' shimmer-text' : ''}`, children: t('Read') }), _jsx("span", { className: "op-meta", children: baseName })] }), after: _jsx(FileErrorDetail, { result: result }), children: [_jsxs("div", { className: "op-card-detail op-card-file-detail", children: [_jsx("code", { className: "op-path", children: file }), _jsx(OpenInTabButton, { filePath: file, ctx: ctx })] }), _jsx(ToolCallDetail, { input: input, result: result, runStreaming: runStreaming, runSucceeded: runSucceeded })] }));
|
|
206
200
|
}
|
|
207
201
|
function BashCard({ input, result, runStreaming, runSucceeded }) {
|
|
208
202
|
const t = useT();
|
|
209
203
|
const obj = (input ?? {});
|
|
210
204
|
const command = obj.command ?? '';
|
|
211
205
|
const desc = obj.description;
|
|
212
|
-
const [open, setOpen] = useState(false);
|
|
213
206
|
const isRunning = runStreaming && !result;
|
|
214
|
-
return (
|
|
207
|
+
return (_jsx(ExpandableCard, { className: "op-bash", head: _jsxs(_Fragment, { children: [_jsx(ResultBadge, { result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }), _jsx("span", { className: `op-title${isRunning ? ' shimmer-text' : ''}`, children: t('Bash') }), desc ? _jsx("span", { className: "op-meta op-desc", children: desc }) : null] }), children: _jsxs("div", { className: "op-card-detail", children: [_jsx("pre", { className: "op-command", children: truncate(command, 400) }), result?.content ? _jsx("pre", { className: "op-output", children: truncate(formatToolOutputForDisplay({ text: result.content }), 4000) }) : null] }) }));
|
|
215
208
|
}
|
|
216
209
|
function GlobCard({ input, result, runStreaming, runSucceeded }) {
|
|
217
210
|
const t = useT();
|
|
218
211
|
const obj = (input ?? {});
|
|
219
|
-
return (_jsx(
|
|
212
|
+
return (_jsx(ExpandableCard, { className: "op-search", head: _jsxs(_Fragment, { children: [_jsx(ResultBadge, { result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }), _jsx("span", { className: "op-title", children: t('Search files') }), _jsxs("span", { className: "op-meta", children: [obj.pattern ?? '*', obj.path ? ` in ${obj.path}` : ''] })] }), children: _jsx(ToolCallDetail, { input: input, result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }) }));
|
|
220
213
|
}
|
|
221
214
|
function GrepCard({ input, result, runStreaming, runSucceeded }) {
|
|
222
215
|
const t = useT();
|
|
223
216
|
const obj = (input ?? {});
|
|
224
|
-
return (_jsx(
|
|
217
|
+
return (_jsx(ExpandableCard, { className: "op-search", head: _jsxs(_Fragment, { children: [_jsx(ResultBadge, { result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }), _jsx("span", { className: "op-title", children: t('Search content') }), _jsxs("span", { className: "op-meta", children: [obj.pattern ?? '', obj.path ? ` in ${obj.path}` : ''] })] }), children: _jsx(ToolCallDetail, { input: input, result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }) }));
|
|
225
218
|
}
|
|
226
219
|
function WebFetchCard({ input, result, runStreaming, runSucceeded }) {
|
|
227
220
|
const t = useT();
|
|
228
221
|
const obj = (input ?? {});
|
|
229
|
-
return (_jsx(
|
|
222
|
+
return (_jsx(ExpandableCard, { className: "op-web", head: _jsxs(_Fragment, { children: [_jsx(ResultBadge, { result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }), _jsx("span", { className: "op-title", children: t('Fetch') }), _jsx("span", { className: "op-meta", children: obj.url ?? '' })] }), children: _jsx(ToolCallDetail, { input: input, result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }) }));
|
|
230
223
|
}
|
|
231
224
|
function WebSearchCard({ input, result, runStreaming, runSucceeded }) {
|
|
232
225
|
const t = useT();
|
|
233
226
|
const obj = (input ?? {});
|
|
234
|
-
return (_jsx(
|
|
227
|
+
return (_jsx(ExpandableCard, { className: "op-web", head: _jsxs(_Fragment, { children: [_jsx(ResultBadge, { result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }), _jsx("span", { className: "op-title", children: t('Search') }), _jsx("span", { className: "op-meta", children: obj.query ?? '' })] }), children: _jsx(ToolCallDetail, { input: input, result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }) }));
|
|
235
228
|
}
|
|
229
|
+
/** Any tool this package has no family card for — an MCP tool, or a card tool such as `assistant_ask_choice` whose form renders elsewhere in the turn. The row still opens to the full call. */
|
|
236
230
|
function GenericCard({ name, input, result, runStreaming, runSucceeded }) {
|
|
237
231
|
const summary = describeInput(input);
|
|
238
|
-
return (
|
|
232
|
+
return (_jsx(ExpandableCard, { className: "op-generic", head: _jsxs(_Fragment, { children: [_jsx(ResultBadge, { result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }), _jsx("span", { className: "op-title", children: name }), summary ? _jsx("span", { className: "op-meta", children: truncate(summary, 200) }) : null] }), after: _jsx(ToolResultMedia, { result: result }), children: _jsx(ToolCallDetail, { input: input, result: result, runStreaming: runStreaming, runSucceeded: runSucceeded }) }));
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* The one row shell every card shares: a real `<button>` head (Enter/Space toggle it) that reports
|
|
236
|
+
* `aria-expanded` and points `aria-controls` at its panel, and a panel that stays mounted while
|
|
237
|
+
* collapsed — a `page.*` DOM reader sees the detail through `textContent` without anyone clicking.
|
|
238
|
+
* `after` renders outside the panel, for content that must stay visible collapsed (an error, media).
|
|
239
|
+
*/
|
|
240
|
+
function ExpandableCard({ className, head, after, children }) {
|
|
241
|
+
const [open, setOpen] = useState(false);
|
|
242
|
+
const panelId = useId();
|
|
243
|
+
return (_jsxs("div", { className: `op-card ${className}`, children: [_jsxs("button", { type: "button", className: "op-card-head", "aria-expanded": open, "aria-controls": panelId, onClick: () => setOpen((o) => !o), children: [head, _jsx("span", { className: "op-expand-chev", "aria-hidden": true, children: _jsx(Icon, { name: open ? 'chevron-down' : 'chevron-right', size: 11 }) })] }), _jsx("div", { id: panelId, className: `accordion-collapsible${open ? ' open' : ''}`, children: _jsx("div", { className: "accordion-collapsible-inner", children: children }) }), after] }));
|
|
244
|
+
}
|
|
245
|
+
/** The call's input, its result (or why there is none), and any extra `children` — see `describeToolCallDetail` for the redaction and formatting rules. */
|
|
246
|
+
function ToolCallDetail({ input, result, runStreaming, runSucceeded, children }) {
|
|
247
|
+
const t = useT();
|
|
248
|
+
const detail = describeToolCallDetail({ input: input, result: result }, { runStreaming: runStreaming, runSucceeded: runSucceeded });
|
|
249
|
+
return (_jsxs("div", { className: "op-card-detail", children: [detail.inputText !== null ? _jsx("pre", { className: "op-command op-input", children: detail.inputText }) : null, detail.outputText !== null ? _jsx("pre", { className: "op-output", children: detail.outputText }) : null, detail.noteKey !== null ? _jsx("p", { className: "op-detail-note", children: t(detail.noteKey) }) : null, children] }));
|
|
239
250
|
}
|
|
240
251
|
function ResultBadge({ result, runStreaming, runSucceeded }) {
|
|
241
252
|
const t = useT();
|
|
@@ -6,5 +6,5 @@ import type { ChatPaneProps } from '../types.js';
|
|
|
6
6
|
* @complexity Time/space: O(n) in rendered messages, agents, and suggestions.
|
|
7
7
|
* @overallScore 100/100
|
|
8
8
|
*/
|
|
9
|
-
export declare function ChatPane({ transport, agents: injectedAgents, runtimeAccess, runtimeStatusPollMs, title, variant, initialMessages, conversationId, initialSelection, selection: controlledSelection, onSelectionChange, runContext, agentControl, onActivityChange, onMessagesChange, redactUserText, onSecretRedacted, onRescanAgents, scanningAgents, daemonOnline, executionMode, apiModeAvailable, onExecutionModeChange, byokRuntime, onByokModelChange, initialDraft, composerHistoryScope, composerHistoryStorage, validateAttachments, deliverTypedAnswer, composerHandle, placeholder, placeholders, suggestions, workingDirectory, initialWorkingDirectory, onChangeWorkingDirectory, workingDirectoryAccess, workingDirectoryControlPlacement, projectFileNames, uploadAttachments, attachmentAccept, disabled, runtimePickerPlacement, composerSlots, header, leadingAccessory, footer, className, style, }: ChatPaneProps): import("react").JSX.Element;
|
|
9
|
+
export declare function ChatPane({ transport, agents: injectedAgents, runtimeAccess, runtimeStatusPollMs, title, variant, initialMessages, conversationId, initialSelection, selection: controlledSelection, onSelectionChange, runContext, agentControl, onActivityChange, onMessagesChange, redactUserText, onSecretRedacted, onRescanAgents, scanningAgents, daemonOnline, executionMode, apiModeAvailable, onExecutionModeChange, byokRuntime, onByokModelChange, initialDraft, composerHistoryScope, composerHistoryStorage, validateAttachments, deliverTypedAnswer, deliverMidRunMessage, composerHandle, placeholder, placeholders, suggestions, workingDirectory, initialWorkingDirectory, onChangeWorkingDirectory, workingDirectoryAccess, workingDirectoryControlPlacement, projectFileNames, uploadAttachments, attachmentAccept, disabled, runtimePickerPlacement, composerSlots, header, leadingAccessory, footer, className, style, }: ChatPaneProps): import("react").JSX.Element;
|
|
10
10
|
//# sourceMappingURL=ChatPane.d.ts.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { SECRET_REDACTED_NOTICE } from '../../../../core/user-text-redaction.js';
|
|
3
|
-
import { useEffect, useRef } from 'react';
|
|
3
|
+
import { useEffect, useMemo, useRef } from 'react';
|
|
4
4
|
import { WorkingDirPicker } from '@jini-ai/ui';
|
|
5
5
|
import { Composer } from '../../../components/Composer.js';
|
|
6
6
|
import { appendComposerDiscovery } from '../../../components/composer-discovery.js';
|
|
@@ -12,6 +12,7 @@ import { useChatPane } from '../hooks/useChatPane.hooks.js';
|
|
|
12
12
|
import { useChatPaneAgentControl } from '../hooks/useChatPaneAgentControl.hooks.js';
|
|
13
13
|
import { useChatPaneComposerPlaceholder } from '../hooks/useChatPaneComposerPlaceholder.hooks.js';
|
|
14
14
|
import { useChatPaneControlsHeight } from '../hooks/useChatPaneControlsHeight.hooks.js';
|
|
15
|
+
import { useChatPaneNewTabLinks } from '../hooks/useChatPaneNewTabLinks.hooks.js';
|
|
15
16
|
import { useChatPaneFileDrop, } from '../hooks/useChatPaneFileDrop.hooks.js';
|
|
16
17
|
import { useChatPaneRuntimeInventory, } from '../hooks/useChatPaneRuntimeInventory.hooks.js';
|
|
17
18
|
import { CHAT_PANE_STYLES } from '../styles.js';
|
|
@@ -179,7 +180,7 @@ function ChatPaneComposerArea({ fileDrop, uploadAttachments, pane, disabled, una
|
|
|
179
180
|
* @complexity Time/space: O(n) in rendered messages, agents, and suggestions.
|
|
180
181
|
* @overallScore 100/100
|
|
181
182
|
*/
|
|
182
|
-
export function ChatPane({ transport, agents: injectedAgents = EMPTY_AGENTS, runtimeAccess, runtimeStatusPollMs = 5_000, title, variant = 'workspace', initialMessages, conversationId, initialSelection, selection: controlledSelection, onSelectionChange, runContext, agentControl, onActivityChange, onMessagesChange, redactUserText, onSecretRedacted, onRescanAgents, scanningAgents = false, daemonOnline = true, executionMode = 'local', apiModeAvailable = false, onExecutionModeChange, byokRuntime, onByokModelChange, initialDraft, composerHistoryScope, composerHistoryStorage, validateAttachments, deliverTypedAnswer, composerHandle, placeholder, placeholders, suggestions = [], workingDirectory, initialWorkingDirectory, onChangeWorkingDirectory, workingDirectoryAccess, workingDirectoryControlPlacement = 'below', projectFileNames, uploadAttachments, attachmentAccept, disabled = false, runtimePickerPlacement = 'up', composerSlots, header, leadingAccessory, footer, className, style, }) {
|
|
183
|
+
export function ChatPane({ transport, agents: injectedAgents = EMPTY_AGENTS, runtimeAccess, runtimeStatusPollMs = 5_000, title, variant = 'workspace', initialMessages, conversationId, initialSelection, selection: controlledSelection, onSelectionChange, runContext, agentControl, onActivityChange, onMessagesChange, redactUserText, onSecretRedacted, onRescanAgents, scanningAgents = false, daemonOnline = true, executionMode = 'local', apiModeAvailable = false, onExecutionModeChange, byokRuntime, onByokModelChange, initialDraft, composerHistoryScope, composerHistoryStorage, validateAttachments, deliverTypedAnswer, deliverMidRunMessage, composerHandle, placeholder, placeholders, suggestions = [], workingDirectory, initialWorkingDirectory, onChangeWorkingDirectory, workingDirectoryAccess, workingDirectoryControlPlacement = 'below', projectFileNames, uploadAttachments, attachmentAccept, disabled = false, runtimePickerPlacement = 'up', composerSlots, header, leadingAccessory, footer, className, style, }) {
|
|
183
184
|
const t = useT();
|
|
184
185
|
const composerPlaceholder = useChatPaneComposerPlaceholder({ placeholder: placeholder, placeholders: placeholders });
|
|
185
186
|
const inventory = useChatPaneRuntimeInventory(definedProps({ source: {
|
|
@@ -216,6 +217,7 @@ export function ChatPane({ transport, agents: injectedAgents = EMPTY_AGENTS, run
|
|
|
216
217
|
workingDirectoryAccess,
|
|
217
218
|
apiModeConfigured,
|
|
218
219
|
deliverTypedAnswer,
|
|
220
|
+
deliverMidRunMessage,
|
|
219
221
|
} }));
|
|
220
222
|
// Mirrors `Composer.tsx`'s own `draftRef`: `composerHandle.insertText` (below) is called from
|
|
221
223
|
// OUTSIDE any render, so it cannot close over `pane.composer.draft` directly — that would freeze
|
|
@@ -268,6 +270,12 @@ export function ChatPane({ transport, agents: injectedAgents = EMPTY_AGENTS, run
|
|
|
268
270
|
footerAccessories: (_jsx(AgentRuntimePicker, { agents: runtimeView.agents, value: pane.selection, onChange: pane.setSelection, ...definedProps({ source: { onRescan: runtimeView.rescanAgents } }), scanning: runtimeView.scanningAgents, daemonOnline: runtimeView.daemonOnline, placement: runtimePickerPlacement, executionMode: executionMode, apiModeAvailable: apiModeAvailable, ...definedProps({ source: { onExecutionModeChange, byokRuntime, onByokModelChange } }) })),
|
|
269
271
|
} });
|
|
270
272
|
const { rootRef, controlsRef } = useChatPaneControlsHeight();
|
|
271
|
-
|
|
273
|
+
const openLinkInNewTab = useChatPaneNewTabLinks();
|
|
274
|
+
// `MessageList` is memoized so a composer keystroke does not re-render the transcript; these two
|
|
275
|
+
// props would otherwise be fresh objects on every render and defeat it.
|
|
276
|
+
const { queuedPrompt, cancelQueued } = pane;
|
|
277
|
+
const pendingPrompt = useMemo(() => (queuedPrompt === null ? null : { text: queuedPrompt, onCancel: cancelQueued }), [queuedPrompt, cancelQueued]);
|
|
278
|
+
const projectFileNameSet = useMemo(() => (projectFileNames === undefined ? undefined : new Set(projectFileNames)), [projectFileNames]);
|
|
279
|
+
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: [_jsx(MessageList, { messages: pane.conversation.messages, isStreaming: pane.conversation.isStreaming, scrollIntent: pane.conversation.scrollIntent, onScrolled: pane.conversation.acknowledgeScroll, pendingPrompt: pendingPrompt, ...(projectFileNameSet === undefined ? {} : { projectFileNames: projectFileNameSet }) }), _jsxs("div", { className: "jini-chat-pane__controls", ref: controlsRef, children: [_jsx(ChatPaneSuggestionsRow, { suggestions: suggestions, onSelect: pane.composer.setDraft, 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: uploadAttachments, pane: pane, disabled: disabled, unavailable: unavailable, placeholder: composerPlaceholder, slots: slots, attachmentAccept: attachmentAccept, workingDirectoryAccess: workingDirectoryAccess, workingDirectoryControlPlacement: workingDirectoryControlPlacement, t: t })] })] }), footer] }));
|
|
272
280
|
}
|
|
273
281
|
//# sourceMappingURL=ChatPane.js.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module create-mid-run-message-poster
|
|
3
|
+
*
|
|
4
|
+
* The client half of a mid-run message: a ready-made {@link DeliverMidRunMessage} that posts text the
|
|
5
|
+
* human sent while a run streams to that run's live agent. Pass it as `ChatPane`'s
|
|
6
|
+
* `deliverMidRunMessage`.
|
|
7
|
+
*
|
|
8
|
+
* ```tsx
|
|
9
|
+
* <ChatPane deliverMidRunMessage={createMidRunMessagePoster({ baseUrl: '', fetch })} />
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* Sibling of `create-typed-answer-poster.ts`. The route is `@jini-ai/daemon`'s
|
|
13
|
+
* `POST /api/runs/:runId/messages`: `202` when the agent took the text into its running turn, `409`
|
|
14
|
+
* with `details.delivery` when it did not.
|
|
15
|
+
*
|
|
16
|
+
* ## Why it resolves an outcome instead of throwing
|
|
17
|
+
*
|
|
18
|
+
* The pane acts differently on each outcome and must never drop the text: `'unsupported'` stops the
|
|
19
|
+
* run and sends the text as the next turn; `'not-running'` and `'failed'` queue it behind the run,
|
|
20
|
+
* which is already ending (or may be — never cut short a run the host could not reach). A 404 means
|
|
21
|
+
* the daemon never saw this run (a host-side run such as a BYOK API turn), so it is `'unsupported'`
|
|
22
|
+
* too: no live process to steer, but an interrupt still works.
|
|
23
|
+
*/
|
|
24
|
+
import { type UserTextRedactionOptions } from '../../../core/user-text-redaction.js';
|
|
25
|
+
import type { DeliverMidRunMessage } from './types.js';
|
|
26
|
+
export interface CreateMidRunMessagePosterOptions extends UserTextRedactionOptions {
|
|
27
|
+
/** Abandon a post after this many ms (reported as `'failed'`). Defaults to 30s. */
|
|
28
|
+
readonly timeoutMs?: number;
|
|
29
|
+
/** Extra headers per request (e.g. a CSRF token). `content-type` is always set and cannot be overridden. */
|
|
30
|
+
readonly headers?: Readonly<Record<string, string>>;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Builds a {@link DeliverMidRunMessage} that POSTs `{text}` to `${baseUrl}/api/runs/:runId/messages`.
|
|
34
|
+
*
|
|
35
|
+
* @param deps.baseUrl - Origin to call; `''` for same-origin.
|
|
36
|
+
* @param deps.fetch - The fetch to use (the browser's, in production).
|
|
37
|
+
* @returns Resolves to the outcome; never rejects.
|
|
38
|
+
* @complexity O(1) — one request per call.
|
|
39
|
+
*/
|
|
40
|
+
export declare function createMidRunMessagePoster({ baseUrl, fetch }: {
|
|
41
|
+
baseUrl: string;
|
|
42
|
+
fetch: typeof globalThis.fetch;
|
|
43
|
+
}, options?: CreateMidRunMessagePosterOptions): DeliverMidRunMessage;
|
|
44
|
+
//# sourceMappingURL=create-mid-run-message-poster.d.ts.map
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module create-mid-run-message-poster
|
|
3
|
+
*
|
|
4
|
+
* The client half of a mid-run message: a ready-made {@link DeliverMidRunMessage} that posts text the
|
|
5
|
+
* human sent while a run streams to that run's live agent. Pass it as `ChatPane`'s
|
|
6
|
+
* `deliverMidRunMessage`.
|
|
7
|
+
*
|
|
8
|
+
* ```tsx
|
|
9
|
+
* <ChatPane deliverMidRunMessage={createMidRunMessagePoster({ baseUrl: '', fetch })} />
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* Sibling of `create-typed-answer-poster.ts`. The route is `@jini-ai/daemon`'s
|
|
13
|
+
* `POST /api/runs/:runId/messages`: `202` when the agent took the text into its running turn, `409`
|
|
14
|
+
* with `details.delivery` when it did not.
|
|
15
|
+
*
|
|
16
|
+
* ## Why it resolves an outcome instead of throwing
|
|
17
|
+
*
|
|
18
|
+
* The pane acts differently on each outcome and must never drop the text: `'unsupported'` stops the
|
|
19
|
+
* run and sends the text as the next turn; `'not-running'` and `'failed'` queue it behind the run,
|
|
20
|
+
* which is already ending (or may be — never cut short a run the host could not reach). A 404 means
|
|
21
|
+
* the daemon never saw this run (a host-side run such as a BYOK API turn), so it is `'unsupported'`
|
|
22
|
+
* too: no live process to steer, but an interrupt still works.
|
|
23
|
+
*/
|
|
24
|
+
import { redactUserText } from '../../../core/user-text-redaction.js';
|
|
25
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
26
|
+
/** Reads `error.details.delivery` off a 409 body; anything unreadable counts as `'unsupported'`. */
|
|
27
|
+
async function conflictDelivery(response) {
|
|
28
|
+
const body = await response.json().catch(() => null);
|
|
29
|
+
const delivery = body?.error?.details?.delivery;
|
|
30
|
+
return delivery === 'not-running' ? 'not-running' : 'unsupported';
|
|
31
|
+
}
|
|
32
|
+
/** Maps the route's response onto an outcome. Only 202 is a delivery. */
|
|
33
|
+
async function deliveryFor(response) {
|
|
34
|
+
if (response.status === 202)
|
|
35
|
+
return 'delivered';
|
|
36
|
+
if (response.status === 409)
|
|
37
|
+
return conflictDelivery(response);
|
|
38
|
+
return response.status === 404 ? 'unsupported' : 'failed';
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Builds a {@link DeliverMidRunMessage} that POSTs `{text}` to `${baseUrl}/api/runs/:runId/messages`.
|
|
42
|
+
*
|
|
43
|
+
* @param deps.baseUrl - Origin to call; `''` for same-origin.
|
|
44
|
+
* @param deps.fetch - The fetch to use (the browser's, in production).
|
|
45
|
+
* @returns Resolves to the outcome; never rejects.
|
|
46
|
+
* @complexity O(1) — one request per call.
|
|
47
|
+
*/
|
|
48
|
+
export function createMidRunMessagePoster({ baseUrl, fetch }, options = {}) {
|
|
49
|
+
const origin = baseUrl.replace(/\/$/u, '');
|
|
50
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
51
|
+
return async ({ runId, text }) => {
|
|
52
|
+
const result = (options.redactUserText ?? redactUserText)({ text });
|
|
53
|
+
if (result.secretRedacted)
|
|
54
|
+
options.onSecretRedacted?.({ secretRedacted: true, count: result.count });
|
|
55
|
+
const controller = new AbortController();
|
|
56
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
57
|
+
const outcome = await fetch(`${origin}/api/runs/${encodeURIComponent(runId)}/messages`, {
|
|
58
|
+
method: 'POST',
|
|
59
|
+
// Same-origin cookie session: the human's own credentials authorize steering their run.
|
|
60
|
+
credentials: 'same-origin',
|
|
61
|
+
headers: { ...options.headers, 'content-type': 'application/json' },
|
|
62
|
+
body: JSON.stringify({ text: result.text }),
|
|
63
|
+
signal: controller.signal,
|
|
64
|
+
}).then(deliveryFor, () => 'failed');
|
|
65
|
+
clearTimeout(timer);
|
|
66
|
+
return outcome;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=create-mid-run-message-poster.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { UserTextRedactionOptions } from '../../../../core/user-text-redaction.js';
|
|
2
2
|
import { type ChatPaneSendBlocker } from '../rules.js';
|
|
3
|
-
import type { ChatPaneActivity, ChatPaneAgent, ChatPaneAttachmentUploadOptions, ChatPaneAgentSelection, ChatPaneRunContext, ChatPaneWorkingDirectoryAccess, DeliverTypedAnswer, TypedAnswerNotice } from '../types.js';
|
|
3
|
+
import type { ChatPaneActivity, ChatPaneAgent, ChatPaneAttachmentUploadOptions, ChatPaneAgentSelection, ChatPaneRunContext, ChatPaneWorkingDirectoryAccess, DeliverMidRunMessage, DeliverTypedAnswer, TypedAnswerNotice } from '../types.js';
|
|
4
4
|
import type { ChatAttachment, ChatMessage } from '@jini-ai/chat';
|
|
5
5
|
import type { ComposerHistoryStoragePort } from '../../../../core/composer-history.js';
|
|
6
6
|
import type { ChatTransport } from '@jini-ai/chat';
|
|
@@ -36,6 +36,8 @@ export interface UseChatPaneOptions extends UserTextRedactionOptions {
|
|
|
36
36
|
apiModeConfigured?: boolean;
|
|
37
37
|
/** See `ChatPaneProps.deliverTypedAnswer`. Omitted keeps today's queue-while-streaming behavior. */
|
|
38
38
|
deliverTypedAnswer?: DeliverTypedAnswer;
|
|
39
|
+
/** See `ChatPaneProps.deliverMidRunMessage`. Omitted keeps today's queue-while-streaming behavior. */
|
|
40
|
+
deliverMidRunMessage?: DeliverMidRunMessage;
|
|
39
41
|
}
|
|
40
42
|
export interface UseChatPaneResult extends UseChatPaneWorkingDirectoryResult {
|
|
41
43
|
/** Show a localized non-secret notice when a paste was removed. */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useUserTextGuard } from '../../../hooks/useUserTextGuard.js';
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
-
import { describeChatPaneSendBlocker, findAwaitedTypedAnswerId, findChatPaneSendBlocker, isChatPaneQueueableBlocker, isTypedAnswerTurn, resolveChatPaneSelection, } from '../rules.js';
|
|
3
|
+
import { describeChatPaneSendBlocker, findAwaitedTypedAnswerId, findChatPaneSendBlocker, findMidRunMessageRunId, isChatPaneQueueableBlocker, isTypedAnswerTurn, resolveChatPaneSelection, } from '../rules.js';
|
|
4
4
|
import { definedProps } from '../../../util/defined-props.js';
|
|
5
5
|
import { cacheAttachmentPreviewSource } from '../../../hooks/attachment-preview-cache.js';
|
|
6
6
|
import { readCachedAttachmentBatchId, writeCachedAttachmentBatchId, } from '../../../hooks/composer-draft-cache.js';
|
|
@@ -34,6 +34,20 @@ async function deliverTypedAnswerSafely({ deliver, text }) {
|
|
|
34
34
|
return 'failed';
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Hands one mid-run message to the host. A throw counts as `'failed'`, which queues the text — the
|
|
39
|
+
* host could not say whether the run took it, and a message must never be dropped.
|
|
40
|
+
*
|
|
41
|
+
* @complexity O(1) plus the host's own request.
|
|
42
|
+
*/
|
|
43
|
+
async function deliverMidRunMessageSafely({ deliver, runId, text }) {
|
|
44
|
+
try {
|
|
45
|
+
return await deliver({ runId, text });
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return 'failed';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
37
51
|
/** Module scope so each branch costs 1 cognitive point instead of 2 (nested one level inside
|
|
38
52
|
* `useChatPane` where it used to live as a ternary chain). */
|
|
39
53
|
function resolveChatPaneActivity(selectedAgent, conversation) {
|
|
@@ -344,20 +358,55 @@ export function useChatPane(options) {
|
|
|
344
358
|
if (latestDraftRef.current === submittedDraft)
|
|
345
359
|
composer.setDraft('');
|
|
346
360
|
}, [awaitedQuestionId, composer, guard.redactText]);
|
|
347
|
-
//
|
|
361
|
+
// A text-only message sent while a run streams goes INTO that run when the host can deliver it.
|
|
362
|
+
// The draft clears before the await so the human sees it go out; whatever the outcome, the text
|
|
363
|
+
// ends up somewhere — in the run, or queued as the next turn (after stopping the run when the
|
|
364
|
+
// agent cannot take input mid-run). Never cancel on 'not-running'/'failed': the run is ending.
|
|
365
|
+
const sendIntoRunningTurn = useCallback(async ({ deliver, runId, turn }) => {
|
|
366
|
+
queuedConversationIdRef.current = options.conversationId;
|
|
367
|
+
composer.setDraft('');
|
|
368
|
+
const outcome = await deliverMidRunMessageSafely({ deliver, runId, text: turn.text });
|
|
369
|
+
if (outcome === 'delivered' || !mountedRef.current)
|
|
370
|
+
return;
|
|
371
|
+
setQueuedTurn(turn);
|
|
372
|
+
if (outcome === 'unsupported')
|
|
373
|
+
conversation.cancel();
|
|
374
|
+
}, [composer, conversation, options.conversationId]);
|
|
375
|
+
// The ordinary next-turn path: into the running run, else queue behind it, else send now.
|
|
348
376
|
const sendOrdinaryTurn = useCallback(async (prompt) => {
|
|
349
|
-
// Queue instead of no-op'ing. `setDraft('')` and NOT `composer.reset()`: reset also discards
|
|
350
|
-
// staged attachments, which this turn still needs when it finally goes out.
|
|
351
377
|
if (isChatPaneQueueableBlocker({ blocker: sendBlocker })) {
|
|
378
|
+
const turn = guard.redact({ text: prompt }, {});
|
|
379
|
+
const deliver = options.deliverMidRunMessage;
|
|
380
|
+
const runId = deliver === undefined ? null : findMidRunMessageRunId({
|
|
381
|
+
blocker: sendBlocker,
|
|
382
|
+
attachmentCount: composer.attachments.length,
|
|
383
|
+
messages: conversation.messages,
|
|
384
|
+
});
|
|
385
|
+
if (deliver !== undefined && runId !== null) {
|
|
386
|
+
await sendIntoRunningTurn({ deliver, runId, turn });
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
// Queue instead of no-op'ing. `setDraft('')` and NOT `composer.reset()`: reset also discards
|
|
390
|
+
// staged attachments, which this turn still needs when it finally goes out.
|
|
352
391
|
queuedConversationIdRef.current = options.conversationId;
|
|
353
|
-
setQueuedTurn(
|
|
392
|
+
setQueuedTurn(turn);
|
|
354
393
|
composer.setDraft('');
|
|
355
394
|
return;
|
|
356
395
|
}
|
|
357
396
|
if (!canSend)
|
|
358
397
|
return;
|
|
359
398
|
await sendPrompt(prompt);
|
|
360
|
-
}, [
|
|
399
|
+
}, [
|
|
400
|
+
canSend,
|
|
401
|
+
composer,
|
|
402
|
+
conversation.messages,
|
|
403
|
+
options.conversationId,
|
|
404
|
+
options.deliverMidRunMessage,
|
|
405
|
+
sendBlocker,
|
|
406
|
+
sendIntoRunningTurn,
|
|
407
|
+
sendPrompt,
|
|
408
|
+
guard.redact,
|
|
409
|
+
]);
|
|
361
410
|
const send = useCallback(async () => {
|
|
362
411
|
const prompt = composerPrompt(composer);
|
|
363
412
|
if (!prompt || deliveringTypedAnswerRef.current)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { MouseEvent as ReactMouseEvent } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Decides whether a click inside the pane should open a link in a new tab.
|
|
4
|
+
*
|
|
5
|
+
* @param input.event - The click, native (React's `nativeEvent` or a DOM listener's own).
|
|
6
|
+
* @param input.root - The pane root; an anchor outside it (one wrapping the whole pane) is ignored.
|
|
7
|
+
* @returns The anchor's resolved absolute URL to open, or `null` to leave the click to the browser.
|
|
8
|
+
* @complexity O(d) for `closest` over the target's ancestor depth d.
|
|
9
|
+
*/
|
|
10
|
+
export declare function newTabLinkHref({ event, root }: {
|
|
11
|
+
event: MouseEvent;
|
|
12
|
+
root: Element;
|
|
13
|
+
}): string | null;
|
|
14
|
+
/**
|
|
15
|
+
* The pane root's `onClickCapture` handler: opens qualifying link clicks in a new tab and cancels
|
|
16
|
+
* the in-tab navigation. See this module's doc for what qualifies.
|
|
17
|
+
*
|
|
18
|
+
* @param _required - Reserved; no required inputs.
|
|
19
|
+
* @param options.openInNewTab - Opener port; defaults to `window.open(url, '_blank', 'noopener,noreferrer')`.
|
|
20
|
+
* @returns A stable click-capture handler.
|
|
21
|
+
*/
|
|
22
|
+
export declare function useChatPaneNewTabLinks(_required?: Record<string, never>, { openInNewTab }?: {
|
|
23
|
+
openInNewTab?: (url: string) => void;
|
|
24
|
+
}): (event: ReactMouseEvent<HTMLElement>) => void;
|
|
25
|
+
//# sourceMappingURL=useChatPaneNewTabLinks.hooks.d.ts.map
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module useChatPaneNewTabLinks
|
|
3
|
+
*
|
|
4
|
+
* Every link inside the chat pane opens in a new tab, so following one never navigates the tab the
|
|
5
|
+
* conversation lives in (owner 2026-10-08: "every link"). One capture-phase click guard on the
|
|
6
|
+
* pane's root absorbs every link source at once — markdown in messages, tool cards, host-registered
|
|
7
|
+
* ext-event cards, A2UI surfaces, the header slot — instead of each renderer setting `target` on its
|
|
8
|
+
* own anchors and the next new card forgetting to.
|
|
9
|
+
*
|
|
10
|
+
* Capture phase, then `preventDefault()`: the guard runs before any anchor's own `onClick`, so a
|
|
11
|
+
* router `<Link>` (React Router, TanStack Router — both skip their in-app navigation once
|
|
12
|
+
* `defaultPrevented` is set) does not ALSO navigate the chat's tab after the new tab opens.
|
|
13
|
+
*
|
|
14
|
+
* Left to the browser: modified or non-primary clicks (the browser already opens those however the
|
|
15
|
+
* user asked), anchors that already say `target="_blank"`, `#` same-document anchors, `download`
|
|
16
|
+
* links, `role="button"` anchors (an action, not a link), and any scheme other than http(s) —
|
|
17
|
+
* `mailto:`/`tel:` hand off to another app without navigating the tab anyway.
|
|
18
|
+
*
|
|
19
|
+
* MCP-UI Views render inside a sandboxed iframe, out of this guard's reach; their proxy page
|
|
20
|
+
* forwards link clicks as `ui/open-link` (`@jini-ai/ui/mcp-ui`'s sandbox proxy) instead.
|
|
21
|
+
*/
|
|
22
|
+
import { useCallback } from 'react';
|
|
23
|
+
const NEW_TAB_PROTOCOLS = new Set(['http:', 'https:']);
|
|
24
|
+
/** Opens `url` in a new tab with no opener or referrer — `rel="noopener noreferrer"` for `window.open`. */
|
|
25
|
+
function openInNewTabDefault(url) {
|
|
26
|
+
window.open(url, '_blank', 'noopener,noreferrer');
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Decides whether a click inside the pane should open a link in a new tab.
|
|
30
|
+
*
|
|
31
|
+
* @param input.event - The click, native (React's `nativeEvent` or a DOM listener's own).
|
|
32
|
+
* @param input.root - The pane root; an anchor outside it (one wrapping the whole pane) is ignored.
|
|
33
|
+
* @returns The anchor's resolved absolute URL to open, or `null` to leave the click to the browser.
|
|
34
|
+
* @complexity O(d) for `closest` over the target's ancestor depth d.
|
|
35
|
+
*/
|
|
36
|
+
export function newTabLinkHref({ event, root }) {
|
|
37
|
+
if (event.defaultPrevented || event.button !== 0)
|
|
38
|
+
return null;
|
|
39
|
+
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)
|
|
40
|
+
return null;
|
|
41
|
+
const target = event.target;
|
|
42
|
+
if (!(target instanceof Element))
|
|
43
|
+
return null;
|
|
44
|
+
const link = target.closest('a[href]');
|
|
45
|
+
if (!(link instanceof HTMLAnchorElement) || !root.contains(link))
|
|
46
|
+
return null;
|
|
47
|
+
if (link.target === '_blank' || link.hasAttribute('download') || link.getAttribute('role') === 'button')
|
|
48
|
+
return null;
|
|
49
|
+
if (link.getAttribute('href').startsWith('#'))
|
|
50
|
+
return null;
|
|
51
|
+
return NEW_TAB_PROTOCOLS.has(link.protocol) ? link.href : null;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The pane root's `onClickCapture` handler: opens qualifying link clicks in a new tab and cancels
|
|
55
|
+
* the in-tab navigation. See this module's doc for what qualifies.
|
|
56
|
+
*
|
|
57
|
+
* @param _required - Reserved; no required inputs.
|
|
58
|
+
* @param options.openInNewTab - Opener port; defaults to `window.open(url, '_blank', 'noopener,noreferrer')`.
|
|
59
|
+
* @returns A stable click-capture handler.
|
|
60
|
+
*/
|
|
61
|
+
export function useChatPaneNewTabLinks(_required = {}, { openInNewTab = openInNewTabDefault } = {}) {
|
|
62
|
+
return useCallback((event) => {
|
|
63
|
+
const href = newTabLinkHref({ event: event.nativeEvent, root: event.currentTarget });
|
|
64
|
+
if (href === null)
|
|
65
|
+
return;
|
|
66
|
+
event.preventDefault();
|
|
67
|
+
openInNewTab(href);
|
|
68
|
+
}, [openInNewTab]);
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=useChatPaneNewTabLinks.hooks.js.map
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
export type { AgentRuntimePickerProps, ByokRuntimeSummary, ChatPaneActivity, ChatPaneAgent, ChatPaneAgentBridgeAccess, ChatPaneAgentControlOptions, ChatPaneAgentOption, ChatPaneAgentSelection, ChatPaneAgentToolAction, ChatPaneAttachmentUploadOptions, ChatPaneComposerHandle, ChatPaneProps, ChatPaneRunContext, ChatPaneRunContextInput, ChatPaneRuntimeAccess, ChatPaneVariant, ChatPaneWorkingDirectoryAccess, DeliverTypedAnswer, RuntimePickerPlacement, TypedAnswerDelivery, TypedAnswerNotice, } from './types.js';
|
|
1
|
+
export type { AgentRuntimePickerProps, ByokRuntimeSummary, ChatPaneActivity, ChatPaneAgent, ChatPaneAgentBridgeAccess, ChatPaneAgentControlOptions, ChatPaneAgentOption, ChatPaneAgentSelection, ChatPaneAgentToolAction, ChatPaneAttachmentUploadOptions, ChatPaneComposerHandle, ChatPaneProps, ChatPaneRunContext, ChatPaneRunContextInput, ChatPaneRuntimeAccess, ChatPaneVariant, ChatPaneWorkingDirectoryAccess, DeliverMidRunMessage, DeliverTypedAnswer, MidRunMessageDelivery, RuntimePickerPlacement, TypedAnswerDelivery, TypedAnswerNotice, } from './types.js';
|
|
2
2
|
export { CHAT_PANE_AGENT_TOOLS, type ChatPaneAgentToolDef, type ChatPaneAgentToolInputSchema, type ChatPaneAgentToolRisk, } from './agent-tools.js';
|
|
3
3
|
export { defaultChatPaneSelection, orderChatPaneAgents, resolveChatPaneSelection, } from './rules.js';
|
|
4
4
|
export { createDaemonAttachmentUploader, type CreateDaemonAttachmentUploaderOptions, } from './create-daemon-attachment-uploader.js';
|
|
5
5
|
export { createMcpUiToolCaller, type CreateMcpUiToolCallerOptions, type McpUiToolCallRequest, } from './create-mcp-ui-tool-caller.js';
|
|
6
|
+
export { createMidRunMessagePoster, type CreateMidRunMessagePosterOptions, } from './create-mid-run-message-poster.js';
|
|
6
7
|
export { createTypedAnswerPoster, type CreateTypedAnswerPosterOptions, } from './create-typed-answer-poster.js';
|
|
7
8
|
export { useChatPane, type UseChatPaneOptions, type UseChatPaneResult, } from './hooks/useChatPane.hooks.js';
|
|
8
9
|
export { useChatPaneAgentControl, type UseChatPaneAgentControlOptions, } from './hooks/useChatPaneAgentControl.hooks.js';
|
|
@@ -2,6 +2,7 @@ export { CHAT_PANE_AGENT_TOOLS, } from './agent-tools.js';
|
|
|
2
2
|
export { defaultChatPaneSelection, orderChatPaneAgents, resolveChatPaneSelection, } from './rules.js';
|
|
3
3
|
export { createDaemonAttachmentUploader, } from './create-daemon-attachment-uploader.js';
|
|
4
4
|
export { createMcpUiToolCaller, } from './create-mcp-ui-tool-caller.js';
|
|
5
|
+
export { createMidRunMessagePoster, } from './create-mid-run-message-poster.js';
|
|
5
6
|
export { createTypedAnswerPoster, } from './create-typed-answer-poster.js';
|
|
6
7
|
export { useChatPane, } from './hooks/useChatPane.hooks.js';
|
|
7
8
|
export { useChatPaneAgentControl, } from './hooks/useChatPaneAgentControl.hooks.js';
|
|
@@ -126,6 +126,19 @@ export declare function isTypedAnswerTurn({ blocker, attachmentCount, messages,
|
|
|
126
126
|
messages: readonly ChatMessage[];
|
|
127
127
|
toolName: string;
|
|
128
128
|
}): boolean;
|
|
129
|
+
/**
|
|
130
|
+
* The run a composer send can go INTO instead of queueing behind, or `null`: a run is streaming
|
|
131
|
+
* ({@link isChatPaneQueueableBlocker}), the turn has no attachments (a mid-run message carries text
|
|
132
|
+
* only — the live agent's stdin takes no files), and the newest message is that run's assistant
|
|
133
|
+
* turn with a known `runId`.
|
|
134
|
+
*
|
|
135
|
+
* @complexity O(1).
|
|
136
|
+
*/
|
|
137
|
+
export declare function findMidRunMessageRunId({ blocker, attachmentCount, messages }: {
|
|
138
|
+
blocker: ChatPaneSendBlocker | null;
|
|
139
|
+
attachmentCount: number;
|
|
140
|
+
messages: readonly ChatMessage[];
|
|
141
|
+
}): string | null;
|
|
129
142
|
/** The English copy (and i18n key) for why a typed answer was not sent. */
|
|
130
143
|
export declare function describeTypedAnswerNotice({ notice }: {
|
|
131
144
|
notice: TypedAnswerNotice;
|