@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.
Files changed (104) hide show
  1. package/dist/agents/model-requirement.d.ts +6 -0
  2. package/dist/agents/model-requirement.js +26 -0
  3. package/dist/chat/chat-client.d.ts +2 -1
  4. package/dist/chat/chat-client.js +2 -0
  5. package/dist/chat/drafts.d.ts +2 -1
  6. package/dist/chat/drafts.js +15 -5
  7. package/dist/chat/logic/picker-entries.d.ts +10 -0
  8. package/dist/chat/logic/picker-entries.js +19 -0
  9. package/dist/chat/logic/timeline.d.ts +5 -0
  10. package/dist/chat/logic/timeline.js +10 -1
  11. package/dist/chat/prompt-hold.d.ts +8 -0
  12. package/dist/chat/prompt-hold.js +16 -0
  13. package/dist/chat/quote.d.ts +4 -0
  14. package/dist/chat/quote.js +19 -0
  15. package/dist/chat/stash.d.ts +8 -2
  16. package/dist/chat/stash.js +21 -5
  17. package/dist/chat/subagent-list.d.ts +7 -1
  18. package/dist/chat/subagent-list.js +13 -1
  19. package/dist/chat/timeline-scroll.d.ts +6 -2
  20. package/dist/chat/timeline-scroll.js +12 -7
  21. package/dist/chat/ui/ChatActivity.js +22 -8
  22. package/dist/chat/ui/Composer.js +103 -19
  23. package/dist/chat/ui/ComposerInput.d.ts +3 -1
  24. package/dist/chat/ui/ComposerInput.js +13 -4
  25. package/dist/chat/ui/Pickers.js +26 -18
  26. package/dist/chat/ui/PromptComposer.d.ts +8 -1
  27. package/dist/chat/ui/PromptComposer.js +18 -6
  28. package/dist/chat/ui/QuoteButton.js +2 -2
  29. package/dist/chat/ui/SubagentControls.d.ts +4 -2
  30. package/dist/chat/ui/SubagentControls.js +12 -8
  31. package/dist/chat/ui/SubagentInfo.d.ts +1 -1
  32. package/dist/chat/ui/SubagentInfo.js +9 -15
  33. package/dist/chat/ui/SubagentStopButton.js +2 -1
  34. package/dist/chat/ui/Timeline.js +24 -3
  35. package/dist/chat/ui/composer/menu-aria.d.ts +10 -0
  36. package/dist/chat/ui/composer/menu-aria.js +18 -0
  37. package/dist/chat/ui/intelligent-ui/UiBlockFrame.js +3 -3
  38. package/dist/chat/ui/intelligent-ui/UiFallbackPart.js +1 -1
  39. package/dist/chat/ui/intelligent-ui/chart-data.d.ts +22 -2
  40. package/dist/chat/ui/intelligent-ui/chart-data.js +83 -19
  41. package/dist/chat/ui/intelligent-ui/renderers/chart.d.ts +1 -1
  42. package/dist/chat/ui/intelligent-ui/renderers/chart.js +99 -35
  43. package/dist/chat/ui/intelligent-ui/renderers/choices.d.ts +1 -1
  44. package/dist/chat/ui/intelligent-ui/renderers/choices.js +8 -2
  45. package/dist/chat/ui/intelligent-ui/renderers/content.js +5 -3
  46. package/dist/chat/ui/intelligent-ui/renderers/inputs.js +9 -4
  47. package/dist/chat/ui/intelligent-ui/renderers/links.js +1 -1
  48. package/dist/chat/ui/intelligent-ui/renderers/text.js +5 -2
  49. package/dist/chat/ui/intelligent-ui/table-data.js +4 -4
  50. package/dist/chat/ui/intelligent-ui/use-truncated.d.ts +1 -0
  51. package/dist/chat/ui/intelligent-ui/use-truncated.js +28 -0
  52. package/dist/chat/ui/quote-selection.d.ts +5 -2
  53. package/dist/chat/ui/quote-selection.js +3 -2
  54. package/dist/chat/ui/rows/MessageRows.js +24 -5
  55. package/dist/chat/ui/rows/Rows.js +1 -1
  56. package/dist/chat/ui/rows/WorkRows.d.ts +3 -1
  57. package/dist/chat/ui/rows/WorkRows.js +4 -4
  58. package/dist/host.d.ts +1 -0
  59. package/dist/host.js +1 -0
  60. package/dist/locales/en/agent-chat.json +39 -10
  61. package/dist/locales/nl/agent-chat.json +39 -10
  62. package/dist/state/chats.d.ts +2 -1
  63. package/dist/state/chats.js +19 -1
  64. package/package.json +29 -4
  65. package/src/agents/model-requirement.ts +36 -0
  66. package/src/chat/chat-client.ts +5 -0
  67. package/src/chat/drafts.ts +27 -7
  68. package/src/chat/logic/picker-entries.ts +25 -0
  69. package/src/chat/logic/timeline.ts +17 -2
  70. package/src/chat/prompt-hold.ts +27 -0
  71. package/src/chat/quote.ts +20 -0
  72. package/src/chat/stash.ts +31 -5
  73. package/src/chat/subagent-list.ts +19 -5
  74. package/src/chat/timeline-scroll.ts +24 -10
  75. package/src/chat/ui/ChatActivity.tsx +35 -9
  76. package/src/chat/ui/Composer.tsx +197 -52
  77. package/src/chat/ui/ComposerInput.tsx +18 -3
  78. package/src/chat/ui/Pickers.tsx +51 -30
  79. package/src/chat/ui/PromptComposer.tsx +37 -8
  80. package/src/chat/ui/QuoteButton.tsx +3 -2
  81. package/src/chat/ui/SubagentControls.tsx +22 -19
  82. package/src/chat/ui/SubagentInfo.tsx +24 -42
  83. package/src/chat/ui/SubagentStopButton.tsx +2 -1
  84. package/src/chat/ui/Timeline.tsx +26 -4
  85. package/src/chat/ui/composer/menu-aria.ts +29 -0
  86. package/src/chat/ui/intelligent-ui/UiBlockFrame.tsx +5 -5
  87. package/src/chat/ui/intelligent-ui/UiFallbackPart.tsx +6 -1
  88. package/src/chat/ui/intelligent-ui/chart-data.ts +106 -20
  89. package/src/chat/ui/intelligent-ui/renderers/chart.tsx +237 -72
  90. package/src/chat/ui/intelligent-ui/renderers/choices.tsx +9 -4
  91. package/src/chat/ui/intelligent-ui/renderers/content.tsx +31 -15
  92. package/src/chat/ui/intelligent-ui/renderers/inputs.tsx +16 -5
  93. package/src/chat/ui/intelligent-ui/renderers/links.tsx +3 -3
  94. package/src/chat/ui/intelligent-ui/renderers/text.tsx +18 -8
  95. package/src/chat/ui/intelligent-ui/table-data.ts +4 -4
  96. package/src/chat/ui/intelligent-ui/use-truncated.ts +33 -0
  97. package/src/chat/ui/quote-selection.ts +6 -4
  98. package/src/chat/ui/rows/MessageRows.tsx +44 -5
  99. package/src/chat/ui/rows/Rows.tsx +3 -1
  100. package/src/chat/ui/rows/WorkRows.tsx +15 -1
  101. package/src/host.ts +3 -0
  102. package/src/locales/en/agent-chat.json +39 -10
  103. package/src/locales/nl/agent-chat.json +39 -10
  104. package/src/state/chats.ts +31 -2
@@ -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
- const chosen = commands[menuIndex];
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
- /* A stashed prompt comes back as text, mentions and skills; its files were never kept. */
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
- setDraft((current) => ({ ...current, text: prompt.text, mentions: [...prompt.mentions], skills: [...prompt.skills] }));
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[menuIndex];
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[menuIndex] ?? skillMatches[0]).name);
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(menuIndex);
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 === menuIndex, 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: () => {
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 === menuIndex, 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: [chatMatches.length > 0 && _jsx(Menu.Label, { children: t('composer.mentions.chats') }), chatMatches.map((chat, index) => (_jsxs("button", { "data-active": index === menuIndex, 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 && (mention.query === '' || chatMatches.length > 0) && (_jsx(Menu.Label, { children: mention.query === '' ? t('composer.mentions.here') : t('composer.mentions.files') })), 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') })), files.map((path, index) => {
829
- const { name, dir } = splitPath(path);
830
- return (_jsxs("button", { "data-active": chatMatches.length + index === menuIndex, 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));
831
- })] })), 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) => {
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 }), _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, onChange: (value, selection, state) => {
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
- }, 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) => {
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) {
@@ -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
- const rows = providers.flatMap((entry) => entry.models.map((model) => ({ kind: 'model', provider: entry, model })));
34
- if (trimmed !== '') {
35
- return rows.filter((row) => matches(row.model, trimmed));
36
- }
37
- const live = rows.filter((row) => !row.model.legacy);
38
- const legacy = rows.filter((row) => row.model.legacy);
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: "cursor-row flex w-full items-center gap-2 rounded-md px-2.5 py-1.5 text-left text-sm text-text-muted", onMouseEnter: () => setIndex(i), onClick: () => choose(entry), 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') })] })] }, `${entry.provider.kind}/${entry.model.slug}`));
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 declare function PromptComposer({ chatId, pending, focused, onShowElsewhere, disabled, hasDraft, denyReason, onAllAnswered, children }: {
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 expanded = !!active;
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: (active.kind === 'chat' ? active.item.kind === 'approval' : active.prompt.asks === 'approval') ? 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) => {
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, title, className }: {
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
- * The way down after the chat's title, each step a way back up, and a close that goes straight back
20
- * to the main agent. A bar that does not draw the title itself passes it, and it opens the way.
21
- * Nothing while the main agent itself is on screen.
22
- */
23
- export function SubagentBreadcrumb({ chatId, title, className }) {
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("nav", { "aria-label": t('subagents.title'), className: clsx('flex min-w-0 items-center gap-1 text-xs', className), children: [title !== undefined && (_jsx("button", { type: "button", className: clsx(CRUMB_LINK, 'min-w-0 shrink'), onClick: () => show(MAIN_AGENT), children: title })), steps.map((step) => (_jsxs(Fragment, { children: [_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))), shown !== undefined && _jsx(SubagentStopButton, { chatId: chatId, item: shown, size: "sm" }), _jsx(IconButton, { icon: X, size: "sm", label: t('subagents.backToMain'), onClick: () => show(MAIN_AGENT) })] }));
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
  }
@@ -1,4 +1,4 @@
1
1
  export declare function SubagentInfo({ chatId, toolUseId }: {
2
2
  chatId: string;
3
3
  toolUseId: string;
4
- }): import("react").JSX.Element | null;
4
+ }): import("react").JSX.Element;