@adecore/agents-react 0.21.1 → 0.21.2
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/status-look.js +2 -3
- package/dist/chat/chat-client.d.ts +1 -1
- package/dist/chat/chat-client.js +2 -2
- package/dist/chat/drafts.js +6 -12
- package/dist/chat/logic/ansi.js +5 -1
- package/dist/chat/logic/fork.js +8 -9
- package/dist/chat/logic/limit.js +17 -14
- package/dist/chat/logic/resume-compaction.js +2 -11
- package/dist/chat/logic/scrubber.js +9 -18
- package/dist/chat/logic/timeline.js +15 -10
- package/dist/chat/logic/tool-catalog.js +3 -4
- package/dist/chat/mentions.js +5 -6
- package/dist/chat/runtime-modes.js +2 -4
- package/dist/chat/stash.js +1 -3
- package/dist/chat/thumbnails.js +3 -4
- package/dist/chat/timeline-scroll.js +39 -45
- package/dist/chat/ui/BookmarkSubmenu.js +1 -5
- package/dist/chat/ui/CodeBlock.js +2 -3
- package/dist/chat/ui/Composer.js +57 -69
- package/dist/chat/ui/Markdown.js +11 -11
- package/dist/chat/ui/Pickers.js +5 -9
- package/dist/chat/ui/QuoteButton.js +2 -3
- package/dist/chat/ui/RunSettings.js +8 -2
- package/dist/chat/ui/Scrubber.js +2 -3
- package/dist/chat/ui/SubagentControls.js +1 -3
- package/dist/chat/ui/Timeline.js +15 -18
- package/dist/chat/ui/TimelineMenu.js +3 -9
- package/dist/chat/ui/UnifiedDiff.js +3 -5
- package/dist/chat/ui/VisualFrame.js +3 -4
- package/dist/chat/ui/composer/keys.js +19 -11
- package/dist/chat/ui/intelligent-ui/render-context.d.ts +1 -1
- package/dist/chat/ui/rows/MessageRows.js +2 -3
- package/dist/chat/ui/rows/WorkRows.d.ts +4 -3
- package/dist/chat/ui/rows/WorkRows.js +19 -14
- package/dist/chat/visual-viewport.d.ts +1 -1
- package/dist/chat/visual-viewport.js +9 -8
- package/dist/port-transport.js +4 -8
- package/dist/state/chats.js +10 -14
- package/dist/state/usage.js +44 -49
- package/dist/storage.js +7 -7
- package/dist/theme.css +6 -14
- package/dist/usage/LimitsList.js +1 -2
- package/dist/usage/UsageBreakdown.js +1 -3
- package/dist/usage/UsageDialog.js +2 -4
- package/dist/usage/format.js +6 -6
- package/dist/usage/money.js +1 -5
- package/dist/usage/summary.js +2 -4
- package/package.json +4 -4
- package/src/agents/status-look.ts +2 -3
- package/src/chat/chat-client.ts +2 -3
- package/src/chat/drafts.ts +6 -14
- package/src/chat/logic/ansi.ts +5 -1
- package/src/chat/logic/fork.ts +8 -9
- package/src/chat/logic/limit.ts +17 -16
- package/src/chat/logic/resume-compaction.ts +2 -11
- package/src/chat/logic/scrubber.ts +13 -17
- package/src/chat/logic/timeline.ts +22 -11
- package/src/chat/logic/tool-catalog.ts +7 -6
- package/src/chat/mentions.ts +5 -6
- package/src/chat/runtime-modes.ts +2 -4
- package/src/chat/stash.ts +1 -3
- package/src/chat/thumbnails.ts +3 -4
- package/src/chat/timeline-scroll.ts +43 -52
- package/src/chat/ui/BookmarkSubmenu.tsx +1 -5
- package/src/chat/ui/CodeBlock.tsx +2 -3
- package/src/chat/ui/Composer.tsx +66 -86
- package/src/chat/ui/Markdown.tsx +13 -17
- package/src/chat/ui/Pickers.tsx +5 -9
- package/src/chat/ui/QuoteButton.tsx +2 -3
- package/src/chat/ui/RunSettings.tsx +9 -2
- package/src/chat/ui/Scrubber.tsx +2 -3
- package/src/chat/ui/SubagentControls.tsx +1 -3
- package/src/chat/ui/SubagentTimeline.tsx +1 -2
- package/src/chat/ui/Timeline.tsx +18 -20
- package/src/chat/ui/TimelineMenu.tsx +3 -9
- package/src/chat/ui/UnifiedDiff.tsx +3 -5
- package/src/chat/ui/VisualFrame.tsx +3 -4
- package/src/chat/ui/composer/keys.ts +21 -11
- package/src/chat/ui/intelligent-ui/render-context.ts +2 -2
- package/src/chat/ui/rehype-chips.ts +1 -1
- package/src/chat/ui/rehype-fade.ts +1 -1
- package/src/chat/ui/remark-html-as-text.ts +1 -1
- package/src/chat/ui/rows/MessageRows.tsx +3 -4
- package/src/chat/ui/rows/WorkRows.tsx +86 -69
- package/src/chat/visual-viewport.ts +10 -8
- package/src/host.ts +1 -3
- package/src/port-transport.ts +4 -8
- package/src/state/chats.ts +11 -14
- package/src/state/usage.ts +49 -60
- package/src/storage.ts +7 -7
- package/src/theme.css +6 -14
- package/src/usage/LimitsList.tsx +2 -4
- package/src/usage/UsageBreakdown.tsx +2 -5
- package/src/usage/UsageDialog.tsx +2 -4
- package/src/usage/format.ts +6 -6
- package/src/usage/money.ts +1 -5
- package/src/usage/summary.ts +2 -4
package/dist/chat/ui/Composer.js
CHANGED
|
@@ -49,8 +49,7 @@ const NOTICE_MS = 4000;
|
|
|
49
49
|
const PROMPT_HISTORY = 50;
|
|
50
50
|
// Commands the composer handles itself; the CLI's own ones are sent through as text.
|
|
51
51
|
const LOCAL_COMMANDS = ['model', 'compact', 'clear'];
|
|
52
|
-
/*
|
|
53
|
-
file's text, or the drag's own text, at the drop point on top of that. */
|
|
52
|
+
/* The card turns dropped files and paths into attachments and chips; CodeMirror would also paste their text at the drop point. */
|
|
54
53
|
const LEAVE_DROPS_TO_THE_CARD = EditorView.domEventHandlers({
|
|
55
54
|
drop: (event) => event.dataTransfer !== null && (event.dataTransfer.types.includes('Files') || carriesMentions(event.dataTransfer.types))
|
|
56
55
|
});
|
|
@@ -88,6 +87,18 @@ function hintedPlaceholder(lead, joiner, hints) {
|
|
|
88
87
|
root.append(lead, extra);
|
|
89
88
|
return root;
|
|
90
89
|
}
|
|
90
|
+
function sendLabelKey(busy, canSteer, delivery) {
|
|
91
|
+
if (!busy) {
|
|
92
|
+
return 'composer.send';
|
|
93
|
+
}
|
|
94
|
+
if (!canSteer) {
|
|
95
|
+
return 'composer.queueButton';
|
|
96
|
+
}
|
|
97
|
+
return delivery === 'steer' ? 'composer.steerButton' : 'composer.queueWithSteerButton';
|
|
98
|
+
}
|
|
99
|
+
function sameQuery(left, right) {
|
|
100
|
+
return left?.start === right?.start && left?.query === right?.query;
|
|
101
|
+
}
|
|
91
102
|
function splitPath(path) {
|
|
92
103
|
const slash = path.lastIndexOf('/');
|
|
93
104
|
return slash < 0 ? { name: path, dir: '' } : { name: path.slice(slash + 1), dir: path.slice(0, slash) };
|
|
@@ -125,7 +136,6 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
125
136
|
const providers = useProviders((s) => s.providers);
|
|
126
137
|
const { approvals, questions } = usePendingRequests(chatId);
|
|
127
138
|
const order = useChatRow(chatId, (row) => row?.order);
|
|
128
|
-
// The structure, which a delta leaves alone. The prompts and the requests are items of their own.
|
|
129
139
|
const items = useChatRow(chatId, (row) => row?.structure);
|
|
130
140
|
const chats = chatHost().useReferableChats();
|
|
131
141
|
const placeholderLead = chatHost().useComposerPlaceholder(scope.id, chatId);
|
|
@@ -136,7 +146,7 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
136
146
|
const capabilities = provider?.capabilities;
|
|
137
147
|
const compaction = capabilities?.compaction;
|
|
138
148
|
const dismissedTurnId = useResumeCompactionDismissal(chatId);
|
|
139
|
-
|
|
149
|
+
// The cheap half of the offer, so a chat that can never make one keeps no timer.
|
|
140
150
|
const couldOfferCompaction = compaction !== undefined && compaction !== 'none' && info.usage.contextTokens >= RESUME_COMPACTION_TOKENS;
|
|
141
151
|
const now = useNow(60_000, couldOfferCompaction);
|
|
142
152
|
const compactionOffer = !couldOfferCompaction
|
|
@@ -156,13 +166,9 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
156
166
|
const quote = draft.quote;
|
|
157
167
|
// The CLI announces its session on the first message, so anything before that is still a blank chat.
|
|
158
168
|
const started = info.agentSessionId !== null || info.usage.turns > 0;
|
|
159
|
-
/*
|
|
160
|
-
* A chat that has started keeps its provider (the CLI holds the thread), and one opened for a
|
|
161
|
-
* named CLI was never going to change it. Either way the models stay switchable. The CLI takes
|
|
162
|
-
* the new one on the restart the next send does anyway. With one provider in the list the
|
|
163
|
-
* picker drops its group headers on its own, so it reads as that CLI's own catalog.
|
|
164
|
-
*/
|
|
165
169
|
const accountChoice = useAccountChoice(info.provider, info.account);
|
|
170
|
+
// A chat that has started keeps its provider (the CLI holds the thread), and so does one opened for a
|
|
171
|
+
// named CLI; its models stay switchable, since the CLI takes a new one on the restart the next send does.
|
|
166
172
|
const pickable = started || providerFixed
|
|
167
173
|
? providers.filter((entry) => entry.kind === info.provider)
|
|
168
174
|
: providers.filter((entry) => entry.installed && entry.capabilities.chat);
|
|
@@ -197,11 +203,11 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
197
203
|
}
|
|
198
204
|
let stale = false;
|
|
199
205
|
const search = () => {
|
|
200
|
-
const
|
|
201
|
-
if (
|
|
206
|
+
const searchFiles = chatHost().searchFiles;
|
|
207
|
+
if (searchFiles === null) {
|
|
202
208
|
return;
|
|
203
209
|
}
|
|
204
|
-
|
|
210
|
+
searchFiles(scope.id, info.cwd, mention.query, MENTION_RESULTS)
|
|
205
211
|
.then((files) => {
|
|
206
212
|
if (!stale) {
|
|
207
213
|
setSearched({ query: mention.query, files });
|
|
@@ -292,15 +298,25 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
292
298
|
setDraft((current) => ({ ...current, quote: '' }));
|
|
293
299
|
inputRef.current?.focus();
|
|
294
300
|
};
|
|
295
|
-
const
|
|
301
|
+
const showError = (error) => {
|
|
302
|
+
setNotice(error instanceof Error ? error.message : String(error));
|
|
303
|
+
};
|
|
304
|
+
/* One configuration at a time; a second pick while one is on its way is dropped. */
|
|
305
|
+
const runConfiguration = (work) => {
|
|
296
306
|
if (configuring.current) {
|
|
297
307
|
return;
|
|
298
308
|
}
|
|
299
309
|
configuring.current = true;
|
|
300
310
|
setConfigurationPending(true);
|
|
301
|
-
void
|
|
302
|
-
.
|
|
303
|
-
.
|
|
311
|
+
void work()
|
|
312
|
+
.catch(showError)
|
|
313
|
+
.finally(() => {
|
|
314
|
+
configuring.current = false;
|
|
315
|
+
setConfigurationPending(false);
|
|
316
|
+
});
|
|
317
|
+
};
|
|
318
|
+
const configure = (patch) => {
|
|
319
|
+
runConfiguration(() => actions.configure(chatId, patch).then(() => {
|
|
304
320
|
if (patch.selection) {
|
|
305
321
|
rememberChatSelection(info.provider, patch.selection);
|
|
306
322
|
}
|
|
@@ -310,40 +326,24 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
310
326
|
if (patch.account) {
|
|
311
327
|
rememberChatAccount(scope.id, info.provider, patch.account);
|
|
312
328
|
}
|
|
313
|
-
})
|
|
314
|
-
.catch((error) => setNotice(error instanceof Error ? error.message : String(error)))
|
|
315
|
-
.finally(() => {
|
|
316
|
-
configuring.current = false;
|
|
317
|
-
setConfigurationPending(false);
|
|
318
|
-
});
|
|
329
|
+
}));
|
|
319
330
|
};
|
|
320
331
|
const chooseAccount = (account) => configure({ account });
|
|
321
332
|
/* A model of another CLI re-points the whole chat; one of this CLI's own is a configure. */
|
|
322
333
|
const chooseModel = (provider, slug) => {
|
|
323
334
|
const selection = { model: slug, options: {} };
|
|
324
335
|
if (provider !== info.provider) {
|
|
325
|
-
|
|
326
|
-
return;
|
|
327
|
-
}
|
|
328
|
-
configuring.current = true;
|
|
329
|
-
setConfigurationPending(true);
|
|
330
|
-
void onRetarget(provider, selection)
|
|
331
|
-
.then(() => rememberChatSelection(provider, selection))
|
|
332
|
-
.catch((error) => setNotice(error instanceof Error ? error.message : String(error)))
|
|
333
|
-
.finally(() => {
|
|
334
|
-
configuring.current = false;
|
|
335
|
-
setConfigurationPending(false);
|
|
336
|
-
});
|
|
336
|
+
runConfiguration(() => onRetarget(provider, selection).then(() => rememberChatSelection(provider, selection)));
|
|
337
337
|
return;
|
|
338
338
|
}
|
|
339
339
|
configure({ selection });
|
|
340
340
|
};
|
|
341
341
|
const stop = (shiftKey) => {
|
|
342
342
|
if (composerStopOf(shiftKey) === 'turn') {
|
|
343
|
-
void actions.stopTurn(chatId, false).catch(
|
|
343
|
+
void actions.stopTurn(chatId, false).catch(showError);
|
|
344
344
|
return;
|
|
345
345
|
}
|
|
346
|
-
chatHost().confirm.stopSubagents(scope.id, chatId, () => void actions.stopTurn(chatId, true).catch(
|
|
346
|
+
chatHost().confirm.stopSubagents(scope.id, chatId, () => void actions.stopTurn(chatId, true).catch(showError));
|
|
347
347
|
};
|
|
348
348
|
/* The host decides whether a turn is in the way; only its refusal asks the person first. */
|
|
349
349
|
const clearThread = async () => {
|
|
@@ -380,17 +380,16 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
380
380
|
return false;
|
|
381
381
|
}
|
|
382
382
|
};
|
|
383
|
-
const same = (a, b) => a?.start === b?.start && a?.query === b?.query;
|
|
384
383
|
/* Which picker the caret opens: `@` for a file, `$` for a skill, or neither. In code a sigil is just text. */
|
|
385
384
|
const trackTriggers = (value, selection, state) => {
|
|
386
385
|
const empty = selection.from === selection.to;
|
|
387
386
|
const tree = empty ? (ensureSyntaxTree(state, selection.from, 50) ?? syntaxTree(state)) : null;
|
|
388
387
|
const caret = tree !== null && !inCode(tree, value, selection.from) ? selection.from : null;
|
|
389
388
|
const skill = caret === null ? null : findSkillQuery(value, caret);
|
|
390
|
-
setSkillQuery((current) => (
|
|
389
|
+
setSkillQuery((current) => (sameQuery(current, skill) ? current : skill));
|
|
391
390
|
// A CLI that does not expand `@path` gets the text as it is, so the picker stays out of the way.
|
|
392
391
|
const next = caret === null || capabilities?.mentions === false ? null : findMentionQuery(value, caret);
|
|
393
|
-
setMention((current) => (
|
|
392
|
+
setMention((current) => (sameQuery(current, next) ? current : next));
|
|
394
393
|
};
|
|
395
394
|
// The inserted text reaches the editor with the render it causes, so the caret waits a frame for it.
|
|
396
395
|
const moveCaret = (caret) => {
|
|
@@ -438,8 +437,7 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
438
437
|
setSkillQuery(null);
|
|
439
438
|
moveCaret(result.caret);
|
|
440
439
|
};
|
|
441
|
-
/* Paths dragged in from a list of files.
|
|
442
|
-
at the end of the prompt and join the draft's mentions, which is what `chat.send` carries. */
|
|
440
|
+
/* Paths dragged in from a list of files. With no mention query to insert into, they land at the end of the prompt. */
|
|
443
441
|
const addMentions = (paths) => {
|
|
444
442
|
const fresh = paths.filter((path) => path !== '');
|
|
445
443
|
if (fresh.length === 0 || capabilities?.mentions === false) {
|
|
@@ -620,6 +618,18 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
620
618
|
setText(history[next].text, history[next].mentions);
|
|
621
619
|
return true;
|
|
622
620
|
};
|
|
621
|
+
/* The arrows walk an open picker round; true when the key was one of them. */
|
|
622
|
+
const moveInMenu = (key, count) => {
|
|
623
|
+
if (key === 'ArrowDown') {
|
|
624
|
+
setMenuIndex((i) => (i + 1) % count);
|
|
625
|
+
return true;
|
|
626
|
+
}
|
|
627
|
+
if (key === 'ArrowUp') {
|
|
628
|
+
setMenuIndex((i) => (i - 1 + count) % count);
|
|
629
|
+
return true;
|
|
630
|
+
}
|
|
631
|
+
return false;
|
|
632
|
+
};
|
|
623
633
|
/* Runs before the editor's own keymap; true means the key is handled and its default prevented. */
|
|
624
634
|
const onKeyDown = (e, view) => {
|
|
625
635
|
// Shift with the paste shortcut pastes a large text inline after all; any other key forgets it.
|
|
@@ -634,8 +644,7 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
634
644
|
setText('', [], []);
|
|
635
645
|
return true;
|
|
636
646
|
}
|
|
637
|
-
// The
|
|
638
|
-
// panels and the palette stay the app's, untouched: Mod+Shift+Enter would otherwise send as well.
|
|
647
|
+
// The app's own shortcuts stay the app's while you type: Mod+Shift+Enter would otherwise send as well.
|
|
639
648
|
if (chatHost().isAppShortcut(e)) {
|
|
640
649
|
return false;
|
|
641
650
|
}
|
|
@@ -653,12 +662,7 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
653
662
|
return true;
|
|
654
663
|
}
|
|
655
664
|
if (commandMenuOpen) {
|
|
656
|
-
if (e.key
|
|
657
|
-
setMenuIndex((i) => (i + 1) % commands.length);
|
|
658
|
-
return true;
|
|
659
|
-
}
|
|
660
|
-
if (e.key === 'ArrowUp') {
|
|
661
|
-
setMenuIndex((i) => (i - 1 + commands.length) % commands.length);
|
|
665
|
+
if (moveInMenu(e.key, commands.length)) {
|
|
662
666
|
return true;
|
|
663
667
|
}
|
|
664
668
|
if (e.key === 'Tab') {
|
|
@@ -673,12 +677,7 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
673
677
|
}
|
|
674
678
|
}
|
|
675
679
|
if (skillMenuOpen) {
|
|
676
|
-
if (e.key
|
|
677
|
-
setMenuIndex((i) => (i + 1) % skillMatches.length);
|
|
678
|
-
return true;
|
|
679
|
-
}
|
|
680
|
-
if (e.key === 'ArrowUp') {
|
|
681
|
-
setMenuIndex((i) => (i - 1 + skillMatches.length) % skillMatches.length);
|
|
680
|
+
if (moveInMenu(e.key, skillMatches.length)) {
|
|
682
681
|
return true;
|
|
683
682
|
}
|
|
684
683
|
if (e.key === 'Tab' || e.key === 'Enter') {
|
|
@@ -687,12 +686,7 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
687
686
|
}
|
|
688
687
|
}
|
|
689
688
|
if (mentionMenuOpen && mentionCount > 0) {
|
|
690
|
-
if (e.key
|
|
691
|
-
setMenuIndex((i) => (i + 1) % mentionCount);
|
|
692
|
-
return true;
|
|
693
|
-
}
|
|
694
|
-
if (e.key === 'ArrowUp') {
|
|
695
|
-
setMenuIndex((i) => (i - 1 + mentionCount) % mentionCount);
|
|
689
|
+
if (moveInMenu(e.key, mentionCount)) {
|
|
696
690
|
return true;
|
|
697
691
|
}
|
|
698
692
|
if (e.key === 'Tab' || e.key === 'Enter') {
|
|
@@ -856,11 +850,5 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
856
850
|
addFiles(Array.from(e.target.files ?? []));
|
|
857
851
|
e.target.value = '';
|
|
858
852
|
inputRef.current?.focus();
|
|
859
|
-
} }), _jsx(RunSettings, { disabled: configurationPending || !writable, providers: pickable, provider: info.provider, selection: info.selection, model: model, runtimeMode: info.runtimeMode, effectiveRuntimeMode: info.effectiveRuntimeMode, permissionMode: info.permissionMode, open: modelPickerOpen, onOpenChange: setModelPickerOpen, onModel: chooseModel, onOption: (id, value) => configure({ selection: { ...info.selection, options: { ...info.selection.options, [id]: value } } }), onMode: (runtimeMode) => configure({ runtimeMode }), usage: info.usage, compactDisabled: busy || !writable, onCompact: compact, account: accountChoice === null ? null : { choice: accountChoice, started, onPick: chooseAccount } }), _jsx(StashPicker, { onRestore: restoreStashed }), Slot !== null && (_jsx(ErrorBoundary, { label: t('composer.slotFailed'), resetKeys: [chatId], compact: true, className: "relative rounded-lg", children: _jsx(Slot, { scopeId: scope.id, chatId: chatId, disabled: !writable, insert: insertIntoDraft }) })), _jsx("span", { className: "grow" }), _jsxs("div", { className: "flex h-9 shrink-0 items-center gap-0.5 rounded-full bg-surface-hover", children: [_jsx("div", { ref: setDictationToolbar, className: "flex items-center pl-1 empty:hidden [&_.icon-btn]:rounded-full" }), busy && (_jsx(Tooltip, { label: composerStopLabel(), name: true, children: _jsx("button", { className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-status-error text-accent-text", onClick: (event) => stop(event.shiftKey), children: _jsx(Icon, { icon: Square, size: 16 }) }) })), (!busy || !isEmptyDraft(draft)) && (_jsx(Tooltip, { label: busy
|
|
860
|
-
? t(canSteer
|
|
861
|
-
? sendDelivery === 'steer'
|
|
862
|
-
? 'composer.steerButton'
|
|
863
|
-
: 'composer.queueWithSteerButton'
|
|
864
|
-
: 'composer.queueButton')
|
|
865
|
-
: t('composer.send'), kbd: KEY_SHORTCUTS.modEnter, name: true, children: _jsx("button", { className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-accent text-accent-text hover:brightness-90 disabled:opacity-50 disabled:hover:brightness-100", disabled: isEmptyDraft(draft) || !writable || guard.tooLong, onClick: (event) => submit(event.altKey), children: _jsx(Icon, { icon: ArrowUp, size: 16 }) }) }))] })] })] }) }), _jsx(PromptDialog, { open: confirmClear, title: t('composer.clear.title'), description: t('composer.clear.description'), confirmLabel: t('composer.clear.confirm'), danger: true, fallbackMessage: t('composer.notice.clearFailed'), onConfirm: forceClearThread, onOpenChange: () => setConfirmClear(false) })] }));
|
|
853
|
+
} }), _jsx(RunSettings, { disabled: configurationPending || !writable, providers: pickable, provider: info.provider, selection: info.selection, model: model, runtimeMode: info.runtimeMode, effectiveRuntimeMode: info.effectiveRuntimeMode, permissionMode: info.permissionMode, open: modelPickerOpen, onOpenChange: setModelPickerOpen, onModel: chooseModel, onOption: (id, value) => configure({ selection: { ...info.selection, options: { ...info.selection.options, [id]: value } } }), onMode: (runtimeMode) => configure({ runtimeMode }), usage: info.usage, compactDisabled: busy || !writable, onCompact: compact, account: accountChoice === null ? null : { choice: accountChoice, started, onPick: chooseAccount } }), _jsx(StashPicker, { onRestore: restoreStashed }), Slot !== null && (_jsx(ErrorBoundary, { label: t('composer.slotFailed'), resetKeys: [chatId], compact: true, className: "relative rounded-lg", children: _jsx(Slot, { scopeId: scope.id, chatId: chatId, disabled: !writable, insert: insertIntoDraft }) })), _jsx("span", { className: "grow" }), _jsxs("div", { className: "flex h-9 shrink-0 items-center gap-0.5 rounded-full bg-surface-hover", children: [_jsx("div", { ref: setDictationToolbar, className: "flex items-center pl-1 empty:hidden [&_.icon-btn]:rounded-full" }), busy && (_jsx(Tooltip, { label: composerStopLabel(), name: true, children: _jsx("button", { className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-status-error text-accent-text", onClick: (event) => stop(event.shiftKey), children: _jsx(Icon, { icon: Square, size: 16 }) }) })), (!busy || !isEmptyDraft(draft)) && (_jsx(Tooltip, { label: t(sendLabelKey(busy, canSteer, sendDelivery)), kbd: KEY_SHORTCUTS.modEnter, name: true, children: _jsx("button", { className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-accent text-accent-text hover:brightness-90 disabled:opacity-50 disabled:hover:brightness-100", disabled: isEmptyDraft(draft) || !writable || guard.tooLong, onClick: (event) => submit(event.altKey), children: _jsx(Icon, { icon: ArrowUp, size: 16 }) }) }))] })] })] }) }), _jsx(PromptDialog, { open: confirmClear, title: t('composer.clear.title'), description: t('composer.clear.description'), confirmLabel: t('composer.clear.confirm'), danger: true, fallbackMessage: t('composer.notice.clearFailed'), onConfirm: forceClearThread, onOpenChange: () => setConfirmClear(false) })] }));
|
|
866
854
|
}
|
package/dist/chat/ui/Markdown.js
CHANGED
|
@@ -54,10 +54,8 @@ function languageOf(className) {
|
|
|
54
54
|
}
|
|
55
55
|
const INLINE_CODE = 'rounded-sm bg-surface-sunken px-1 py-px font-mono text-code';
|
|
56
56
|
/*
|
|
57
|
-
* A path an agent wrote down
|
|
58
|
-
*
|
|
59
|
-
* be clicked. `data-file-path` is what the thread's own right-click menu reads, so a link answers to
|
|
60
|
-
* both ways of opening it.
|
|
57
|
+
* A path an agent wrote down. It keeps the shape of the code chip it would otherwise be, so a sentence
|
|
58
|
+
* full of them does not shift; `data-file-path` is what the thread's right-click menu reads.
|
|
61
59
|
*/
|
|
62
60
|
function FileLink({ target, className, children }) {
|
|
63
61
|
const cwd = useFileLinkCwd();
|
|
@@ -72,8 +70,10 @@ function InlineCode({ text }) {
|
|
|
72
70
|
}
|
|
73
71
|
return (_jsx(FileLink, { target: target, className: `${INLINE_CODE} text-accent hover:underline`, children: text }));
|
|
74
72
|
}
|
|
75
|
-
/*
|
|
76
|
-
|
|
73
|
+
/*
|
|
74
|
+
* The path a link target spells, without the one scheme that still means a file on the host's machine.
|
|
75
|
+
* A href nobody encoded is left as it is, since decoding throws on a stray percent.
|
|
76
|
+
*/
|
|
77
77
|
function hrefPath(href) {
|
|
78
78
|
const bare = href.replace(/^file:\/\//, '');
|
|
79
79
|
try {
|
|
@@ -89,6 +89,9 @@ function MarkdownLink({ href, children }) {
|
|
|
89
89
|
if (target !== null) {
|
|
90
90
|
return (_jsx(FileLink, { target: target, className: "text-accent underline underline-offset-2", children: children }));
|
|
91
91
|
}
|
|
92
|
+
return _jsx(ExternalLink, { href: href, children: children });
|
|
93
|
+
}
|
|
94
|
+
function ExternalLink({ href, children }) {
|
|
92
95
|
return (_jsx("a", { href: href, target: "_blank", rel: "noreferrer", children: children }));
|
|
93
96
|
}
|
|
94
97
|
function MarkdownCode({ className, children, plain = false }) {
|
|
@@ -118,9 +121,7 @@ const plainComponents = {
|
|
|
118
121
|
code({ className, children }) {
|
|
119
122
|
return (_jsx(MarkdownCode, { className: className, plain: true, children: children }));
|
|
120
123
|
},
|
|
121
|
-
a
|
|
122
|
-
return (_jsx("a", { href: href, target: "_blank", rel: "noreferrer", children: children }));
|
|
123
|
-
}
|
|
124
|
+
a: ExternalLink
|
|
124
125
|
};
|
|
125
126
|
// A reply sits under the heading of its message, an `h3`, so its own headings start one level below
|
|
126
127
|
// that. Only `aria-level` moves. The tag stays, since the styles and a copy of the thread read it.
|
|
@@ -186,8 +187,7 @@ const PLUGINS_WITH_BREAKS = [remarkGfm, remarkBreaks];
|
|
|
186
187
|
const MESSAGE_PLUGINS = [remarkGfm, remarkHtmlAsText, remarkBreaks];
|
|
187
188
|
const FADE_PLUGINS = [rehypeFadeWords];
|
|
188
189
|
const NO_PLUGINS = [];
|
|
189
|
-
/* One block of a reply. It renders no element of its own, so prose
|
|
190
|
-
direct children and keeps its first and last margins. */
|
|
190
|
+
/* One block of a reply. It renders no element of its own, so prose keeps its first and last margins. */
|
|
191
191
|
const ReplyBlock = memo(function ReplyBlock({ text, fade, open, reply }) {
|
|
192
192
|
return (_jsx(CodeStreamingContext.Provider, { value: open, children: _jsx(ShellCodeBlockReplyContext.Provider, { value: reply ?? null, children: _jsx(ReactMarkdown, { remarkPlugins: REPLY_PLUGINS, rehypePlugins: fade ? FADE_PLUGINS : NO_PLUGINS, components: replyComponents, children: text }) }) }));
|
|
193
193
|
});
|
package/dist/chat/ui/Pickers.js
CHANGED
|
@@ -13,11 +13,9 @@ function matches(model, query) {
|
|
|
13
13
|
return model.name.toLowerCase().includes(query) || model.slug.toLowerCase().includes(query);
|
|
14
14
|
}
|
|
15
15
|
/*
|
|
16
|
-
* Which model answers:
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* another provider's model is also how a chat picks its provider. A chat bound to one CLI passes
|
|
20
|
-
* only that CLI, which drops the group headers and leaves its own catalog to choose from.
|
|
16
|
+
* Which model answers: a search field over one group per provider, with the legacy models behind an
|
|
17
|
+
* expander. Before the first message the list carries every installed CLI, so picking another
|
|
18
|
+
* provider's model is also how a chat picks its provider; with one CLI the group headers drop.
|
|
21
19
|
*/
|
|
22
20
|
export function ModelPicker({ providers, provider, selection, open, onOpenChange, onChange, kbd = '/model', side = 'top', trigger = 'plain', finalFocus, footer }) {
|
|
23
21
|
const { t } = useTranslation('agent-chat');
|
|
@@ -96,10 +94,8 @@ export function ModelPicker({ providers, provider, selection, open, onOpenChange
|
|
|
96
94
|
})] }), footer !== undefined && _jsx("div", { className: "border-t border-border-soft px-3 py-2 text-xs text-text-faint", children: footer })] })] }));
|
|
97
95
|
}
|
|
98
96
|
/*
|
|
99
|
-
* Prompts put aside with Cmd+S
|
|
100
|
-
*
|
|
101
|
-
* place. Restoring drops the text, the mentions and the skills into this composer; the files a
|
|
102
|
-
* draft held are named on the row but not kept, because their bytes never go to storage.
|
|
97
|
+
* Prompts put aside with Cmd+S, on a shelf the whole app shares, since a stashed prompt usually moves
|
|
98
|
+
* to another chat. A draft's files are named on the row but not kept: their bytes never go to storage.
|
|
103
99
|
*/
|
|
104
100
|
export function StashPicker({ onRestore }) {
|
|
105
101
|
const { t } = useTranslation('agent-chat');
|
|
@@ -13,9 +13,8 @@ function lastLineOf(range) {
|
|
|
13
13
|
return rects.at(-1) ?? null;
|
|
14
14
|
}
|
|
15
15
|
/*
|
|
16
|
-
* A button under a selection in an answer that hands it to the composer.
|
|
17
|
-
*
|
|
18
|
-
* pointer. It only shows once the pointer is up, so a drag never has it jump along.
|
|
16
|
+
* A button under a selection in an answer that hands it to the composer. It shows once the pointer is
|
|
17
|
+
* up: quoting on the selection alone grew the composer mid-drag and moved the thread under the pointer.
|
|
19
18
|
*/
|
|
20
19
|
export function QuoteButton({ thread, frame, taker }) {
|
|
21
20
|
const { t } = useTranslation('agent-chat');
|
|
@@ -47,6 +47,12 @@ function contextTone(fraction) {
|
|
|
47
47
|
function modelValue(provider, slug) {
|
|
48
48
|
return `${provider}/${slug}`;
|
|
49
49
|
}
|
|
50
|
+
/* Each provider's current or legacy models; a provider with none of them is left out. */
|
|
51
|
+
function modelGroupsOf(providers, legacy) {
|
|
52
|
+
return providers
|
|
53
|
+
.map((entry) => ({ entry, models: entry.models.filter((row) => (row.legacy === true) === legacy) }))
|
|
54
|
+
.filter((group) => group.models.length > 0);
|
|
55
|
+
}
|
|
50
56
|
/*
|
|
51
57
|
* Everything that decides how the next turn runs, behind one pill: the model, its own knobs and the
|
|
52
58
|
* permission mode. Model and effective permissions stay visible as the composer narrows.
|
|
@@ -68,8 +74,8 @@ export function RunSettings({ disabled = false, providers, provider, selection,
|
|
|
68
74
|
summary.push({ id: CONTEXT_OPTION, label: formatTokens(usage.contextWindow ?? 0), ring: true });
|
|
69
75
|
}
|
|
70
76
|
const grouped = providers.length > 1;
|
|
71
|
-
const live = providers
|
|
72
|
-
const legacy = providers
|
|
77
|
+
const live = modelGroupsOf(providers, false);
|
|
78
|
+
const legacy = modelGroupsOf(providers, true);
|
|
73
79
|
const chosenLegacy = owner?.models.find((row) => row.legacy && row.slug === selection.model);
|
|
74
80
|
const chosenModel = modelValue(provider, selection.model);
|
|
75
81
|
const shownMode = effectiveRuntimeMode ?? runtimeMode;
|
package/dist/chat/ui/Scrubber.js
CHANGED
|
@@ -34,9 +34,8 @@ const CARD_ACTIONS = {
|
|
|
34
34
|
const NOTHING_FOUND = [];
|
|
35
35
|
/*
|
|
36
36
|
* The strip at the left edge of a chat view: a tick per message of the person, per wake by tasks and
|
|
37
|
-
* per other message
|
|
38
|
-
*
|
|
39
|
-
* open, a short mark at the strip's right edge stands beside every tick with a hit under it.
|
|
37
|
+
* per other bookmarked message, the ones on screen bright. One hover card follows the pointer, so a
|
|
38
|
+
* thousand ticks are a thousand spans and not a thousand popups. An open find marks every tick it hits.
|
|
40
39
|
*/
|
|
41
40
|
export const Scrubber = memo(function Scrubber({ ticks, firstInView, lastInView, onPick, chat = null, found = NOTHING_FOUND, foundCurrent = null }) {
|
|
42
41
|
const { t } = useTranslation('agent-chat');
|
|
@@ -7,9 +7,7 @@ import { breadcrumbOf, MAIN_AGENT, trailTo, useOpenableSubagents, useSubagentTra
|
|
|
7
7
|
import { SubagentStopButton } from './SubagentStopButton.js';
|
|
8
8
|
import { Icon, IconButton } from '@adecore/ui';
|
|
9
9
|
const CRUMB_LINK = 'truncate text-text-muted hover:text-text';
|
|
10
|
-
/*
|
|
11
|
-
* The chat's own title where the bar draws it. While a sub-agent stands in the chat's place it is the first crumb of the way down, so pressing it goes back to the main agent.
|
|
12
|
-
*/
|
|
10
|
+
/* The chat's own title in the bar; while a sub-agent stands in the chat's place, pressing it goes back to the main agent. */
|
|
13
11
|
export function SubagentTitleCrumb({ chatId, className, children }) {
|
|
14
12
|
const { trail, show } = useSubagentTrail(chatId);
|
|
15
13
|
if (trail.length === 0) {
|
package/dist/chat/ui/Timeline.js
CHANGED
|
@@ -37,6 +37,11 @@ import { AgentIcon } from '../../agents/AgentIcon.js';
|
|
|
37
37
|
import { useModelName } from '../../agents/model-name.js';
|
|
38
38
|
import { ContextMenu, EmptyState, ErrorBoundary, Icon, SectionLabel } from '@adecore/ui';
|
|
39
39
|
const ESTIMATED_ROW_PX = 56;
|
|
40
|
+
/* A jump puts its row at the top and stops following the end, so the reader stays where they landed. */
|
|
41
|
+
function showRowAtTop(virtualizer, followRef, index) {
|
|
42
|
+
followRef.current = false;
|
|
43
|
+
virtualizer.scrollToIndex(index, { align: 'start' });
|
|
44
|
+
}
|
|
40
45
|
/*
|
|
41
46
|
* A thread before its first message: which model answers, what the lines into this node let it read,
|
|
42
47
|
* and the folder it works in, so a first question can lean on all three.
|
|
@@ -191,7 +196,7 @@ export function Timeline({ chatId, composer, overlay }) {
|
|
|
191
196
|
useEffect(() => registerTimeline(chatId, scrollRef.current, (enabled) => {
|
|
192
197
|
followRef.current = enabled;
|
|
193
198
|
}, () => coveredHeightRef.current), [chatId]);
|
|
194
|
-
//
|
|
199
|
+
// Nothing here builds with the React Compiler, so its memoization rule has nothing to break.
|
|
195
200
|
// oxlint-disable-next-line react/incompatible-library
|
|
196
201
|
const virtualizer = useVirtualizer({
|
|
197
202
|
count: rows.length,
|
|
@@ -209,11 +214,9 @@ export function Timeline({ chatId, composer, overlay }) {
|
|
|
209
214
|
});
|
|
210
215
|
const tail = lastReplyLength ?? rows.length;
|
|
211
216
|
/*
|
|
212
|
-
*
|
|
213
|
-
* that
|
|
214
|
-
*
|
|
215
|
-
* The scroller's own bottom is a fact rather than an estimate, and the total size changing is
|
|
216
|
-
* what says a row was measured again, so this runs for every one of those.
|
|
217
|
+
* Rows are measured as they render, so the end moves while it is drawn (a diff that highlights, an
|
|
218
|
+
* image that loads). `scrollToIndex` aims at where a row was when asked and fell short; the
|
|
219
|
+
* scroller's own bottom is a fact, and a new total size says a row was measured again.
|
|
217
220
|
*/
|
|
218
221
|
const totalSize = virtualizer.getTotalSize();
|
|
219
222
|
useLayoutEffect(() => {
|
|
@@ -259,8 +262,7 @@ export function Timeline({ chatId, composer, overlay }) {
|
|
|
259
262
|
const paddingLeft = threadPaddingLeft(frame.width, showsScrubber);
|
|
260
263
|
/*
|
|
261
264
|
* Where each message starts, from the virtualizer's measurements (an estimate for a row it never
|
|
262
|
-
* drew), so reading the scroll position costs no layout.
|
|
263
|
-
* every scroll already.
|
|
265
|
+
* drew), so reading the scroll position costs no layout.
|
|
264
266
|
*/
|
|
265
267
|
const measurements = virtualizer.measurementsCache;
|
|
266
268
|
const starts = ticks.map((tick) => measurements[tick.rowIndex]?.start ?? tick.rowIndex * ESTIMATED_ROW_PX);
|
|
@@ -314,8 +316,7 @@ export function Timeline({ chatId, composer, overlay }) {
|
|
|
314
316
|
setSeeking({ itemId: tick.bookmark.itemId, key: keyOf(chatId) });
|
|
315
317
|
return;
|
|
316
318
|
}
|
|
317
|
-
followRef.
|
|
318
|
-
virtualizer.scrollToIndex(tick.rowIndex, { align: 'start' });
|
|
319
|
+
showRowAtTop(virtualizer, followRef, tick.rowIndex);
|
|
319
320
|
};
|
|
320
321
|
useLayoutEffect(() => {
|
|
321
322
|
if (seeking === null) {
|
|
@@ -336,8 +337,7 @@ export function Timeline({ chatId, composer, overlay }) {
|
|
|
336
337
|
else {
|
|
337
338
|
setSeeking(null);
|
|
338
339
|
if (jump.kind === 'row') {
|
|
339
|
-
followRef.
|
|
340
|
-
virtualizer.scrollToIndex(jump.index, { align: 'start' });
|
|
340
|
+
showRowAtTop(virtualizer, followRef, jump.index);
|
|
341
341
|
setLanding({ target: seeking.block, key: seeking.key });
|
|
342
342
|
}
|
|
343
343
|
}
|
|
@@ -346,8 +346,7 @@ export function Timeline({ chatId, composer, overlay }) {
|
|
|
346
346
|
const index = rows.findIndex((row) => row.id === seeking.itemId);
|
|
347
347
|
if (index !== -1) {
|
|
348
348
|
setSeeking(null);
|
|
349
|
-
followRef
|
|
350
|
-
virtualizer.scrollToIndex(index, { align: 'start' });
|
|
349
|
+
showRowAtTop(virtualizer, followRef, index);
|
|
351
350
|
return;
|
|
352
351
|
}
|
|
353
352
|
const item = items?.[seeking.itemId];
|
|
@@ -399,8 +398,7 @@ export function Timeline({ chatId, composer, overlay }) {
|
|
|
399
398
|
const found = changes !== -1 ? changes : rows.findIndex((row) => row.id === opening?.id || row.id === `start-${turnId}` || row.id === `fold-${turnId}`);
|
|
400
399
|
if (found !== -1) {
|
|
401
400
|
setRevealing(null);
|
|
402
|
-
followRef
|
|
403
|
-
virtualizer.scrollToIndex(found, { align: 'start' });
|
|
401
|
+
showRowAtTop(virtualizer, followRef, found);
|
|
404
402
|
useTimelineFlash.getState().flash(keyOf(chatId), rows[found].id, changes !== -1);
|
|
405
403
|
return;
|
|
406
404
|
}
|
|
@@ -452,8 +450,7 @@ export function Timeline({ chatId, composer, overlay }) {
|
|
|
452
450
|
return;
|
|
453
451
|
}
|
|
454
452
|
subagents.add(found.branch.id);
|
|
455
|
-
followRef.
|
|
456
|
-
virtualizer.scrollToIndex(found.index, { align: 'start' });
|
|
453
|
+
showRowAtTop(virtualizer, followRef, found.index);
|
|
457
454
|
};
|
|
458
455
|
return (_jsx(FileLinkContext.Provider, { value: info?.cwd ?? null, children: _jsxs("div", { ref: frameRef, className: "relative flex min-h-0 grow flex-col", "data-file-cwd": info?.cwd ?? '', "data-file-scope-id": scope.id, children: [searching && chatFind.bar, _jsxs(ContextMenu.Root, { children: [_jsx("div", { ref: scrollRef,
|
|
459
456
|
// No CSS scroll-padding here: Chromium counts the sticky composer's caret as hidden behind
|
|
@@ -9,11 +9,7 @@ import { useChatScope } from '../../scope.js';
|
|
|
9
9
|
import { useChatRow } from '../../state/chats.js';
|
|
10
10
|
import { openFileLink, useFileLinkCwd, useFileLinkScopeId } from './file-links.js';
|
|
11
11
|
import { ContextMenu, copyText, DisabledReason, EDIT_SHORTCUTS, Icon, Kbd, selectAllWithin } from '@adecore/ui';
|
|
12
|
-
/*
|
|
13
|
-
* The menu behind a right-click in a thread. Copy is the reason it exists. Everything in a thread
|
|
14
|
-
* is text a person may want out of it, and a whole message, a code block or the markdown an answer
|
|
15
|
-
* was written in are each a different amount of that.
|
|
16
|
-
*/
|
|
12
|
+
/* The menu behind a right-click in a thread, mostly for copying: a whole message, a code block or an answer's markdown. */
|
|
17
13
|
export function TimelineMenuPopup({ target, thread, chatId = null }) {
|
|
18
14
|
const { t } = useTranslation('agent-chat');
|
|
19
15
|
const message = target.row === null ? null : messageTextOf(target.row);
|
|
@@ -36,10 +32,8 @@ export function TimelineMenuPopup({ target, thread, chatId = null }) {
|
|
|
36
32
|
return (_jsxs(ContextMenu.Popup, { children: [_jsxs(ContextMenu.Item, { disabled: target.selection === '', onClick: () => copyText(target.selection), children: [_jsx(Icon, { icon: Copy, size: 14 }), " ", t('common.action.copy'), " ", _jsx(Kbd, { shortcut: EDIT_SHORTCUTS.copy })] }), message !== null && (_jsxs(ContextMenu.Item, { onClick: () => copyText(message), children: [_jsx(Icon, { icon: MessageSquare, size: 14 }), " ", t('timeline.menu.copyMessage')] })), target.code !== null && (_jsxs(ContextMenu.Item, { onClick: () => copyText(target.code ?? ''), children: [_jsx(Icon, { icon: Braces, size: 14 }), " ", t('timeline.menu.copyCode')] })), markdown !== null && (_jsxs(ContextMenu.Item, { onClick: () => copyText(markdown), children: [_jsx(Icon, { icon: FileText, size: 14 }), " ", t('timeline.menu.copyMarkdown')] })), chatId !== null && turnId !== null && fork !== null && (_jsx(DisabledReason, { reason: forkBlocked, children: _jsxs(ContextMenu.Item, { disabled: forkBlocked !== null, onClick: () => fork(chatId, turnId), children: [_jsx(Icon, { icon: GitFork, size: 14 }), " ", t('timeline.menu.forkFromHere')] }) })), chatId !== null && markable !== null && _jsx(BookmarkMenuItems, { chatId: chatId, itemId: markable }), _jsx(ContextMenu.Separator, {}), _jsxs(ContextMenu.Item, { onClick: () => selectAllWithin(thread.current), children: [_jsx(Icon, { icon: Scan, size: 14 }), " ", t('common.action.selectAll')] }), target.path !== null && fileLinks !== null && (_jsxs(_Fragment, { children: [_jsx(ContextMenu.Separator, {}), _jsxs(ContextMenu.Item, { onClick: openInPreview, children: [_jsx(Icon, { icon: Eye, size: 14 }), " ", t('timeline.menu.openInPreview')] })] }))] }));
|
|
37
33
|
}
|
|
38
34
|
/*
|
|
39
|
-
* Place a bookmark on a message, or rename and remove the one it has
|
|
40
|
-
*
|
|
41
|
-
* bookmark's line over the message and so has to be on screen; the strip scrolls there, the thread
|
|
42
|
-
* was clicked on it.
|
|
35
|
+
* Place a bookmark on a message, or rename and remove the one it has, from the thread and from the strip.
|
|
36
|
+
* `onName` runs when a row opens the name field, which sits over the message and so has to be on screen.
|
|
43
37
|
*/
|
|
44
38
|
export function BookmarkMenuItems({ chatId, itemId, onName }) {
|
|
45
39
|
const { t } = useTranslation('agent-chat');
|
|
@@ -6,9 +6,8 @@ import { chatHost } from '../../host.js';
|
|
|
6
6
|
import { useDiffTheme } from './diff-theme.js';
|
|
7
7
|
import { fullFileDiff } from './full-diff.js';
|
|
8
8
|
/*
|
|
9
|
-
* A patch the CLI reported itself
|
|
10
|
-
*
|
|
11
|
-
* A diff without hunks at all is shown as its own lines, so nothing is ever swallowed.
|
|
9
|
+
* A patch the CLI reported itself. The renderer needs a file header, which a CLI that only sends hunks
|
|
10
|
+
* gets from the path we know; a diff without hunks is shown as its own lines, so nothing is swallowed.
|
|
12
11
|
*/
|
|
13
12
|
function asPatch(change) {
|
|
14
13
|
if (!/^@@/m.test(change.diff)) {
|
|
@@ -21,8 +20,7 @@ function asPatch(change) {
|
|
|
21
20
|
const to = change.kind === 'delete' ? '/dev/null' : `b/${change.path}`;
|
|
22
21
|
return `--- ${from}\n+++ ${to}\n${change.diff}`;
|
|
23
22
|
}
|
|
24
|
-
/* Lets a diff with a view of its own run to
|
|
25
|
-
there instead of under its last line; the lines themselves keep their height. */
|
|
23
|
+
/* Lets a diff with a view of its own run to its bottom, so a short one's scrollbar sits there and not under its last line. */
|
|
26
24
|
const FILL_CSS = `
|
|
27
25
|
:host { display: flex; flex-direction: column; flex-grow: 1; }
|
|
28
26
|
pre { flex-grow: 1; align-content: start; }
|
|
@@ -19,10 +19,9 @@ function personActsIn(frame) {
|
|
|
19
19
|
return frame.ownerDocument.activeElement === frame && navigator.userActivation?.isActive !== false;
|
|
20
20
|
}
|
|
21
21
|
/*
|
|
22
|
-
* One visual's page in a frame
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* box keeps one height while the page loads and when it fails, so nothing under it moves.
|
|
22
|
+
* One visual's page in a frame that loads the app's sandbox host page; the bridge hands it the page and
|
|
23
|
+
* the theme. The frame never gets `allow-same-origin`, which keeps the page on an opaque origin even
|
|
24
|
+
* when the host page is served on the app's own. Its box keeps one height while it loads or fails.
|
|
26
25
|
*/
|
|
27
26
|
export function VisualFrame({ chatId, visual, fill = false, className }) {
|
|
28
27
|
const { t } = useTranslation('agent-chat');
|