@adecore/agents-react 0.21.1 → 0.22.0
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/agents/status-look.js +2 -3
- package/dist/chat/chat-client.d.ts +3 -2
- package/dist/chat/chat-client.js +4 -2
- package/dist/chat/drafts.d.ts +2 -1
- package/dist/chat/drafts.js +21 -17
- package/dist/chat/logic/ansi.js +5 -1
- package/dist/chat/logic/fork.js +8 -9
- package/dist/chat/logic/limit.js +17 -14
- package/dist/chat/logic/picker-entries.d.ts +10 -0
- package/dist/chat/logic/picker-entries.js +19 -0
- package/dist/chat/logic/resume-compaction.js +2 -11
- package/dist/chat/logic/scrubber.js +9 -18
- package/dist/chat/logic/timeline.d.ts +5 -0
- package/dist/chat/logic/timeline.js +25 -11
- package/dist/chat/logic/tool-catalog.js +3 -4
- package/dist/chat/mentions.js +5 -6
- 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/runtime-modes.js +2 -4
- package/dist/chat/stash.d.ts +8 -2
- package/dist/chat/stash.js +22 -8
- package/dist/chat/subagent-list.d.ts +7 -1
- package/dist/chat/subagent-list.js +13 -1
- package/dist/chat/thumbnails.js +3 -4
- package/dist/chat/timeline-scroll.d.ts +6 -2
- package/dist/chat/timeline-scroll.js +45 -46
- package/dist/chat/ui/BookmarkSubmenu.js +1 -5
- package/dist/chat/ui/ChatActivity.js +22 -8
- package/dist/chat/ui/CodeBlock.js +2 -3
- package/dist/chat/ui/Composer.js +160 -88
- package/dist/chat/ui/ComposerInput.d.ts +3 -1
- package/dist/chat/ui/ComposerInput.js +13 -4
- package/dist/chat/ui/Markdown.js +11 -11
- package/dist/chat/ui/Pickers.js +31 -27
- package/dist/chat/ui/PromptComposer.d.ts +8 -1
- package/dist/chat/ui/PromptComposer.js +18 -6
- package/dist/chat/ui/QuoteButton.js +4 -5
- package/dist/chat/ui/RunSettings.js +8 -2
- package/dist/chat/ui/Scrubber.js +2 -3
- package/dist/chat/ui/SubagentControls.d.ts +4 -2
- package/dist/chat/ui/SubagentControls.js +13 -11
- 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 +39 -21
- package/dist/chat/ui/TimelineMenu.js +3 -9
- package/dist/chat/ui/UnifiedDiff.js +3 -5
- package/dist/chat/ui/VisualFrame.js +3 -4
- package/dist/chat/ui/composer/keys.js +19 -11
- 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/render-context.d.ts +1 -1
- 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 +26 -8
- package/dist/chat/ui/rows/Rows.js +1 -1
- package/dist/chat/ui/rows/WorkRows.d.ts +7 -4
- package/dist/chat/ui/rows/WorkRows.js +22 -17
- package/dist/chat/visual-viewport.d.ts +1 -1
- package/dist/chat/visual-viewport.js +9 -8
- package/dist/host.d.ts +1 -0
- package/dist/host.js +1 -0
- package/dist/locales/en/agent-chat.json +30 -7
- package/dist/locales/nl/agent-chat.json +30 -7
- package/dist/port-transport.js +4 -8
- package/dist/state/chats.d.ts +2 -1
- package/dist/state/chats.js +29 -15
- package/dist/state/usage.js +44 -49
- package/dist/storage.js +7 -7
- package/dist/theme.css +6 -14
- package/dist/usage/LimitsList.js +1 -2
- package/dist/usage/UsageBreakdown.js +1 -3
- package/dist/usage/UsageDialog.js +2 -4
- package/dist/usage/format.js +6 -6
- package/dist/usage/money.js +1 -5
- package/dist/usage/summary.js +2 -4
- package/package.json +24 -4
- package/src/agents/model-requirement.ts +36 -0
- package/src/agents/status-look.ts +2 -3
- package/src/chat/chat-client.ts +7 -3
- package/src/chat/drafts.ts +33 -21
- package/src/chat/logic/ansi.ts +5 -1
- package/src/chat/logic/fork.ts +8 -9
- package/src/chat/logic/limit.ts +17 -16
- package/src/chat/logic/picker-entries.ts +25 -0
- package/src/chat/logic/resume-compaction.ts +2 -11
- package/src/chat/logic/scrubber.ts +13 -17
- package/src/chat/logic/timeline.ts +39 -13
- package/src/chat/logic/tool-catalog.ts +7 -6
- package/src/chat/mentions.ts +5 -6
- package/src/chat/prompt-hold.ts +27 -0
- package/src/chat/quote.ts +20 -0
- package/src/chat/runtime-modes.ts +2 -4
- package/src/chat/stash.ts +32 -8
- package/src/chat/subagent-list.ts +19 -5
- package/src/chat/thumbnails.ts +3 -4
- package/src/chat/timeline-scroll.ts +58 -53
- package/src/chat/ui/BookmarkSubmenu.tsx +1 -5
- package/src/chat/ui/ChatActivity.tsx +35 -9
- package/src/chat/ui/CodeBlock.tsx +2 -3
- package/src/chat/ui/Composer.tsx +263 -138
- package/src/chat/ui/ComposerInput.tsx +18 -3
- package/src/chat/ui/Markdown.tsx +13 -17
- package/src/chat/ui/Pickers.tsx +56 -39
- package/src/chat/ui/PromptComposer.tsx +37 -8
- package/src/chat/ui/QuoteButton.tsx +5 -5
- package/src/chat/ui/RunSettings.tsx +9 -2
- package/src/chat/ui/Scrubber.tsx +2 -3
- package/src/chat/ui/SubagentControls.tsx +23 -22
- package/src/chat/ui/SubagentInfo.tsx +24 -42
- package/src/chat/ui/SubagentStopButton.tsx +2 -1
- package/src/chat/ui/SubagentTimeline.tsx +1 -2
- package/src/chat/ui/Timeline.tsx +44 -24
- package/src/chat/ui/TimelineMenu.tsx +3 -9
- package/src/chat/ui/UnifiedDiff.tsx +3 -5
- package/src/chat/ui/VisualFrame.tsx +3 -4
- package/src/chat/ui/composer/keys.ts +21 -11
- package/src/chat/ui/composer/menu-aria.ts +29 -0
- package/src/chat/ui/intelligent-ui/render-context.ts +2 -2
- package/src/chat/ui/quote-selection.ts +6 -4
- package/src/chat/ui/rehype-chips.ts +1 -1
- package/src/chat/ui/rehype-fade.ts +1 -1
- package/src/chat/ui/remark-html-as-text.ts +1 -1
- package/src/chat/ui/rows/MessageRows.tsx +47 -9
- package/src/chat/ui/rows/Rows.tsx +3 -1
- package/src/chat/ui/rows/WorkRows.tsx +101 -70
- package/src/chat/visual-viewport.ts +10 -8
- package/src/host.ts +4 -3
- package/src/locales/en/agent-chat.json +30 -7
- package/src/locales/nl/agent-chat.json +30 -7
- package/src/port-transport.ts +4 -8
- package/src/state/chats.ts +42 -16
- package/src/state/usage.ts +49 -60
- package/src/storage.ts +7 -7
- package/src/theme.css +6 -14
- package/src/usage/LimitsList.tsx +2 -4
- package/src/usage/UsageBreakdown.tsx +2 -5
- package/src/usage/UsageDialog.tsx +2 -4
- package/src/usage/format.ts +6 -6
- package/src/usage/money.ts +1 -5
- package/src/usage/summary.ts +2 -4
package/dist/chat/ui/Composer.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
|
|
2
|
+
import { useContext, useEffect, useId, useMemo, useRef, useState, useSyncExternalStore } from 'react';
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
4
|
import { indentLess, indentMore, insertNewline } from '@codemirror/commands';
|
|
5
5
|
import { ensureSyntaxTree, syntaxTree } from '@codemirror/language';
|
|
@@ -22,12 +22,14 @@ import { PROMPT_MAX_CHARS, pasteBecomesAttachment, pastedTextName, promptGuard,
|
|
|
22
22
|
import { withQuote } from '../quote.js';
|
|
23
23
|
import { rememberChatAccount, rememberChatPreferences, rememberChatSelection } from '../preferences.js';
|
|
24
24
|
import { STASH_SHORTCUT, stashDraft, useStash } from '../stash.js';
|
|
25
|
-
import { pageTimeline, scrollTimelineToEnd, subscribeTimelineEnd, timelineAtEnd } from '../timeline-scroll.js';
|
|
25
|
+
import { jumpToTimelineItem, pageTimeline, scrollTimelineToEnd, subscribeTimelineEnd, timelineAtEnd } from '../timeline-scroll.js';
|
|
26
26
|
import { ChatActivity } from './ChatActivity.js';
|
|
27
27
|
import { ChatReferenceChip } from './ChatReferenceChip.js';
|
|
28
28
|
import { chipDecorations } from './composer/chips.js';
|
|
29
|
+
import { clampMenuIndex, composerMenuAria, optionIdOf } from './composer/menu-aria.js';
|
|
29
30
|
import { enterAction, inCode, inFenceBody, inOpenFence, listItemAt, recallDirection, tabSpaces } from './composer/keys.js';
|
|
30
31
|
import { ComposerInput } from './ComposerInput.js';
|
|
32
|
+
import { holdLiftsIn } from '../prompt-hold.js';
|
|
31
33
|
import { PromptComposer } from './PromptComposer.js';
|
|
32
34
|
import { QuoteTakerContext } from './quote-selection.js';
|
|
33
35
|
import { LimitDock } from './LimitState.js';
|
|
@@ -49,8 +51,7 @@ const NOTICE_MS = 4000;
|
|
|
49
51
|
const PROMPT_HISTORY = 50;
|
|
50
52
|
// Commands the composer handles itself; the CLI's own ones are sent through as text.
|
|
51
53
|
const LOCAL_COMMANDS = ['model', 'compact', 'clear'];
|
|
52
|
-
/*
|
|
53
|
-
file's text, or the drag's own text, at the drop point on top of that. */
|
|
54
|
+
/* The card turns dropped files and paths into attachments and chips; CodeMirror would also paste their text at the drop point. */
|
|
54
55
|
const LEAVE_DROPS_TO_THE_CARD = EditorView.domEventHandlers({
|
|
55
56
|
drop: (event) => event.dataTransfer !== null && (event.dataTransfer.types.includes('Files') || carriesMentions(event.dataTransfer.types))
|
|
56
57
|
});
|
|
@@ -88,11 +89,69 @@ function hintedPlaceholder(lead, joiner, hints) {
|
|
|
88
89
|
root.append(lead, extra);
|
|
89
90
|
return root;
|
|
90
91
|
}
|
|
92
|
+
function sendLabelKey(busy, canSteer, delivery) {
|
|
93
|
+
if (!busy) {
|
|
94
|
+
return 'composer.send';
|
|
95
|
+
}
|
|
96
|
+
if (!canSteer) {
|
|
97
|
+
return 'composer.queueButton';
|
|
98
|
+
}
|
|
99
|
+
return delivery === 'steer' ? 'composer.steerButton' : 'composer.queueWithSteerButton';
|
|
100
|
+
}
|
|
101
|
+
function sameQuery(left, right) {
|
|
102
|
+
return left?.start === right?.start && left?.query === right?.query;
|
|
103
|
+
}
|
|
91
104
|
function splitPath(path) {
|
|
92
105
|
const slash = path.lastIndexOf('/');
|
|
93
106
|
return slash < 0 ? { name: path, dir: '' } : { name: path.slice(slash + 1), dir: path.slice(0, slash) };
|
|
94
107
|
}
|
|
95
108
|
// Keep the editor mounted while a pending request takes over, so its selection and draft survive.
|
|
109
|
+
/*
|
|
110
|
+
* Keeps a waiting prompt card behind the draft while the person types. Typing, pasting and dictating
|
|
111
|
+
* all change the draft, so `noteInput` is called from its change handler; the hold lifts after a pause.
|
|
112
|
+
*/
|
|
113
|
+
function usePromptHold() {
|
|
114
|
+
const typedAt = useRef(null);
|
|
115
|
+
const composing = useRef(false);
|
|
116
|
+
const timer = useRef(null);
|
|
117
|
+
const [state, setState] = useState({ held: false, releasedBy: null });
|
|
118
|
+
const clearTimer = () => {
|
|
119
|
+
if (timer.current !== null) {
|
|
120
|
+
clearTimeout(timer.current);
|
|
121
|
+
timer.current = null;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
const release = (reason) => {
|
|
125
|
+
clearTimer();
|
|
126
|
+
typedAt.current = null;
|
|
127
|
+
composing.current = false;
|
|
128
|
+
setState((current) => (current.held ? { held: false, releasedBy: reason } : current));
|
|
129
|
+
};
|
|
130
|
+
const schedule = () => {
|
|
131
|
+
clearTimer();
|
|
132
|
+
const wait = holdLiftsIn({ typedAt: typedAt.current, composing: composing.current }, Date.now());
|
|
133
|
+
if (wait !== null) {
|
|
134
|
+
timer.current = setTimeout(() => release('pause'), wait);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
const noteInput = () => {
|
|
138
|
+
typedAt.current = Date.now();
|
|
139
|
+
setState({ held: true, releasedBy: null });
|
|
140
|
+
schedule();
|
|
141
|
+
};
|
|
142
|
+
const setComposing = (next) => {
|
|
143
|
+
composing.current = next;
|
|
144
|
+
if (next) {
|
|
145
|
+
setState({ held: true, releasedBy: null });
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
typedAt.current = Date.now();
|
|
149
|
+
}
|
|
150
|
+
schedule();
|
|
151
|
+
};
|
|
152
|
+
useEffect(() => clearTimer, []);
|
|
153
|
+
return { held: state.held, releasedBy: state.releasedBy, release, noteInput, setComposing };
|
|
154
|
+
}
|
|
96
155
|
export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabled, readOnly = false, providerFixed, onSend, onRetarget }) {
|
|
97
156
|
const { t } = useTranslation('agent-chat');
|
|
98
157
|
const sendDelivery = chatHost().useSendDelivery();
|
|
@@ -116,6 +175,7 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
116
175
|
const { isApplePlatform, notify } = chatHost();
|
|
117
176
|
const registerQuoteTaker = useContext(QuoteTakerContext);
|
|
118
177
|
const inputRef = useRef(null);
|
|
178
|
+
const hold = usePromptHold();
|
|
119
179
|
// Taking a queued message back waits on the machine, and what was typed meanwhile is what it merges with.
|
|
120
180
|
const draftRef = useRef(draft);
|
|
121
181
|
const fileInputRef = useRef(null);
|
|
@@ -125,7 +185,6 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
125
185
|
const providers = useProviders((s) => s.providers);
|
|
126
186
|
const { approvals, questions } = usePendingRequests(chatId);
|
|
127
187
|
const order = useChatRow(chatId, (row) => row?.order);
|
|
128
|
-
// The structure, which a delta leaves alone. The prompts and the requests are items of their own.
|
|
129
188
|
const items = useChatRow(chatId, (row) => row?.structure);
|
|
130
189
|
const chats = chatHost().useReferableChats();
|
|
131
190
|
const placeholderLead = chatHost().useComposerPlaceholder(scope.id, chatId);
|
|
@@ -136,7 +195,7 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
136
195
|
const capabilities = provider?.capabilities;
|
|
137
196
|
const compaction = capabilities?.compaction;
|
|
138
197
|
const dismissedTurnId = useResumeCompactionDismissal(chatId);
|
|
139
|
-
|
|
198
|
+
// The cheap half of the offer, so a chat that can never make one keeps no timer.
|
|
140
199
|
const couldOfferCompaction = compaction !== undefined && compaction !== 'none' && info.usage.contextTokens >= RESUME_COMPACTION_TOKENS;
|
|
141
200
|
const now = useNow(60_000, couldOfferCompaction);
|
|
142
201
|
const compactionOffer = !couldOfferCompaction
|
|
@@ -154,15 +213,12 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
154
213
|
const queue = info.queue ?? [];
|
|
155
214
|
const text = draft.text;
|
|
156
215
|
const quote = draft.quote;
|
|
216
|
+
const quoteSource = draft.quoteSource;
|
|
157
217
|
// The CLI announces its session on the first message, so anything before that is still a blank chat.
|
|
158
218
|
const started = info.agentSessionId !== null || info.usage.turns > 0;
|
|
159
|
-
/*
|
|
160
|
-
* A chat that has started keeps its provider (the CLI holds the thread), and one opened for a
|
|
161
|
-
* named CLI was never going to change it. Either way the models stay switchable. The CLI takes
|
|
162
|
-
* the new one on the restart the next send does anyway. With one provider in the list the
|
|
163
|
-
* picker drops its group headers on its own, so it reads as that CLI's own catalog.
|
|
164
|
-
*/
|
|
165
219
|
const accountChoice = useAccountChoice(info.provider, info.account);
|
|
220
|
+
// A chat that has started keeps its provider (the CLI holds the thread), and so does one opened for a
|
|
221
|
+
// named CLI; its models stay switchable, since the CLI takes a new one on the restart the next send does.
|
|
166
222
|
const pickable = started || providerFixed
|
|
167
223
|
? providers.filter((entry) => entry.kind === info.provider)
|
|
168
224
|
: providers.filter((entry) => entry.installed && entry.capabilities.chat);
|
|
@@ -179,8 +235,8 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
179
235
|
setDraft((current) => ({ ...current, text: joinDraftText(current.text, offered) }));
|
|
180
236
|
inputRef.current?.focus();
|
|
181
237
|
}), [chatId]);
|
|
182
|
-
useEffect(() => registerQuoteTaker?.((selected) => {
|
|
183
|
-
setDraft((current) => ({ ...current, quote: selected }));
|
|
238
|
+
useEffect(() => registerQuoteTaker?.((selected, itemId) => {
|
|
239
|
+
setDraft((current) => ({ ...current, quote: selected, quoteSource: itemId }));
|
|
184
240
|
inputRef.current?.focus();
|
|
185
241
|
}), [registerQuoteTaker]);
|
|
186
242
|
useEffect(() => {
|
|
@@ -197,11 +253,11 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
197
253
|
}
|
|
198
254
|
let stale = false;
|
|
199
255
|
const search = () => {
|
|
200
|
-
const
|
|
201
|
-
if (
|
|
256
|
+
const searchFiles = chatHost().searchFiles;
|
|
257
|
+
if (searchFiles === null) {
|
|
202
258
|
return;
|
|
203
259
|
}
|
|
204
|
-
|
|
260
|
+
searchFiles(scope.id, info.cwd, mention.query, MENTION_RESULTS)
|
|
205
261
|
.then((files) => {
|
|
206
262
|
if (!stale) {
|
|
207
263
|
setSearched({ query: mention.query, files });
|
|
@@ -280,6 +336,22 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
280
336
|
const files = mention === null ? [] : searched.files;
|
|
281
337
|
const chatMatches = useMemo(() => (mention === null ? [] : chatSuggestions(chats, mention.query, [chatId, ...draft.chats], CHAT_RESULTS)), [chatId, chats, draft.chats, mention]);
|
|
282
338
|
const mentionCount = chatMatches.length + files.length;
|
|
339
|
+
// One menu is open at a time, so they share an id.
|
|
340
|
+
const listId = useId();
|
|
341
|
+
const mentionsLabelId = `${listId}-chats`;
|
|
342
|
+
const filesLabelId = `${listId}-files`;
|
|
343
|
+
const filesLabelShown = mention !== null && (mention.query === '' || chatMatches.length > 0);
|
|
344
|
+
const menuOpen = commandMenuOpen || skillMenuOpen || mentionMenuOpen;
|
|
345
|
+
const menuCount = commandMenuOpen ? commands.length : skillMenuOpen ? skillMatches.length : mentionCount;
|
|
346
|
+
// The list can shrink under a stale index; every reader takes the one that still points at an option.
|
|
347
|
+
const activeIndex = clampMenuIndex(menuIndex, menuCount);
|
|
348
|
+
const inputAria = useMemo(() => composerMenuAria({
|
|
349
|
+
label: t('composer.inputLabel'),
|
|
350
|
+
listId,
|
|
351
|
+
open: menuOpen,
|
|
352
|
+
activeIndex,
|
|
353
|
+
count: menuCount
|
|
354
|
+
}), [t, listId, menuOpen, activeIndex, menuCount]);
|
|
283
355
|
const editorExtensions = useMemo(() => [chipDecorations({ mentions: draft.mentions, skills: draft.skills }), LEAVE_DROPS_TO_THE_CARD], [draft.mentions, draft.skills]);
|
|
284
356
|
const guard = promptGuard(withQuote(quote, text));
|
|
285
357
|
const setText = (next, mentions = draft.mentions, chosen = draft.skills) => {
|
|
@@ -289,18 +361,28 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
289
361
|
setDraft(EMPTY_DRAFT);
|
|
290
362
|
};
|
|
291
363
|
const removeQuote = () => {
|
|
292
|
-
setDraft((current) => ({ ...current, quote: '' }));
|
|
364
|
+
setDraft((current) => ({ ...current, quote: '', quoteSource: null }));
|
|
293
365
|
inputRef.current?.focus();
|
|
294
366
|
};
|
|
295
|
-
const
|
|
367
|
+
const showError = (error) => {
|
|
368
|
+
setNotice(error instanceof Error ? error.message : String(error));
|
|
369
|
+
};
|
|
370
|
+
/* One configuration at a time; a second pick while one is on its way is dropped. */
|
|
371
|
+
const runConfiguration = (work) => {
|
|
296
372
|
if (configuring.current) {
|
|
297
373
|
return;
|
|
298
374
|
}
|
|
299
375
|
configuring.current = true;
|
|
300
376
|
setConfigurationPending(true);
|
|
301
|
-
void
|
|
302
|
-
.
|
|
303
|
-
.
|
|
377
|
+
void work()
|
|
378
|
+
.catch(showError)
|
|
379
|
+
.finally(() => {
|
|
380
|
+
configuring.current = false;
|
|
381
|
+
setConfigurationPending(false);
|
|
382
|
+
});
|
|
383
|
+
};
|
|
384
|
+
const configure = (patch) => {
|
|
385
|
+
runConfiguration(() => actions.configure(chatId, patch).then(() => {
|
|
304
386
|
if (patch.selection) {
|
|
305
387
|
rememberChatSelection(info.provider, patch.selection);
|
|
306
388
|
}
|
|
@@ -310,40 +392,24 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
310
392
|
if (patch.account) {
|
|
311
393
|
rememberChatAccount(scope.id, info.provider, patch.account);
|
|
312
394
|
}
|
|
313
|
-
})
|
|
314
|
-
.catch((error) => setNotice(error instanceof Error ? error.message : String(error)))
|
|
315
|
-
.finally(() => {
|
|
316
|
-
configuring.current = false;
|
|
317
|
-
setConfigurationPending(false);
|
|
318
|
-
});
|
|
395
|
+
}));
|
|
319
396
|
};
|
|
320
397
|
const chooseAccount = (account) => configure({ account });
|
|
321
398
|
/* A model of another CLI re-points the whole chat; one of this CLI's own is a configure. */
|
|
322
399
|
const chooseModel = (provider, slug) => {
|
|
323
400
|
const selection = { model: slug, options: {} };
|
|
324
401
|
if (provider !== info.provider) {
|
|
325
|
-
|
|
326
|
-
return;
|
|
327
|
-
}
|
|
328
|
-
configuring.current = true;
|
|
329
|
-
setConfigurationPending(true);
|
|
330
|
-
void onRetarget(provider, selection)
|
|
331
|
-
.then(() => rememberChatSelection(provider, selection))
|
|
332
|
-
.catch((error) => setNotice(error instanceof Error ? error.message : String(error)))
|
|
333
|
-
.finally(() => {
|
|
334
|
-
configuring.current = false;
|
|
335
|
-
setConfigurationPending(false);
|
|
336
|
-
});
|
|
402
|
+
runConfiguration(() => onRetarget(provider, selection).then(() => rememberChatSelection(provider, selection)));
|
|
337
403
|
return;
|
|
338
404
|
}
|
|
339
405
|
configure({ selection });
|
|
340
406
|
};
|
|
341
407
|
const stop = (shiftKey) => {
|
|
342
408
|
if (composerStopOf(shiftKey) === 'turn') {
|
|
343
|
-
void actions.stopTurn(chatId, false).catch(
|
|
409
|
+
void actions.stopTurn(chatId, false).catch(showError);
|
|
344
410
|
return;
|
|
345
411
|
}
|
|
346
|
-
chatHost().confirm.stopSubagents(scope.id, chatId, () => void actions.stopTurn(chatId, true).catch(
|
|
412
|
+
chatHost().confirm.stopSubagents(scope.id, chatId, () => void actions.stopTurn(chatId, true).catch(showError));
|
|
347
413
|
};
|
|
348
414
|
/* The host decides whether a turn is in the way; only its refusal asks the person first. */
|
|
349
415
|
const clearThread = async () => {
|
|
@@ -380,17 +446,16 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
380
446
|
return false;
|
|
381
447
|
}
|
|
382
448
|
};
|
|
383
|
-
const same = (a, b) => a?.start === b?.start && a?.query === b?.query;
|
|
384
449
|
/* Which picker the caret opens: `@` for a file, `$` for a skill, or neither. In code a sigil is just text. */
|
|
385
450
|
const trackTriggers = (value, selection, state) => {
|
|
386
451
|
const empty = selection.from === selection.to;
|
|
387
452
|
const tree = empty ? (ensureSyntaxTree(state, selection.from, 50) ?? syntaxTree(state)) : null;
|
|
388
453
|
const caret = tree !== null && !inCode(tree, value, selection.from) ? selection.from : null;
|
|
389
454
|
const skill = caret === null ? null : findSkillQuery(value, caret);
|
|
390
|
-
setSkillQuery((current) => (
|
|
455
|
+
setSkillQuery((current) => (sameQuery(current, skill) ? current : skill));
|
|
391
456
|
// A CLI that does not expand `@path` gets the text as it is, so the picker stays out of the way.
|
|
392
457
|
const next = caret === null || capabilities?.mentions === false ? null : findMentionQuery(value, caret);
|
|
393
|
-
setMention((current) => (
|
|
458
|
+
setMention((current) => (sameQuery(current, next) ? current : next));
|
|
394
459
|
};
|
|
395
460
|
// The inserted text reaches the editor with the render it causes, so the caret waits a frame for it.
|
|
396
461
|
const moveCaret = (caret) => {
|
|
@@ -438,8 +503,7 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
438
503
|
setSkillQuery(null);
|
|
439
504
|
moveCaret(result.caret);
|
|
440
505
|
};
|
|
441
|
-
/* Paths dragged in from a list of files.
|
|
442
|
-
at the end of the prompt and join the draft's mentions, which is what `chat.send` carries. */
|
|
506
|
+
/* Paths dragged in from a list of files. With no mention query to insert into, they land at the end of the prompt. */
|
|
443
507
|
const addMentions = (paths) => {
|
|
444
508
|
const fresh = paths.filter((path) => path !== '');
|
|
445
509
|
if (fresh.length === 0 || capabilities?.mentions === false) {
|
|
@@ -488,7 +552,8 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
488
552
|
setNotice(`${rejected.name}: ${rejected.reason}`);
|
|
489
553
|
return;
|
|
490
554
|
}
|
|
491
|
-
|
|
555
|
+
hold.release('send');
|
|
556
|
+
const chosen = commands[activeIndex];
|
|
492
557
|
if (commandQuery !== null && chosen?.local && runCommand(chosen.name)) {
|
|
493
558
|
clearDraft();
|
|
494
559
|
return;
|
|
@@ -509,6 +574,7 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
509
574
|
mentions: presentMentions(trimmed, draft.mentions),
|
|
510
575
|
skills: presentSkills(trimmed, draft.skills),
|
|
511
576
|
chats: draft.chats,
|
|
577
|
+
quoteSource: quote !== '' && quoteSource !== null ? { itemId: quoteSource } : undefined,
|
|
512
578
|
attachments: draft.attachments
|
|
513
579
|
});
|
|
514
580
|
clearDraft();
|
|
@@ -516,9 +582,20 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
516
582
|
setSkillQuery(null);
|
|
517
583
|
setHistoryIndex(null);
|
|
518
584
|
};
|
|
519
|
-
/*
|
|
585
|
+
/*
|
|
586
|
+
* A stashed prompt comes back as text, mentions and skills; its files were never kept. Its quote
|
|
587
|
+
* keeps leading to its reply in the chat it came from, and is plain text in any other.
|
|
588
|
+
*/
|
|
520
589
|
const restoreStashed = (prompt) => {
|
|
521
|
-
|
|
590
|
+
const stashedQuote = prompt.quote;
|
|
591
|
+
const here = stashedQuote !== undefined && stashedQuote.chatId === chatId;
|
|
592
|
+
setDraft((current) => ({
|
|
593
|
+
...current,
|
|
594
|
+
text: stashedQuote === undefined || here ? prompt.text : withQuote(stashedQuote.text, prompt.text),
|
|
595
|
+
mentions: [...prompt.mentions],
|
|
596
|
+
skills: [...prompt.skills],
|
|
597
|
+
...(here ? { quote: stashedQuote.text, quoteSource: stashedQuote.itemId } : {})
|
|
598
|
+
}));
|
|
522
599
|
setHistoryIndex(null);
|
|
523
600
|
inputRef.current?.focus();
|
|
524
601
|
};
|
|
@@ -597,7 +674,7 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
597
674
|
}
|
|
598
675
|
return;
|
|
599
676
|
}
|
|
600
|
-
if (stashDraft(draft)) {
|
|
677
|
+
if (stashDraft(draft, chatId)) {
|
|
601
678
|
clearDraft();
|
|
602
679
|
setMention(null);
|
|
603
680
|
setSkillQuery(null);
|
|
@@ -620,6 +697,18 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
620
697
|
setText(history[next].text, history[next].mentions);
|
|
621
698
|
return true;
|
|
622
699
|
};
|
|
700
|
+
/* The arrows walk an open picker round; true when the key was one of them. */
|
|
701
|
+
const moveInMenu = (key, count) => {
|
|
702
|
+
if (key === 'ArrowDown') {
|
|
703
|
+
setMenuIndex((i) => (i + 1) % count);
|
|
704
|
+
return true;
|
|
705
|
+
}
|
|
706
|
+
if (key === 'ArrowUp') {
|
|
707
|
+
setMenuIndex((i) => (i - 1 + count) % count);
|
|
708
|
+
return true;
|
|
709
|
+
}
|
|
710
|
+
return false;
|
|
711
|
+
};
|
|
623
712
|
/* Runs before the editor's own keymap; true means the key is handled and its default prevented. */
|
|
624
713
|
const onKeyDown = (e, view) => {
|
|
625
714
|
// Shift with the paste shortcut pastes a large text inline after all; any other key forgets it.
|
|
@@ -634,8 +723,7 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
634
723
|
setText('', [], []);
|
|
635
724
|
return true;
|
|
636
725
|
}
|
|
637
|
-
// The
|
|
638
|
-
// panels and the palette stay the app's, untouched: Mod+Shift+Enter would otherwise send as well.
|
|
726
|
+
// The app's own shortcuts stay the app's while you type: Mod+Shift+Enter would otherwise send as well.
|
|
639
727
|
if (chatHost().isAppShortcut(e)) {
|
|
640
728
|
return false;
|
|
641
729
|
}
|
|
@@ -653,16 +741,11 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
653
741
|
return true;
|
|
654
742
|
}
|
|
655
743
|
if (commandMenuOpen) {
|
|
656
|
-
if (e.key
|
|
657
|
-
setMenuIndex((i) => (i + 1) % commands.length);
|
|
658
|
-
return true;
|
|
659
|
-
}
|
|
660
|
-
if (e.key === 'ArrowUp') {
|
|
661
|
-
setMenuIndex((i) => (i - 1 + commands.length) % commands.length);
|
|
744
|
+
if (moveInMenu(e.key, commands.length)) {
|
|
662
745
|
return true;
|
|
663
746
|
}
|
|
664
747
|
if (e.key === 'Tab') {
|
|
665
|
-
const chosen = commands[
|
|
748
|
+
const chosen = commands[activeIndex];
|
|
666
749
|
if (chosen.skill) {
|
|
667
750
|
chooseSkill(chosen.name);
|
|
668
751
|
}
|
|
@@ -673,30 +756,20 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
673
756
|
}
|
|
674
757
|
}
|
|
675
758
|
if (skillMenuOpen) {
|
|
676
|
-
if (e.key
|
|
677
|
-
setMenuIndex((i) => (i + 1) % skillMatches.length);
|
|
678
|
-
return true;
|
|
679
|
-
}
|
|
680
|
-
if (e.key === 'ArrowUp') {
|
|
681
|
-
setMenuIndex((i) => (i - 1 + skillMatches.length) % skillMatches.length);
|
|
759
|
+
if (moveInMenu(e.key, skillMatches.length)) {
|
|
682
760
|
return true;
|
|
683
761
|
}
|
|
684
762
|
if (e.key === 'Tab' || e.key === 'Enter') {
|
|
685
|
-
chooseSkill((skillMatches[
|
|
763
|
+
chooseSkill((skillMatches[activeIndex] ?? skillMatches[0]).name);
|
|
686
764
|
return true;
|
|
687
765
|
}
|
|
688
766
|
}
|
|
689
767
|
if (mentionMenuOpen && mentionCount > 0) {
|
|
690
|
-
if (e.key
|
|
691
|
-
setMenuIndex((i) => (i + 1) % mentionCount);
|
|
692
|
-
return true;
|
|
693
|
-
}
|
|
694
|
-
if (e.key === 'ArrowUp') {
|
|
695
|
-
setMenuIndex((i) => (i - 1 + mentionCount) % mentionCount);
|
|
768
|
+
if (moveInMenu(e.key, mentionCount)) {
|
|
696
769
|
return true;
|
|
697
770
|
}
|
|
698
771
|
if (e.key === 'Tab' || e.key === 'Enter') {
|
|
699
|
-
chooseMentionAt(
|
|
772
|
+
chooseMentionAt(activeIndex);
|
|
700
773
|
return true;
|
|
701
774
|
}
|
|
702
775
|
}
|
|
@@ -823,7 +896,7 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
823
896
|
addMentions(droppedMentions(e.dataTransfer));
|
|
824
897
|
addFiles(filesOf(e.dataTransfer));
|
|
825
898
|
inputRef.current?.focus();
|
|
826
|
-
}, children: _jsxs(PromptComposer, { chatId: chatId, pending: pending, focused: focused, onShowElsewhere: answerPromptsElsewhere, disabled: disabled, hasDraft: !isEmptyDraft(draft), denyReason: capabilities?.denyReason === true, onAllAnswered: () => inputRef.current?.focus(), children: [_jsx(LimitDock, { chatId: chatId, info: info }), compactionOffer && (_jsx(ResumeCompactionDock, { tokens: compactionOffer.tokens, onCompact: compact, onDismiss: () => dismissResumeCompaction(scope, chatId, compactionOffer.turnId) })), commandMenuOpen && (_jsx("div", { className: "border-b border-border px-1.5 py-1.5", children: commands.map((command, index) => (_jsxs("button", { "data-active": index ===
|
|
899
|
+
}, children: _jsxs(PromptComposer, { chatId: chatId, pending: pending, focused: focused, onShowElsewhere: answerPromptsElsewhere, disabled: disabled, hasDraft: !isEmptyDraft(draft), denyReason: capabilities?.denyReason === true, onAllAnswered: () => inputRef.current?.focus(), hold: hold, children: [_jsx(LimitDock, { chatId: chatId, info: info }), compactionOffer && (_jsx(ResumeCompactionDock, { tokens: compactionOffer.tokens, onCompact: compact, onDismiss: () => dismissResumeCompaction(scope, chatId, compactionOffer.turnId) })), commandMenuOpen && (_jsx("div", { id: listId, role: "listbox", "aria-label": t('composer.menu.commands'), className: "border-b border-border px-1.5 py-1.5", children: commands.map((command, index) => (_jsxs("button", { id: optionIdOf(listId, index), role: "option", "aria-selected": index === activeIndex, tabIndex: -1, "data-active": index === activeIndex, className: "cursor-row flex w-full items-center gap-2 rounded-md px-2 py-1 text-left text-xs text-text-muted", onMouseEnter: () => setMenuIndex(index), onMouseDown: (e) => e.preventDefault(), onClick: () => {
|
|
827
900
|
setMenuIndex(index);
|
|
828
901
|
if (command.skill) {
|
|
829
902
|
chooseSkill(command.name);
|
|
@@ -831,10 +904,10 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
831
904
|
}
|
|
832
905
|
setText(`/${command.name}`);
|
|
833
906
|
inputRef.current?.focus();
|
|
834
|
-
}, children: [_jsx(Icon, { icon: command.skill ? Zap : SquareSlash, size: 12, className: clsx('shrink-0', command.skill ? 'text-skill' : 'text-text-faint') }), _jsxs("span", { className: "font-mono text-text", children: ["/", command.name] }), _jsx("span", { className: "text-text-faint", children: command.hint })] }, command.name))) })), skillMenuOpen && (_jsx("div", { className: "border-b border-border px-1.5 py-1.5", children: skillMatches.map((skill, index) => (_jsxs("button", { "data-active": index ===
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
907
|
+
}, children: [_jsx(Icon, { icon: command.skill ? Zap : SquareSlash, size: 12, className: clsx('shrink-0', command.skill ? 'text-skill' : 'text-text-faint') }), _jsxs("span", { className: "font-mono text-text", children: ["/", command.name] }), _jsx("span", { className: "text-text-faint", children: command.hint })] }, command.name))) })), skillMenuOpen && (_jsx("div", { id: listId, role: "listbox", "aria-label": t('composer.menu.skills'), className: "border-b border-border px-1.5 py-1.5", children: skillMatches.map((skill, index) => (_jsxs("button", { id: optionIdOf(listId, index), role: "option", "aria-selected": index === activeIndex, tabIndex: -1, "data-active": index === activeIndex, className: "cursor-row flex w-full items-start gap-2 rounded-md px-2 py-1 text-left text-xs text-text-muted", onMouseEnter: () => setMenuIndex(index), onMouseDown: (e) => e.preventDefault(), onClick: () => chooseSkill(skill.name), children: [_jsx(Icon, { icon: Zap, size: 12, className: "mt-0.5 shrink-0 text-skill" }), _jsxs("span", { className: "flex min-w-0 flex-col", children: [_jsxs("span", { className: "truncate font-mono text-text", children: ["$", skill.name] }), skill.description !== '' && _jsx("span", { className: "line-clamp-1 text-text-faint", children: skill.description })] })] }, skill.name))) })), mentionMenuOpen && (_jsxs("div", { className: "border-b border-border px-1.5 py-1.5", children: [mentionCount > 0 && (_jsxs("div", { id: listId, role: "listbox", "aria-label": t('composer.menu.mentions'), children: [chatMatches.length > 0 && (_jsxs("div", { role: "group", "aria-labelledby": mentionsLabelId, children: [_jsx(Menu.Label, { id: mentionsLabelId, children: t('composer.mentions.chats') }), chatMatches.map((chat, index) => (_jsxs("button", { id: optionIdOf(listId, index), role: "option", "aria-selected": index === activeIndex, tabIndex: -1, "data-active": index === activeIndex, className: "cursor-row flex w-full items-center gap-2 rounded-md px-2 py-1 text-left text-xs text-text-muted", onMouseEnter: () => setMenuIndex(index), onMouseDown: (e) => e.preventDefault(), onClick: () => chooseChat(chat.id), children: [_jsx(Icon, { icon: MessageSquare, size: 14, className: "shrink-0 text-text-faint" }), _jsx("span", { className: "truncate text-text", children: chat.title || t('chatReference.untitled') })] }, chat.id)))] })), files.length > 0 && (_jsxs("div", { role: "group", ...(filesLabelShown ? { 'aria-labelledby': filesLabelId } : { 'aria-label': t('composer.mentions.files') }), children: [filesLabelShown && (_jsx(Menu.Label, { id: filesLabelId, children: mention.query === '' ? t('composer.mentions.here') : t('composer.mentions.files') })), files.map((path, index) => {
|
|
908
|
+
const { name, dir } = splitPath(path);
|
|
909
|
+
return (_jsxs("button", { id: optionIdOf(listId, chatMatches.length + index), role: "option", "aria-selected": chatMatches.length + index === activeIndex, tabIndex: -1, "data-active": chatMatches.length + index === activeIndex, className: "cursor-row flex w-full items-center gap-2 rounded-md px-2 py-1 text-left text-xs text-text-muted", onMouseEnter: () => setMenuIndex(chatMatches.length + index), onMouseDown: (e) => e.preventDefault(), onClick: () => chooseMention(path), children: [_jsx(FileIcon, { path: path, size: 14 }), _jsx("span", { className: "truncate font-mono text-text", children: name }), dir && _jsx("span", { className: "min-w-0 truncate text-text-faint", children: dir })] }, path));
|
|
910
|
+
})] }))] })), mentionCount === 0 && (_jsx("div", { className: "px-2 py-1 text-xs text-text-faint", children: mention.query ? t('composer.mentions.noMatch') : t('composer.mentions.empty') }))] })), queue.length > 0 && (_jsxs("div", { className: "flex flex-col gap-1 border-b border-border px-2 py-1.5", children: [info.queuePaused === true && _jsx("p", { className: "px-1.5 text-xs text-text-muted", children: t('composer.queue.paused') }), queue.map((message) => {
|
|
838
911
|
const sendNow = () => {
|
|
839
912
|
scrollTimelineToEnd(chatId, 'instant');
|
|
840
913
|
void actions.sendNow(chatId, message.id).catch(() => undefined);
|
|
@@ -842,7 +915,11 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
842
915
|
const unqueue = () => void actions.unqueue(chatId, message.id).catch(() => undefined);
|
|
843
916
|
const edit = () => void takeBack(message);
|
|
844
917
|
return (_jsxs(ContextMenu.Root, { children: [_jsxs(ContextMenu.Trigger, { className: "group/queued flex items-center gap-2 rounded-md px-1.5 py-1 text-xs text-text-muted", children: [_jsx(Icon, { icon: Clock, size: 12, className: "shrink-0 text-text-faint" }), _jsx("span", { className: "min-w-0 grow truncate", children: message.text || t('composer.queue.attachments', { count: message.attachments?.length ?? 0 }) }), _jsxs(ButtonGroup, { render: _jsx("span", {}), className: "opacity-0 transition-opacity group-hover/queued:opacity-100 focus-within:opacity-100", children: [_jsx(IconButton, { icon: FastForward, size: "2xs", label: t('composer.queue.sendNow'), onClick: sendNow }), _jsx(IconButton, { icon: Pencil, size: "2xs", label: t('common.action.edit'), disabled: takingBack !== null, onClick: edit }), _jsx(IconButton, { icon: X, size: "2xs", label: t('common.action.remove'), onClick: unqueue })] })] }), _jsxs(ContextMenu.Popup, { children: [_jsxs(ContextMenu.Item, { onClick: sendNow, children: [_jsx(Icon, { icon: FastForward, size: 14 }), " ", t('composer.queue.sendNow')] }), _jsxs(ContextMenu.Item, { disabled: takingBack !== null, onClick: edit, children: [_jsx(Icon, { icon: Pencil, size: 14 }), " ", t('common.action.edit')] }), _jsxs(ContextMenu.Item, { disabled: message.text === '', onClick: () => copyText(message.text), children: [_jsx(Icon, { icon: Copy, size: 14 }), " ", t('common.action.copy')] }), _jsx(ContextMenu.Separator, {}), _jsxs(ContextMenu.Item, { className: "text-status-error", onClick: unqueue, children: [_jsx(Icon, { icon: X, size: 14 }), " ", t('common.action.remove')] })] })] }, message.id));
|
|
845
|
-
})] })), quote !== '' && (_jsxs("div", { className: "flex items-center gap-2 px-5 pt-4 @max-md/composer:px-3.5 @max-md/composer:pt-3", children: [_jsx("span", { className: "min-w-0 grow truncate border-l-2 border-border-strong pl-2.5 text-xs text-text-muted", children: quote })
|
|
918
|
+
})] })), quote !== '' && (_jsxs("div", { className: "flex items-center gap-2 px-5 pt-4 @max-md/composer:px-3.5 @max-md/composer:pt-3", children: [quoteSource === null ? (_jsx("span", { className: "min-w-0 grow truncate border-l-2 border-border-strong pl-2.5 text-xs text-text-muted", children: quote })) : (_jsx(Tooltip, { label: t('composer.quote.goToSource'), children: _jsx("button", { type: "button", 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", onClick: () => jumpToTimelineItem(scope.keyOf(chatId), quoteSource, {
|
|
919
|
+
flash: true,
|
|
920
|
+
onMissing: () => setNotice(t('rows.user.quoteGone'))
|
|
921
|
+
}), children: quote }) })), _jsx(IconButton, { icon: X, size: "2xs", label: t('composer.quote.remove'), onClick: removeQuote })] })), draft.chats.length > 0 && (_jsx("div", { className: "flex flex-wrap gap-1.5 px-5 pt-4 @max-md/composer:px-3.5 @max-md/composer:pt-3", children: draft.chats.map((id) => (_jsx(ChatReferenceChip, { chatId: id, onRemove: () => removeChat(id) }, id))) })), draft.attachments.length > 0 && (_jsx("div", { className: "flex flex-wrap gap-2 px-5 pt-4 @max-md/composer:px-3.5 @max-md/composer:pt-3", children: draft.attachments.map((attachment, index) => (_jsxs("div", { className: "group/thumb relative", children: [isImageAttachment(attachment.mime) ? (_jsx(UploadThumb, { upload: attachment })) : (_jsxs("span", { className: "flex h-14 w-36 flex-col justify-center gap-0.5 rounded-lg border border-border bg-surface-sunken px-2.5", children: [_jsxs("span", { className: "flex items-center gap-1.5 text-xs text-text", children: [_jsx(Icon, { icon: Paperclip, size: 12, className: "shrink-0 text-text-faint" }), _jsx("span", { className: "truncate", children: attachment.name })] }), _jsx("span", { className: "pl-5 text-xs text-text-faint", children: formatBytes(uploadBytes(attachment)) })] })), _jsx(IconButton, { icon: X, size: "2xs", label: t('composer.removeAttachment', { name: attachment.name }), className: "absolute -top-1.5 -right-1.5 border border-border bg-surface-raised opacity-0 transition-opacity group-hover/thumb:opacity-100 focus-visible:opacity-100", onClick: () => removeAttachment(index) })] }, `${attachment.name}-${index}`))) })), _jsx(ComposerInput, { ref: inputRef, dictationToolbar: dictationToolbar, className: "composer-input select-text text-sm leading-normal text-text", value: text, placeholder: placeholder, disabled: !writable, tabbable: focused, extensions: editorExtensions, aria: inputAria, onChange: (value, selection, state) => {
|
|
922
|
+
hold.noteInput();
|
|
846
923
|
setText(value);
|
|
847
924
|
setMenuIndex(0);
|
|
848
925
|
trackTriggers(value, selection, state);
|
|
@@ -852,15 +929,10 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
852
929
|
}, onSelectionChange: trackTriggers, onBlur: () => {
|
|
853
930
|
setMention(null);
|
|
854
931
|
setSkillQuery(null);
|
|
855
|
-
|
|
932
|
+
hold.release('blur');
|
|
933
|
+
}, onComposing: hold.setComposing, onKeyDown: onKeyDown, onPaste: onPaste }), notice && (_jsx("div", { className: "px-5 pb-1 text-xs text-status-error @max-md/composer:px-3.5", role: "alert", children: 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')}`] })), _jsxs("div", { className: "flex items-center gap-2 pt-3 pr-3.5 pb-3.5 pl-5 @max-md/composer:gap-1.5 @max-md/composer:p-3 @max-md/composer:pt-2.5", children: [attachable && (_jsx(Tooltip, { label: t('composer.attach'), name: true, children: _jsx("button", { type: "button", className: "grid h-8 w-8 shrink-0 place-items-center rounded-full border border-border text-text-muted hover:bg-surface-hover hover:text-text disabled:opacity-50", disabled: !writable, onClick: () => fileInputRef.current?.click(), children: _jsx(Icon, { icon: Plus, size: 16 }) }) })), _jsx("input", { ref: fileInputRef, type: "file", multiple: true, hidden: true, onChange: (e) => {
|
|
856
934
|
addFiles(Array.from(e.target.files ?? []));
|
|
857
935
|
e.target.value = '';
|
|
858
936
|
inputRef.current?.focus();
|
|
859
|
-
} }), _jsx(RunSettings, { disabled: configurationPending || !writable, providers: pickable, provider: info.provider, selection: info.selection, model: model, runtimeMode: info.runtimeMode, effectiveRuntimeMode: info.effectiveRuntimeMode, permissionMode: info.permissionMode, open: modelPickerOpen, onOpenChange: setModelPickerOpen, onModel: chooseModel, onOption: (id, value) => configure({ selection: { ...info.selection, options: { ...info.selection.options, [id]: value } } }), onMode: (runtimeMode) => configure({ runtimeMode }), usage: info.usage, compactDisabled: busy || !writable, onCompact: compact, account: accountChoice === null ? null : { choice: accountChoice, started, onPick: chooseAccount } }), _jsx(StashPicker, { onRestore: restoreStashed }), Slot !== null && (_jsx(ErrorBoundary, { label: t('composer.slotFailed'), resetKeys: [chatId], compact: true, className: "relative rounded-lg", children: _jsx(Slot, { scopeId: scope.id, chatId: chatId, disabled: !writable, insert: insertIntoDraft }) })), _jsx("span", { className: "grow" }), _jsxs("div", { className: "flex h-9 shrink-0 items-center gap-0.5 rounded-full bg-surface-hover", children: [_jsx("div", { ref: setDictationToolbar, className: "flex items-center pl-1 empty:hidden [&_.icon-btn]:rounded-full" }), busy && (_jsx(Tooltip, { label: composerStopLabel(), name: true, children: _jsx("button", { className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-status-error text-accent-text", onClick: (event) => stop(event.shiftKey), children: _jsx(Icon, { icon: Square, size: 16 }) }) })), (!busy || !isEmptyDraft(draft)) && (_jsx(Tooltip, { label: busy
|
|
860
|
-
? t(canSteer
|
|
861
|
-
? sendDelivery === 'steer'
|
|
862
|
-
? 'composer.steerButton'
|
|
863
|
-
: 'composer.queueWithSteerButton'
|
|
864
|
-
: 'composer.queueButton')
|
|
865
|
-
: t('composer.send'), kbd: KEY_SHORTCUTS.modEnter, name: true, children: _jsx("button", { className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-accent text-accent-text hover:brightness-90 disabled:opacity-50 disabled:hover:brightness-100", disabled: isEmptyDraft(draft) || !writable || guard.tooLong, onClick: (event) => submit(event.altKey), children: _jsx(Icon, { icon: ArrowUp, size: 16 }) }) }))] })] })] }) }), _jsx(PromptDialog, { open: confirmClear, title: t('composer.clear.title'), description: t('composer.clear.description'), confirmLabel: t('composer.clear.confirm'), danger: true, fallbackMessage: t('composer.notice.clearFailed'), onConfirm: forceClearThread, onOpenChange: () => setConfirmClear(false) })] }));
|
|
937
|
+
} }), _jsx(RunSettings, { disabled: configurationPending || !writable, providers: pickable, provider: info.provider, selection: info.selection, model: model, runtimeMode: info.runtimeMode, effectiveRuntimeMode: info.effectiveRuntimeMode, permissionMode: info.permissionMode, open: modelPickerOpen, onOpenChange: setModelPickerOpen, onModel: chooseModel, onOption: (id, value) => configure({ selection: { ...info.selection, options: { ...info.selection.options, [id]: value } } }), onMode: (runtimeMode) => configure({ runtimeMode }), usage: info.usage, compactDisabled: busy || !writable, onCompact: compact, account: accountChoice === null ? null : { choice: accountChoice, started, onPick: chooseAccount } }), _jsx(StashPicker, { onRestore: restoreStashed }), Slot !== null && (_jsx(ErrorBoundary, { label: t('composer.slotFailed'), resetKeys: [chatId], compact: true, className: "relative rounded-lg", children: _jsx(Slot, { scopeId: scope.id, chatId: chatId, disabled: !writable, insert: insertIntoDraft }) })), _jsx("span", { className: "grow" }), _jsxs("div", { className: "flex h-9 shrink-0 items-center gap-0.5 rounded-full bg-surface-hover", children: [_jsx("div", { ref: setDictationToolbar, className: "flex items-center pl-1 empty:hidden [&_.icon-btn]:rounded-full" }), busy && (_jsx(Tooltip, { label: composerStopLabel(), name: true, children: _jsx("button", { className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-status-error text-accent-text", onClick: (event) => stop(event.shiftKey), children: _jsx(Icon, { icon: Square, size: 16 }) }) })), (!busy || !isEmptyDraft(draft)) && (_jsx(Tooltip, { label: t(sendLabelKey(busy, canSteer, sendDelivery)), kbd: KEY_SHORTCUTS.modEnter, name: true, children: _jsx("button", { className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-accent text-accent-text hover:brightness-90 disabled:opacity-50 disabled:hover:brightness-100", disabled: isEmptyDraft(draft) || !writable || guard.tooLong, onClick: (event) => submit(event.altKey), children: _jsx(Icon, { icon: ArrowUp, size: 16 }) }) }))] })] })] }) }), _jsx(PromptDialog, { open: confirmClear, title: t('composer.clear.title'), description: t('composer.clear.description'), confirmLabel: t('composer.clear.confirm'), danger: true, fallbackMessage: t('composer.notice.clearFailed'), onConfirm: forceClearThread, onOpenChange: () => setConfirmClear(false) })] }));
|
|
866
938
|
}
|
|
@@ -19,11 +19,13 @@ interface ComposerInputProps {
|
|
|
19
19
|
tabbable: boolean;
|
|
20
20
|
className?: string;
|
|
21
21
|
extensions?: Extension;
|
|
22
|
+
aria?: Readonly<Record<string, string>>;
|
|
22
23
|
onChange(text: string, selection: InputSelection, state: EditorState): void;
|
|
23
24
|
onSelectionChange?(text: string, selection: InputSelection, state: EditorState): void;
|
|
24
25
|
onBlur?(): void;
|
|
26
|
+
onComposing?(composing: boolean): void;
|
|
25
27
|
onKeyDown?(event: KeyboardEvent, view: EditorView): boolean;
|
|
26
28
|
onPaste?(event: ClipboardEvent, view: EditorView): boolean;
|
|
27
29
|
}
|
|
28
|
-
export declare function ComposerInput({ ref, value, placeholder, disabled, tabbable, className, dictationToolbar, extensions, ...handlers }: ComposerInputProps): import("react").JSX.Element;
|
|
30
|
+
export declare function ComposerInput({ ref, value, placeholder, disabled, tabbable, className, dictationToolbar, extensions, aria, ...handlers }: ComposerInputProps): import("react").JSX.Element;
|
|
29
31
|
export {};
|
|
@@ -6,12 +6,13 @@ import { chatHost } from '../../host.js';
|
|
|
6
6
|
import { composerEditorExtensions, externalChange } from './composer/editor.js';
|
|
7
7
|
import { insertAtSelection } from './composer/insert.js';
|
|
8
8
|
const NO_EXTENSIONS = [];
|
|
9
|
+
const NO_ARIA = {};
|
|
9
10
|
/*
|
|
10
11
|
* A multi-line text field on CodeMirror, controlled by a plain string. The editor draws the text
|
|
11
12
|
* itself, so a decoration may change a font or add padding without walking the caret off its
|
|
12
13
|
* character, which a textarea with a painted layer behind it could not.
|
|
13
14
|
*/
|
|
14
|
-
export function ComposerInput({ ref, value, placeholder, disabled, tabbable, className, dictationToolbar, extensions = NO_EXTENSIONS, ...handlers }) {
|
|
15
|
+
export function ComposerInput({ ref, value, placeholder, disabled, tabbable, className, dictationToolbar, extensions = NO_EXTENSIONS, aria = NO_ARIA, ...handlers }) {
|
|
15
16
|
const hostRef = useRef(null);
|
|
16
17
|
const targetRef = useRef(null);
|
|
17
18
|
const viewRef = useRef(null);
|
|
@@ -47,7 +48,15 @@ export function ComposerInput({ ref, value, placeholder, disabled, tabbable, cla
|
|
|
47
48
|
compartments.extensions.of([]),
|
|
48
49
|
Prec.highest(EditorView.domEventHandlers({
|
|
49
50
|
keydown: (event, target) => latest.current.handlers.onKeyDown?.(event, target) ?? false,
|
|
50
|
-
paste: (event, target) => latest.current.handlers.onPaste?.(event, target) ?? false
|
|
51
|
+
paste: (event, target) => latest.current.handlers.onPaste?.(event, target) ?? false,
|
|
52
|
+
compositionstart: () => {
|
|
53
|
+
latest.current.handlers.onComposing?.(true);
|
|
54
|
+
return false;
|
|
55
|
+
},
|
|
56
|
+
compositionend: () => {
|
|
57
|
+
latest.current.handlers.onComposing?.(false);
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
51
60
|
})),
|
|
52
61
|
EditorView.updateListener.of((update) => {
|
|
53
62
|
const current = latest.current.handlers;
|
|
@@ -80,13 +89,13 @@ export function ComposerInput({ ref, value, placeholder, disabled, tabbable, cla
|
|
|
80
89
|
viewRef.current?.dispatch({
|
|
81
90
|
effects: [
|
|
82
91
|
// Spellcheck stays on, as it was on the textarea this editor replaced.
|
|
83
|
-
compartments.attributes.reconfigure(EditorView.contentAttributes.of({ spellcheck: 'true', tabindex: tabbable ? '0' : '-1' })),
|
|
92
|
+
compartments.attributes.reconfigure(EditorView.contentAttributes.of({ ...aria, spellcheck: 'true', tabindex: tabbable ? '0' : '-1' })),
|
|
84
93
|
compartments.editable.reconfigure([EditorView.editable.of(!disabled), EditorState.readOnly.of(disabled)]),
|
|
85
94
|
compartments.placeholder.reconfigure(placeholderText(placeholder)),
|
|
86
95
|
compartments.extensions.reconfigure(extensions)
|
|
87
96
|
]
|
|
88
97
|
});
|
|
89
|
-
}, [compartments, tabbable, disabled, placeholder, extensions]);
|
|
98
|
+
}, [compartments, tabbable, disabled, placeholder, extensions, aria]);
|
|
90
99
|
useLayoutEffect(() => {
|
|
91
100
|
const view = viewRef.current;
|
|
92
101
|
if (!view || value === reported.current) {
|