@adecore/agents-react 0.21.2 → 0.22.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/model-requirement.d.ts +6 -0
- package/dist/agents/model-requirement.js +26 -0
- package/dist/chat/chat-client.d.ts +2 -1
- package/dist/chat/chat-client.js +2 -0
- package/dist/chat/drafts.d.ts +2 -1
- package/dist/chat/drafts.js +15 -5
- package/dist/chat/logic/picker-entries.d.ts +10 -0
- package/dist/chat/logic/picker-entries.js +19 -0
- package/dist/chat/logic/timeline.d.ts +5 -0
- package/dist/chat/logic/timeline.js +10 -1
- package/dist/chat/prompt-hold.d.ts +8 -0
- package/dist/chat/prompt-hold.js +16 -0
- package/dist/chat/quote.d.ts +4 -0
- package/dist/chat/quote.js +19 -0
- package/dist/chat/stash.d.ts +8 -2
- package/dist/chat/stash.js +21 -5
- package/dist/chat/subagent-list.d.ts +7 -1
- package/dist/chat/subagent-list.js +13 -1
- package/dist/chat/timeline-scroll.d.ts +6 -2
- package/dist/chat/timeline-scroll.js +12 -7
- package/dist/chat/ui/ChatActivity.js +22 -8
- package/dist/chat/ui/Composer.js +103 -19
- package/dist/chat/ui/ComposerInput.d.ts +3 -1
- package/dist/chat/ui/ComposerInput.js +13 -4
- package/dist/chat/ui/Pickers.js +26 -18
- package/dist/chat/ui/PromptComposer.d.ts +8 -1
- package/dist/chat/ui/PromptComposer.js +18 -6
- package/dist/chat/ui/QuoteButton.js +2 -2
- package/dist/chat/ui/SubagentControls.d.ts +4 -2
- package/dist/chat/ui/SubagentControls.js +12 -8
- package/dist/chat/ui/SubagentInfo.d.ts +1 -1
- package/dist/chat/ui/SubagentInfo.js +9 -15
- package/dist/chat/ui/SubagentStopButton.js +2 -1
- package/dist/chat/ui/Timeline.js +24 -3
- package/dist/chat/ui/composer/menu-aria.d.ts +10 -0
- package/dist/chat/ui/composer/menu-aria.js +18 -0
- package/dist/chat/ui/intelligent-ui/UiBlockFrame.js +3 -3
- package/dist/chat/ui/intelligent-ui/UiFallbackPart.js +1 -1
- package/dist/chat/ui/intelligent-ui/chart-data.d.ts +22 -2
- package/dist/chat/ui/intelligent-ui/chart-data.js +83 -19
- package/dist/chat/ui/intelligent-ui/renderers/chart.d.ts +1 -1
- package/dist/chat/ui/intelligent-ui/renderers/chart.js +99 -35
- package/dist/chat/ui/intelligent-ui/renderers/choices.d.ts +1 -1
- package/dist/chat/ui/intelligent-ui/renderers/choices.js +8 -2
- package/dist/chat/ui/intelligent-ui/renderers/content.js +5 -3
- package/dist/chat/ui/intelligent-ui/renderers/inputs.js +9 -4
- package/dist/chat/ui/intelligent-ui/renderers/links.js +1 -1
- package/dist/chat/ui/intelligent-ui/renderers/text.js +5 -2
- package/dist/chat/ui/intelligent-ui/table-data.js +4 -4
- package/dist/chat/ui/intelligent-ui/use-truncated.d.ts +1 -0
- package/dist/chat/ui/intelligent-ui/use-truncated.js +28 -0
- package/dist/chat/ui/quote-selection.d.ts +5 -2
- package/dist/chat/ui/quote-selection.js +3 -2
- package/dist/chat/ui/rows/MessageRows.js +24 -5
- package/dist/chat/ui/rows/Rows.js +1 -1
- package/dist/chat/ui/rows/WorkRows.d.ts +3 -1
- package/dist/chat/ui/rows/WorkRows.js +4 -4
- package/dist/host.d.ts +1 -0
- package/dist/host.js +1 -0
- package/dist/locales/en/agent-chat.json +39 -10
- package/dist/locales/nl/agent-chat.json +39 -10
- package/dist/state/chats.d.ts +2 -1
- package/dist/state/chats.js +19 -1
- package/package.json +29 -4
- package/src/agents/model-requirement.ts +36 -0
- package/src/chat/chat-client.ts +5 -0
- package/src/chat/drafts.ts +27 -7
- package/src/chat/logic/picker-entries.ts +25 -0
- package/src/chat/logic/timeline.ts +17 -2
- package/src/chat/prompt-hold.ts +27 -0
- package/src/chat/quote.ts +20 -0
- package/src/chat/stash.ts +31 -5
- package/src/chat/subagent-list.ts +19 -5
- package/src/chat/timeline-scroll.ts +24 -10
- package/src/chat/ui/ChatActivity.tsx +35 -9
- package/src/chat/ui/Composer.tsx +197 -52
- package/src/chat/ui/ComposerInput.tsx +18 -3
- package/src/chat/ui/Pickers.tsx +51 -30
- package/src/chat/ui/PromptComposer.tsx +37 -8
- package/src/chat/ui/QuoteButton.tsx +3 -2
- package/src/chat/ui/SubagentControls.tsx +22 -19
- package/src/chat/ui/SubagentInfo.tsx +24 -42
- package/src/chat/ui/SubagentStopButton.tsx +2 -1
- package/src/chat/ui/Timeline.tsx +26 -4
- package/src/chat/ui/composer/menu-aria.ts +29 -0
- package/src/chat/ui/intelligent-ui/UiBlockFrame.tsx +5 -5
- package/src/chat/ui/intelligent-ui/UiFallbackPart.tsx +6 -1
- package/src/chat/ui/intelligent-ui/chart-data.ts +106 -20
- package/src/chat/ui/intelligent-ui/renderers/chart.tsx +237 -72
- package/src/chat/ui/intelligent-ui/renderers/choices.tsx +9 -4
- package/src/chat/ui/intelligent-ui/renderers/content.tsx +31 -15
- package/src/chat/ui/intelligent-ui/renderers/inputs.tsx +16 -5
- package/src/chat/ui/intelligent-ui/renderers/links.tsx +3 -3
- package/src/chat/ui/intelligent-ui/renderers/text.tsx +18 -8
- package/src/chat/ui/intelligent-ui/table-data.ts +4 -4
- package/src/chat/ui/intelligent-ui/use-truncated.ts +33 -0
- package/src/chat/ui/quote-selection.ts +6 -4
- package/src/chat/ui/rows/MessageRows.tsx +44 -5
- package/src/chat/ui/rows/Rows.tsx +3 -1
- package/src/chat/ui/rows/WorkRows.tsx +15 -1
- package/src/host.ts +3 -0
- package/src/locales/en/agent-chat.json +39 -10
- package/src/locales/nl/agent-chat.json +39 -10
- package/src/state/chats.ts +31 -2
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';
|
|
@@ -104,6 +106,52 @@ function splitPath(path) {
|
|
|
104
106
|
return slash < 0 ? { name: path, dir: '' } : { name: path.slice(slash + 1), dir: path.slice(0, slash) };
|
|
105
107
|
}
|
|
106
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
|
+
}
|
|
107
155
|
export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabled, readOnly = false, providerFixed, onSend, onRetarget }) {
|
|
108
156
|
const { t } = useTranslation('agent-chat');
|
|
109
157
|
const sendDelivery = chatHost().useSendDelivery();
|
|
@@ -127,6 +175,7 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
127
175
|
const { isApplePlatform, notify } = chatHost();
|
|
128
176
|
const registerQuoteTaker = useContext(QuoteTakerContext);
|
|
129
177
|
const inputRef = useRef(null);
|
|
178
|
+
const hold = usePromptHold();
|
|
130
179
|
// Taking a queued message back waits on the machine, and what was typed meanwhile is what it merges with.
|
|
131
180
|
const draftRef = useRef(draft);
|
|
132
181
|
const fileInputRef = useRef(null);
|
|
@@ -164,6 +213,7 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
164
213
|
const queue = info.queue ?? [];
|
|
165
214
|
const text = draft.text;
|
|
166
215
|
const quote = draft.quote;
|
|
216
|
+
const quoteSource = draft.quoteSource;
|
|
167
217
|
// The CLI announces its session on the first message, so anything before that is still a blank chat.
|
|
168
218
|
const started = info.agentSessionId !== null || info.usage.turns > 0;
|
|
169
219
|
const accountChoice = useAccountChoice(info.provider, info.account);
|
|
@@ -185,8 +235,8 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
185
235
|
setDraft((current) => ({ ...current, text: joinDraftText(current.text, offered) }));
|
|
186
236
|
inputRef.current?.focus();
|
|
187
237
|
}), [chatId]);
|
|
188
|
-
useEffect(() => registerQuoteTaker?.((selected) => {
|
|
189
|
-
setDraft((current) => ({ ...current, quote: selected }));
|
|
238
|
+
useEffect(() => registerQuoteTaker?.((selected, itemId) => {
|
|
239
|
+
setDraft((current) => ({ ...current, quote: selected, quoteSource: itemId }));
|
|
190
240
|
inputRef.current?.focus();
|
|
191
241
|
}), [registerQuoteTaker]);
|
|
192
242
|
useEffect(() => {
|
|
@@ -286,6 +336,22 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
286
336
|
const files = mention === null ? [] : searched.files;
|
|
287
337
|
const chatMatches = useMemo(() => (mention === null ? [] : chatSuggestions(chats, mention.query, [chatId, ...draft.chats], CHAT_RESULTS)), [chatId, chats, draft.chats, mention]);
|
|
288
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]);
|
|
289
355
|
const editorExtensions = useMemo(() => [chipDecorations({ mentions: draft.mentions, skills: draft.skills }), LEAVE_DROPS_TO_THE_CARD], [draft.mentions, draft.skills]);
|
|
290
356
|
const guard = promptGuard(withQuote(quote, text));
|
|
291
357
|
const setText = (next, mentions = draft.mentions, chosen = draft.skills) => {
|
|
@@ -295,7 +361,7 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
295
361
|
setDraft(EMPTY_DRAFT);
|
|
296
362
|
};
|
|
297
363
|
const removeQuote = () => {
|
|
298
|
-
setDraft((current) => ({ ...current, quote: '' }));
|
|
364
|
+
setDraft((current) => ({ ...current, quote: '', quoteSource: null }));
|
|
299
365
|
inputRef.current?.focus();
|
|
300
366
|
};
|
|
301
367
|
const showError = (error) => {
|
|
@@ -486,7 +552,8 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
486
552
|
setNotice(`${rejected.name}: ${rejected.reason}`);
|
|
487
553
|
return;
|
|
488
554
|
}
|
|
489
|
-
|
|
555
|
+
hold.release('send');
|
|
556
|
+
const chosen = commands[activeIndex];
|
|
490
557
|
if (commandQuery !== null && chosen?.local && runCommand(chosen.name)) {
|
|
491
558
|
clearDraft();
|
|
492
559
|
return;
|
|
@@ -507,6 +574,7 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
507
574
|
mentions: presentMentions(trimmed, draft.mentions),
|
|
508
575
|
skills: presentSkills(trimmed, draft.skills),
|
|
509
576
|
chats: draft.chats,
|
|
577
|
+
quoteSource: quote !== '' && quoteSource !== null ? { itemId: quoteSource } : undefined,
|
|
510
578
|
attachments: draft.attachments
|
|
511
579
|
});
|
|
512
580
|
clearDraft();
|
|
@@ -514,9 +582,20 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
514
582
|
setSkillQuery(null);
|
|
515
583
|
setHistoryIndex(null);
|
|
516
584
|
};
|
|
517
|
-
/*
|
|
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
|
+
*/
|
|
518
589
|
const restoreStashed = (prompt) => {
|
|
519
|
-
|
|
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
|
+
}));
|
|
520
599
|
setHistoryIndex(null);
|
|
521
600
|
inputRef.current?.focus();
|
|
522
601
|
};
|
|
@@ -595,7 +674,7 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
595
674
|
}
|
|
596
675
|
return;
|
|
597
676
|
}
|
|
598
|
-
if (stashDraft(draft)) {
|
|
677
|
+
if (stashDraft(draft, chatId)) {
|
|
599
678
|
clearDraft();
|
|
600
679
|
setMention(null);
|
|
601
680
|
setSkillQuery(null);
|
|
@@ -666,7 +745,7 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
666
745
|
return true;
|
|
667
746
|
}
|
|
668
747
|
if (e.key === 'Tab') {
|
|
669
|
-
const chosen = commands[
|
|
748
|
+
const chosen = commands[activeIndex];
|
|
670
749
|
if (chosen.skill) {
|
|
671
750
|
chooseSkill(chosen.name);
|
|
672
751
|
}
|
|
@@ -681,7 +760,7 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
681
760
|
return true;
|
|
682
761
|
}
|
|
683
762
|
if (e.key === 'Tab' || e.key === 'Enter') {
|
|
684
|
-
chooseSkill((skillMatches[
|
|
763
|
+
chooseSkill((skillMatches[activeIndex] ?? skillMatches[0]).name);
|
|
685
764
|
return true;
|
|
686
765
|
}
|
|
687
766
|
}
|
|
@@ -690,7 +769,7 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
690
769
|
return true;
|
|
691
770
|
}
|
|
692
771
|
if (e.key === 'Tab' || e.key === 'Enter') {
|
|
693
|
-
chooseMentionAt(
|
|
772
|
+
chooseMentionAt(activeIndex);
|
|
694
773
|
return true;
|
|
695
774
|
}
|
|
696
775
|
}
|
|
@@ -817,7 +896,7 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
817
896
|
addMentions(droppedMentions(e.dataTransfer));
|
|
818
897
|
addFiles(filesOf(e.dataTransfer));
|
|
819
898
|
inputRef.current?.focus();
|
|
820
|
-
}, 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: () => {
|
|
821
900
|
setMenuIndex(index);
|
|
822
901
|
if (command.skill) {
|
|
823
902
|
chooseSkill(command.name);
|
|
@@ -825,10 +904,10 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
825
904
|
}
|
|
826
905
|
setText(`/${command.name}`);
|
|
827
906
|
inputRef.current?.focus();
|
|
828
|
-
}, 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 ===
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
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) => {
|
|
832
911
|
const sendNow = () => {
|
|
833
912
|
scrollTimelineToEnd(chatId, 'instant');
|
|
834
913
|
void actions.sendNow(chatId, message.id).catch(() => undefined);
|
|
@@ -836,7 +915,11 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
836
915
|
const unqueue = () => void actions.unqueue(chatId, message.id).catch(() => undefined);
|
|
837
916
|
const edit = () => void takeBack(message);
|
|
838
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));
|
|
839
|
-
})] })), 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();
|
|
840
923
|
setText(value);
|
|
841
924
|
setMenuIndex(0);
|
|
842
925
|
trackTriggers(value, selection, state);
|
|
@@ -846,7 +929,8 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
846
929
|
}, onSelectionChange: trackTriggers, onBlur: () => {
|
|
847
930
|
setMention(null);
|
|
848
931
|
setSkillQuery(null);
|
|
849
|
-
|
|
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) => {
|
|
850
934
|
addFiles(Array.from(e.target.files ?? []));
|
|
851
935
|
e.target.value = '';
|
|
852
936
|
inputRef.current?.focus();
|
|
@@ -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) {
|
package/dist/chat/ui/Pickers.js
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { useContext, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
4
|
import { Bookmark, ChevronDown, ChevronRight, Search, Trash2 } from 'lucide-react';
|
|
5
5
|
import { AgentIcon } from '../../agents/AgentIcon.js';
|
|
6
6
|
import { ProviderLogo } from '../../agents/ProviderLogo.js';
|
|
7
7
|
import { agentChipOf, modelName } from '../../agents/model-name.js';
|
|
8
|
+
import { modelRequirementOf } from '../../agents/model-requirement.js';
|
|
9
|
+
import { chatHost } from '../../host.js';
|
|
10
|
+
import { ChatScopeContext } from '../../scope.js';
|
|
11
|
+
import { useReportedModels } from '../../state/chats.js';
|
|
12
|
+
import { pickerEntriesOf } from '../logic/picker-entries.js';
|
|
8
13
|
import { forgetStashed, STASH_SHORTCUT, useStash } from '../stash.js';
|
|
9
14
|
import { Icon, IconButton, Menu, Popover, Tooltip } from '@adecore/ui';
|
|
10
15
|
const triggerClass = 'flex h-7 shrink-0 items-center gap-1 whitespace-nowrap rounded-md px-2 text-xs text-text-muted hover:bg-surface-hover hover:text-text data-[popup-open]:bg-surface-active data-[popup-open]:text-text';
|
|
11
16
|
const chipClass = 'flex h-6 min-w-0 shrink items-center gap-1.5 whitespace-nowrap rounded-md bg-surface-hover px-2 text-xs text-text hover:bg-surface-active data-[popup-open]:bg-surface-active';
|
|
12
|
-
function matches(model, query) {
|
|
13
|
-
return model.name.toLowerCase().includes(query) || model.slug.toLowerCase().includes(query);
|
|
14
|
-
}
|
|
15
17
|
/*
|
|
16
18
|
* Which model answers: a search field over one group per provider, with the legacy models behind an
|
|
17
19
|
* expander. Before the first message the list carries every installed CLI, so picking another
|
|
@@ -29,18 +31,13 @@ export function ModelPicker({ providers, provider, selection, open, onOpenChange
|
|
|
29
31
|
const chip = agentChipOf(providers, provider, selection.model);
|
|
30
32
|
const grouped = providers.length > 1;
|
|
31
33
|
const trimmed = query.trim().toLowerCase();
|
|
32
|
-
const entries = useMemo(() =>
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (legacy.length === 0) {
|
|
40
|
-
return live;
|
|
41
|
-
}
|
|
42
|
-
return legacyOpen ? [...live, { kind: 'legacy', count: legacy.length }, ...legacy] : [...live, { kind: 'legacy', count: legacy.length }];
|
|
43
|
-
}, [providers, trimmed, legacyOpen]);
|
|
34
|
+
const entries = useMemo(() => pickerEntriesOf(providers, trimmed, legacyOpen), [providers, trimmed, legacyOpen]);
|
|
35
|
+
const scope = useContext(ChatScopeContext);
|
|
36
|
+
const machine = chatHost().useMachineLabel(scope?.id ?? '');
|
|
37
|
+
const reported = useReportedModels(provider);
|
|
38
|
+
const requirementOf = (entry) => modelRequirementOf(entry.model, entry.provider, reported);
|
|
39
|
+
// A CLI that is too old blocks all its models; one way to its settings under the group says it once.
|
|
40
|
+
const blockedKinds = new Set(entries.flatMap((entry) => (entry.kind === 'model' && requirementOf(entry) !== null ? [entry.provider.kind] : [])));
|
|
44
41
|
/* Closing forgets the search, so the next open (a click or `/model`) starts on the whole list. */
|
|
45
42
|
const setOpen = (next) => {
|
|
46
43
|
if (!next) {
|
|
@@ -63,6 +60,9 @@ export function ModelPicker({ providers, provider, selection, open, onOpenChange
|
|
|
63
60
|
inputRef.current?.focus();
|
|
64
61
|
return;
|
|
65
62
|
}
|
|
63
|
+
if (requirementOf(entry) !== null) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
66
|
onChange(entry.provider.kind, entry.model.slug);
|
|
67
67
|
setOpen(false);
|
|
68
68
|
};
|
|
@@ -89,8 +89,16 @@ export function ModelPicker({ providers, provider, selection, open, onOpenChange
|
|
|
89
89
|
}
|
|
90
90
|
const previous = entries[i - 1];
|
|
91
91
|
const first = grouped && (previous === undefined || previous.kind !== 'model' || previous.provider.kind !== entry.provider.kind);
|
|
92
|
+
const requirement = requirementOf(entry);
|
|
93
|
+
const next = entries[i + 1];
|
|
94
|
+
const lastOfGroup = next === undefined || next.kind !== 'model' || next.provider.kind !== entry.provider.kind;
|
|
92
95
|
const chosen = entry.provider.kind === provider && entry.model.slug === selection.model;
|
|
93
|
-
return (_jsxs("div", { children: [first && (_jsxs(Menu.Label, { className: "flex items-center gap-1.5", children: [_jsx(AgentIcon, { kind: entry.provider.kind, size: 12 }), entry.provider.name] })), _jsxs("button", { role: "option", "aria-selected": chosen, "data-active": i === index, className:
|
|
96
|
+
return (_jsxs("div", { children: [first && (_jsxs(Menu.Label, { className: "flex items-center gap-1.5", children: [_jsx(AgentIcon, { kind: entry.provider.kind, size: 12 }), entry.provider.name] })), _jsxs("button", { role: "option", "aria-selected": chosen, "aria-disabled": requirement !== null || undefined, "data-active": i === index, className: `cursor-row flex w-full flex-col items-stretch gap-0.5 rounded-md px-2.5 py-1.5 text-left text-sm ${requirement === null ? 'text-text-muted' : 'text-text-faint'}`, onMouseEnter: () => setIndex(i), onClick: () => choose(entry), children: [_jsxs("span", { className: "flex w-full items-center gap-2", children: [_jsx(Menu.Check, { kind: "radio", checked: chosen }), _jsx("span", { className: "min-w-0 truncate", children: entry.model.name }), entry.model.badge && (_jsx("span", { className: "rounded bg-accent-soft px-1 text-[11px] leading-5 font-medium uppercase text-accent", children: entry.model.badge })), _jsx("span", { className: "grow" }), entry.model.legacy && _jsx("span", { className: "text-xs text-text-faint", children: t('pickers.model.legacy') })] }), requirement !== null && (_jsx("span", { className: "pl-6 text-xs text-text-faint", children: machine === null
|
|
97
|
+
? t('pickers.model.requiresHere', { cli: requirement.cli, version: requirement.minVersion })
|
|
98
|
+
: t('pickers.model.requires', { cli: requirement.cli, version: requirement.minVersion, machine }) }))] }), lastOfGroup && blockedKinds.has(entry.provider.kind) && (_jsx("button", { type: "button", className: "cursor-row flex w-full items-center rounded-md px-2.5 py-1.5 text-left text-xs text-text-muted", onClick: () => {
|
|
99
|
+
setOpen(false);
|
|
100
|
+
chatHost().openSettings('providers');
|
|
101
|
+
}, children: t('pickers.model.providerSettings') }))] }, `${entry.provider.kind}/${entry.model.slug}`));
|
|
94
102
|
})] }), footer !== undefined && _jsx("div", { className: "border-t border-border-soft px-3 py-2 text-xs text-text-faint", children: footer })] })] }));
|
|
95
103
|
}
|
|
96
104
|
/*
|
|
@@ -103,5 +111,5 @@ export function StashPicker({ onRestore }) {
|
|
|
103
111
|
if (prompts.length === 0) {
|
|
104
112
|
return null;
|
|
105
113
|
}
|
|
106
|
-
return (_jsxs(Popover.Root, { children: [_jsx(Tooltip, { label: t('pickers.stash.title'), kbd: STASH_SHORTCUT, children: _jsxs(Popover.Trigger, { className: triggerClass, children: [_jsx(Icon, { icon: Bookmark, size: 12 }), _jsx("span", { className: "tabular-nums", children: prompts.length })] }) }), _jsx(Popover.Popup, { variant: "picker", side: "top", className: "w-80", children: _jsx("div", { className: "max-h-72 overflow-auto p-1", children: prompts.map((prompt) => (_jsxs("div", { className: "group/stash flex items-start gap-1", children: [_jsxs(Popover.Close, { className: "flex min-w-0 grow flex-col gap-0.5 rounded-md px-2 py-1.5 text-left text-xs text-text-muted hover:bg-surface-hover hover:text-text", onClick: () => onRestore(prompt), children: [_jsx("span", { className: "line-clamp-2 whitespace-pre-wrap", children: prompt.text || t('pickers.stash.noText') }), prompt.attachments.length > 0 && (_jsx("span", { className: "text-text-faint", children: t('pickers.stash.files', { count: prompt.attachments.length }) }))] }), _jsx(IconButton, { icon: Trash2, size: "xs", label: t('common.action.delete'), className: "mt-1 opacity-0 group-hover/stash:opacity-100 focus-visible:opacity-100", onClick: () => forgetStashed(prompt.id) })] }, prompt.id))) }) })] }));
|
|
114
|
+
return (_jsxs(Popover.Root, { children: [_jsx(Tooltip, { label: t('pickers.stash.title'), kbd: STASH_SHORTCUT, children: _jsxs(Popover.Trigger, { className: triggerClass, children: [_jsx(Icon, { icon: Bookmark, size: 12 }), _jsx("span", { className: "tabular-nums", children: prompts.length })] }) }), _jsx(Popover.Popup, { variant: "picker", side: "top", className: "w-80", children: _jsx("div", { className: "max-h-72 overflow-auto p-1", children: prompts.map((prompt) => (_jsxs("div", { className: "group/stash flex items-start gap-1", children: [_jsxs(Popover.Close, { className: "flex min-w-0 grow flex-col gap-0.5 rounded-md px-2 py-1.5 text-left text-xs text-text-muted hover:bg-surface-hover hover:text-text", onClick: () => onRestore(prompt), children: [_jsx("span", { className: "line-clamp-2 whitespace-pre-wrap", children: prompt.text || prompt.quote?.text || t('pickers.stash.noText') }), prompt.attachments.length > 0 && (_jsx("span", { className: "text-text-faint", children: t('pickers.stash.files', { count: prompt.attachments.length }) }))] }), _jsx(IconButton, { icon: Trash2, size: "xs", label: t('common.action.delete'), className: "mt-1 opacity-0 group-hover/stash:opacity-100 focus-visible:opacity-100", onClick: () => forgetStashed(prompt.id) })] }, prompt.id))) }) })] }));
|
|
107
115
|
}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { type ReactNode } from 'react';
|
|
2
|
+
import type { HoldRelease } from '../prompt-hold.js';
|
|
2
3
|
import { type PendingPrompt } from '../../prompts/logic/prompts.js';
|
|
3
|
-
export
|
|
4
|
+
export interface PromptHold {
|
|
5
|
+
held: boolean;
|
|
6
|
+
releasedBy: HoldRelease | null;
|
|
7
|
+
release(reason: HoldRelease): void;
|
|
8
|
+
}
|
|
9
|
+
export declare function PromptComposer({ chatId, pending, focused, onShowElsewhere, disabled, hasDraft, denyReason, onAllAnswered, hold, children }: {
|
|
4
10
|
chatId: string;
|
|
5
11
|
pending: PendingPrompt[];
|
|
6
12
|
focused: boolean;
|
|
@@ -9,5 +15,6 @@ export declare function PromptComposer({ chatId, pending, focused, onShowElsewhe
|
|
|
9
15
|
hasDraft: boolean;
|
|
10
16
|
denyReason: boolean;
|
|
11
17
|
onAllAnswered?: () => void;
|
|
18
|
+
hold?: PromptHold;
|
|
12
19
|
children: ReactNode;
|
|
13
20
|
}): import("react").JSX.Element;
|
|
@@ -11,12 +11,12 @@ import { PromptView } from '../../prompts/ui/PromptView.js';
|
|
|
11
11
|
import { chatHost } from '../../host.js';
|
|
12
12
|
import { useChatScope } from '../../scope.js';
|
|
13
13
|
import { useChatActions } from '../actions.js';
|
|
14
|
-
import { Icon } from '@adecore/ui';
|
|
14
|
+
import { Button, Icon } from '@adecore/ui';
|
|
15
15
|
/* The one being read stays in front; otherwise blocking before optional, oldest first, as everywhere else. */
|
|
16
16
|
function pickSubject(waiting, activeId) {
|
|
17
17
|
return waiting.find((subject) => promptIdOf(subject) === activeId) ?? orderPrompts(waiting, isBlockingSubject, promptCreatedAt)[0] ?? null;
|
|
18
18
|
}
|
|
19
|
-
export function PromptComposer({ chatId, pending, focused, onShowElsewhere, disabled, hasDraft, denyReason, onAllAnswered, children }) {
|
|
19
|
+
export function PromptComposer({ chatId, pending, focused, onShowElsewhere, disabled, hasDraft, denyReason, onAllAnswered, hold, children }) {
|
|
20
20
|
const { t } = useTranslation('agent-chat');
|
|
21
21
|
const { id } = useChatScope();
|
|
22
22
|
const actions = useChatActions();
|
|
@@ -30,9 +30,21 @@ export function PromptComposer({ chatId, pending, focused, onShowElsewhere, disa
|
|
|
30
30
|
const session = usePromptSession({ prompts: subjects, idOf: promptIdOf, pick: pickSubject, disabled });
|
|
31
31
|
const ref = useRef(null);
|
|
32
32
|
const { active, activeId: activeKey } = session;
|
|
33
|
-
const
|
|
33
|
+
const asksApproval = active?.kind === 'chat' ? active.item.kind === 'approval' : active?.prompt.asks === 'approval';
|
|
34
|
+
const held = !!active && (hold?.held ?? false);
|
|
35
|
+
const expanded = !!active && !held;
|
|
36
|
+
// The card that waited behind the draft, so a blur that released it is not mistaken for one released long before.
|
|
37
|
+
const heldKey = useRef(null);
|
|
34
38
|
const refocus = useFocusAfterAnswer(activeKey, ref, () => onAllAnswered?.());
|
|
35
39
|
useEffect(() => {
|
|
40
|
+
if (held) {
|
|
41
|
+
heldKey.current = activeKey;
|
|
42
|
+
}
|
|
43
|
+
}, [held, activeKey]);
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (hold?.releasedBy === 'blur' && heldKey.current === activeKey) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
36
48
|
if (focused && expanded && !elsewhere && !ref.current?.querySelector('.prompt-card')?.contains(document.activeElement)) {
|
|
37
49
|
// Keys meant for the draft may still be coming, and on a button the next Space would answer the card.
|
|
38
50
|
if (hasDraft) {
|
|
@@ -42,8 +54,8 @@ export function PromptComposer({ chatId, pending, focused, onShowElsewhere, disa
|
|
|
42
54
|
focusPromptStart(ref.current);
|
|
43
55
|
}
|
|
44
56
|
}
|
|
45
|
-
}, [activeKey, expanded, focused, elsewhere, hasDraft]);
|
|
46
|
-
return (_jsxs("div", { ref: ref, onPointerDownCapture: session.onPointerDownCapture, onClickCapture: session.onClickCapture, children: [active && elsewhere && (_jsxs("button", { type: "button", className: "flex w-full items-center gap-2 px-3.5 py-3 text-left text-sm text-text-muted hover:text-text", onClick: onShowElsewhere, children: [_jsx(Icon, { icon:
|
|
57
|
+
}, [activeKey, expanded, focused, elsewhere, hasDraft, hold?.releasedBy]);
|
|
58
|
+
return (_jsxs("div", { ref: ref, onPointerDownCapture: session.onPointerDownCapture, onClickCapture: session.onClickCapture, children: [active && elsewhere && (_jsxs("button", { type: "button", className: "flex w-full items-center gap-2 px-3.5 py-3 text-left text-sm text-text-muted hover:text-text", onClick: onShowElsewhere, children: [_jsx(Icon, { icon: asksApproval ? Hand : MessageCircleQuestionMark, size: 16, className: "shrink-0 text-status-needs-you" }), _jsx("span", { className: "grow", children: t('composer.waitingBelow') }), _jsx(Icon, { icon: ArrowDown, size: 16, className: "shrink-0" })] })), active && !elsewhere && (_jsx("div", { hidden: !expanded, children: _jsx(PromptView, { subject: active, draft: session.draftOf(promptIdOf(active)), onDraft: (draft) => session.setDraft(promptIdOf(active), draft), onAction: (action) => {
|
|
47
59
|
refocus.hold();
|
|
48
60
|
void session
|
|
49
61
|
.act(active, () => answerPrompt(active, action, actions))
|
|
@@ -52,5 +64,5 @@ export function PromptComposer({ chatId, pending, focused, onShowElsewhere, disa
|
|
|
52
64
|
refocus.release();
|
|
53
65
|
}
|
|
54
66
|
});
|
|
55
|
-
}, more: session.waiting.length - 1, hasDraft: hasDraft, denyReason: denyReason, disabled: disabled, sending: session.sending, error: session.errorOf(promptIdOf(active)) }, activeKey) })), _jsx("div", { hidden: expanded, children: children })] }));
|
|
67
|
+
}, more: session.waiting.length - 1, hasDraft: hasDraft, denyReason: denyReason, disabled: disabled, sending: session.sending, error: session.errorOf(promptIdOf(active)) }, activeKey) })), held && active && (_jsxs("div", { role: "status", className: "flex items-center gap-2 border-b border-border px-3.5 py-2.5", children: [_jsx(Icon, { icon: asksApproval ? Hand : MessageCircleQuestionMark, size: 16, className: "shrink-0 text-status-needs-you" }), _jsx("span", { className: "grow text-sm text-text-muted", children: t(asksApproval ? 'composer.held.approval' : 'composer.held.question') }), _jsx(Button, { size: "sm", onMouseDown: (e) => e.preventDefault(), onClick: () => hold?.release('show'), children: t('composer.held.show') })] })), _jsx("div", { hidden: expanded, children: children })] }));
|
|
56
68
|
}
|
|
@@ -35,7 +35,7 @@ export function QuoteButton({ thread, frame, taker }) {
|
|
|
35
35
|
const zoom = box.offsetWidth > 0 ? bounds.width / box.offsetWidth : 1;
|
|
36
36
|
const x = Math.min(Math.max((line.right - bounds.left) / zoom, EDGE_PX), box.offsetWidth - EDGE_PX);
|
|
37
37
|
const y = (line.bottom - bounds.top) / zoom + GAP_PX;
|
|
38
|
-
setOffer({ quote, x: Math.round(x), y: Math.round(y) });
|
|
38
|
+
setOffer({ quote: quote.text, itemId: quote.itemId, x: Math.round(x), y: Math.round(y) });
|
|
39
39
|
};
|
|
40
40
|
const press = () => {
|
|
41
41
|
pressed = true;
|
|
@@ -67,7 +67,7 @@ export function QuoteButton({ thread, frame, taker }) {
|
|
|
67
67
|
// Pressing a button moves the selection, and the selection is what it quotes.
|
|
68
68
|
onMouseDown: (e) => e.preventDefault(), onClick: () => {
|
|
69
69
|
document.getSelection()?.removeAllRanges();
|
|
70
|
-
taker.current?.(offer.quote);
|
|
70
|
+
taker.current?.(offer.quote, offer.itemId);
|
|
71
71
|
setOffer(null);
|
|
72
72
|
}, children: [_jsx(Icon, { icon: Quote, size: 12 }), t('composer.quote.add')] }));
|
|
73
73
|
}
|
|
@@ -4,8 +4,10 @@ export declare function SubagentTitleCrumb({ chatId, className, children }: {
|
|
|
4
4
|
className?: string;
|
|
5
5
|
children: ReactNode;
|
|
6
6
|
}): import("react").JSX.Element;
|
|
7
|
-
export declare function SubagentBreadcrumb({ chatId,
|
|
7
|
+
export declare function SubagentBreadcrumb({ chatId, className }: {
|
|
8
8
|
chatId: string;
|
|
9
|
-
title?: string;
|
|
10
9
|
className?: string;
|
|
11
10
|
}): import("react").JSX.Element | null;
|
|
11
|
+
export declare function SubagentActions({ chatId }: {
|
|
12
|
+
chatId: string;
|
|
13
|
+
}): import("react").JSX.Element | null;
|
|
@@ -15,20 +15,24 @@ export function SubagentTitleCrumb({ chatId, className, children }) {
|
|
|
15
15
|
}
|
|
16
16
|
return (_jsx("button", { type: "button", className: clsx('flex min-w-0 items-center', className), onClick: () => show(MAIN_AGENT), children: children }));
|
|
17
17
|
}
|
|
18
|
-
/*
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
/* The way down to the sub-agent on screen, each step but the last a way back up. Nothing while the main agent is on screen. */
|
|
19
|
+
export function SubagentBreadcrumb({ chatId, className }) {
|
|
20
|
+
const { t } = useTranslation('agent-chat');
|
|
21
|
+
const { trail, show } = useSubagentTrail(chatId);
|
|
22
|
+
if (trail.length === 0) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
return (_jsx("nav", { "aria-label": t('subagents.title'), className: clsx('flex min-w-0 items-center gap-1', className), children: breadcrumbOf(trail).map((step) => (_jsxs(Fragment, { children: [step.depth > 1 && _jsx(Icon, { icon: ChevronRight, size: 12, className: "shrink-0 text-text-faint" }), step.current ? (_jsx("span", { "aria-current": "page", className: "min-w-0 truncate text-text", children: step.label })) : (_jsx("button", { type: "button", className: clsx(CRUMB_LINK, 'min-w-0 shrink'), onClick: () => show(trailTo(trail, step.depth)), children: step.label }))] }, step.depth))) }));
|
|
26
|
+
}
|
|
27
|
+
/* Stops the sub-agent on screen where that is on offer, and closes it straight back to the main agent. */
|
|
28
|
+
export function SubagentActions({ chatId }) {
|
|
24
29
|
const { t } = useTranslation('agent-chat');
|
|
25
30
|
const { trail, show } = useSubagentTrail(chatId);
|
|
26
31
|
const subagents = useOpenableSubagents(chatId);
|
|
27
32
|
if (trail.length === 0) {
|
|
28
33
|
return null;
|
|
29
34
|
}
|
|
30
|
-
const steps = breadcrumbOf(trail);
|
|
31
35
|
// Only a row of the chat's own thread can be stopped; a grandchild belongs to the sub-agent that opened it.
|
|
32
36
|
const shown = trail.length === 1 ? subagents.find((item) => item.toolUseId === trail[0].toolUseId) : undefined;
|
|
33
|
-
return (_jsxs("
|
|
37
|
+
return (_jsxs("div", { className: "flex shrink-0 items-center gap-1", children: [shown !== undefined && _jsx(SubagentStopButton, { chatId: chatId, item: shown, size: "sm" }), _jsx(IconButton, { icon: X, size: "sm", label: t('subagents.backToMain'), onClick: () => show(MAIN_AGENT) })] }));
|
|
34
38
|
}
|