@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/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';
|
|
@@ -98,8 +100,7 @@ const PROMPT_HISTORY = 50;
|
|
|
98
100
|
// Commands the composer handles itself; the CLI's own ones are sent through as text.
|
|
99
101
|
const LOCAL_COMMANDS = ['model', 'compact', 'clear'];
|
|
100
102
|
|
|
101
|
-
/*
|
|
102
|
-
file's text, or the drag's own text, at the drop point on top of that. */
|
|
103
|
+
/* The card turns dropped files and paths into attachments and chips; CodeMirror would also paste their text at the drop point. */
|
|
103
104
|
const LEAVE_DROPS_TO_THE_CARD = EditorView.domEventHandlers({
|
|
104
105
|
drop: (event) => event.dataTransfer !== null && (event.dataTransfer.types.includes('Files') || carriesMentions(event.dataTransfer.types))
|
|
105
106
|
});
|
|
@@ -158,12 +159,79 @@ function hintedPlaceholder(lead: string, joiner: string, hints: ReadonlyArray<{
|
|
|
158
159
|
return root;
|
|
159
160
|
}
|
|
160
161
|
|
|
162
|
+
function sendLabelKey(busy: boolean, canSteer: boolean, delivery: 'queue' | 'steer'): string {
|
|
163
|
+
if (!busy) {
|
|
164
|
+
return 'composer.send';
|
|
165
|
+
}
|
|
166
|
+
if (!canSteer) {
|
|
167
|
+
return 'composer.queueButton';
|
|
168
|
+
}
|
|
169
|
+
return delivery === 'steer' ? 'composer.steerButton' : 'composer.queueWithSteerButton';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function sameQuery(left: MentionQuery | null, right: MentionQuery | null): boolean {
|
|
173
|
+
return left?.start === right?.start && left?.query === right?.query;
|
|
174
|
+
}
|
|
175
|
+
|
|
161
176
|
function splitPath(path: string): { name: string; dir: string } {
|
|
162
177
|
const slash = path.lastIndexOf('/');
|
|
163
178
|
return slash < 0 ? { name: path, dir: '' } : { name: path.slice(slash + 1), dir: path.slice(0, slash) };
|
|
164
179
|
}
|
|
165
180
|
|
|
166
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
|
+
|
|
167
235
|
export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabled, readOnly = false, providerFixed, onSend, onRetarget }: ComposerProps) {
|
|
168
236
|
const { t } = useTranslation('agent-chat');
|
|
169
237
|
const sendDelivery = chatHost().useSendDelivery();
|
|
@@ -187,6 +255,7 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
187
255
|
const { isApplePlatform, notify } = chatHost();
|
|
188
256
|
const registerQuoteTaker = useContext(QuoteTakerContext);
|
|
189
257
|
const inputRef = useRef<ComposerInputHandle>(null);
|
|
258
|
+
const hold = usePromptHold();
|
|
190
259
|
// Taking a queued message back waits on the machine, and what was typed meanwhile is what it merges with.
|
|
191
260
|
const draftRef = useRef(draft);
|
|
192
261
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
@@ -196,7 +265,6 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
196
265
|
const providers = useProviders((s) => s.providers);
|
|
197
266
|
const { approvals, questions } = usePendingRequests(chatId);
|
|
198
267
|
const order = useChatRow(chatId, (row) => row?.order);
|
|
199
|
-
// The structure, which a delta leaves alone. The prompts and the requests are items of their own.
|
|
200
268
|
const items = useChatRow(chatId, (row) => row?.structure);
|
|
201
269
|
const chats = chatHost().useReferableChats();
|
|
202
270
|
const placeholderLead = chatHost().useComposerPlaceholder(scope.id, chatId);
|
|
@@ -208,7 +276,7 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
208
276
|
const capabilities = provider?.capabilities;
|
|
209
277
|
const compaction = capabilities?.compaction;
|
|
210
278
|
const dismissedTurnId = useResumeCompactionDismissal(chatId);
|
|
211
|
-
|
|
279
|
+
// The cheap half of the offer, so a chat that can never make one keeps no timer.
|
|
212
280
|
const couldOfferCompaction = compaction !== undefined && compaction !== 'none' && info.usage.contextTokens >= RESUME_COMPACTION_TOKENS;
|
|
213
281
|
const now = useNow(60_000, couldOfferCompaction);
|
|
214
282
|
const compactionOffer = !couldOfferCompaction
|
|
@@ -226,15 +294,12 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
226
294
|
const queue = info.queue ?? [];
|
|
227
295
|
const text = draft.text;
|
|
228
296
|
const quote = draft.quote;
|
|
297
|
+
const quoteSource = draft.quoteSource;
|
|
229
298
|
// The CLI announces its session on the first message, so anything before that is still a blank chat.
|
|
230
299
|
const started = info.agentSessionId !== null || info.usage.turns > 0;
|
|
231
|
-
/*
|
|
232
|
-
* A chat that has started keeps its provider (the CLI holds the thread), and one opened for a
|
|
233
|
-
* named CLI was never going to change it. Either way the models stay switchable. The CLI takes
|
|
234
|
-
* the new one on the restart the next send does anyway. With one provider in the list the
|
|
235
|
-
* picker drops its group headers on its own, so it reads as that CLI's own catalog.
|
|
236
|
-
*/
|
|
237
300
|
const accountChoice = useAccountChoice(info.provider, info.account);
|
|
301
|
+
// A chat that has started keeps its provider (the CLI holds the thread), and so does one opened for a
|
|
302
|
+
// named CLI; its models stay switchable, since the CLI takes a new one on the restart the next send does.
|
|
238
303
|
const pickable =
|
|
239
304
|
started || providerFixed
|
|
240
305
|
? providers.filter((entry) => entry.kind === info.provider)
|
|
@@ -262,8 +327,8 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
262
327
|
|
|
263
328
|
useEffect(
|
|
264
329
|
() =>
|
|
265
|
-
registerQuoteTaker?.((selected) => {
|
|
266
|
-
setDraft((current) => ({ ...current, quote: selected }));
|
|
330
|
+
registerQuoteTaker?.((selected, itemId) => {
|
|
331
|
+
setDraft((current) => ({ ...current, quote: selected, quoteSource: itemId }));
|
|
267
332
|
inputRef.current?.focus();
|
|
268
333
|
}),
|
|
269
334
|
[registerQuoteTaker]
|
|
@@ -284,11 +349,11 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
284
349
|
}
|
|
285
350
|
let stale = false;
|
|
286
351
|
const search = (): void => {
|
|
287
|
-
const
|
|
288
|
-
if (
|
|
352
|
+
const searchFiles = chatHost().searchFiles;
|
|
353
|
+
if (searchFiles === null) {
|
|
289
354
|
return;
|
|
290
355
|
}
|
|
291
|
-
|
|
356
|
+
searchFiles(scope.id, info.cwd, mention.query, MENTION_RESULTS)
|
|
292
357
|
.then((files) => {
|
|
293
358
|
if (!stale) {
|
|
294
359
|
setSearched({ query: mention.query, files });
|
|
@@ -375,6 +440,26 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
375
440
|
[chatId, chats, draft.chats, mention]
|
|
376
441
|
);
|
|
377
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
|
+
);
|
|
378
463
|
const editorExtensions = useMemo(
|
|
379
464
|
() => [chipDecorations({ mentions: draft.mentions, skills: draft.skills }), LEAVE_DROPS_TO_THE_CARD],
|
|
380
465
|
[draft.mentions, draft.skills]
|
|
@@ -390,19 +475,32 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
390
475
|
};
|
|
391
476
|
|
|
392
477
|
const removeQuote = (): void => {
|
|
393
|
-
setDraft((current) => ({ ...current, quote: '' }));
|
|
478
|
+
setDraft((current) => ({ ...current, quote: '', quoteSource: null }));
|
|
394
479
|
inputRef.current?.focus();
|
|
395
480
|
};
|
|
396
481
|
|
|
397
|
-
const
|
|
482
|
+
const showError = (error: unknown): void => {
|
|
483
|
+
setNotice(error instanceof Error ? error.message : String(error));
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
/* One configuration at a time; a second pick while one is on its way is dropped. */
|
|
487
|
+
const runConfiguration = (work: () => Promise<void>): void => {
|
|
398
488
|
if (configuring.current) {
|
|
399
489
|
return;
|
|
400
490
|
}
|
|
401
491
|
configuring.current = true;
|
|
402
492
|
setConfigurationPending(true);
|
|
403
|
-
void
|
|
404
|
-
.
|
|
405
|
-
.
|
|
493
|
+
void work()
|
|
494
|
+
.catch(showError)
|
|
495
|
+
.finally(() => {
|
|
496
|
+
configuring.current = false;
|
|
497
|
+
setConfigurationPending(false);
|
|
498
|
+
});
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
const configure = (patch: { selection?: ModelSelection; runtimeMode?: RuntimeMode; account?: string }): void => {
|
|
502
|
+
runConfiguration(() =>
|
|
503
|
+
actions.configure(chatId, patch).then(() => {
|
|
406
504
|
if (patch.selection) {
|
|
407
505
|
rememberChatSelection(info.provider, patch.selection);
|
|
408
506
|
}
|
|
@@ -413,11 +511,7 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
413
511
|
rememberChatAccount(scope.id, info.provider, patch.account);
|
|
414
512
|
}
|
|
415
513
|
})
|
|
416
|
-
|
|
417
|
-
.finally(() => {
|
|
418
|
-
configuring.current = false;
|
|
419
|
-
setConfigurationPending(false);
|
|
420
|
-
});
|
|
514
|
+
);
|
|
421
515
|
};
|
|
422
516
|
|
|
423
517
|
const chooseAccount = (account: string): void => configure({ account });
|
|
@@ -426,18 +520,7 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
426
520
|
const chooseModel = (provider: AgentKind, slug: string): void => {
|
|
427
521
|
const selection: ModelSelection = { model: slug, options: {} };
|
|
428
522
|
if (provider !== info.provider) {
|
|
429
|
-
|
|
430
|
-
return;
|
|
431
|
-
}
|
|
432
|
-
configuring.current = true;
|
|
433
|
-
setConfigurationPending(true);
|
|
434
|
-
void onRetarget(provider, selection)
|
|
435
|
-
.then(() => rememberChatSelection(provider, selection))
|
|
436
|
-
.catch((error: unknown) => setNotice(error instanceof Error ? error.message : String(error)))
|
|
437
|
-
.finally(() => {
|
|
438
|
-
configuring.current = false;
|
|
439
|
-
setConfigurationPending(false);
|
|
440
|
-
});
|
|
523
|
+
runConfiguration(() => onRetarget(provider, selection).then(() => rememberChatSelection(provider, selection)));
|
|
441
524
|
return;
|
|
442
525
|
}
|
|
443
526
|
configure({ selection });
|
|
@@ -445,14 +528,10 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
445
528
|
|
|
446
529
|
const stop = (shiftKey: boolean): void => {
|
|
447
530
|
if (composerStopOf(shiftKey) === 'turn') {
|
|
448
|
-
void actions.stopTurn(chatId, false).catch(
|
|
531
|
+
void actions.stopTurn(chatId, false).catch(showError);
|
|
449
532
|
return;
|
|
450
533
|
}
|
|
451
|
-
chatHost().confirm.stopSubagents(
|
|
452
|
-
scope.id,
|
|
453
|
-
chatId,
|
|
454
|
-
() => void actions.stopTurn(chatId, true).catch((error: unknown) => setNotice(error instanceof Error ? error.message : String(error)))
|
|
455
|
-
);
|
|
534
|
+
chatHost().confirm.stopSubagents(scope.id, chatId, () => void actions.stopTurn(chatId, true).catch(showError));
|
|
456
535
|
};
|
|
457
536
|
|
|
458
537
|
/* The host decides whether a turn is in the way; only its refusal asks the person first. */
|
|
@@ -493,18 +572,16 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
493
572
|
}
|
|
494
573
|
};
|
|
495
574
|
|
|
496
|
-
const same = (a: MentionQuery | null, b: MentionQuery | null): boolean => a?.start === b?.start && a?.query === b?.query;
|
|
497
|
-
|
|
498
575
|
/* Which picker the caret opens: `@` for a file, `$` for a skill, or neither. In code a sigil is just text. */
|
|
499
576
|
const trackTriggers = (value: string, selection: TextRange, state: EditorState): void => {
|
|
500
577
|
const empty = selection.from === selection.to;
|
|
501
578
|
const tree = empty ? (ensureSyntaxTree(state, selection.from, 50) ?? syntaxTree(state)) : null;
|
|
502
579
|
const caret = tree !== null && !inCode(tree, value, selection.from) ? selection.from : null;
|
|
503
580
|
const skill = caret === null ? null : findSkillQuery(value, caret);
|
|
504
|
-
setSkillQuery((current) => (
|
|
581
|
+
setSkillQuery((current) => (sameQuery(current, skill) ? current : skill));
|
|
505
582
|
// A CLI that does not expand `@path` gets the text as it is, so the picker stays out of the way.
|
|
506
583
|
const next = caret === null || capabilities?.mentions === false ? null : findMentionQuery(value, caret);
|
|
507
|
-
setMention((current) => (
|
|
584
|
+
setMention((current) => (sameQuery(current, next) ? current : next));
|
|
508
585
|
};
|
|
509
586
|
|
|
510
587
|
// The inserted text reaches the editor with the render it causes, so the caret waits a frame for it.
|
|
@@ -558,8 +635,7 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
558
635
|
moveCaret(result.caret);
|
|
559
636
|
};
|
|
560
637
|
|
|
561
|
-
/* Paths dragged in from a list of files.
|
|
562
|
-
at the end of the prompt and join the draft's mentions, which is what `chat.send` carries. */
|
|
638
|
+
/* Paths dragged in from a list of files. With no mention query to insert into, they land at the end of the prompt. */
|
|
563
639
|
const addMentions = (paths: string[]): void => {
|
|
564
640
|
const fresh = paths.filter((path) => path !== '');
|
|
565
641
|
if (fresh.length === 0 || capabilities?.mentions === false) {
|
|
@@ -619,7 +695,8 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
619
695
|
setNotice(`${rejected.name}: ${rejected.reason}`);
|
|
620
696
|
return;
|
|
621
697
|
}
|
|
622
|
-
|
|
698
|
+
hold.release('send');
|
|
699
|
+
const chosen = commands[activeIndex];
|
|
623
700
|
if (commandQuery !== null && chosen?.local && runCommand(chosen.name)) {
|
|
624
701
|
clearDraft();
|
|
625
702
|
return;
|
|
@@ -640,6 +717,7 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
640
717
|
mentions: presentMentions(trimmed, draft.mentions),
|
|
641
718
|
skills: presentSkills(trimmed, draft.skills),
|
|
642
719
|
chats: draft.chats,
|
|
720
|
+
quoteSource: quote !== '' && quoteSource !== null ? { itemId: quoteSource } : undefined,
|
|
643
721
|
attachments: draft.attachments
|
|
644
722
|
});
|
|
645
723
|
clearDraft();
|
|
@@ -648,9 +726,20 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
648
726
|
setHistoryIndex(null);
|
|
649
727
|
};
|
|
650
728
|
|
|
651
|
-
/*
|
|
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
|
+
*/
|
|
652
733
|
const restoreStashed = (prompt: StashedPrompt): void => {
|
|
653
|
-
|
|
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
|
+
}));
|
|
654
743
|
setHistoryIndex(null);
|
|
655
744
|
inputRef.current?.focus();
|
|
656
745
|
};
|
|
@@ -683,7 +772,7 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
683
772
|
failed(e);
|
|
684
773
|
return;
|
|
685
774
|
}
|
|
686
|
-
const taken: Omit<ChatDraft, 'quote'> = {
|
|
775
|
+
const taken: Omit<ChatDraft, 'quote' | 'quoteSource'> = {
|
|
687
776
|
text: message.text,
|
|
688
777
|
mentions: message.mentions ?? [],
|
|
689
778
|
skills: message.skills ?? [],
|
|
@@ -727,7 +816,7 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
727
816
|
}
|
|
728
817
|
return;
|
|
729
818
|
}
|
|
730
|
-
if (stashDraft(draft)) {
|
|
819
|
+
if (stashDraft(draft, chatId)) {
|
|
731
820
|
clearDraft();
|
|
732
821
|
setMention(null);
|
|
733
822
|
setSkillQuery(null);
|
|
@@ -752,6 +841,19 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
752
841
|
return true;
|
|
753
842
|
};
|
|
754
843
|
|
|
844
|
+
/* The arrows walk an open picker round; true when the key was one of them. */
|
|
845
|
+
const moveInMenu = (key: string, count: number): boolean => {
|
|
846
|
+
if (key === 'ArrowDown') {
|
|
847
|
+
setMenuIndex((i) => (i + 1) % count);
|
|
848
|
+
return true;
|
|
849
|
+
}
|
|
850
|
+
if (key === 'ArrowUp') {
|
|
851
|
+
setMenuIndex((i) => (i - 1 + count) % count);
|
|
852
|
+
return true;
|
|
853
|
+
}
|
|
854
|
+
return false;
|
|
855
|
+
};
|
|
856
|
+
|
|
755
857
|
/* Runs before the editor's own keymap; true means the key is handled and its default prevented. */
|
|
756
858
|
const onKeyDown = (e: KeyboardEvent, view: EditorView): boolean => {
|
|
757
859
|
// Shift with the paste shortcut pastes a large text inline after all; any other key forgets it.
|
|
@@ -766,8 +868,7 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
766
868
|
setText('', [], []);
|
|
767
869
|
return true;
|
|
768
870
|
}
|
|
769
|
-
// The
|
|
770
|
-
// panels and the palette stay the app's, untouched: Mod+Shift+Enter would otherwise send as well.
|
|
871
|
+
// The app's own shortcuts stay the app's while you type: Mod+Shift+Enter would otherwise send as well.
|
|
771
872
|
if (chatHost().isAppShortcut(e)) {
|
|
772
873
|
return false;
|
|
773
874
|
}
|
|
@@ -785,16 +886,11 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
785
886
|
return true;
|
|
786
887
|
}
|
|
787
888
|
if (commandMenuOpen) {
|
|
788
|
-
if (e.key
|
|
789
|
-
setMenuIndex((i) => (i + 1) % commands.length);
|
|
790
|
-
return true;
|
|
791
|
-
}
|
|
792
|
-
if (e.key === 'ArrowUp') {
|
|
793
|
-
setMenuIndex((i) => (i - 1 + commands.length) % commands.length);
|
|
889
|
+
if (moveInMenu(e.key, commands.length)) {
|
|
794
890
|
return true;
|
|
795
891
|
}
|
|
796
892
|
if (e.key === 'Tab') {
|
|
797
|
-
const chosen = commands[
|
|
893
|
+
const chosen = commands[activeIndex]!;
|
|
798
894
|
if (chosen.skill) {
|
|
799
895
|
chooseSkill(chosen.name);
|
|
800
896
|
} else {
|
|
@@ -804,30 +900,20 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
804
900
|
}
|
|
805
901
|
}
|
|
806
902
|
if (skillMenuOpen) {
|
|
807
|
-
if (e.key
|
|
808
|
-
setMenuIndex((i) => (i + 1) % skillMatches.length);
|
|
809
|
-
return true;
|
|
810
|
-
}
|
|
811
|
-
if (e.key === 'ArrowUp') {
|
|
812
|
-
setMenuIndex((i) => (i - 1 + skillMatches.length) % skillMatches.length);
|
|
903
|
+
if (moveInMenu(e.key, skillMatches.length)) {
|
|
813
904
|
return true;
|
|
814
905
|
}
|
|
815
906
|
if (e.key === 'Tab' || e.key === 'Enter') {
|
|
816
|
-
chooseSkill((skillMatches[
|
|
907
|
+
chooseSkill((skillMatches[activeIndex] ?? skillMatches[0]!).name);
|
|
817
908
|
return true;
|
|
818
909
|
}
|
|
819
910
|
}
|
|
820
911
|
if (mentionMenuOpen && mentionCount > 0) {
|
|
821
|
-
if (e.key
|
|
822
|
-
setMenuIndex((i) => (i + 1) % mentionCount);
|
|
823
|
-
return true;
|
|
824
|
-
}
|
|
825
|
-
if (e.key === 'ArrowUp') {
|
|
826
|
-
setMenuIndex((i) => (i - 1 + mentionCount) % mentionCount);
|
|
912
|
+
if (moveInMenu(e.key, mentionCount)) {
|
|
827
913
|
return true;
|
|
828
914
|
}
|
|
829
915
|
if (e.key === 'Tab' || e.key === 'Enter') {
|
|
830
|
-
chooseMentionAt(
|
|
916
|
+
chooseMentionAt(activeIndex);
|
|
831
917
|
return true;
|
|
832
918
|
}
|
|
833
919
|
}
|
|
@@ -946,9 +1032,8 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
946
1032
|
|
|
947
1033
|
return (
|
|
948
1034
|
<div className="chat-composer-content pointer-events-none relative z-10 w-full">
|
|
949
|
-
{/*
|
|
950
|
-
|
|
951
|
-
{/* A read-only composer sits under something that stands in the thread's place, whose end is not this one. */}
|
|
1035
|
+
{/* Over the composer rather than in the thread, since the composer's height is the one it always clears.
|
|
1036
|
+
A read-only composer sits under something that stands in the thread's place, whose end is not this one. */}
|
|
952
1037
|
{!atEnd && !readOnly && (
|
|
953
1038
|
<div className="absolute inset-x-0 bottom-full mb-2 flex justify-center">
|
|
954
1039
|
<Tooltip label={t('composer.jumpToEnd')} name>
|
|
@@ -969,8 +1054,7 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
969
1054
|
? 'border-accent bg-[color-mix(in_srgb,var(--accent-soft)_60%,var(--surface-raised))]'
|
|
970
1055
|
: 'border-border bg-[color-mix(in_srgb,var(--surface-raised)_92%,transparent)]'
|
|
971
1056
|
)}
|
|
972
|
-
/* The whole card takes a dropped file
|
|
973
|
-
of its own leaves it alone. */
|
|
1057
|
+
/* The whole card takes a dropped file, so a layout around it that takes drops of its own leaves it alone. */
|
|
974
1058
|
data-takes-drop="all"
|
|
975
1059
|
onDragOver={(e) => {
|
|
976
1060
|
if (writable && (e.dataTransfer.types.includes('Files') || carriesMentions(e.dataTransfer.types))) {
|
|
@@ -1002,6 +1086,7 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
1002
1086
|
hasDraft={!isEmptyDraft(draft)}
|
|
1003
1087
|
denyReason={capabilities?.denyReason === true}
|
|
1004
1088
|
onAllAnswered={() => inputRef.current?.focus()}
|
|
1089
|
+
hold={hold}
|
|
1005
1090
|
>
|
|
1006
1091
|
<LimitDock chatId={chatId} info={info} />
|
|
1007
1092
|
{compactionOffer && (
|
|
@@ -1012,11 +1097,15 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
1012
1097
|
/>
|
|
1013
1098
|
)}
|
|
1014
1099
|
{commandMenuOpen && (
|
|
1015
|
-
<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">
|
|
1016
1101
|
{commands.map((command, index) => (
|
|
1017
1102
|
<button
|
|
1018
1103
|
key={command.name}
|
|
1019
|
-
|
|
1104
|
+
id={optionIdOf(listId, index)}
|
|
1105
|
+
role="option"
|
|
1106
|
+
aria-selected={index === activeIndex}
|
|
1107
|
+
tabIndex={-1}
|
|
1108
|
+
data-active={index === activeIndex}
|
|
1020
1109
|
className="cursor-row flex w-full items-center gap-2 rounded-md px-2 py-1 text-left text-xs text-text-muted"
|
|
1021
1110
|
onMouseEnter={() => setMenuIndex(index)}
|
|
1022
1111
|
onMouseDown={(e) => e.preventDefault()}
|
|
@@ -1042,11 +1131,15 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
1042
1131
|
</div>
|
|
1043
1132
|
)}
|
|
1044
1133
|
{skillMenuOpen && (
|
|
1045
|
-
<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">
|
|
1046
1135
|
{skillMatches.map((skill, index) => (
|
|
1047
1136
|
<button
|
|
1048
1137
|
key={skill.name}
|
|
1049
|
-
|
|
1138
|
+
id={optionIdOf(listId, index)}
|
|
1139
|
+
role="option"
|
|
1140
|
+
aria-selected={index === activeIndex}
|
|
1141
|
+
tabIndex={-1}
|
|
1142
|
+
data-active={index === activeIndex}
|
|
1050
1143
|
className="cursor-row flex w-full items-start gap-2 rounded-md px-2 py-1 text-left text-xs text-text-muted"
|
|
1051
1144
|
onMouseEnter={() => setMenuIndex(index)}
|
|
1052
1145
|
onMouseDown={(e) => e.preventDefault()}
|
|
@@ -1063,45 +1156,70 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
1063
1156
|
)}
|
|
1064
1157
|
{mentionMenuOpen && (
|
|
1065
1158
|
<div className="border-b border-border px-1.5 py-1.5">
|
|
1066
|
-
{
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
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>
|
|
1082
1217
|
)}
|
|
1083
1218
|
{mentionCount === 0 && (
|
|
1084
1219
|
<div className="px-2 py-1 text-xs text-text-faint">
|
|
1085
1220
|
{mention.query ? t('composer.mentions.noMatch') : t('composer.mentions.empty')}
|
|
1086
1221
|
</div>
|
|
1087
1222
|
)}
|
|
1088
|
-
{files.map((path, index) => {
|
|
1089
|
-
const { name, dir } = splitPath(path);
|
|
1090
|
-
return (
|
|
1091
|
-
<button
|
|
1092
|
-
key={path}
|
|
1093
|
-
data-active={chatMatches.length + index === menuIndex}
|
|
1094
|
-
className="cursor-row flex w-full items-center gap-2 rounded-md px-2 py-1 text-left text-xs text-text-muted"
|
|
1095
|
-
onMouseEnter={() => setMenuIndex(chatMatches.length + index)}
|
|
1096
|
-
onMouseDown={(e) => e.preventDefault()}
|
|
1097
|
-
onClick={() => chooseMention(path)}
|
|
1098
|
-
>
|
|
1099
|
-
<FileIcon path={path} size={14} />
|
|
1100
|
-
<span className="truncate font-mono text-text">{name}</span>
|
|
1101
|
-
{dir && <span className="min-w-0 truncate text-text-faint">{dir}</span>}
|
|
1102
|
-
</button>
|
|
1103
|
-
);
|
|
1104
|
-
})}
|
|
1105
1223
|
</div>
|
|
1106
1224
|
)}
|
|
1107
1225
|
{queue.length > 0 && (
|
|
@@ -1158,7 +1276,24 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
1158
1276
|
)}
|
|
1159
1277
|
{quote !== '' && (
|
|
1160
1278
|
<div className="flex items-center gap-2 px-5 pt-4 @max-md/composer:px-3.5 @max-md/composer:pt-3">
|
|
1161
|
-
|
|
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
|
+
)}
|
|
1162
1297
|
<IconButton icon={X} size="2xs" label={t('composer.quote.remove')} onClick={removeQuote} />
|
|
1163
1298
|
</div>
|
|
1164
1299
|
)}
|
|
@@ -1204,7 +1339,9 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
1204
1339
|
disabled={!writable}
|
|
1205
1340
|
tabbable={focused}
|
|
1206
1341
|
extensions={editorExtensions}
|
|
1342
|
+
aria={inputAria}
|
|
1207
1343
|
onChange={(value, selection, state) => {
|
|
1344
|
+
hold.noteInput();
|
|
1208
1345
|
setText(value);
|
|
1209
1346
|
setMenuIndex(0);
|
|
1210
1347
|
trackTriggers(value, selection, state);
|
|
@@ -1216,7 +1353,9 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
1216
1353
|
onBlur={() => {
|
|
1217
1354
|
setMention(null);
|
|
1218
1355
|
setSkillQuery(null);
|
|
1356
|
+
hold.release('blur');
|
|
1219
1357
|
}}
|
|
1358
|
+
onComposing={hold.setComposing}
|
|
1220
1359
|
onKeyDown={onKeyDown}
|
|
1221
1360
|
onPaste={onPaste}
|
|
1222
1361
|
/>
|
|
@@ -1300,21 +1439,7 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
1300
1439
|
</Tooltip>
|
|
1301
1440
|
)}
|
|
1302
1441
|
{(!busy || !isEmptyDraft(draft)) && (
|
|
1303
|
-
<Tooltip
|
|
1304
|
-
label={
|
|
1305
|
-
busy
|
|
1306
|
-
? t(
|
|
1307
|
-
canSteer
|
|
1308
|
-
? sendDelivery === 'steer'
|
|
1309
|
-
? 'composer.steerButton'
|
|
1310
|
-
: 'composer.queueWithSteerButton'
|
|
1311
|
-
: 'composer.queueButton'
|
|
1312
|
-
)
|
|
1313
|
-
: t('composer.send')
|
|
1314
|
-
}
|
|
1315
|
-
kbd={KEY_SHORTCUTS.modEnter}
|
|
1316
|
-
name
|
|
1317
|
-
>
|
|
1442
|
+
<Tooltip label={t(sendLabelKey(busy, canSteer, sendDelivery))} kbd={KEY_SHORTCUTS.modEnter} name>
|
|
1318
1443
|
<button
|
|
1319
1444
|
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"
|
|
1320
1445
|
disabled={isEmptyDraft(draft) || !writable || guard.tooLong}
|