@adecore/agents-react 0.21.1 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/model-requirement.d.ts +6 -0
- package/dist/agents/model-requirement.js +26 -0
- package/dist/agents/status-look.js +2 -3
- package/dist/chat/chat-client.d.ts +3 -2
- package/dist/chat/chat-client.js +4 -2
- package/dist/chat/drafts.d.ts +2 -1
- package/dist/chat/drafts.js +21 -17
- package/dist/chat/logic/ansi.js +5 -1
- package/dist/chat/logic/fork.js +8 -9
- package/dist/chat/logic/limit.js +17 -14
- package/dist/chat/logic/picker-entries.d.ts +10 -0
- package/dist/chat/logic/picker-entries.js +19 -0
- package/dist/chat/logic/resume-compaction.js +2 -11
- package/dist/chat/logic/scrubber.js +9 -18
- package/dist/chat/logic/timeline.d.ts +5 -0
- package/dist/chat/logic/timeline.js +25 -11
- package/dist/chat/logic/tool-catalog.js +3 -4
- package/dist/chat/mentions.js +5 -6
- package/dist/chat/prompt-hold.d.ts +8 -0
- package/dist/chat/prompt-hold.js +16 -0
- package/dist/chat/quote.d.ts +4 -0
- package/dist/chat/quote.js +19 -0
- package/dist/chat/runtime-modes.js +2 -4
- package/dist/chat/stash.d.ts +8 -2
- package/dist/chat/stash.js +22 -8
- package/dist/chat/subagent-list.d.ts +7 -1
- package/dist/chat/subagent-list.js +13 -1
- package/dist/chat/thumbnails.js +3 -4
- package/dist/chat/timeline-scroll.d.ts +6 -2
- package/dist/chat/timeline-scroll.js +45 -46
- package/dist/chat/ui/BookmarkSubmenu.js +1 -5
- package/dist/chat/ui/ChatActivity.js +22 -8
- package/dist/chat/ui/CodeBlock.js +2 -3
- package/dist/chat/ui/Composer.js +160 -88
- package/dist/chat/ui/ComposerInput.d.ts +3 -1
- package/dist/chat/ui/ComposerInput.js +13 -4
- package/dist/chat/ui/Markdown.js +11 -11
- package/dist/chat/ui/Pickers.js +31 -27
- package/dist/chat/ui/PromptComposer.d.ts +8 -1
- package/dist/chat/ui/PromptComposer.js +18 -6
- package/dist/chat/ui/QuoteButton.js +4 -5
- package/dist/chat/ui/RunSettings.js +8 -2
- package/dist/chat/ui/Scrubber.js +2 -3
- package/dist/chat/ui/SubagentControls.d.ts +4 -2
- package/dist/chat/ui/SubagentControls.js +13 -11
- package/dist/chat/ui/SubagentInfo.d.ts +1 -1
- package/dist/chat/ui/SubagentInfo.js +9 -15
- package/dist/chat/ui/SubagentStopButton.js +2 -1
- package/dist/chat/ui/Timeline.js +39 -21
- package/dist/chat/ui/TimelineMenu.js +3 -9
- package/dist/chat/ui/UnifiedDiff.js +3 -5
- package/dist/chat/ui/VisualFrame.js +3 -4
- package/dist/chat/ui/composer/keys.js +19 -11
- package/dist/chat/ui/composer/menu-aria.d.ts +10 -0
- package/dist/chat/ui/composer/menu-aria.js +18 -0
- package/dist/chat/ui/intelligent-ui/render-context.d.ts +1 -1
- package/dist/chat/ui/quote-selection.d.ts +5 -2
- package/dist/chat/ui/quote-selection.js +3 -2
- package/dist/chat/ui/rows/MessageRows.js +26 -8
- package/dist/chat/ui/rows/Rows.js +1 -1
- package/dist/chat/ui/rows/WorkRows.d.ts +7 -4
- package/dist/chat/ui/rows/WorkRows.js +22 -17
- package/dist/chat/visual-viewport.d.ts +1 -1
- package/dist/chat/visual-viewport.js +9 -8
- package/dist/host.d.ts +1 -0
- package/dist/host.js +1 -0
- package/dist/locales/en/agent-chat.json +30 -7
- package/dist/locales/nl/agent-chat.json +30 -7
- package/dist/port-transport.js +4 -8
- package/dist/state/chats.d.ts +2 -1
- package/dist/state/chats.js +29 -15
- package/dist/state/usage.js +44 -49
- package/dist/storage.js +7 -7
- package/dist/theme.css +6 -14
- package/dist/usage/LimitsList.js +1 -2
- package/dist/usage/UsageBreakdown.js +1 -3
- package/dist/usage/UsageDialog.js +2 -4
- package/dist/usage/format.js +6 -6
- package/dist/usage/money.js +1 -5
- package/dist/usage/summary.js +2 -4
- package/package.json +24 -4
- package/src/agents/model-requirement.ts +36 -0
- package/src/agents/status-look.ts +2 -3
- package/src/chat/chat-client.ts +7 -3
- package/src/chat/drafts.ts +33 -21
- package/src/chat/logic/ansi.ts +5 -1
- package/src/chat/logic/fork.ts +8 -9
- package/src/chat/logic/limit.ts +17 -16
- package/src/chat/logic/picker-entries.ts +25 -0
- package/src/chat/logic/resume-compaction.ts +2 -11
- package/src/chat/logic/scrubber.ts +13 -17
- package/src/chat/logic/timeline.ts +39 -13
- package/src/chat/logic/tool-catalog.ts +7 -6
- package/src/chat/mentions.ts +5 -6
- package/src/chat/prompt-hold.ts +27 -0
- package/src/chat/quote.ts +20 -0
- package/src/chat/runtime-modes.ts +2 -4
- package/src/chat/stash.ts +32 -8
- package/src/chat/subagent-list.ts +19 -5
- package/src/chat/thumbnails.ts +3 -4
- package/src/chat/timeline-scroll.ts +58 -53
- package/src/chat/ui/BookmarkSubmenu.tsx +1 -5
- package/src/chat/ui/ChatActivity.tsx +35 -9
- package/src/chat/ui/CodeBlock.tsx +2 -3
- package/src/chat/ui/Composer.tsx +263 -138
- package/src/chat/ui/ComposerInput.tsx +18 -3
- package/src/chat/ui/Markdown.tsx +13 -17
- package/src/chat/ui/Pickers.tsx +56 -39
- package/src/chat/ui/PromptComposer.tsx +37 -8
- package/src/chat/ui/QuoteButton.tsx +5 -5
- package/src/chat/ui/RunSettings.tsx +9 -2
- package/src/chat/ui/Scrubber.tsx +2 -3
- package/src/chat/ui/SubagentControls.tsx +23 -22
- package/src/chat/ui/SubagentInfo.tsx +24 -42
- package/src/chat/ui/SubagentStopButton.tsx +2 -1
- package/src/chat/ui/SubagentTimeline.tsx +1 -2
- package/src/chat/ui/Timeline.tsx +44 -24
- package/src/chat/ui/TimelineMenu.tsx +3 -9
- package/src/chat/ui/UnifiedDiff.tsx +3 -5
- package/src/chat/ui/VisualFrame.tsx +3 -4
- package/src/chat/ui/composer/keys.ts +21 -11
- package/src/chat/ui/composer/menu-aria.ts +29 -0
- package/src/chat/ui/intelligent-ui/render-context.ts +2 -2
- package/src/chat/ui/quote-selection.ts +6 -4
- package/src/chat/ui/rehype-chips.ts +1 -1
- package/src/chat/ui/rehype-fade.ts +1 -1
- package/src/chat/ui/remark-html-as-text.ts +1 -1
- package/src/chat/ui/rows/MessageRows.tsx +47 -9
- package/src/chat/ui/rows/Rows.tsx +3 -1
- package/src/chat/ui/rows/WorkRows.tsx +101 -70
- package/src/chat/visual-viewport.ts +10 -8
- package/src/host.ts +4 -3
- package/src/locales/en/agent-chat.json +30 -7
- package/src/locales/nl/agent-chat.json +30 -7
- package/src/port-transport.ts +4 -8
- package/src/state/chats.ts +42 -16
- package/src/state/usage.ts +49 -60
- package/src/storage.ts +7 -7
- package/src/theme.css +6 -14
- package/src/usage/LimitsList.tsx +2 -4
- package/src/usage/UsageBreakdown.tsx +2 -5
- package/src/usage/UsageDialog.tsx +2 -4
- package/src/usage/format.ts +6 -6
- package/src/usage/money.ts +1 -5
- package/src/usage/summary.ts +2 -4
|
@@ -1,20 +1,18 @@
|
|
|
1
|
-
import { Fragment
|
|
2
|
-
import clsx from 'clsx';
|
|
1
|
+
import { Fragment } from 'react';
|
|
3
2
|
import { useTranslation } from 'react-i18next';
|
|
4
|
-
import { ChevronDown } from 'lucide-react';
|
|
5
3
|
import { entryTimeOf, statusWordOf, subagentFacts, taskIdOf } from '../subagent-list';
|
|
6
4
|
import { useSubagentItem } from '../subagent-view';
|
|
7
5
|
import { StatusIcon } from './ChatActivity';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
6
|
+
import { SubagentActions, SubagentBreadcrumb } from './SubagentControls';
|
|
7
|
+
import { useNow } from '@adecore/ui';
|
|
10
8
|
import { useModelName } from '../../agents/model-name';
|
|
11
9
|
import { useChatRow } from '../../state/chats';
|
|
12
10
|
import { chatHost } from '../../host';
|
|
13
11
|
import { useChatScope } from '../../scope';
|
|
14
12
|
|
|
15
13
|
/*
|
|
16
|
-
* What a sub-agent is, over its conversation: its state and time, the model it runs on
|
|
17
|
-
* spent, and the
|
|
14
|
+
* What a sub-agent is, over its conversation: the way down to it, its state and time, the model it runs on
|
|
15
|
+
* and what it spent, and the way out. Only the way down and out for a conversation whose row the thread lost.
|
|
18
16
|
*/
|
|
19
17
|
export function SubagentInfo({ chatId, toolUseId }: { chatId: string; toolUseId: string }) {
|
|
20
18
|
const { t } = useTranslation('agent-chat');
|
|
@@ -24,43 +22,27 @@ export function SubagentInfo({ chatId, toolUseId }: { chatId: string; toolUseId:
|
|
|
24
22
|
const task = chatHost().tasks.useTask(id, item === null ? null : taskIdOf(item));
|
|
25
23
|
const modelName = useModelName(provider, item?.model ?? '');
|
|
26
24
|
const now = useNow(1000, item?.status === 'running');
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
return null;
|
|
30
|
-
}
|
|
31
|
-
const word = statusWordOf(item, task);
|
|
32
|
-
const facts = subagentFacts(item, item.model === undefined ? null : modelName);
|
|
25
|
+
const word = item === null ? null : statusWordOf(item, task);
|
|
26
|
+
const facts = item === null ? [] : subagentFacts(item, item.model === undefined ? null : modelName);
|
|
33
27
|
return (
|
|
34
|
-
<section aria-label={t('subagents.info.label')} className="shrink-0 border-b border-border px-4 py-
|
|
35
|
-
<div className="
|
|
36
|
-
<
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
<
|
|
42
|
-
|
|
43
|
-
<span className="min-w-0 truncate">{fact}</span>
|
|
44
|
-
</Fragment>
|
|
45
|
-
))}
|
|
46
|
-
<span className="grow" />
|
|
47
|
-
{item.prompt && (
|
|
48
|
-
<button
|
|
49
|
-
type="button"
|
|
50
|
-
className="flex shrink-0 items-center gap-1.5 text-text-muted hover:text-text"
|
|
51
|
-
aria-expanded={open}
|
|
52
|
-
onClick={() => setOpen((current) => !current)}
|
|
53
|
-
>
|
|
54
|
-
<Icon icon={ChevronDown} size={12} className={clsx('transition-transform', open && 'rotate-180')} />
|
|
55
|
-
{open ? t('subagents.info.hideTask') : t('subagents.info.showTask')}
|
|
56
|
-
</button>
|
|
57
|
-
)}
|
|
58
|
-
</div>
|
|
59
|
-
{open && item.prompt && (
|
|
60
|
-
<div className="max-h-60 overflow-auto rounded-md border border-border bg-surface-raised px-3 py-2 text-xs select-text">
|
|
61
|
-
<Markdown text={item.prompt} />
|
|
62
|
-
</div>
|
|
28
|
+
<section aria-label={t('subagents.info.label')} className="shrink-0 border-b border-border px-4 py-1.5">
|
|
29
|
+
<div className="flex min-w-0 items-center gap-2 text-xs text-text-faint">
|
|
30
|
+
<SubagentBreadcrumb chatId={chatId} className="max-w-1/3 shrink-0 font-medium" />
|
|
31
|
+
{item !== null && word !== null && (
|
|
32
|
+
<>
|
|
33
|
+
<StatusIcon word={word} />
|
|
34
|
+
<span className="sr-only">{t(`common.status.${word}`)}</span>
|
|
35
|
+
<span className="shrink-0 text-text-muted tabular-nums">{entryTimeOf(item, task, now)}</span>
|
|
36
|
+
</>
|
|
63
37
|
)}
|
|
38
|
+
{facts.map((fact) => (
|
|
39
|
+
<Fragment key={fact}>
|
|
40
|
+
<span aria-hidden="true">·</span>
|
|
41
|
+
<span className="min-w-0 truncate">{fact}</span>
|
|
42
|
+
</Fragment>
|
|
43
|
+
))}
|
|
44
|
+
<span className="grow" />
|
|
45
|
+
<SubagentActions chatId={chatId} />
|
|
64
46
|
</div>
|
|
65
47
|
</section>
|
|
66
48
|
);
|
|
@@ -30,7 +30,8 @@ export function SubagentStopButton({
|
|
|
30
30
|
}
|
|
31
31
|
const send = (): void => {
|
|
32
32
|
void actions.stopSubagent(chatId, item.toolUseId).catch((e: unknown) => {
|
|
33
|
-
const
|
|
33
|
+
const stale = e instanceof Error && 'code' in e && e.code === 'subagent-stale';
|
|
34
|
+
const description = stale ? t('subagents.stale') : e instanceof Error ? e.message : t('subagents.noAnswer');
|
|
34
35
|
chatHost().notify({ kind: 'error', title: t('subagents.stopFailed'), description });
|
|
35
36
|
});
|
|
36
37
|
};
|
|
@@ -136,8 +136,7 @@ export function SubagentTimeline({ chatId, toolUseId }: { chatId: string; toolUs
|
|
|
136
136
|
loadEarlier();
|
|
137
137
|
}}
|
|
138
138
|
>
|
|
139
|
-
{/* The
|
|
140
|
-
it belongs to is not passed on and the fork item stays out. */}
|
|
139
|
+
{/* The chat's own menu; this transcript is read-only, so no chat is passed and the fork item stays out. */}
|
|
141
140
|
<ContextMenu.Root>
|
|
142
141
|
<ContextMenu.Trigger
|
|
143
142
|
ref={threadRef}
|
package/src/chat/ui/Timeline.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { type CSSProperties, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type ReactNode } from 'react';
|
|
1
|
+
import { type CSSProperties, type RefObject, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type ReactNode } from 'react';
|
|
2
2
|
import { useTranslation } from 'react-i18next';
|
|
3
|
-
import { useVirtualizer } from '@tanstack/react-virtual';
|
|
3
|
+
import { useVirtualizer, type Virtualizer } from '@tanstack/react-virtual';
|
|
4
4
|
import clsx from 'clsx';
|
|
5
5
|
import { LoaderCircle } from 'lucide-react';
|
|
6
6
|
import type { ChatBookmark } from '@adecore/agent-contracts';
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
registerTurnJumper,
|
|
17
17
|
setTimelineAtEnd,
|
|
18
18
|
wantsEarlier,
|
|
19
|
+
type ItemJumpOptions,
|
|
19
20
|
type ReadingAnchor,
|
|
20
21
|
type TurnTarget
|
|
21
22
|
} from '../timeline-scroll';
|
|
@@ -66,6 +67,14 @@ import { useModelName } from '../../agents/model-name';
|
|
|
66
67
|
import { ContextMenu, EmptyState, ErrorBoundary, Icon, SectionLabel } from '@adecore/ui';
|
|
67
68
|
|
|
68
69
|
const ESTIMATED_ROW_PX = 56;
|
|
70
|
+
// How long a jump waits for a thread that is still loading before it calls the item missing.
|
|
71
|
+
const JUMP_THREAD_WAIT_MS = 8000;
|
|
72
|
+
|
|
73
|
+
/* A jump puts its row at the top and stops following the end, so the reader stays where they landed. */
|
|
74
|
+
function showRowAtTop(virtualizer: Virtualizer<HTMLDivElement, Element>, followRef: RefObject<boolean>, index: number): void {
|
|
75
|
+
followRef.current = false;
|
|
76
|
+
virtualizer.scrollToIndex(index, { align: 'start' });
|
|
77
|
+
}
|
|
69
78
|
|
|
70
79
|
/*
|
|
71
80
|
* A thread before its first message: which model answers, what the lines into this node let it read,
|
|
@@ -216,7 +225,7 @@ export function Timeline({ chatId, composer, overlay }: { chatId: string; compos
|
|
|
216
225
|
const marks = useMemo(() => bookmarkRows(rows, bookmarks ?? [], items ?? {}), [rows, bookmarks, items]);
|
|
217
226
|
const ticks = useMemo(() => ticksOf(rows, marks), [rows, marks]);
|
|
218
227
|
// A message a jump is on its way to; it may first have to open the turn its reply folded into.
|
|
219
|
-
const [seeking, setSeeking] = useState<{ itemId: string; block?: UiReplyTarget; key: string } | null>(null);
|
|
228
|
+
const [seeking, setSeeking] = useState<{ itemId: string; block?: UiReplyTarget; key: string; options?: ItemJumpOptions } | null>(null);
|
|
220
229
|
const [landing, setLanding] = useState<{ target: UiReplyTarget; key: string } | null>(null);
|
|
221
230
|
// A turn a jump is on its way to, which waits for the page its messages are on to come in.
|
|
222
231
|
const [revealing, setRevealing] = useState<{ turnId: string; target: TurnTarget } | null>(null);
|
|
@@ -288,7 +297,7 @@ export function Timeline({ chatId, composer, overlay }: { chatId: string; compos
|
|
|
288
297
|
[chatId]
|
|
289
298
|
);
|
|
290
299
|
|
|
291
|
-
//
|
|
300
|
+
// Nothing here builds with the React Compiler, so its memoization rule has nothing to break.
|
|
292
301
|
// oxlint-disable-next-line react/incompatible-library
|
|
293
302
|
const virtualizer = useVirtualizer({
|
|
294
303
|
count: rows.length,
|
|
@@ -307,11 +316,9 @@ export function Timeline({ chatId, composer, overlay }: { chatId: string; compos
|
|
|
307
316
|
});
|
|
308
317
|
const tail = lastReplyLength ?? rows.length;
|
|
309
318
|
/*
|
|
310
|
-
*
|
|
311
|
-
* that
|
|
312
|
-
*
|
|
313
|
-
* The scroller's own bottom is a fact rather than an estimate, and the total size changing is
|
|
314
|
-
* what says a row was measured again, so this runs for every one of those.
|
|
319
|
+
* Rows are measured as they render, so the end moves while it is drawn (a diff that highlights, an
|
|
320
|
+
* image that loads). `scrollToIndex` aims at where a row was when asked and fell short; the
|
|
321
|
+
* scroller's own bottom is a fact, and a new total size says a row was measured again.
|
|
315
322
|
*/
|
|
316
323
|
const totalSize = virtualizer.getTotalSize();
|
|
317
324
|
useLayoutEffect(() => {
|
|
@@ -361,8 +368,7 @@ export function Timeline({ chatId, composer, overlay }: { chatId: string; compos
|
|
|
361
368
|
|
|
362
369
|
/*
|
|
363
370
|
* Where each message starts, from the virtualizer's measurements (an estimate for a row it never
|
|
364
|
-
* drew), so reading the scroll position costs no layout.
|
|
365
|
-
* every scroll already.
|
|
371
|
+
* drew), so reading the scroll position costs no layout.
|
|
366
372
|
*/
|
|
367
373
|
const measurements = virtualizer.measurementsCache;
|
|
368
374
|
const starts = ticks.map((tick) => measurements[tick.rowIndex]?.start ?? tick.rowIndex * ESTIMATED_ROW_PX);
|
|
@@ -418,8 +424,7 @@ export function Timeline({ chatId, composer, overlay }: { chatId: string; compos
|
|
|
418
424
|
setSeeking({ itemId: tick.bookmark.itemId, key: keyOf(chatId) });
|
|
419
425
|
return;
|
|
420
426
|
}
|
|
421
|
-
followRef.
|
|
422
|
-
virtualizer.scrollToIndex(tick.rowIndex, { align: 'start' });
|
|
427
|
+
showRowAtTop(virtualizer, followRef, tick.rowIndex);
|
|
423
428
|
};
|
|
424
429
|
|
|
425
430
|
useLayoutEffect(() => {
|
|
@@ -439,8 +444,7 @@ export function Timeline({ chatId, composer, overlay }: { chatId: string; compos
|
|
|
439
444
|
} else {
|
|
440
445
|
setSeeking(null);
|
|
441
446
|
if (jump.kind === 'row') {
|
|
442
|
-
followRef.
|
|
443
|
-
virtualizer.scrollToIndex(jump.index, { align: 'start' });
|
|
447
|
+
showRowAtTop(virtualizer, followRef, jump.index);
|
|
444
448
|
setLanding({ target: seeking.block, key: seeking.key });
|
|
445
449
|
}
|
|
446
450
|
}
|
|
@@ -449,11 +453,17 @@ export function Timeline({ chatId, composer, overlay }: { chatId: string; compos
|
|
|
449
453
|
const index = rows.findIndex((row) => row.id === seeking.itemId);
|
|
450
454
|
if (index !== -1) {
|
|
451
455
|
setSeeking(null);
|
|
452
|
-
followRef
|
|
453
|
-
|
|
456
|
+
showRowAtTop(virtualizer, followRef, index);
|
|
457
|
+
if (seeking.options?.flash) {
|
|
458
|
+
useTimelineFlash.getState().flash(seeking.key, seeking.itemId, false);
|
|
459
|
+
}
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
// The thread has not loaded yet, so nothing says the item is missing; the effect below bounds the wait.
|
|
463
|
+
if (items === undefined) {
|
|
454
464
|
return;
|
|
455
465
|
}
|
|
456
|
-
const item = items
|
|
466
|
+
const item = items[seeking.itemId];
|
|
457
467
|
if (item === undefined && cursor !== null) {
|
|
458
468
|
loadEarlier();
|
|
459
469
|
return;
|
|
@@ -464,12 +474,24 @@ export function Timeline({ chatId, composer, overlay }: { chatId: string; compos
|
|
|
464
474
|
return;
|
|
465
475
|
}
|
|
466
476
|
setSeeking(null);
|
|
477
|
+
seeking.options?.onMissing?.();
|
|
467
478
|
}, [seeking, rows, items, turns, virtualizer, cursor, loadEarlier, keyOf, chatId]);
|
|
479
|
+
const waitingForThread = seeking !== null && items === undefined;
|
|
480
|
+
useEffect(() => {
|
|
481
|
+
if (!waitingForThread) {
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
const timer = window.setTimeout(() => {
|
|
485
|
+
setSeeking(null);
|
|
486
|
+
seeking?.options?.onMissing?.();
|
|
487
|
+
}, JUMP_THREAD_WAIT_MS);
|
|
488
|
+
return () => window.clearTimeout(timer);
|
|
489
|
+
}, [waitingForThread, seeking]);
|
|
468
490
|
useEffect(
|
|
469
491
|
() =>
|
|
470
|
-
registerItemJumper(keyOf(chatId), (itemId) => {
|
|
492
|
+
registerItemJumper(keyOf(chatId), (itemId, options) => {
|
|
471
493
|
setLanding(null);
|
|
472
|
-
setSeeking({ itemId, key: keyOf(chatId) });
|
|
494
|
+
setSeeking({ itemId, key: keyOf(chatId), options });
|
|
473
495
|
}),
|
|
474
496
|
[keyOf, chatId]
|
|
475
497
|
);
|
|
@@ -505,8 +527,7 @@ export function Timeline({ chatId, composer, overlay }: { chatId: string; compos
|
|
|
505
527
|
const found = changes !== -1 ? changes : rows.findIndex((row) => row.id === opening?.id || row.id === `start-${turnId}` || row.id === `fold-${turnId}`);
|
|
506
528
|
if (found !== -1) {
|
|
507
529
|
setRevealing(null);
|
|
508
|
-
followRef
|
|
509
|
-
virtualizer.scrollToIndex(found, { align: 'start' });
|
|
530
|
+
showRowAtTop(virtualizer, followRef, found);
|
|
510
531
|
useTimelineFlash.getState().flash(keyOf(chatId), rows[found]!.id, changes !== -1);
|
|
511
532
|
return;
|
|
512
533
|
}
|
|
@@ -564,8 +585,7 @@ export function Timeline({ chatId, composer, overlay }: { chatId: string; compos
|
|
|
564
585
|
return;
|
|
565
586
|
}
|
|
566
587
|
subagents.add(found.branch.id);
|
|
567
|
-
followRef.
|
|
568
|
-
virtualizer.scrollToIndex(found.index, { align: 'start' });
|
|
588
|
+
showRowAtTop(virtualizer, followRef, found.index);
|
|
569
589
|
};
|
|
570
590
|
|
|
571
591
|
return (
|
|
@@ -11,11 +11,7 @@ import { useChatRow } from '../../state/chats';
|
|
|
11
11
|
import { openFileLink, useFileLinkCwd, useFileLinkScopeId } from './file-links';
|
|
12
12
|
import { ContextMenu, copyText, DisabledReason, EDIT_SHORTCUTS, Icon, Kbd, selectAllWithin } from '@adecore/ui';
|
|
13
13
|
|
|
14
|
-
/*
|
|
15
|
-
* The menu behind a right-click in a thread. Copy is the reason it exists. Everything in a thread
|
|
16
|
-
* is text a person may want out of it, and a whole message, a code block or the markdown an answer
|
|
17
|
-
* was written in are each a different amount of that.
|
|
18
|
-
*/
|
|
14
|
+
/* The menu behind a right-click in a thread, mostly for copying: a whole message, a code block or an answer's markdown. */
|
|
19
15
|
export function TimelineMenuPopup({
|
|
20
16
|
target,
|
|
21
17
|
thread,
|
|
@@ -88,10 +84,8 @@ export function TimelineMenuPopup({
|
|
|
88
84
|
}
|
|
89
85
|
|
|
90
86
|
/*
|
|
91
|
-
* Place a bookmark on a message, or rename and remove the one it has
|
|
92
|
-
*
|
|
93
|
-
* bookmark's line over the message and so has to be on screen; the strip scrolls there, the thread
|
|
94
|
-
* was clicked on it.
|
|
87
|
+
* Place a bookmark on a message, or rename and remove the one it has, from the thread and from the strip.
|
|
88
|
+
* `onName` runs when a row opens the name field, which sits over the message and so has to be on screen.
|
|
95
89
|
*/
|
|
96
90
|
export function BookmarkMenuItems({ chatId, itemId, onName }: { chatId: string; itemId: string; onName?: () => void }) {
|
|
97
91
|
const { t } = useTranslation('agent-chat');
|
|
@@ -7,9 +7,8 @@ import { useDiffTheme } from './diff-theme';
|
|
|
7
7
|
import { fullFileDiff, type DiffContents } from './full-diff';
|
|
8
8
|
|
|
9
9
|
/*
|
|
10
|
-
* A patch the CLI reported itself
|
|
11
|
-
*
|
|
12
|
-
* A diff without hunks at all is shown as its own lines, so nothing is ever swallowed.
|
|
10
|
+
* A patch the CLI reported itself. The renderer needs a file header, which a CLI that only sends hunks
|
|
11
|
+
* gets from the path we know; a diff without hunks is shown as its own lines, so nothing is swallowed.
|
|
13
12
|
*/
|
|
14
13
|
function asPatch(change: ChatFileChange): string | null {
|
|
15
14
|
if (!/^@@/m.test(change.diff)) {
|
|
@@ -23,8 +22,7 @@ function asPatch(change: ChatFileChange): string | null {
|
|
|
23
22
|
return `--- ${from}\n+++ ${to}\n${change.diff}`;
|
|
24
23
|
}
|
|
25
24
|
|
|
26
|
-
/* Lets a diff with a view of its own run to
|
|
27
|
-
there instead of under its last line; the lines themselves keep their height. */
|
|
25
|
+
/* 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. */
|
|
28
26
|
const FILL_CSS = `
|
|
29
27
|
:host { display: flex; flex-direction: column; flex-grow: 1; }
|
|
30
28
|
pre { flex-grow: 1; align-content: start; }
|
|
@@ -28,10 +28,9 @@ function personActsIn(frame: HTMLIFrameElement): boolean {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/*
|
|
31
|
-
* One visual's page in a frame
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* box keeps one height while the page loads and when it fails, so nothing under it moves.
|
|
31
|
+
* One visual's page in a frame that loads the app's sandbox host page; the bridge hands it the page and
|
|
32
|
+
* the theme. The frame never gets `allow-same-origin`, which keeps the page on an opaque origin even
|
|
33
|
+
* when the host page is served on the app's own. Its box keeps one height while it loads or fails.
|
|
35
34
|
*/
|
|
36
35
|
export function VisualFrame({ chatId, visual, fill = false, className }: VisualFrameProps) {
|
|
37
36
|
const { t } = useTranslation('agent-chat');
|
|
@@ -60,25 +60,35 @@ export function enterAction(keys: EnterKeys, { inOpenFence, list, column }: Ente
|
|
|
60
60
|
return list.empty ? 'leave-list' : 'continue-list';
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
/*
|
|
64
|
-
|
|
63
|
+
/* The innermost fenced code block around `pos`, if any. */
|
|
64
|
+
function enclosingFence(tree: Tree, pos: number): SyntaxNode | null {
|
|
65
65
|
for (let node: SyntaxNode | null = tree.resolveInner(pos, -1); node !== null; node = node.parent) {
|
|
66
66
|
if (node.name === 'FencedCode') {
|
|
67
|
-
return
|
|
67
|
+
return node;
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
|
-
return
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* A fence nobody has closed yet has its opening mark only. */
|
|
74
|
+
function isOpenFence(fence: SyntaxNode): boolean {
|
|
75
|
+
return fence.getChildren('CodeMark').length < 2;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* Whether `pos` sits inside a fenced code block that has an opening fence and no closing one yet. */
|
|
79
|
+
export function inOpenFence(tree: Tree, pos: number): boolean {
|
|
80
|
+
const fence = enclosingFence(tree, pos);
|
|
81
|
+
return fence !== null && pos > fence.from && isOpenFence(fence);
|
|
71
82
|
}
|
|
72
83
|
|
|
73
84
|
/* Whether `pos` sits in the body of a fenced code block, open or closed: below its opening line and before its end. */
|
|
74
85
|
export function inFenceBody(tree: Tree, text: string, pos: number): boolean {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
return openingEnd !== -1 && pos > openingEnd && (pos < node.to || node.getChildren('CodeMark').length < 2);
|
|
79
|
-
}
|
|
86
|
+
const fence = enclosingFence(tree, pos);
|
|
87
|
+
if (fence === null) {
|
|
88
|
+
return false;
|
|
80
89
|
}
|
|
81
|
-
|
|
90
|
+
const openingEnd = text.indexOf('\n', fence.from);
|
|
91
|
+
return openingEnd !== -1 && pos > openingEnd && (pos < fence.to || isOpenFence(fence));
|
|
82
92
|
}
|
|
83
93
|
|
|
84
94
|
/* The spaces Tab inserts at `column`, up to the next stop of four, the way a code editor lines up. */
|
|
@@ -97,7 +107,7 @@ export function inCode(tree: Tree, text: string, pos: number): boolean {
|
|
|
97
107
|
let blockFrom: number | null = null;
|
|
98
108
|
for (let node: SyntaxNode | null = tree.resolveInner(pos, -1); node !== null; node = node.parent) {
|
|
99
109
|
if (node.name === 'FencedCode') {
|
|
100
|
-
return pos > node.from && (pos < node.to || node
|
|
110
|
+
return pos > node.from && (pos < node.to || isOpenFence(node));
|
|
101
111
|
}
|
|
102
112
|
if (node.name === 'InlineCode' && pos > node.from && pos < node.to) {
|
|
103
113
|
return true;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface ComposerMenuAriaInput {
|
|
2
|
+
label: string;
|
|
3
|
+
listId: string;
|
|
4
|
+
open: boolean;
|
|
5
|
+
/* Counts across every group of the menu, the way the keyboard moves through it. */
|
|
6
|
+
activeIndex: number;
|
|
7
|
+
count: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/* The index that still points at an option once the list has shrunk under a stale one. */
|
|
11
|
+
export function clampMenuIndex(index: number, count: number): number {
|
|
12
|
+
return Math.min(index, Math.max(count - 1, 0));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function optionIdOf(listId: string, index: number): string {
|
|
16
|
+
return `${listId}-${index}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/*
|
|
20
|
+
* The attributes the editor's content element takes while a suggestion menu stands beside it. The
|
|
21
|
+
* element keeps its `textbox` role; ARIA 1.2 lets it name the option the arrow keys are on.
|
|
22
|
+
*/
|
|
23
|
+
export function composerMenuAria({ label, listId, open, activeIndex, count }: ComposerMenuAriaInput): Record<string, string> {
|
|
24
|
+
return {
|
|
25
|
+
'aria-label': label,
|
|
26
|
+
'aria-autocomplete': 'list',
|
|
27
|
+
...(open && count > 0 ? { 'aria-controls': listId, 'aria-activedescendant': optionIdOf(listId, activeIndex) } : {})
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { createContext, type ReactNode } from 'react';
|
|
2
2
|
import type { UiComponentName, UiProps, UiViewNode } from '@adecore/intelligent-ui';
|
|
3
|
-
|
|
4
|
-
export type { UiLinkTarget } from '@adecore/intelligent-ui/links';
|
|
5
3
|
import type { UiLinkTarget } from '@adecore/intelligent-ui/links';
|
|
6
4
|
|
|
5
|
+
export type { UiLinkTarget };
|
|
6
|
+
|
|
7
7
|
/* How the host lets a link be drawn: a chip it can open, or plain text it cannot. */
|
|
8
8
|
export interface UiLink {
|
|
9
9
|
state: 'chip' | 'plain';
|
|
@@ -4,7 +4,8 @@ import { quoteOf } from '../quote';
|
|
|
4
4
|
/* The text of an answer (`rows/MessageRows.tsx`), the only part of a thread a selection quotes from. */
|
|
5
5
|
const ANSWER = '[data-quote-answer]';
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
/* `itemId` is the reply the quote came from, when the selection sits in a row that names one. */
|
|
8
|
+
export type QuoteTaker = (quote: string, itemId: string | null) => void;
|
|
8
9
|
|
|
9
10
|
/* Registers the composer below a thread as the one its quote button hands a quote to, until the returned call. */
|
|
10
11
|
export const QuoteTakerContext = createContext<((take: QuoteTaker) => () => void) | null>(null);
|
|
@@ -35,8 +36,8 @@ function selectsOutside(range: Range, answer: Element): boolean {
|
|
|
35
36
|
return false;
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
/* The quote the selection makes, when it lies in one answer of this thread. */
|
|
39
|
-
export function selectedAnswerQuote(selection: Selection | null, thread: HTMLElement | null): string | null {
|
|
39
|
+
/* The quote the selection makes, when it lies in one answer of this thread, and the reply it was taken from. */
|
|
40
|
+
export function selectedAnswerQuote(selection: Selection | null, thread: HTMLElement | null): { text: string; itemId: string | null } | null {
|
|
40
41
|
if (thread === null || selection === null || selection.isCollapsed || selection.rangeCount === 0) {
|
|
41
42
|
return null;
|
|
42
43
|
}
|
|
@@ -49,5 +50,6 @@ export function selectedAnswerQuote(selection: Selection | null, thread: HTMLEle
|
|
|
49
50
|
if (!inside && selectsOutside(range, answer)) {
|
|
50
51
|
return null;
|
|
51
52
|
}
|
|
52
|
-
|
|
53
|
+
const text = quoteOf(selection.toString());
|
|
54
|
+
return text === null ? null : { text, itemId: answer.closest('[data-find-item]')?.getAttribute('data-find-item') ?? null };
|
|
53
55
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { chipText, tokenizeChips, type ChipSegment } from '../mentions';
|
|
2
2
|
|
|
3
|
-
/* Just the part of hast this plugin touches; the package's own types are not a dependency
|
|
3
|
+
/* Just the part of hast this plugin touches; the package's own types are not a dependency here. */
|
|
4
4
|
interface HastText {
|
|
5
5
|
type: 'text';
|
|
6
6
|
value: string;
|
|
@@ -11,10 +11,13 @@ import type {
|
|
|
11
11
|
ChatUiChoiceOrigin,
|
|
12
12
|
ChatUserItem
|
|
13
13
|
} from '@adecore/agent-contracts';
|
|
14
|
+
import { WAITING_NOTE_PREFIX } from '@adecore/agent-contracts';
|
|
14
15
|
import { fileBadge, formatBytes, isImageAttachment } from '../../attachments';
|
|
15
16
|
import { useChatRow, useCurrentItem } from '../../../state/chats';
|
|
16
17
|
import { chatHost } from '../../../host';
|
|
17
18
|
import { useChatScope } from '../../../scope';
|
|
19
|
+
import { splitQuote } from '../../quote';
|
|
20
|
+
import { jumpToTimelineItem } from '../../timeline-scroll';
|
|
18
21
|
import { useProviders } from '../../../state/providers';
|
|
19
22
|
import { ChatReferenceChip } from '../ChatReferenceChip';
|
|
20
23
|
import { ImageThumb } from '../ImageView';
|
|
@@ -86,7 +89,19 @@ export function UserRow({ chatId, item }: { chatId: string; item: ChatUserItem }
|
|
|
86
89
|
const long = item.text.split('\n').length > USER_FOLD_LINES || item.text.length > USER_FOLD_CHARS;
|
|
87
90
|
const attachments = item.attachments ?? [];
|
|
88
91
|
const chats = item.chats ?? [];
|
|
92
|
+
const [missing, setMissing] = useState(false);
|
|
93
|
+
const { keyOf } = useChatScope();
|
|
89
94
|
useOpenForFind(item.id, 'text', setOpen);
|
|
95
|
+
// A quote only leads somewhere when the message still opens with the blockquote it was sent with.
|
|
96
|
+
const quoted = item.quoteSource === undefined ? null : splitQuote(item.text);
|
|
97
|
+
const source = item.quoteSource?.itemId;
|
|
98
|
+
|
|
99
|
+
function goToSource(): void {
|
|
100
|
+
if (source !== undefined) {
|
|
101
|
+
setMissing(false);
|
|
102
|
+
jumpToTimelineItem(keyOf(chatId), source, { flash: true, onMissing: () => setMissing(true) });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
90
105
|
return (
|
|
91
106
|
<div data-find-item={item.id} className="flex flex-col items-end">
|
|
92
107
|
<MessageHeading>{t('rows.user.heading')}</MessageHeading>
|
|
@@ -112,7 +127,31 @@ export function UserRow({ chatId, item }: { chatId: string; item: ChatUserItem }
|
|
|
112
127
|
{item.text !== '' && (
|
|
113
128
|
<div className="relative max-w-[80%] rounded-2xl bg-surface-active px-3.5 py-2.5 text-sm wrap-anywhere text-text select-text">
|
|
114
129
|
<div data-find-field="text" className={clsx(long && !open && FOLD)}>
|
|
115
|
-
|
|
130
|
+
{quoted !== null && source !== undefined ? (
|
|
131
|
+
<>
|
|
132
|
+
{/* A div, since the quote is block markdown and a button holds none. */}
|
|
133
|
+
<Tooltip label={t('composer.quote.goToSource')}>
|
|
134
|
+
<div
|
|
135
|
+
role="button"
|
|
136
|
+
tabIndex={0}
|
|
137
|
+
className="mb-2 cursor-pointer rounded-sm border-l-2 border-border-strong pl-2.5 text-text-muted hover:bg-text/5 hover:text-text focus-visible:outline-2 focus-visible:outline-accent"
|
|
138
|
+
onClick={goToSource}
|
|
139
|
+
onKeyDown={(e) => {
|
|
140
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
141
|
+
e.preventDefault();
|
|
142
|
+
goToSource();
|
|
143
|
+
}
|
|
144
|
+
}}
|
|
145
|
+
>
|
|
146
|
+
<MessageMarkdown text={quoted.quote} />
|
|
147
|
+
</div>
|
|
148
|
+
</Tooltip>
|
|
149
|
+
{missing && <div className="mb-2 text-xs text-text-faint">{t('rows.user.quoteGone')}</div>}
|
|
150
|
+
{quoted.rest !== '' && <MessageMarkdown text={quoted.rest} mentions={item.mentions} skills={item.skills} />}
|
|
151
|
+
</>
|
|
152
|
+
) : (
|
|
153
|
+
<MessageMarkdown text={item.text} mentions={item.mentions} skills={item.skills} />
|
|
154
|
+
)}
|
|
116
155
|
</div>
|
|
117
156
|
{long && (
|
|
118
157
|
<button className="mt-1 flex items-center gap-1 text-xs text-text-muted hover:text-text" onClick={() => setOpen((o) => !o)}>
|
|
@@ -364,7 +403,7 @@ export function NoteRow({ id, level, text, from }: { id: string; level: 'info' |
|
|
|
364
403
|
{open ? t('rows.note.hide') : t('rows.note.show')}
|
|
365
404
|
</button>
|
|
366
405
|
)}
|
|
367
|
-
{from !== undefined && <OpenChatButton chatId={from} />}
|
|
406
|
+
{from !== undefined && <OpenChatButton chatId={from} label={id.startsWith(WAITING_NOTE_PREFIX) ? 'openWaiting' : 'openFork'} />}
|
|
368
407
|
</div>
|
|
369
408
|
{open && rest !== '' && (
|
|
370
409
|
<div data-find-field="output" className="mt-1 text-text-muted select-text">
|
|
@@ -376,8 +415,8 @@ export function NoteRow({ id, level, text, from }: { id: string; level: 'info' |
|
|
|
376
415
|
);
|
|
377
416
|
}
|
|
378
417
|
|
|
379
|
-
/* "Open fork" on a note a fork sent, while that
|
|
380
|
-
function OpenChatButton({ chatId }: { chatId: string }) {
|
|
418
|
+
/* "Open fork" on a note a fork sent, or "Open to answer" on one about a waiting child, while that chat is still in the project. */
|
|
419
|
+
function OpenChatButton({ chatId, label }: { chatId: string; label: 'openFork' | 'openWaiting' }) {
|
|
381
420
|
const { t } = useTranslation('agent-chat');
|
|
382
421
|
const place = chatHost().useChatPlace(chatId);
|
|
383
422
|
if (place.title === null) {
|
|
@@ -385,15 +424,14 @@ function OpenChatButton({ chatId }: { chatId: string }) {
|
|
|
385
424
|
}
|
|
386
425
|
return (
|
|
387
426
|
<button className="text-text-muted hover:text-text" onClick={place.go}>
|
|
388
|
-
{t(
|
|
427
|
+
{t(`rows.note.${label}`)}
|
|
389
428
|
</button>
|
|
390
429
|
);
|
|
391
430
|
}
|
|
392
431
|
|
|
393
432
|
/*
|
|
394
|
-
* The header of a turn the agent started itself, in place of
|
|
395
|
-
*
|
|
396
|
-
* "Show result". When the CLI named the sub-agent it woke up about, the header opens that row.
|
|
433
|
+
* The header of a turn the agent started itself, in place of a message of the person. What the
|
|
434
|
+
* sub-agent came back with lives on its own row, which the header opens when the CLI named it.
|
|
397
435
|
*/
|
|
398
436
|
export function AgentTurnRow({ label, onOpen }: { label: string; onOpen?: () => void }) {
|
|
399
437
|
const line = 'mb-0.5 flex h-7 w-full items-center gap-2 text-left text-xs text-text-muted';
|
|
@@ -430,7 +468,7 @@ export function CompactionRow({ preTokens }: { preTokens: number | null }) {
|
|
|
430
468
|
);
|
|
431
469
|
}
|
|
432
470
|
|
|
433
|
-
const DECISION_ICON: Record<Exclude<ChatApprovalItem['decision'], 'pending'>,
|
|
471
|
+
const DECISION_ICON: Record<Exclude<ChatApprovalItem['decision'], 'pending'>, ReactNode> = {
|
|
434
472
|
allow: <Icon icon={Check} size={12} />,
|
|
435
473
|
'allow-always': <Icon icon={Check} size={12} />,
|
|
436
474
|
deny: <Icon icon={X} size={12} />,
|
|
@@ -67,7 +67,9 @@ export function Row({ row, chatId, toggleGroup, toggleTurn, toggleSubagent, open
|
|
|
67
67
|
case 'forks':
|
|
68
68
|
return <ForksRow chatId={chatId} turnId={row.turnId} />;
|
|
69
69
|
case 'changed-files':
|
|
70
|
-
return
|
|
70
|
+
return (
|
|
71
|
+
<ChangedFilesRow chatId={chatId} turnId={row.turnId} tools={row.tools} diff={row.diff} checkpoint={row.checkpoint} baseMoved={row.baseMoved} />
|
|
72
|
+
);
|
|
71
73
|
case 'approval':
|
|
72
74
|
return <ApprovalHistoryRow item={row.item} />;
|
|
73
75
|
case 'question':
|