@adecore/agents-react 0.24.0 → 0.24.2
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/chat/ui/Composer.js +35 -23
- package/dist/chat/ui/EditDiff.js +4 -3
- package/dist/chat/ui/UnifiedDiff.js +4 -3
- package/dist/chat/ui/composer/surface.d.ts +1 -0
- package/dist/chat/ui/composer/surface.js +44 -0
- package/dist/chat/ui/diff/ReviewDiff.js +2 -2
- package/dist/chat/ui/rows/WorkRows.js +3 -3
- package/dist/lazy.js +5 -7
- package/package.json +8 -8
- package/src/chat/ui/Composer.tsx +112 -96
- package/src/chat/ui/EditDiff.tsx +6 -5
- package/src/chat/ui/UnifiedDiff.tsx +6 -5
- package/src/chat/ui/composer/surface.ts +45 -0
- package/src/chat/ui/diff/ReviewDiff.tsx +3 -3
- package/src/chat/ui/rows/WorkRows.tsx +3 -3
- package/src/lazy.tsx +5 -7
package/dist/chat/ui/Composer.js
CHANGED
|
@@ -39,6 +39,7 @@ import { useNotice } from './composer/use-notice.js';
|
|
|
39
39
|
import { usePendingRequests } from './composer/use-pending-requests.js';
|
|
40
40
|
import { usePromptHistory } from './composer/use-prompt-history.js';
|
|
41
41
|
import { usePromptHold } from './composer/use-prompt-hold.js';
|
|
42
|
+
import { surfacePartOf } from './composer/surface.js';
|
|
42
43
|
import { useTakeBack } from './composer/use-take-back.js';
|
|
43
44
|
function sendLabelKey(busy, steerable, delivery) {
|
|
44
45
|
if (!busy) {
|
|
@@ -121,29 +122,40 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
121
122
|
setText: (next) => box.setText(next),
|
|
122
123
|
submit
|
|
123
124
|
});
|
|
125
|
+
const pointSurface = (event) => {
|
|
126
|
+
event.currentTarget.toggleAttribute('data-text-cursor', surfacePartOf(event.target, event.currentTarget) !== 'action');
|
|
127
|
+
};
|
|
128
|
+
const pressSurface = (event) => {
|
|
129
|
+
if (!writable || event.button !== 0 || event.defaultPrevented || surfacePartOf(event.target, event.currentTarget) !== 'surface') {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
// Nothing around the field selects, so the default would only blur it on the way, closing its picker and lifting a held prompt card.
|
|
133
|
+
event.preventDefault();
|
|
134
|
+
inputRef.current?.setCaret(Infinity);
|
|
135
|
+
};
|
|
124
136
|
return (_jsxs("div", { className: "chat-composer-content pointer-events-none relative z-10 w-full", children: [!atEnd && !readOnly && (_jsx("div", { className: "absolute inset-x-0 bottom-full mb-2 flex justify-center", children: _jsx(Tooltip, { label: t('composer.jumpToEnd'), name: true, children: _jsx(Surface, { render: _jsx("button", { onClick: () => scrollTimelineToEnd(chatId) }), className: "pointer-events-auto grid h-8 w-8 place-items-center rounded-full text-text-muted hover:text-text", children: _jsx(Icon, { icon: ChevronDown, size: 16 }) }) }) })), _jsx(ChatActivity, { chatId: chatId }), _jsx("div", { className: clsx('@container/composer pointer-events-auto flex flex-col overflow-hidden rounded-2xl border shadow-float backdrop-blur-[14px] focus-ring-within', files.dragging
|
|
125
137
|
? 'border-accent bg-[color-mix(in_srgb,var(--accent-soft)_60%,var(--surface-raised))]'
|
|
126
|
-
: 'border-border bg-[color-mix(in_srgb,var(--surface-raised)_92%,transparent)]'), ...files.dropTarget, children:
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
138
|
+
: 'border-border bg-[color-mix(in_srgb,var(--surface-raised)_92%,transparent)]'), ...files.dropTarget, children: _jsx(PromptComposer, { chatId: chatId, pending: pending, focused: focused, onShowElsewhere: answerPromptsElsewhere, disabled: disabled, hasDraft: !empty, denyReason: capabilities?.denyReason === true, onAllAnswered: () => inputRef.current?.focus(), hold: hold, children: _jsxs("div", { className: clsx(writable && 'data-[text-cursor]:cursor-text'), onPointerOver: pointSurface, onMouseDown: pressSurface, children: [_jsx(LimitDock, { chatId: chatId, info: info }), _jsx(GoalDock, { chatId: chatId, info: info, goals: capabilities?.goals }), compactionOffer && (_jsx(ResumeCompactionDock, { tokens: compactionOffer.tokens, onCompact: controls.compact, onDismiss: () => dismissResumeCompaction(scope, chatId, compactionOffer.turnId) })), _jsx(ComposerMenu, { menus: menus }), queue.length > 0 && (_jsx(QueuedMessages, { chatId: chatId, queue: queue, paused: info.queuePaused === true, takingBack: takingBack !== null, onEdit: (message) => void takeBack(message) })), draft.quote !== '' && (_jsx(DraftQuote, { chatId: chatId, quote: draft.quote, source: draft.quoteSource, onRemove: box.removeQuote, onSourceGone: () => notice.setNotice(t('rows.user.quoteGone')) })), draft.chats.length > 0 && _jsx(DraftChats, { chats: draft.chats, onRemove: box.removeChat }), draft.attachments.length > 0 && _jsx(DraftAttachments, { attachments: draft.attachments, onRemove: box.removeAttachment }), _jsx(ComposerInput, { ref: inputRef, dictationToolbar: dictationToolbar, className: "composer-input select-text text-sm leading-normal text-text", value: draft.text, placeholder: placeholder, disabled: !writable, tabbable: focused, extensions: editorExtensions, aria: menus.inputAria, onChange: (value, selection, state) => {
|
|
139
|
+
hold.noteInput();
|
|
140
|
+
box.setText(value);
|
|
141
|
+
menus.setMenuIndex(0);
|
|
142
|
+
menus.trackTriggers(value, selection, state);
|
|
143
|
+
history.edited(value);
|
|
144
|
+
}, onSelectionChange: menus.trackTriggers, onBlur: () => {
|
|
145
|
+
menus.close();
|
|
146
|
+
hold.release('blur');
|
|
147
|
+
}, onComposing: hold.setComposing, onKeyDown: onKeyDown, onPaste: files.onPaste }), notice.notice && (_jsx("div", { className: "px-5 pb-1 text-xs text-status-error @max-md/composer:px-3.5", role: "alert", children: notice.notice })), guard.visible && (_jsxs("div", { className: clsx('px-5 pb-1 text-right text-xs tabular-nums @max-md/composer:px-3.5', guard.tooLong ? 'text-status-error' : 'text-text-faint'), children: [formatNumber(guard.count), " / ", formatNumber(PROMPT_MAX_CHARS), guard.tooLong && ` ${t('composer.tooLong')}`] })), _jsx(ComposerControls, { chatId: chatId, info: info, providerFixed: providerFixed, writable: writable, attachable: capabilities?.attachments !== false, controls: controls, send: {
|
|
148
|
+
visible: !busy || !empty,
|
|
149
|
+
disabled: empty || !writable || guard.tooLong,
|
|
150
|
+
label: t(sendLabelKey(busy, steerable, sendDelivery)),
|
|
151
|
+
kbd: sendShortcut(sendKey, draft.text.includes('\n')),
|
|
152
|
+
onSend: (alternate) => submit(alternate ? 'alternate' : 'default')
|
|
153
|
+
}, dictationToolbar: setDictationToolbar, onFiles: (picked) => {
|
|
154
|
+
files.addFiles(picked);
|
|
155
|
+
inputRef.current?.focus();
|
|
156
|
+
}, onRestore: restoreStashed, insert: (added) => {
|
|
157
|
+
if (writable) {
|
|
158
|
+
inputRef.current?.insert(added);
|
|
159
|
+
}
|
|
160
|
+
} })] }) }) }), _jsx(PromptDialog, { open: controls.confirmClear, title: t('composer.clear.title'), description: t('composer.clear.description'), confirmLabel: t('composer.clear.confirm'), danger: true, fallbackMessage: t('composer.notice.clearFailed'), onConfirm: controls.forceClearThread, onOpenChange: controls.cancelClear })] }));
|
|
149
161
|
}
|
package/dist/chat/ui/EditDiff.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
import { LazyBoundary } from '@adecore/ui';
|
|
3
4
|
import { useTranslation } from 'react-i18next';
|
|
4
5
|
import { lazyNamed } from '../../lazy.js';
|
|
5
6
|
import { diffSourceBudget, legacyDiffBudget } from './diff/diff-source-budget.js';
|
|
@@ -17,7 +18,7 @@ export default function EditDiff({ change, allowEditor = true, maxHeight }) {
|
|
|
17
18
|
const legacyMode = !allowEditor || loadEngine === undefined;
|
|
18
19
|
const legacyAllowed = useMemo(() => legacyMode && legacyDiffBudget(change.before, change.after), [legacyMode, change.before, change.after]);
|
|
19
20
|
const omitted = _jsx(DiffOmitted, { path: change.path });
|
|
20
|
-
const fallback = legacyAllowed ? (_jsx(DiffFallbackBoundary, { fallback: omitted, resetKeys: [change.path, change.before, change.after], children: _jsx(
|
|
21
|
+
const fallback = legacyAllowed ? (_jsx(DiffFallbackBoundary, { fallback: omitted, resetKeys: [change.path, change.before, change.after], children: _jsx(LazyBoundary, { fallback: _jsx("div", { className: "px-3 py-2 text-xs text-text-faint", children: t('work.loadingDiff') }), children: _jsx(LegacyEditDiff, { change: change, maxHeight: maxHeight }) }) })) : (omitted);
|
|
21
22
|
if (!sourceAllowed) {
|
|
22
23
|
return omitted;
|
|
23
24
|
}
|
|
@@ -25,5 +26,5 @@ export default function EditDiff({ change, allowEditor = true, maxHeight }) {
|
|
|
25
26
|
return fallback;
|
|
26
27
|
}
|
|
27
28
|
const unavailable = _jsx(DiffUnavailable, { path: change.path });
|
|
28
|
-
return (_jsx(DiffFallbackBoundary, { fallback: unavailable, resetKeys: [change.path, change.before, change.after, loadEngine], children: _jsx(
|
|
29
|
+
return (_jsx(DiffFallbackBoundary, { fallback: unavailable, resetKeys: [change.path, change.before, change.after, loadEngine], children: _jsx(LazyBoundary, { fallback: _jsx("div", { className: "px-3 py-2 text-xs text-text-faint", children: t('work.loadingDiff') }), children: _jsx(EditorEditDiff, { change: change, loadEngine: loadEngine, fallback: unavailable, maxHeight: maxHeight, sourceChecked: true }) }) }));
|
|
29
30
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
import { LazyBoundary } from '@adecore/ui';
|
|
3
4
|
import { useTranslation } from 'react-i18next';
|
|
4
5
|
import { lazyNamed } from '../../lazy.js';
|
|
5
6
|
import { diffSourceBudget, legacyDiffBudget, patchSourceBudget } from './diff/diff-source-budget.js';
|
|
@@ -23,7 +24,7 @@ export default function UnifiedDiff(props) {
|
|
|
23
24
|
const legacyAllowed = useMemo(() => legacyMode && legacyDiffBudget(oldText, newText, change.diff), [legacyMode, oldText, newText, change.diff]);
|
|
24
25
|
const rawAllowed = useMemo(() => hasHunks || legacyDiffBudget('', '', change.diff), [hasHunks, change.diff]);
|
|
25
26
|
const omitted = _jsx(DiffOmitted, { path: change.path });
|
|
26
|
-
const fallback = legacyAllowed ? (_jsx(DiffFallbackBoundary, { fallback: omitted, resetKeys: [change.path, change.diff, oldText, newText, diffStyle], children: _jsx(
|
|
27
|
+
const fallback = legacyAllowed ? (_jsx(DiffFallbackBoundary, { fallback: omitted, resetKeys: [change.path, change.diff, oldText, newText, diffStyle], children: _jsx(LazyBoundary, { fallback: _jsx("div", { className: "px-3 py-2 text-xs text-text-faint", children: t('work.loadingDiff') }), children: _jsx(LegacyUnifiedDiff, { ...props }) }) })) : (omitted);
|
|
27
28
|
if (!sourceAllowed) {
|
|
28
29
|
return omitted;
|
|
29
30
|
}
|
|
@@ -34,5 +35,5 @@ export default function UnifiedDiff(props) {
|
|
|
34
35
|
return fallback;
|
|
35
36
|
}
|
|
36
37
|
const unavailable = _jsx(DiffUnavailable, { path: change.path });
|
|
37
|
-
return (_jsx(DiffFallbackBoundary, { fallback: unavailable, resetKeys: [change.path, change.diff, oldText, newText, ignoreWhitespace, diffStyle, loadEngine], children: _jsx(
|
|
38
|
+
return (_jsx(DiffFallbackBoundary, { fallback: unavailable, resetKeys: [change.path, change.diff, oldText, newText, ignoreWhitespace, diffStyle, loadEngine], children: _jsx(LazyBoundary, { fallback: _jsx("div", { className: "px-3 py-2 text-xs text-text-faint", children: t('work.loadingDiff') }), children: _jsx(EditorPatchDiff, { change: change, sourceChecked: true, contents: contents, ignoreWhitespace: ignoreWhitespace, wrap: overflow === 'wrap', fill: fill, maxHeight: maxHeight, layout: diffStyle, loadEngine: loadEngine, fallback: unavailable }) }) }));
|
|
38
39
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function surfacePartOf(target: EventTarget | null, surface: Element): 'input' | 'action' | 'surface';
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/* Anything with an action of its own; `data-composer-action` marks one a role cannot name, such as a control of the host's. */
|
|
2
|
+
const ACTIONS = [
|
|
3
|
+
'a',
|
|
4
|
+
'button',
|
|
5
|
+
'input',
|
|
6
|
+
'select',
|
|
7
|
+
'textarea',
|
|
8
|
+
'label',
|
|
9
|
+
'summary',
|
|
10
|
+
'[contenteditable]:not([contenteditable="false"])',
|
|
11
|
+
'[tabindex]:not([tabindex="-1"])',
|
|
12
|
+
'[role="button"]',
|
|
13
|
+
'[role="link"]',
|
|
14
|
+
'[role="menuitem"]',
|
|
15
|
+
'[role="menuitemcheckbox"]',
|
|
16
|
+
'[role="menuitemradio"]',
|
|
17
|
+
'[role="option"]',
|
|
18
|
+
'[role="listbox"]',
|
|
19
|
+
'[role="checkbox"]',
|
|
20
|
+
'[role="radio"]',
|
|
21
|
+
'[role="switch"]',
|
|
22
|
+
'[role="tab"]',
|
|
23
|
+
'[role="slider"]',
|
|
24
|
+
'[role="combobox"]',
|
|
25
|
+
'[data-composer-action]'
|
|
26
|
+
].join(', ');
|
|
27
|
+
/*
|
|
28
|
+
* Where a pointer is on the composer: in the text field, on an action, or on the space around them,
|
|
29
|
+
* which stands in for the text field. A target outside the surface is an action, as an event from a
|
|
30
|
+
* popup portaled out of it still reaches the surface through React.
|
|
31
|
+
*/
|
|
32
|
+
export function surfacePartOf(target, surface) {
|
|
33
|
+
// Not `instanceof Element`, which fails for an element of another window.
|
|
34
|
+
const element = target;
|
|
35
|
+
if (typeof element?.closest !== 'function' || !surface.contains(element)) {
|
|
36
|
+
return 'action';
|
|
37
|
+
}
|
|
38
|
+
const input = element.closest('.cm-editor');
|
|
39
|
+
if (input !== null && surface.contains(input)) {
|
|
40
|
+
return 'input';
|
|
41
|
+
}
|
|
42
|
+
const action = element.closest(ACTIONS);
|
|
43
|
+
return action !== null && surface.contains(action) ? 'action' : 'surface';
|
|
44
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { LazyBoundary } from '@adecore/ui';
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
4
|
import { lazyNamed } from '../../../lazy.js';
|
|
5
5
|
const EditDiff = lazyNamed(() => import('../EditDiff.js'), 'default');
|
|
6
6
|
const UnifiedDiff = lazyNamed(() => import('../UnifiedDiff.js'), 'default');
|
|
7
7
|
export function ReviewDiff({ entry, maxHeight = 480 }) {
|
|
8
8
|
const { t } = useTranslation('agent-chat');
|
|
9
|
-
return (_jsx("div", { "data-review-entry": entry.key, children: _jsx(
|
|
9
|
+
return (_jsx("div", { "data-review-entry": entry.key, children: _jsx(LazyBoundary, { fallback: _jsx("p", { className: "px-3 py-2 text-xs text-text-faint", children: t('work.loadingDiff') }), children: entry.change.kind === 'edit' ? (_jsx(EditDiff, { change: entry.change.edit, maxHeight: maxHeight })) : (_jsx(UnifiedDiff, { change: entry.change.patch, contents: entry.change.contents, maxHeight: maxHeight })) }) }));
|
|
10
10
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Fragment,
|
|
2
|
+
import { Fragment, useEffect, useState } from 'react';
|
|
3
3
|
import clsx from 'clsx';
|
|
4
4
|
import i18next from 'i18next';
|
|
5
5
|
import { useTranslation } from 'react-i18next';
|
|
@@ -13,7 +13,7 @@ import { useChatScope } from '../../../scope.js';
|
|
|
13
13
|
import { useCurrentItem } from '../../../state/chats.js';
|
|
14
14
|
import { useTimelineFlash } from '../../timeline-flash.js';
|
|
15
15
|
import { formatClockDuration, formatElapsedShort } from '@adecore/ui/format';
|
|
16
|
-
import { Icon, MiddleTruncate, Spinner, useTickingText } from '@adecore/ui';
|
|
16
|
+
import { Icon, LazyBoundary, MiddleTruncate, Spinner, useTickingText } from '@adecore/ui';
|
|
17
17
|
import { ROW_GUTTER, toolIcon } from '../icons.js';
|
|
18
18
|
import { AnsiOutput } from '../AnsiOutput.js';
|
|
19
19
|
import { RowTime } from '../RowTime.js';
|
|
@@ -48,7 +48,7 @@ export function ToggleLine({ icon, label, detail, command, path, open = false, o
|
|
|
48
48
|
/* The diff renderers load lazily, so a diff waits on them under a line that says so. */
|
|
49
49
|
function DiffSuspense({ children }) {
|
|
50
50
|
const { t } = useTranslation('agent-chat');
|
|
51
|
-
return _jsx(
|
|
51
|
+
return _jsx(LazyBoundary, { fallback: _jsx("div", { className: "px-3 py-2 text-xs text-text-faint", children: t('work.loadingDiff') }), children: children });
|
|
52
52
|
}
|
|
53
53
|
/* A file in a changed files card, which opens its diff under it. */
|
|
54
54
|
function ChangedFileLine({ path, open, onToggle, trailing, children }) {
|
package/dist/lazy.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { lazy } from 'react';
|
|
3
|
+
import { LoadedComponent } from '@adecore/ui';
|
|
3
4
|
const loaders = [];
|
|
4
5
|
let register = null;
|
|
5
6
|
const openErrorListeners = new Set();
|
|
@@ -29,12 +30,9 @@ export function onLazyOpenError(listener) {
|
|
|
29
30
|
* module the prefetcher already fetched.
|
|
30
31
|
*/
|
|
31
32
|
export function lazyNamed(load, name, options) {
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
loaded ??= component;
|
|
36
|
-
return component;
|
|
37
|
-
};
|
|
33
|
+
// Through `LoadedComponent`, so a `LazyBoundary` around it draws the moment the module lands.
|
|
34
|
+
const loaded = new LoadedComponent(async () => (await load())[name]);
|
|
35
|
+
const open = () => loaded.load();
|
|
38
36
|
if (options?.prefetch !== false) {
|
|
39
37
|
loaders.push(open);
|
|
40
38
|
register?.(open);
|
|
@@ -52,7 +50,7 @@ export function lazyNamed(load, name, options) {
|
|
|
52
50
|
}
|
|
53
51
|
});
|
|
54
52
|
const LazyNamed = (props) => {
|
|
55
|
-
const Component = loaded ?? Lazy;
|
|
53
|
+
const Component = loaded.current ?? Lazy;
|
|
56
54
|
return _jsx(Component, { ...props });
|
|
57
55
|
};
|
|
58
56
|
return LazyNamed;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adecore/agents-react",
|
|
3
|
-
"version": "0.24.
|
|
3
|
+
"version": "0.24.2",
|
|
4
4
|
"description": "React agent chat threads, composers, approvals, provider accounts and usage views.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -67,11 +67,11 @@
|
|
|
67
67
|
"test:visuals": "bun --config=integration.bunfig.toml --conditions=source test ./src/chat/visual-viewport.integration.test.ts"
|
|
68
68
|
},
|
|
69
69
|
"dependencies": {
|
|
70
|
-
"@adecore/agent-contracts": "0.24.
|
|
71
|
-
"@adecore/editor": "0.24.
|
|
72
|
-
"@adecore/editor-react": "0.24.
|
|
73
|
-
"@adecore/intelligent-ui": "0.24.
|
|
74
|
-
"@adecore/merge": "0.24.
|
|
70
|
+
"@adecore/agent-contracts": "0.24.2",
|
|
71
|
+
"@adecore/editor": "0.24.2",
|
|
72
|
+
"@adecore/editor-react": "0.24.2",
|
|
73
|
+
"@adecore/intelligent-ui": "0.24.2",
|
|
74
|
+
"@adecore/merge": "0.24.2",
|
|
75
75
|
"@codemirror/commands": "^6.11.1",
|
|
76
76
|
"@codemirror/language": "^6.12.4",
|
|
77
77
|
"@codemirror/state": "^6.7.6",
|
|
@@ -93,14 +93,14 @@
|
|
|
93
93
|
"zustand": "^5.0.15"
|
|
94
94
|
},
|
|
95
95
|
"peerDependencies": {
|
|
96
|
-
"@adecore/ui": "^0.24.
|
|
96
|
+
"@adecore/ui": "^0.24.2",
|
|
97
97
|
"i18next": "^26.4.2",
|
|
98
98
|
"react": "^19.3.0",
|
|
99
99
|
"react-dom": "^19.3.0",
|
|
100
100
|
"react-i18next": "^17.0.15"
|
|
101
101
|
},
|
|
102
102
|
"devDependencies": {
|
|
103
|
-
"@adecore/ui": "0.24.
|
|
103
|
+
"@adecore/ui": "0.24.2",
|
|
104
104
|
"@types/bun": "^1.4.2",
|
|
105
105
|
"@types/react": "^19.3.0",
|
|
106
106
|
"@types/react-dom": "^19.3.0",
|
package/src/chat/ui/Composer.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
|
|
1
|
+
import { useEffect, useMemo, useRef, useState, useSyncExternalStore, type MouseEvent, type PointerEvent } from 'react';
|
|
2
2
|
import { useTranslation } from 'react-i18next';
|
|
3
3
|
import clsx from 'clsx';
|
|
4
4
|
import { ChevronDown } from 'lucide-react';
|
|
@@ -40,6 +40,7 @@ import { useNotice } from './composer/use-notice';
|
|
|
40
40
|
import { usePendingRequests } from './composer/use-pending-requests';
|
|
41
41
|
import { usePromptHistory } from './composer/use-prompt-history';
|
|
42
42
|
import { usePromptHold } from './composer/use-prompt-hold';
|
|
43
|
+
import { surfacePartOf } from './composer/surface';
|
|
43
44
|
import { useTakeBack } from './composer/use-take-back';
|
|
44
45
|
|
|
45
46
|
interface ComposerProps {
|
|
@@ -154,6 +155,19 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
154
155
|
submit
|
|
155
156
|
});
|
|
156
157
|
|
|
158
|
+
const pointSurface = (event: PointerEvent<HTMLDivElement>) => {
|
|
159
|
+
event.currentTarget.toggleAttribute('data-text-cursor', surfacePartOf(event.target, event.currentTarget) !== 'action');
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const pressSurface = (event: MouseEvent<HTMLDivElement>) => {
|
|
163
|
+
if (!writable || event.button !== 0 || event.defaultPrevented || surfacePartOf(event.target, event.currentTarget) !== 'surface') {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
// Nothing around the field selects, so the default would only blur it on the way, closing its picker and lifting a held prompt card.
|
|
167
|
+
event.preventDefault();
|
|
168
|
+
inputRef.current?.setCaret(Infinity);
|
|
169
|
+
};
|
|
170
|
+
|
|
157
171
|
return (
|
|
158
172
|
<div className="chat-composer-content pointer-events-none relative z-10 w-full">
|
|
159
173
|
{/* Over the composer rather than in the thread, since the composer's height is the one it always clears.
|
|
@@ -191,104 +205,106 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
191
205
|
onAllAnswered={() => inputRef.current?.focus()}
|
|
192
206
|
hold={hold}
|
|
193
207
|
>
|
|
194
|
-
<
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
208
|
+
<div className={clsx(writable && 'data-[text-cursor]:cursor-text')} onPointerOver={pointSurface} onMouseDown={pressSurface}>
|
|
209
|
+
<LimitDock chatId={chatId} info={info} />
|
|
210
|
+
<GoalDock chatId={chatId} info={info} goals={capabilities?.goals} />
|
|
211
|
+
{compactionOffer && (
|
|
212
|
+
<ResumeCompactionDock
|
|
213
|
+
tokens={compactionOffer.tokens}
|
|
214
|
+
onCompact={controls.compact}
|
|
215
|
+
onDismiss={() => dismissResumeCompaction(scope, chatId, compactionOffer.turnId)}
|
|
216
|
+
/>
|
|
217
|
+
)}
|
|
218
|
+
<ComposerMenu menus={menus} />
|
|
219
|
+
{queue.length > 0 && (
|
|
220
|
+
<QueuedMessages
|
|
221
|
+
chatId={chatId}
|
|
222
|
+
queue={queue}
|
|
223
|
+
paused={info.queuePaused === true}
|
|
224
|
+
takingBack={takingBack !== null}
|
|
225
|
+
onEdit={(message) => void takeBack(message)}
|
|
226
|
+
/>
|
|
227
|
+
)}
|
|
228
|
+
{draft.quote !== '' && (
|
|
229
|
+
<DraftQuote
|
|
230
|
+
chatId={chatId}
|
|
231
|
+
quote={draft.quote}
|
|
232
|
+
source={draft.quoteSource}
|
|
233
|
+
onRemove={box.removeQuote}
|
|
234
|
+
onSourceGone={() => notice.setNotice(t('rows.user.quoteGone'))}
|
|
235
|
+
/>
|
|
236
|
+
)}
|
|
237
|
+
{draft.chats.length > 0 && <DraftChats chats={draft.chats} onRemove={box.removeChat} />}
|
|
238
|
+
{draft.attachments.length > 0 && <DraftAttachments attachments={draft.attachments} onRemove={box.removeAttachment} />}
|
|
239
|
+
<ComposerInput
|
|
240
|
+
ref={inputRef}
|
|
241
|
+
dictationToolbar={dictationToolbar}
|
|
242
|
+
className="composer-input select-text text-sm leading-normal text-text"
|
|
243
|
+
value={draft.text}
|
|
244
|
+
placeholder={placeholder}
|
|
245
|
+
disabled={!writable}
|
|
246
|
+
tabbable={focused}
|
|
247
|
+
extensions={editorExtensions}
|
|
248
|
+
aria={menus.inputAria}
|
|
249
|
+
onChange={(value, selection, state) => {
|
|
250
|
+
hold.noteInput();
|
|
251
|
+
box.setText(value);
|
|
252
|
+
menus.setMenuIndex(0);
|
|
253
|
+
menus.trackTriggers(value, selection, state);
|
|
254
|
+
history.edited(value);
|
|
255
|
+
}}
|
|
256
|
+
onSelectionChange={menus.trackTriggers}
|
|
257
|
+
onBlur={() => {
|
|
258
|
+
menus.close();
|
|
259
|
+
hold.release('blur');
|
|
260
|
+
}}
|
|
261
|
+
onComposing={hold.setComposing}
|
|
262
|
+
onKeyDown={onKeyDown}
|
|
263
|
+
onPaste={files.onPaste}
|
|
201
264
|
/>
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
265
|
+
{notice.notice && (
|
|
266
|
+
<div className="px-5 pb-1 text-xs text-status-error @max-md/composer:px-3.5" role="alert">
|
|
267
|
+
{notice.notice}
|
|
268
|
+
</div>
|
|
269
|
+
)}
|
|
270
|
+
{guard.visible && (
|
|
271
|
+
<div
|
|
272
|
+
className={clsx(
|
|
273
|
+
'px-5 pb-1 text-right text-xs tabular-nums @max-md/composer:px-3.5',
|
|
274
|
+
guard.tooLong ? 'text-status-error' : 'text-text-faint'
|
|
275
|
+
)}
|
|
276
|
+
>
|
|
277
|
+
{formatNumber(guard.count)} / {formatNumber(PROMPT_MAX_CHARS)}
|
|
278
|
+
{guard.tooLong && ` ${t('composer.tooLong')}`}
|
|
279
|
+
</div>
|
|
280
|
+
)}
|
|
281
|
+
<ComposerControls
|
|
206
282
|
chatId={chatId}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
283
|
+
info={info}
|
|
284
|
+
providerFixed={providerFixed}
|
|
285
|
+
writable={writable}
|
|
286
|
+
attachable={capabilities?.attachments !== false}
|
|
287
|
+
controls={controls}
|
|
288
|
+
send={{
|
|
289
|
+
visible: !busy || !empty,
|
|
290
|
+
disabled: empty || !writable || guard.tooLong,
|
|
291
|
+
label: t(sendLabelKey(busy, steerable, sendDelivery)),
|
|
292
|
+
kbd: sendShortcut(sendKey, draft.text.includes('\n')),
|
|
293
|
+
onSend: (alternate) => submit(alternate ? 'alternate' : 'default')
|
|
294
|
+
}}
|
|
295
|
+
dictationToolbar={setDictationToolbar}
|
|
296
|
+
onFiles={(picked) => {
|
|
297
|
+
files.addFiles(picked);
|
|
298
|
+
inputRef.current?.focus();
|
|
299
|
+
}}
|
|
300
|
+
onRestore={restoreStashed}
|
|
301
|
+
insert={(added) => {
|
|
302
|
+
if (writable) {
|
|
303
|
+
inputRef.current?.insert(added);
|
|
304
|
+
}
|
|
305
|
+
}}
|
|
211
306
|
/>
|
|
212
|
-
|
|
213
|
-
{draft.quote !== '' && (
|
|
214
|
-
<DraftQuote
|
|
215
|
-
chatId={chatId}
|
|
216
|
-
quote={draft.quote}
|
|
217
|
-
source={draft.quoteSource}
|
|
218
|
-
onRemove={box.removeQuote}
|
|
219
|
-
onSourceGone={() => notice.setNotice(t('rows.user.quoteGone'))}
|
|
220
|
-
/>
|
|
221
|
-
)}
|
|
222
|
-
{draft.chats.length > 0 && <DraftChats chats={draft.chats} onRemove={box.removeChat} />}
|
|
223
|
-
{draft.attachments.length > 0 && <DraftAttachments attachments={draft.attachments} onRemove={box.removeAttachment} />}
|
|
224
|
-
<ComposerInput
|
|
225
|
-
ref={inputRef}
|
|
226
|
-
dictationToolbar={dictationToolbar}
|
|
227
|
-
className="composer-input select-text text-sm leading-normal text-text"
|
|
228
|
-
value={draft.text}
|
|
229
|
-
placeholder={placeholder}
|
|
230
|
-
disabled={!writable}
|
|
231
|
-
tabbable={focused}
|
|
232
|
-
extensions={editorExtensions}
|
|
233
|
-
aria={menus.inputAria}
|
|
234
|
-
onChange={(value, selection, state) => {
|
|
235
|
-
hold.noteInput();
|
|
236
|
-
box.setText(value);
|
|
237
|
-
menus.setMenuIndex(0);
|
|
238
|
-
menus.trackTriggers(value, selection, state);
|
|
239
|
-
history.edited(value);
|
|
240
|
-
}}
|
|
241
|
-
onSelectionChange={menus.trackTriggers}
|
|
242
|
-
onBlur={() => {
|
|
243
|
-
menus.close();
|
|
244
|
-
hold.release('blur');
|
|
245
|
-
}}
|
|
246
|
-
onComposing={hold.setComposing}
|
|
247
|
-
onKeyDown={onKeyDown}
|
|
248
|
-
onPaste={files.onPaste}
|
|
249
|
-
/>
|
|
250
|
-
{notice.notice && (
|
|
251
|
-
<div className="px-5 pb-1 text-xs text-status-error @max-md/composer:px-3.5" role="alert">
|
|
252
|
-
{notice.notice}
|
|
253
|
-
</div>
|
|
254
|
-
)}
|
|
255
|
-
{guard.visible && (
|
|
256
|
-
<div
|
|
257
|
-
className={clsx(
|
|
258
|
-
'px-5 pb-1 text-right text-xs tabular-nums @max-md/composer:px-3.5',
|
|
259
|
-
guard.tooLong ? 'text-status-error' : 'text-text-faint'
|
|
260
|
-
)}
|
|
261
|
-
>
|
|
262
|
-
{formatNumber(guard.count)} / {formatNumber(PROMPT_MAX_CHARS)}
|
|
263
|
-
{guard.tooLong && ` ${t('composer.tooLong')}`}
|
|
264
|
-
</div>
|
|
265
|
-
)}
|
|
266
|
-
<ComposerControls
|
|
267
|
-
chatId={chatId}
|
|
268
|
-
info={info}
|
|
269
|
-
providerFixed={providerFixed}
|
|
270
|
-
writable={writable}
|
|
271
|
-
attachable={capabilities?.attachments !== false}
|
|
272
|
-
controls={controls}
|
|
273
|
-
send={{
|
|
274
|
-
visible: !busy || !empty,
|
|
275
|
-
disabled: empty || !writable || guard.tooLong,
|
|
276
|
-
label: t(sendLabelKey(busy, steerable, sendDelivery)),
|
|
277
|
-
kbd: sendShortcut(sendKey, draft.text.includes('\n')),
|
|
278
|
-
onSend: (alternate) => submit(alternate ? 'alternate' : 'default')
|
|
279
|
-
}}
|
|
280
|
-
dictationToolbar={setDictationToolbar}
|
|
281
|
-
onFiles={(picked) => {
|
|
282
|
-
files.addFiles(picked);
|
|
283
|
-
inputRef.current?.focus();
|
|
284
|
-
}}
|
|
285
|
-
onRestore={restoreStashed}
|
|
286
|
-
insert={(added) => {
|
|
287
|
-
if (writable) {
|
|
288
|
-
inputRef.current?.insert(added);
|
|
289
|
-
}
|
|
290
|
-
}}
|
|
291
|
-
/>
|
|
307
|
+
</div>
|
|
292
308
|
</PromptComposer>
|
|
293
309
|
</div>
|
|
294
310
|
<PromptDialog
|
package/src/chat/ui/EditDiff.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { LazyBoundary } from '@adecore/ui';
|
|
2
3
|
import { useTranslation } from 'react-i18next';
|
|
3
4
|
import { lazyNamed } from '../../lazy';
|
|
4
5
|
import { diffSourceBudget, legacyDiffBudget } from './diff/diff-source-budget';
|
|
@@ -22,9 +23,9 @@ export default function EditDiff({ change, allowEditor = true, maxHeight }: { ch
|
|
|
22
23
|
const omitted = <DiffOmitted path={change.path} />;
|
|
23
24
|
const fallback = legacyAllowed ? (
|
|
24
25
|
<DiffFallbackBoundary fallback={omitted} resetKeys={[change.path, change.before, change.after]}>
|
|
25
|
-
<
|
|
26
|
+
<LazyBoundary fallback={<div className="px-3 py-2 text-xs text-text-faint">{t('work.loadingDiff')}</div>}>
|
|
26
27
|
<LegacyEditDiff change={change} maxHeight={maxHeight} />
|
|
27
|
-
</
|
|
28
|
+
</LazyBoundary>
|
|
28
29
|
</DiffFallbackBoundary>
|
|
29
30
|
) : (
|
|
30
31
|
omitted
|
|
@@ -38,9 +39,9 @@ export default function EditDiff({ change, allowEditor = true, maxHeight }: { ch
|
|
|
38
39
|
const unavailable = <DiffUnavailable path={change.path} />;
|
|
39
40
|
return (
|
|
40
41
|
<DiffFallbackBoundary fallback={unavailable} resetKeys={[change.path, change.before, change.after, loadEngine]}>
|
|
41
|
-
<
|
|
42
|
+
<LazyBoundary fallback={<div className="px-3 py-2 text-xs text-text-faint">{t('work.loadingDiff')}</div>}>
|
|
42
43
|
<EditorEditDiff change={change} loadEngine={loadEngine!} fallback={unavailable} maxHeight={maxHeight} sourceChecked />
|
|
43
|
-
</
|
|
44
|
+
</LazyBoundary>
|
|
44
45
|
</DiffFallbackBoundary>
|
|
45
46
|
);
|
|
46
47
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { LazyBoundary } from '@adecore/ui';
|
|
2
3
|
import { useTranslation } from 'react-i18next';
|
|
3
4
|
import { lazyNamed } from '../../lazy';
|
|
4
5
|
import { diffSourceBudget, legacyDiffBudget, patchSourceBudget } from './diff/diff-source-budget';
|
|
@@ -27,9 +28,9 @@ export default function UnifiedDiff(props: UnifiedDiffProps) {
|
|
|
27
28
|
const omitted = <DiffOmitted path={change.path} />;
|
|
28
29
|
const fallback = legacyAllowed ? (
|
|
29
30
|
<DiffFallbackBoundary fallback={omitted} resetKeys={[change.path, change.diff, oldText, newText, diffStyle]}>
|
|
30
|
-
<
|
|
31
|
+
<LazyBoundary fallback={<div className="px-3 py-2 text-xs text-text-faint">{t('work.loadingDiff')}</div>}>
|
|
31
32
|
<LegacyUnifiedDiff {...props} />
|
|
32
|
-
</
|
|
33
|
+
</LazyBoundary>
|
|
33
34
|
</DiffFallbackBoundary>
|
|
34
35
|
) : (
|
|
35
36
|
omitted
|
|
@@ -46,7 +47,7 @@ export default function UnifiedDiff(props: UnifiedDiffProps) {
|
|
|
46
47
|
const unavailable = <DiffUnavailable path={change.path} />;
|
|
47
48
|
return (
|
|
48
49
|
<DiffFallbackBoundary fallback={unavailable} resetKeys={[change.path, change.diff, oldText, newText, ignoreWhitespace, diffStyle, loadEngine]}>
|
|
49
|
-
<
|
|
50
|
+
<LazyBoundary fallback={<div className="px-3 py-2 text-xs text-text-faint">{t('work.loadingDiff')}</div>}>
|
|
50
51
|
<EditorPatchDiff
|
|
51
52
|
change={change}
|
|
52
53
|
sourceChecked
|
|
@@ -59,7 +60,7 @@ export default function UnifiedDiff(props: UnifiedDiffProps) {
|
|
|
59
60
|
loadEngine={loadEngine!}
|
|
60
61
|
fallback={unavailable}
|
|
61
62
|
/>
|
|
62
|
-
</
|
|
63
|
+
</LazyBoundary>
|
|
63
64
|
</DiffFallbackBoundary>
|
|
64
65
|
);
|
|
65
66
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/* Anything with an action of its own; `data-composer-action` marks one a role cannot name, such as a control of the host's. */
|
|
2
|
+
const ACTIONS = [
|
|
3
|
+
'a',
|
|
4
|
+
'button',
|
|
5
|
+
'input',
|
|
6
|
+
'select',
|
|
7
|
+
'textarea',
|
|
8
|
+
'label',
|
|
9
|
+
'summary',
|
|
10
|
+
'[contenteditable]:not([contenteditable="false"])',
|
|
11
|
+
'[tabindex]:not([tabindex="-1"])',
|
|
12
|
+
'[role="button"]',
|
|
13
|
+
'[role="link"]',
|
|
14
|
+
'[role="menuitem"]',
|
|
15
|
+
'[role="menuitemcheckbox"]',
|
|
16
|
+
'[role="menuitemradio"]',
|
|
17
|
+
'[role="option"]',
|
|
18
|
+
'[role="listbox"]',
|
|
19
|
+
'[role="checkbox"]',
|
|
20
|
+
'[role="radio"]',
|
|
21
|
+
'[role="switch"]',
|
|
22
|
+
'[role="tab"]',
|
|
23
|
+
'[role="slider"]',
|
|
24
|
+
'[role="combobox"]',
|
|
25
|
+
'[data-composer-action]'
|
|
26
|
+
].join(', ');
|
|
27
|
+
|
|
28
|
+
/*
|
|
29
|
+
* Where a pointer is on the composer: in the text field, on an action, or on the space around them,
|
|
30
|
+
* which stands in for the text field. A target outside the surface is an action, as an event from a
|
|
31
|
+
* popup portaled out of it still reaches the surface through React.
|
|
32
|
+
*/
|
|
33
|
+
export function surfacePartOf(target: EventTarget | null, surface: Element): 'input' | 'action' | 'surface' {
|
|
34
|
+
// Not `instanceof Element`, which fails for an element of another window.
|
|
35
|
+
const element = target as Element | null;
|
|
36
|
+
if (typeof element?.closest !== 'function' || !surface.contains(element)) {
|
|
37
|
+
return 'action';
|
|
38
|
+
}
|
|
39
|
+
const input = element.closest('.cm-editor');
|
|
40
|
+
if (input !== null && surface.contains(input)) {
|
|
41
|
+
return 'input';
|
|
42
|
+
}
|
|
43
|
+
const action = element.closest(ACTIONS);
|
|
44
|
+
return action !== null && surface.contains(action) ? 'action' : 'surface';
|
|
45
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { LazyBoundary } from '@adecore/ui';
|
|
2
2
|
import { useTranslation } from 'react-i18next';
|
|
3
3
|
import { lazyNamed } from '../../../lazy';
|
|
4
4
|
import type { ReviewEntry } from './review-plan';
|
|
@@ -10,13 +10,13 @@ export function ReviewDiff({ entry, maxHeight = 480 }: { entry: ReviewEntry; max
|
|
|
10
10
|
const { t } = useTranslation('agent-chat');
|
|
11
11
|
return (
|
|
12
12
|
<div data-review-entry={entry.key}>
|
|
13
|
-
<
|
|
13
|
+
<LazyBoundary fallback={<p className="px-3 py-2 text-xs text-text-faint">{t('work.loadingDiff')}</p>}>
|
|
14
14
|
{entry.change.kind === 'edit' ? (
|
|
15
15
|
<EditDiff change={entry.change.edit} maxHeight={maxHeight} />
|
|
16
16
|
) : (
|
|
17
17
|
<UnifiedDiff change={entry.change.patch} contents={entry.change.contents} maxHeight={maxHeight} />
|
|
18
18
|
)}
|
|
19
|
-
</
|
|
19
|
+
</LazyBoundary>
|
|
20
20
|
</div>
|
|
21
21
|
);
|
|
22
22
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Fragment,
|
|
1
|
+
import { Fragment, useEffect, useState, type Dispatch, type ReactNode, type SetStateAction } from 'react';
|
|
2
2
|
import clsx from 'clsx';
|
|
3
3
|
import i18next from 'i18next';
|
|
4
4
|
import { useTranslation } from 'react-i18next';
|
|
@@ -28,7 +28,7 @@ import { useChatScope } from '../../../scope';
|
|
|
28
28
|
import { useCurrentItem } from '../../../state/chats';
|
|
29
29
|
import { useTimelineFlash } from '../../timeline-flash';
|
|
30
30
|
import { formatClockDuration, formatElapsedShort } from '@adecore/ui/format';
|
|
31
|
-
import { Icon, MiddleTruncate, Spinner, useTickingText } from '@adecore/ui';
|
|
31
|
+
import { Icon, LazyBoundary, MiddleTruncate, Spinner, useTickingText } from '@adecore/ui';
|
|
32
32
|
import { ROW_GUTTER, toolIcon } from '../icons';
|
|
33
33
|
import { AnsiOutput } from '../AnsiOutput';
|
|
34
34
|
import { RowTime } from '../RowTime';
|
|
@@ -122,7 +122,7 @@ export function ToggleLine({
|
|
|
122
122
|
/* The diff renderers load lazily, so a diff waits on them under a line that says so. */
|
|
123
123
|
function DiffSuspense({ children }: { children: ReactNode }) {
|
|
124
124
|
const { t } = useTranslation('agent-chat');
|
|
125
|
-
return <
|
|
125
|
+
return <LazyBoundary fallback={<div className="px-3 py-2 text-xs text-text-faint">{t('work.loadingDiff')}</div>}>{children}</LazyBoundary>;
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
/* A file in a changed files card, which opens its diff under it. */
|
package/src/lazy.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { lazy, type ComponentProps, type ComponentType } from 'react';
|
|
2
|
+
import { LoadedComponent } from '@adecore/ui';
|
|
2
3
|
|
|
3
4
|
type Loader = () => Promise<unknown>;
|
|
4
5
|
|
|
@@ -38,12 +39,9 @@ export function lazyNamed<Module extends Record<Name, ComponentType<any>>, Name
|
|
|
38
39
|
name: Name,
|
|
39
40
|
options?: { readonly prefetch?: boolean }
|
|
40
41
|
): ComponentType<ComponentProps<Module[Name]>> {
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
loaded ??= component;
|
|
45
|
-
return component;
|
|
46
|
-
};
|
|
42
|
+
// Through `LoadedComponent`, so a `LazyBoundary` around it draws the moment the module lands.
|
|
43
|
+
const loaded = new LoadedComponent<ComponentType<any>>(async () => (await load())[name]);
|
|
44
|
+
const open = (): Promise<ComponentType<any>> => loaded.load();
|
|
47
45
|
if (options?.prefetch !== false) {
|
|
48
46
|
loaders.push(open);
|
|
49
47
|
register?.(open);
|
|
@@ -60,7 +58,7 @@ export function lazyNamed<Module extends Record<Name, ComponentType<any>>, Name
|
|
|
60
58
|
}
|
|
61
59
|
});
|
|
62
60
|
const LazyNamed = (props: ComponentProps<Module[Name]>) => {
|
|
63
|
-
const Component: ComponentType<any> = loaded ?? Lazy;
|
|
61
|
+
const Component: ComponentType<any> = loaded.current ?? Lazy;
|
|
64
62
|
return <Component {...props} />;
|
|
65
63
|
};
|
|
66
64
|
return LazyNamed;
|