@adecore/agents-react 0.21.2 → 0.22.1
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/agents/model-requirement.d.ts +6 -0
- package/dist/agents/model-requirement.js +26 -0
- package/dist/chat/chat-client.d.ts +2 -1
- package/dist/chat/chat-client.js +2 -0
- package/dist/chat/drafts.d.ts +2 -1
- package/dist/chat/drafts.js +15 -5
- package/dist/chat/logic/picker-entries.d.ts +10 -0
- package/dist/chat/logic/picker-entries.js +19 -0
- package/dist/chat/logic/timeline.d.ts +5 -0
- package/dist/chat/logic/timeline.js +10 -1
- package/dist/chat/prompt-hold.d.ts +8 -0
- package/dist/chat/prompt-hold.js +16 -0
- package/dist/chat/quote.d.ts +4 -0
- package/dist/chat/quote.js +19 -0
- package/dist/chat/stash.d.ts +8 -2
- package/dist/chat/stash.js +21 -5
- package/dist/chat/subagent-list.d.ts +7 -1
- package/dist/chat/subagent-list.js +13 -1
- package/dist/chat/timeline-scroll.d.ts +6 -2
- package/dist/chat/timeline-scroll.js +12 -7
- package/dist/chat/ui/ChatActivity.js +22 -8
- package/dist/chat/ui/Composer.js +103 -19
- package/dist/chat/ui/ComposerInput.d.ts +3 -1
- package/dist/chat/ui/ComposerInput.js +13 -4
- package/dist/chat/ui/Pickers.js +26 -18
- package/dist/chat/ui/PromptComposer.d.ts +8 -1
- package/dist/chat/ui/PromptComposer.js +18 -6
- package/dist/chat/ui/QuoteButton.js +2 -2
- package/dist/chat/ui/SubagentControls.d.ts +4 -2
- package/dist/chat/ui/SubagentControls.js +12 -8
- package/dist/chat/ui/SubagentInfo.d.ts +1 -1
- package/dist/chat/ui/SubagentInfo.js +9 -15
- package/dist/chat/ui/SubagentStopButton.js +2 -1
- package/dist/chat/ui/Timeline.js +24 -3
- package/dist/chat/ui/composer/menu-aria.d.ts +10 -0
- package/dist/chat/ui/composer/menu-aria.js +18 -0
- package/dist/chat/ui/intelligent-ui/UiBlockFrame.js +3 -3
- package/dist/chat/ui/intelligent-ui/UiFallbackPart.js +1 -1
- package/dist/chat/ui/intelligent-ui/chart-data.d.ts +22 -2
- package/dist/chat/ui/intelligent-ui/chart-data.js +83 -19
- package/dist/chat/ui/intelligent-ui/renderers/chart.d.ts +1 -1
- package/dist/chat/ui/intelligent-ui/renderers/chart.js +99 -35
- package/dist/chat/ui/intelligent-ui/renderers/choices.d.ts +1 -1
- package/dist/chat/ui/intelligent-ui/renderers/choices.js +8 -2
- package/dist/chat/ui/intelligent-ui/renderers/content.js +5 -3
- package/dist/chat/ui/intelligent-ui/renderers/inputs.js +9 -4
- package/dist/chat/ui/intelligent-ui/renderers/links.js +1 -1
- package/dist/chat/ui/intelligent-ui/renderers/text.js +5 -2
- package/dist/chat/ui/intelligent-ui/table-data.js +4 -4
- package/dist/chat/ui/intelligent-ui/use-truncated.d.ts +1 -0
- package/dist/chat/ui/intelligent-ui/use-truncated.js +28 -0
- package/dist/chat/ui/quote-selection.d.ts +5 -2
- package/dist/chat/ui/quote-selection.js +3 -2
- package/dist/chat/ui/rows/MessageRows.js +24 -5
- package/dist/chat/ui/rows/Rows.js +1 -1
- package/dist/chat/ui/rows/WorkRows.d.ts +3 -1
- package/dist/chat/ui/rows/WorkRows.js +4 -4
- package/dist/host.d.ts +1 -0
- package/dist/host.js +1 -0
- package/dist/locales/en/agent-chat.json +39 -10
- package/dist/locales/nl/agent-chat.json +39 -10
- package/dist/state/chats.d.ts +2 -1
- package/dist/state/chats.js +19 -1
- package/package.json +29 -4
- package/src/agents/model-requirement.ts +36 -0
- package/src/chat/chat-client.ts +5 -0
- package/src/chat/drafts.ts +27 -7
- package/src/chat/logic/picker-entries.ts +25 -0
- package/src/chat/logic/timeline.ts +17 -2
- package/src/chat/prompt-hold.ts +27 -0
- package/src/chat/quote.ts +20 -0
- package/src/chat/stash.ts +31 -5
- package/src/chat/subagent-list.ts +19 -5
- package/src/chat/timeline-scroll.ts +24 -10
- package/src/chat/ui/ChatActivity.tsx +35 -9
- package/src/chat/ui/Composer.tsx +197 -52
- package/src/chat/ui/ComposerInput.tsx +18 -3
- package/src/chat/ui/Pickers.tsx +51 -30
- package/src/chat/ui/PromptComposer.tsx +37 -8
- package/src/chat/ui/QuoteButton.tsx +3 -2
- package/src/chat/ui/SubagentControls.tsx +22 -19
- package/src/chat/ui/SubagentInfo.tsx +24 -42
- package/src/chat/ui/SubagentStopButton.tsx +2 -1
- package/src/chat/ui/Timeline.tsx +26 -4
- package/src/chat/ui/composer/menu-aria.ts +29 -0
- package/src/chat/ui/intelligent-ui/UiBlockFrame.tsx +5 -5
- package/src/chat/ui/intelligent-ui/UiFallbackPart.tsx +6 -1
- package/src/chat/ui/intelligent-ui/chart-data.ts +106 -20
- package/src/chat/ui/intelligent-ui/renderers/chart.tsx +237 -72
- package/src/chat/ui/intelligent-ui/renderers/choices.tsx +9 -4
- package/src/chat/ui/intelligent-ui/renderers/content.tsx +31 -15
- package/src/chat/ui/intelligent-ui/renderers/inputs.tsx +16 -5
- package/src/chat/ui/intelligent-ui/renderers/links.tsx +3 -3
- package/src/chat/ui/intelligent-ui/renderers/text.tsx +18 -8
- package/src/chat/ui/intelligent-ui/table-data.ts +4 -4
- package/src/chat/ui/intelligent-ui/use-truncated.ts +33 -0
- package/src/chat/ui/quote-selection.ts +6 -4
- package/src/chat/ui/rows/MessageRows.tsx +44 -5
- package/src/chat/ui/rows/Rows.tsx +3 -1
- package/src/chat/ui/rows/WorkRows.tsx +15 -1
- package/src/host.ts +3 -0
- package/src/locales/en/agent-chat.json +39 -10
- package/src/locales/nl/agent-chat.json +39 -10
- package/src/state/chats.ts +31 -2
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// A page is a screen minus a little, so the line you were reading is still on it after the jump.
|
|
2
2
|
const PAGE_OVERLAP = 0.9;
|
|
3
3
|
|
|
4
|
+
// How long a jump asked before its thread registered stays up for that thread.
|
|
5
|
+
const PENDING_JUMP_TTL_MS = 10_000;
|
|
6
|
+
|
|
4
7
|
type Scroller = {
|
|
5
8
|
element: HTMLElement;
|
|
6
9
|
follow: (enabled: boolean) => void;
|
|
@@ -103,14 +106,17 @@ export function stepTimelineMessage(key: string, direction: -1 | 1): boolean {
|
|
|
103
106
|
*/
|
|
104
107
|
function waitingJumps<T>(): { register(key: string, jump: (target: T) => void): () => void; jump(key: string, target: T): boolean } {
|
|
105
108
|
const jumpers = new Map<string, (target: T) => void>();
|
|
106
|
-
const waiting = new Map<string, T>();
|
|
109
|
+
const waiting = new Map<string, { target: T; at: number }>();
|
|
107
110
|
return {
|
|
108
111
|
register(key, jump) {
|
|
109
112
|
jumpers.set(key, jump);
|
|
110
|
-
|
|
111
|
-
|
|
113
|
+
const pending = waiting.get(key);
|
|
114
|
+
if (pending !== undefined) {
|
|
112
115
|
waiting.delete(key);
|
|
113
|
-
jump
|
|
116
|
+
// A jump nobody picked up for long is not what the person wants any more.
|
|
117
|
+
if (Date.now() - pending.at <= PENDING_JUMP_TTL_MS) {
|
|
118
|
+
jump(pending.target);
|
|
119
|
+
}
|
|
114
120
|
}
|
|
115
121
|
return () => {
|
|
116
122
|
if (jumpers.get(key) === jump) {
|
|
@@ -121,7 +127,7 @@ function waitingJumps<T>(): { register(key: string, jump: (target: T) => void):
|
|
|
121
127
|
jump(key, target) {
|
|
122
128
|
const jump = jumpers.get(key);
|
|
123
129
|
if (jump === undefined) {
|
|
124
|
-
waiting.set(key, target);
|
|
130
|
+
waiting.set(key, { target, at: Date.now() });
|
|
125
131
|
return false;
|
|
126
132
|
}
|
|
127
133
|
jump(target);
|
|
@@ -130,15 +136,23 @@ function waitingJumps<T>(): { register(key: string, jump: (target: T) => void):
|
|
|
130
136
|
};
|
|
131
137
|
}
|
|
132
138
|
|
|
133
|
-
|
|
139
|
+
/* What a jump to an item may ask beyond scrolling to it. */
|
|
140
|
+
export interface ItemJumpOptions {
|
|
141
|
+
/* Lights the row up once it is in view. */
|
|
142
|
+
flash?: boolean;
|
|
143
|
+
/* Runs when the item is nowhere in the thread, after every earlier page has been tried. */
|
|
144
|
+
onMissing?(): void;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const itemJumps = waitingJumps<{ itemId: string; options: ItemJumpOptions }>();
|
|
134
148
|
|
|
135
|
-
export function registerItemJumper(key: string, jump: (itemId: string) => void): () => void {
|
|
136
|
-
return itemJumps.register(key, jump);
|
|
149
|
+
export function registerItemJumper(key: string, jump: (itemId: string, options: ItemJumpOptions) => void): () => void {
|
|
150
|
+
return itemJumps.register(key, ({ itemId, options }) => jump(itemId, options));
|
|
137
151
|
}
|
|
138
152
|
|
|
139
153
|
/* False when that chat has no thread on screen; the jump then waits for the next one that registers. */
|
|
140
|
-
export function jumpToTimelineItem(key: string, itemId: string): boolean {
|
|
141
|
-
return itemJumps.jump(key, itemId);
|
|
154
|
+
export function jumpToTimelineItem(key: string, itemId: string, options: ItemJumpOptions = {}): boolean {
|
|
155
|
+
return itemJumps.jump(key, { itemId, options });
|
|
142
156
|
}
|
|
143
157
|
|
|
144
158
|
/* Which part of a turn a jump lands on: the message that opened it, or the files it changed. */
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { useMemo, type ReactNode } from 'react';
|
|
2
2
|
import clsx from 'clsx';
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
|
-
import { Activity, Square, SquareTerminal } from 'lucide-react';
|
|
4
|
+
import { Activity, MessageCircleQuestionMark, Square, SquareTerminal } from 'lucide-react';
|
|
5
5
|
import type { ChatBackgroundTask, ChatSubagentItem } from '@adecore/agent-contracts';
|
|
6
6
|
import { backgroundCounts } from '../activity';
|
|
7
|
-
import { badgeCountOf, entryTimeOf, flyoutSubagents, statusWordOf, subagentTitle, summaryWordOf, taskIdOf } from '../subagent-list';
|
|
7
|
+
import { badgeCountOf, entryTimeOf, flyoutSubagents, progressOf, statusWordOf, subagentTitle, summaryWordOf, taskIdOf, taskRowStateOf } from '../subagent-list';
|
|
8
8
|
import { useSubagentSupport } from '../subagent-support';
|
|
9
9
|
import { canOpenSubagent, crumbOf, openFromMain, useSubagentTrail } from '../subagent-view';
|
|
10
10
|
import { SubagentStopButton } from './SubagentStopButton';
|
|
11
11
|
import { formatElapsedShort } from '@adecore/ui/format';
|
|
12
12
|
import { Icon, IconButton, Popover, Surface, useNow } from '@adecore/ui';
|
|
13
|
-
import { useChatRow } from '../../state/chats';
|
|
13
|
+
import { useChatRow, useChats } from '../../state/chats';
|
|
14
14
|
import { chatHost } from '../../host';
|
|
15
15
|
import { useChatScope } from '../../scope';
|
|
16
16
|
import { useChatActions } from '../actions';
|
|
@@ -28,8 +28,20 @@ export function ChatActivity({ chatId }: { chatId: string }) {
|
|
|
28
28
|
const order = useChatRow(chatId, (row) => row?.order) ?? NO_ITEMS;
|
|
29
29
|
const structure = useChatRow(chatId, (row) => row?.structure);
|
|
30
30
|
const subagents = useMemo(() => (structure === undefined ? [] : flyoutSubagents(order, structure)), [order, structure]);
|
|
31
|
-
const { id } = useChatScope();
|
|
31
|
+
const { id, keyOf } = useChatScope();
|
|
32
32
|
const tasks = chatHost().tasks.useTasks(id);
|
|
33
|
+
const statuses = useChats((s) => s.statusByKey);
|
|
34
|
+
const progress = useMemo(
|
|
35
|
+
() =>
|
|
36
|
+
progressOf(
|
|
37
|
+
subagents.map((item) => {
|
|
38
|
+
const taskId = taskIdOf(item);
|
|
39
|
+
const child = item.childId === undefined ? undefined : statuses[keyOf(item.childId)]?.info;
|
|
40
|
+
return taskRowStateOf(item, taskId === null ? null : (tasks?.[taskId] ?? null), child);
|
|
41
|
+
})
|
|
42
|
+
),
|
|
43
|
+
[subagents, tasks, statuses, keyOf]
|
|
44
|
+
);
|
|
33
45
|
const words = useMemo(
|
|
34
46
|
() =>
|
|
35
47
|
subagents.map((item) => {
|
|
@@ -44,13 +56,25 @@ export function ChatActivity({ chatId }: { chatId: string }) {
|
|
|
44
56
|
}
|
|
45
57
|
const { shells, monitors } = backgroundCounts(background);
|
|
46
58
|
const Slot = chatHost().SubagentSlot;
|
|
59
|
+
const counting = progress.active > 0 && progress.total > 1;
|
|
60
|
+
const label = counting
|
|
61
|
+
? [t('activity.progress', { done: progress.done, total: progress.total }), progress.waiting > 0 && t('activity.needsYou', { count: progress.waiting })]
|
|
62
|
+
.filter(Boolean)
|
|
63
|
+
.join(', ')
|
|
64
|
+
: t('activity.subagents', { count: badgeCountOf(words) });
|
|
47
65
|
return (
|
|
48
66
|
<div className="mb-3 flex flex-wrap items-center gap-2">
|
|
49
67
|
{subagents.length > 0 &&
|
|
50
68
|
(Slot === null ? (
|
|
51
69
|
<Flyout
|
|
52
|
-
icon={
|
|
53
|
-
|
|
70
|
+
icon={
|
|
71
|
+
progress.waiting > 0 ? (
|
|
72
|
+
<Icon icon={MessageCircleQuestionMark} size={14} className="shrink-0 text-status-needs-you" />
|
|
73
|
+
) : (
|
|
74
|
+
<StatusIcon word={summaryWordOf(words)} />
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
label={label}
|
|
54
78
|
heading={t('activity.subagents', { count: subagents.length })}
|
|
55
79
|
>
|
|
56
80
|
<SubagentRows chatId={chatId} items={subagents} />
|
|
@@ -83,7 +107,7 @@ function Flyout({ icon, label, heading = label, children }: { icon: ReactNode; l
|
|
|
83
107
|
<Popover.Root>
|
|
84
108
|
<Surface
|
|
85
109
|
render={<Popover.Trigger />}
|
|
86
|
-
className="pointer-events-auto inline-flex h-7.5 items-center gap-1.5 rounded-full px-3 text-xs text-text-muted hover:text-text"
|
|
110
|
+
className="pointer-events-auto inline-flex h-7.5 items-center gap-1.5 rounded-full px-3 text-xs text-text-muted tabular-nums hover:text-text"
|
|
87
111
|
>
|
|
88
112
|
{icon}
|
|
89
113
|
{label}
|
|
@@ -148,11 +172,13 @@ function SubagentRows({ chatId, items }: { chatId: string; items: readonly ChatS
|
|
|
148
172
|
|
|
149
173
|
function SubagentEntry({ chatId, item, now }: { chatId: string; item: ChatSubagentItem; now: number }) {
|
|
150
174
|
const { t } = useTranslation('agent-chat');
|
|
151
|
-
const { id } = useChatScope();
|
|
175
|
+
const { id, keyOf } = useChatScope();
|
|
152
176
|
const refused = useSubagentSupport((s) => s.unsupported[id] === true);
|
|
153
177
|
const task = chatHost().tasks.useTask(id, taskIdOf(item));
|
|
154
178
|
const { show } = useSubagentTrail(chatId);
|
|
179
|
+
const child = useChats((s) => (item.childId === undefined ? undefined : s.statusByKey[keyOf(item.childId)]?.info));
|
|
155
180
|
const word = statusWordOf(item, task);
|
|
181
|
+
const state = taskRowStateOf(item, task, child);
|
|
156
182
|
return (
|
|
157
183
|
<ActivityRow
|
|
158
184
|
icon={<StatusIcon word={word} />}
|
|
@@ -160,7 +186,7 @@ function SubagentEntry({ chatId, item, now }: { chatId: string; item: ChatSubage
|
|
|
160
186
|
<>
|
|
161
187
|
{subagentTitle(item)}
|
|
162
188
|
{/* The icon says the state to the eye; a screen reader hears it with the title. */}
|
|
163
|
-
<span className="sr-only">, {t(`common.status.${word}`)}</span>
|
|
189
|
+
<span className="sr-only">, {state === 'waiting' ? t('rows.task.waiting') : t(`common.status.${word}`)}</span>
|
|
164
190
|
</>
|
|
165
191
|
}
|
|
166
192
|
time={entryTimeOf(item, task, now)}
|
package/src/chat/ui/Composer.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
|
|
1
|
+
import { useContext, useEffect, useId, useMemo, useRef, useState, useSyncExternalStore } from 'react';
|
|
2
2
|
import { useTranslation } from 'react-i18next';
|
|
3
3
|
import { indentLess, indentMore, insertNewline } from '@codemirror/commands';
|
|
4
4
|
import { ensureSyntaxTree, syntaxTree } from '@codemirror/language';
|
|
@@ -52,13 +52,15 @@ import { PROMPT_MAX_CHARS, pasteBecomesAttachment, pastedTextName, promptGuard,
|
|
|
52
52
|
import { withQuote } from '../quote';
|
|
53
53
|
import { rememberChatAccount, rememberChatPreferences, rememberChatSelection } from '../preferences';
|
|
54
54
|
import { STASH_SHORTCUT, stashDraft, type StashedPrompt, useStash } from '../stash';
|
|
55
|
-
import { pageTimeline, scrollTimelineToEnd, subscribeTimelineEnd, timelineAtEnd } from '../timeline-scroll';
|
|
55
|
+
import { jumpToTimelineItem, pageTimeline, scrollTimelineToEnd, subscribeTimelineEnd, timelineAtEnd } from '../timeline-scroll';
|
|
56
56
|
import { ChatActivity } from './ChatActivity';
|
|
57
57
|
import { ChatReferenceChip } from './ChatReferenceChip';
|
|
58
58
|
import { chipDecorations } from './composer/chips';
|
|
59
|
+
import { clampMenuIndex, composerMenuAria, optionIdOf } from './composer/menu-aria';
|
|
59
60
|
import { enterAction, inCode, inFenceBody, inOpenFence, listItemAt, recallDirection, tabSpaces } from './composer/keys';
|
|
60
61
|
import { ComposerInput, type ComposerInputHandle } from './ComposerInput';
|
|
61
|
-
import {
|
|
62
|
+
import { holdLiftsIn, type HoldRelease } from '../prompt-hold';
|
|
63
|
+
import { PromptComposer, type PromptHold } from './PromptComposer';
|
|
62
64
|
import { QuoteTakerContext } from './quote-selection';
|
|
63
65
|
import { LimitDock } from './LimitState';
|
|
64
66
|
import { ResumeCompactionDock } from './ResumeCompactionDock';
|
|
@@ -177,6 +179,59 @@ function splitPath(path: string): { name: string; dir: string } {
|
|
|
177
179
|
}
|
|
178
180
|
|
|
179
181
|
// Keep the editor mounted while a pending request takes over, so its selection and draft survive.
|
|
182
|
+
/*
|
|
183
|
+
* Keeps a waiting prompt card behind the draft while the person types. Typing, pasting and dictating
|
|
184
|
+
* all change the draft, so `noteInput` is called from its change handler; the hold lifts after a pause.
|
|
185
|
+
*/
|
|
186
|
+
function usePromptHold(): PromptHold & { noteInput(): void; setComposing(composing: boolean): void } {
|
|
187
|
+
const typedAt = useRef<number | null>(null);
|
|
188
|
+
const composing = useRef(false);
|
|
189
|
+
const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
190
|
+
const [state, setState] = useState<{ held: boolean; releasedBy: HoldRelease | null }>({ held: false, releasedBy: null });
|
|
191
|
+
|
|
192
|
+
const clearTimer = (): void => {
|
|
193
|
+
if (timer.current !== null) {
|
|
194
|
+
clearTimeout(timer.current);
|
|
195
|
+
timer.current = null;
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const release = (reason: HoldRelease): void => {
|
|
200
|
+
clearTimer();
|
|
201
|
+
typedAt.current = null;
|
|
202
|
+
composing.current = false;
|
|
203
|
+
setState((current) => (current.held ? { held: false, releasedBy: reason } : current));
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const schedule = (): void => {
|
|
207
|
+
clearTimer();
|
|
208
|
+
const wait = holdLiftsIn({ typedAt: typedAt.current, composing: composing.current }, Date.now());
|
|
209
|
+
if (wait !== null) {
|
|
210
|
+
timer.current = setTimeout(() => release('pause'), wait);
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const noteInput = (): void => {
|
|
215
|
+
typedAt.current = Date.now();
|
|
216
|
+
setState({ held: true, releasedBy: null });
|
|
217
|
+
schedule();
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const setComposing = (next: boolean): void => {
|
|
221
|
+
composing.current = next;
|
|
222
|
+
if (next) {
|
|
223
|
+
setState({ held: true, releasedBy: null });
|
|
224
|
+
} else {
|
|
225
|
+
typedAt.current = Date.now();
|
|
226
|
+
}
|
|
227
|
+
schedule();
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
useEffect(() => clearTimer, []);
|
|
231
|
+
|
|
232
|
+
return { held: state.held, releasedBy: state.releasedBy, release, noteInput, setComposing };
|
|
233
|
+
}
|
|
234
|
+
|
|
180
235
|
export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabled, readOnly = false, providerFixed, onSend, onRetarget }: ComposerProps) {
|
|
181
236
|
const { t } = useTranslation('agent-chat');
|
|
182
237
|
const sendDelivery = chatHost().useSendDelivery();
|
|
@@ -200,6 +255,7 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
200
255
|
const { isApplePlatform, notify } = chatHost();
|
|
201
256
|
const registerQuoteTaker = useContext(QuoteTakerContext);
|
|
202
257
|
const inputRef = useRef<ComposerInputHandle>(null);
|
|
258
|
+
const hold = usePromptHold();
|
|
203
259
|
// Taking a queued message back waits on the machine, and what was typed meanwhile is what it merges with.
|
|
204
260
|
const draftRef = useRef(draft);
|
|
205
261
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
@@ -238,6 +294,7 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
238
294
|
const queue = info.queue ?? [];
|
|
239
295
|
const text = draft.text;
|
|
240
296
|
const quote = draft.quote;
|
|
297
|
+
const quoteSource = draft.quoteSource;
|
|
241
298
|
// The CLI announces its session on the first message, so anything before that is still a blank chat.
|
|
242
299
|
const started = info.agentSessionId !== null || info.usage.turns > 0;
|
|
243
300
|
const accountChoice = useAccountChoice(info.provider, info.account);
|
|
@@ -270,8 +327,8 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
270
327
|
|
|
271
328
|
useEffect(
|
|
272
329
|
() =>
|
|
273
|
-
registerQuoteTaker?.((selected) => {
|
|
274
|
-
setDraft((current) => ({ ...current, quote: selected }));
|
|
330
|
+
registerQuoteTaker?.((selected, itemId) => {
|
|
331
|
+
setDraft((current) => ({ ...current, quote: selected, quoteSource: itemId }));
|
|
275
332
|
inputRef.current?.focus();
|
|
276
333
|
}),
|
|
277
334
|
[registerQuoteTaker]
|
|
@@ -383,6 +440,26 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
383
440
|
[chatId, chats, draft.chats, mention]
|
|
384
441
|
);
|
|
385
442
|
const mentionCount = chatMatches.length + files.length;
|
|
443
|
+
// One menu is open at a time, so they share an id.
|
|
444
|
+
const listId = useId();
|
|
445
|
+
const mentionsLabelId = `${listId}-chats`;
|
|
446
|
+
const filesLabelId = `${listId}-files`;
|
|
447
|
+
const filesLabelShown = mention !== null && (mention.query === '' || chatMatches.length > 0);
|
|
448
|
+
const menuOpen = commandMenuOpen || skillMenuOpen || mentionMenuOpen;
|
|
449
|
+
const menuCount = commandMenuOpen ? commands.length : skillMenuOpen ? skillMatches.length : mentionCount;
|
|
450
|
+
// The list can shrink under a stale index; every reader takes the one that still points at an option.
|
|
451
|
+
const activeIndex = clampMenuIndex(menuIndex, menuCount);
|
|
452
|
+
const inputAria = useMemo(
|
|
453
|
+
() =>
|
|
454
|
+
composerMenuAria({
|
|
455
|
+
label: t('composer.inputLabel'),
|
|
456
|
+
listId,
|
|
457
|
+
open: menuOpen,
|
|
458
|
+
activeIndex,
|
|
459
|
+
count: menuCount
|
|
460
|
+
}),
|
|
461
|
+
[t, listId, menuOpen, activeIndex, menuCount]
|
|
462
|
+
);
|
|
386
463
|
const editorExtensions = useMemo(
|
|
387
464
|
() => [chipDecorations({ mentions: draft.mentions, skills: draft.skills }), LEAVE_DROPS_TO_THE_CARD],
|
|
388
465
|
[draft.mentions, draft.skills]
|
|
@@ -398,7 +475,7 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
398
475
|
};
|
|
399
476
|
|
|
400
477
|
const removeQuote = (): void => {
|
|
401
|
-
setDraft((current) => ({ ...current, quote: '' }));
|
|
478
|
+
setDraft((current) => ({ ...current, quote: '', quoteSource: null }));
|
|
402
479
|
inputRef.current?.focus();
|
|
403
480
|
};
|
|
404
481
|
|
|
@@ -618,7 +695,8 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
618
695
|
setNotice(`${rejected.name}: ${rejected.reason}`);
|
|
619
696
|
return;
|
|
620
697
|
}
|
|
621
|
-
|
|
698
|
+
hold.release('send');
|
|
699
|
+
const chosen = commands[activeIndex];
|
|
622
700
|
if (commandQuery !== null && chosen?.local && runCommand(chosen.name)) {
|
|
623
701
|
clearDraft();
|
|
624
702
|
return;
|
|
@@ -639,6 +717,7 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
639
717
|
mentions: presentMentions(trimmed, draft.mentions),
|
|
640
718
|
skills: presentSkills(trimmed, draft.skills),
|
|
641
719
|
chats: draft.chats,
|
|
720
|
+
quoteSource: quote !== '' && quoteSource !== null ? { itemId: quoteSource } : undefined,
|
|
642
721
|
attachments: draft.attachments
|
|
643
722
|
});
|
|
644
723
|
clearDraft();
|
|
@@ -647,9 +726,20 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
647
726
|
setHistoryIndex(null);
|
|
648
727
|
};
|
|
649
728
|
|
|
650
|
-
/*
|
|
729
|
+
/*
|
|
730
|
+
* A stashed prompt comes back as text, mentions and skills; its files were never kept. Its quote
|
|
731
|
+
* keeps leading to its reply in the chat it came from, and is plain text in any other.
|
|
732
|
+
*/
|
|
651
733
|
const restoreStashed = (prompt: StashedPrompt): void => {
|
|
652
|
-
|
|
734
|
+
const stashedQuote = prompt.quote;
|
|
735
|
+
const here = stashedQuote !== undefined && stashedQuote.chatId === chatId;
|
|
736
|
+
setDraft((current) => ({
|
|
737
|
+
...current,
|
|
738
|
+
text: stashedQuote === undefined || here ? prompt.text : withQuote(stashedQuote.text, prompt.text),
|
|
739
|
+
mentions: [...prompt.mentions],
|
|
740
|
+
skills: [...prompt.skills],
|
|
741
|
+
...(here ? { quote: stashedQuote.text, quoteSource: stashedQuote.itemId } : {})
|
|
742
|
+
}));
|
|
653
743
|
setHistoryIndex(null);
|
|
654
744
|
inputRef.current?.focus();
|
|
655
745
|
};
|
|
@@ -682,7 +772,7 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
682
772
|
failed(e);
|
|
683
773
|
return;
|
|
684
774
|
}
|
|
685
|
-
const taken: Omit<ChatDraft, 'quote'> = {
|
|
775
|
+
const taken: Omit<ChatDraft, 'quote' | 'quoteSource'> = {
|
|
686
776
|
text: message.text,
|
|
687
777
|
mentions: message.mentions ?? [],
|
|
688
778
|
skills: message.skills ?? [],
|
|
@@ -726,7 +816,7 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
726
816
|
}
|
|
727
817
|
return;
|
|
728
818
|
}
|
|
729
|
-
if (stashDraft(draft)) {
|
|
819
|
+
if (stashDraft(draft, chatId)) {
|
|
730
820
|
clearDraft();
|
|
731
821
|
setMention(null);
|
|
732
822
|
setSkillQuery(null);
|
|
@@ -800,7 +890,7 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
800
890
|
return true;
|
|
801
891
|
}
|
|
802
892
|
if (e.key === 'Tab') {
|
|
803
|
-
const chosen = commands[
|
|
893
|
+
const chosen = commands[activeIndex]!;
|
|
804
894
|
if (chosen.skill) {
|
|
805
895
|
chooseSkill(chosen.name);
|
|
806
896
|
} else {
|
|
@@ -814,7 +904,7 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
814
904
|
return true;
|
|
815
905
|
}
|
|
816
906
|
if (e.key === 'Tab' || e.key === 'Enter') {
|
|
817
|
-
chooseSkill((skillMatches[
|
|
907
|
+
chooseSkill((skillMatches[activeIndex] ?? skillMatches[0]!).name);
|
|
818
908
|
return true;
|
|
819
909
|
}
|
|
820
910
|
}
|
|
@@ -823,7 +913,7 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
823
913
|
return true;
|
|
824
914
|
}
|
|
825
915
|
if (e.key === 'Tab' || e.key === 'Enter') {
|
|
826
|
-
chooseMentionAt(
|
|
916
|
+
chooseMentionAt(activeIndex);
|
|
827
917
|
return true;
|
|
828
918
|
}
|
|
829
919
|
}
|
|
@@ -996,6 +1086,7 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
996
1086
|
hasDraft={!isEmptyDraft(draft)}
|
|
997
1087
|
denyReason={capabilities?.denyReason === true}
|
|
998
1088
|
onAllAnswered={() => inputRef.current?.focus()}
|
|
1089
|
+
hold={hold}
|
|
999
1090
|
>
|
|
1000
1091
|
<LimitDock chatId={chatId} info={info} />
|
|
1001
1092
|
{compactionOffer && (
|
|
@@ -1006,11 +1097,15 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
1006
1097
|
/>
|
|
1007
1098
|
)}
|
|
1008
1099
|
{commandMenuOpen && (
|
|
1009
|
-
<div className="border-b border-border px-1.5 py-1.5">
|
|
1100
|
+
<div id={listId} role="listbox" aria-label={t('composer.menu.commands')} className="border-b border-border px-1.5 py-1.5">
|
|
1010
1101
|
{commands.map((command, index) => (
|
|
1011
1102
|
<button
|
|
1012
1103
|
key={command.name}
|
|
1013
|
-
|
|
1104
|
+
id={optionIdOf(listId, index)}
|
|
1105
|
+
role="option"
|
|
1106
|
+
aria-selected={index === activeIndex}
|
|
1107
|
+
tabIndex={-1}
|
|
1108
|
+
data-active={index === activeIndex}
|
|
1014
1109
|
className="cursor-row flex w-full items-center gap-2 rounded-md px-2 py-1 text-left text-xs text-text-muted"
|
|
1015
1110
|
onMouseEnter={() => setMenuIndex(index)}
|
|
1016
1111
|
onMouseDown={(e) => e.preventDefault()}
|
|
@@ -1036,11 +1131,15 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
1036
1131
|
</div>
|
|
1037
1132
|
)}
|
|
1038
1133
|
{skillMenuOpen && (
|
|
1039
|
-
<div className="border-b border-border px-1.5 py-1.5">
|
|
1134
|
+
<div id={listId} role="listbox" aria-label={t('composer.menu.skills')} className="border-b border-border px-1.5 py-1.5">
|
|
1040
1135
|
{skillMatches.map((skill, index) => (
|
|
1041
1136
|
<button
|
|
1042
1137
|
key={skill.name}
|
|
1043
|
-
|
|
1138
|
+
id={optionIdOf(listId, index)}
|
|
1139
|
+
role="option"
|
|
1140
|
+
aria-selected={index === activeIndex}
|
|
1141
|
+
tabIndex={-1}
|
|
1142
|
+
data-active={index === activeIndex}
|
|
1044
1143
|
className="cursor-row flex w-full items-start gap-2 rounded-md px-2 py-1 text-left text-xs text-text-muted"
|
|
1045
1144
|
onMouseEnter={() => setMenuIndex(index)}
|
|
1046
1145
|
onMouseDown={(e) => e.preventDefault()}
|
|
@@ -1057,45 +1156,70 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
1057
1156
|
)}
|
|
1058
1157
|
{mentionMenuOpen && (
|
|
1059
1158
|
<div className="border-b border-border px-1.5 py-1.5">
|
|
1060
|
-
{
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1159
|
+
{mentionCount > 0 && (
|
|
1160
|
+
<div id={listId} role="listbox" aria-label={t('composer.menu.mentions')}>
|
|
1161
|
+
{chatMatches.length > 0 && (
|
|
1162
|
+
<div role="group" aria-labelledby={mentionsLabelId}>
|
|
1163
|
+
<Menu.Label id={mentionsLabelId}>{t('composer.mentions.chats')}</Menu.Label>
|
|
1164
|
+
{chatMatches.map((chat, index) => (
|
|
1165
|
+
<button
|
|
1166
|
+
key={chat.id}
|
|
1167
|
+
id={optionIdOf(listId, index)}
|
|
1168
|
+
role="option"
|
|
1169
|
+
aria-selected={index === activeIndex}
|
|
1170
|
+
tabIndex={-1}
|
|
1171
|
+
data-active={index === activeIndex}
|
|
1172
|
+
className="cursor-row flex w-full items-center gap-2 rounded-md px-2 py-1 text-left text-xs text-text-muted"
|
|
1173
|
+
onMouseEnter={() => setMenuIndex(index)}
|
|
1174
|
+
onMouseDown={(e) => e.preventDefault()}
|
|
1175
|
+
onClick={() => chooseChat(chat.id)}
|
|
1176
|
+
>
|
|
1177
|
+
<Icon icon={MessageSquare} size={14} className="shrink-0 text-text-faint" />
|
|
1178
|
+
<span className="truncate text-text">{chat.title || t('chatReference.untitled')}</span>
|
|
1179
|
+
</button>
|
|
1180
|
+
))}
|
|
1181
|
+
</div>
|
|
1182
|
+
)}
|
|
1183
|
+
{files.length > 0 && (
|
|
1184
|
+
<div
|
|
1185
|
+
role="group"
|
|
1186
|
+
{...(filesLabelShown ? { 'aria-labelledby': filesLabelId } : { 'aria-label': t('composer.mentions.files') })}
|
|
1187
|
+
>
|
|
1188
|
+
{filesLabelShown && (
|
|
1189
|
+
<Menu.Label id={filesLabelId}>
|
|
1190
|
+
{mention.query === '' ? t('composer.mentions.here') : t('composer.mentions.files')}
|
|
1191
|
+
</Menu.Label>
|
|
1192
|
+
)}
|
|
1193
|
+
{files.map((path, index) => {
|
|
1194
|
+
const { name, dir } = splitPath(path);
|
|
1195
|
+
return (
|
|
1196
|
+
<button
|
|
1197
|
+
key={path}
|
|
1198
|
+
id={optionIdOf(listId, chatMatches.length + index)}
|
|
1199
|
+
role="option"
|
|
1200
|
+
aria-selected={chatMatches.length + index === activeIndex}
|
|
1201
|
+
tabIndex={-1}
|
|
1202
|
+
data-active={chatMatches.length + index === activeIndex}
|
|
1203
|
+
className="cursor-row flex w-full items-center gap-2 rounded-md px-2 py-1 text-left text-xs text-text-muted"
|
|
1204
|
+
onMouseEnter={() => setMenuIndex(chatMatches.length + index)}
|
|
1205
|
+
onMouseDown={(e) => e.preventDefault()}
|
|
1206
|
+
onClick={() => chooseMention(path)}
|
|
1207
|
+
>
|
|
1208
|
+
<FileIcon path={path} size={14} />
|
|
1209
|
+
<span className="truncate font-mono text-text">{name}</span>
|
|
1210
|
+
{dir && <span className="min-w-0 truncate text-text-faint">{dir}</span>}
|
|
1211
|
+
</button>
|
|
1212
|
+
);
|
|
1213
|
+
})}
|
|
1214
|
+
</div>
|
|
1215
|
+
)}
|
|
1216
|
+
</div>
|
|
1076
1217
|
)}
|
|
1077
1218
|
{mentionCount === 0 && (
|
|
1078
1219
|
<div className="px-2 py-1 text-xs text-text-faint">
|
|
1079
1220
|
{mention.query ? t('composer.mentions.noMatch') : t('composer.mentions.empty')}
|
|
1080
1221
|
</div>
|
|
1081
1222
|
)}
|
|
1082
|
-
{files.map((path, index) => {
|
|
1083
|
-
const { name, dir } = splitPath(path);
|
|
1084
|
-
return (
|
|
1085
|
-
<button
|
|
1086
|
-
key={path}
|
|
1087
|
-
data-active={chatMatches.length + index === menuIndex}
|
|
1088
|
-
className="cursor-row flex w-full items-center gap-2 rounded-md px-2 py-1 text-left text-xs text-text-muted"
|
|
1089
|
-
onMouseEnter={() => setMenuIndex(chatMatches.length + index)}
|
|
1090
|
-
onMouseDown={(e) => e.preventDefault()}
|
|
1091
|
-
onClick={() => chooseMention(path)}
|
|
1092
|
-
>
|
|
1093
|
-
<FileIcon path={path} size={14} />
|
|
1094
|
-
<span className="truncate font-mono text-text">{name}</span>
|
|
1095
|
-
{dir && <span className="min-w-0 truncate text-text-faint">{dir}</span>}
|
|
1096
|
-
</button>
|
|
1097
|
-
);
|
|
1098
|
-
})}
|
|
1099
1223
|
</div>
|
|
1100
1224
|
)}
|
|
1101
1225
|
{queue.length > 0 && (
|
|
@@ -1152,7 +1276,24 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
1152
1276
|
)}
|
|
1153
1277
|
{quote !== '' && (
|
|
1154
1278
|
<div className="flex items-center gap-2 px-5 pt-4 @max-md/composer:px-3.5 @max-md/composer:pt-3">
|
|
1155
|
-
|
|
1279
|
+
{quoteSource === null ? (
|
|
1280
|
+
<span className="min-w-0 grow truncate border-l-2 border-border-strong pl-2.5 text-xs text-text-muted">{quote}</span>
|
|
1281
|
+
) : (
|
|
1282
|
+
<Tooltip label={t('composer.quote.goToSource')}>
|
|
1283
|
+
<button
|
|
1284
|
+
type="button"
|
|
1285
|
+
className="min-w-0 grow truncate border-l-2 border-border-strong pl-2.5 text-left text-xs text-text-muted hover:text-text"
|
|
1286
|
+
onClick={() =>
|
|
1287
|
+
jumpToTimelineItem(scope.keyOf(chatId), quoteSource, {
|
|
1288
|
+
flash: true,
|
|
1289
|
+
onMissing: () => setNotice(t('rows.user.quoteGone'))
|
|
1290
|
+
})
|
|
1291
|
+
}
|
|
1292
|
+
>
|
|
1293
|
+
{quote}
|
|
1294
|
+
</button>
|
|
1295
|
+
</Tooltip>
|
|
1296
|
+
)}
|
|
1156
1297
|
<IconButton icon={X} size="2xs" label={t('composer.quote.remove')} onClick={removeQuote} />
|
|
1157
1298
|
</div>
|
|
1158
1299
|
)}
|
|
@@ -1198,7 +1339,9 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
1198
1339
|
disabled={!writable}
|
|
1199
1340
|
tabbable={focused}
|
|
1200
1341
|
extensions={editorExtensions}
|
|
1342
|
+
aria={inputAria}
|
|
1201
1343
|
onChange={(value, selection, state) => {
|
|
1344
|
+
hold.noteInput();
|
|
1202
1345
|
setText(value);
|
|
1203
1346
|
setMenuIndex(0);
|
|
1204
1347
|
trackTriggers(value, selection, state);
|
|
@@ -1210,7 +1353,9 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
1210
1353
|
onBlur={() => {
|
|
1211
1354
|
setMention(null);
|
|
1212
1355
|
setSkillQuery(null);
|
|
1356
|
+
hold.release('blur');
|
|
1213
1357
|
}}
|
|
1358
|
+
onComposing={hold.setComposing}
|
|
1214
1359
|
onKeyDown={onKeyDown}
|
|
1215
1360
|
onPaste={onPaste}
|
|
1216
1361
|
/>
|
|
@@ -29,9 +29,13 @@ interface ComposerInputProps {
|
|
|
29
29
|
className?: string;
|
|
30
30
|
/* Keep the reference stable. A new one reconfigures the editor. */
|
|
31
31
|
extensions?: Extension;
|
|
32
|
+
/* Attributes for the editable element, such as its label and the option a menu points at; keep the reference stable. */
|
|
33
|
+
aria?: Readonly<Record<string, string>>;
|
|
32
34
|
onChange(text: string, selection: InputSelection, state: EditorState): void;
|
|
33
35
|
onSelectionChange?(text: string, selection: InputSelection, state: EditorState): void;
|
|
34
36
|
onBlur?(): void;
|
|
37
|
+
/* An IME composition starts or ends; the text it builds is not in the document until it ends. */
|
|
38
|
+
onComposing?(composing: boolean): void;
|
|
35
39
|
/* Runs before the editor's own keymap; true means the key is handled and its default prevented. */
|
|
36
40
|
onKeyDown?(event: KeyboardEvent, view: EditorView): boolean;
|
|
37
41
|
/* Runs before the editor pastes the plain text itself; true means the paste is handled. */
|
|
@@ -40,6 +44,8 @@ interface ComposerInputProps {
|
|
|
40
44
|
|
|
41
45
|
const NO_EXTENSIONS: Extension = [];
|
|
42
46
|
|
|
47
|
+
const NO_ARIA: Readonly<Record<string, string>> = {};
|
|
48
|
+
|
|
43
49
|
/*
|
|
44
50
|
* A multi-line text field on CodeMirror, controlled by a plain string. The editor draws the text
|
|
45
51
|
* itself, so a decoration may change a font or add padding without walking the caret off its
|
|
@@ -54,6 +60,7 @@ export function ComposerInput({
|
|
|
54
60
|
className,
|
|
55
61
|
dictationToolbar,
|
|
56
62
|
extensions = NO_EXTENSIONS,
|
|
63
|
+
aria = NO_ARIA,
|
|
57
64
|
...handlers
|
|
58
65
|
}: ComposerInputProps) {
|
|
59
66
|
const hostRef = useRef<HTMLDivElement>(null);
|
|
@@ -94,7 +101,15 @@ export function ComposerInput({
|
|
|
94
101
|
Prec.highest(
|
|
95
102
|
EditorView.domEventHandlers({
|
|
96
103
|
keydown: (event, target) => latest.current.handlers.onKeyDown?.(event, target) ?? false,
|
|
97
|
-
paste: (event, target) => latest.current.handlers.onPaste?.(event, target) ?? false
|
|
104
|
+
paste: (event, target) => latest.current.handlers.onPaste?.(event, target) ?? false,
|
|
105
|
+
compositionstart: () => {
|
|
106
|
+
latest.current.handlers.onComposing?.(true);
|
|
107
|
+
return false;
|
|
108
|
+
},
|
|
109
|
+
compositionend: () => {
|
|
110
|
+
latest.current.handlers.onComposing?.(false);
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
98
113
|
})
|
|
99
114
|
),
|
|
100
115
|
EditorView.updateListener.of((update) => {
|
|
@@ -128,13 +143,13 @@ export function ComposerInput({
|
|
|
128
143
|
viewRef.current?.dispatch({
|
|
129
144
|
effects: [
|
|
130
145
|
// Spellcheck stays on, as it was on the textarea this editor replaced.
|
|
131
|
-
compartments.attributes.reconfigure(EditorView.contentAttributes.of({ spellcheck: 'true', tabindex: tabbable ? '0' : '-1' })),
|
|
146
|
+
compartments.attributes.reconfigure(EditorView.contentAttributes.of({ ...aria, spellcheck: 'true', tabindex: tabbable ? '0' : '-1' })),
|
|
132
147
|
compartments.editable.reconfigure([EditorView.editable.of(!disabled), EditorState.readOnly.of(disabled)]),
|
|
133
148
|
compartments.placeholder.reconfigure(placeholderText(placeholder)),
|
|
134
149
|
compartments.extensions.reconfigure(extensions)
|
|
135
150
|
]
|
|
136
151
|
});
|
|
137
|
-
}, [compartments, tabbable, disabled, placeholder, extensions]);
|
|
152
|
+
}, [compartments, tabbable, disabled, placeholder, extensions, aria]);
|
|
138
153
|
|
|
139
154
|
useLayoutEffect(() => {
|
|
140
155
|
const view = viewRef.current;
|