@adecore/agents-react 0.21.0 → 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/UiBlockFrame.d.ts +14 -1
- package/dist/chat/ui/intelligent-ui/UiBlockFrame.js +47 -5
- package/dist/chat/ui/intelligent-ui/UiReply.d.ts +3 -1
- package/dist/chat/ui/intelligent-ui/UiReply.js +34 -30
- package/dist/chat/ui/intelligent-ui/block-local.d.ts +9 -0
- package/dist/chat/ui/intelligent-ui/block-local.js +30 -1
- package/dist/chat/ui/intelligent-ui/node-text.d.ts +6 -0
- package/dist/chat/ui/intelligent-ui/node-text.js +54 -0
- package/dist/chat/ui/intelligent-ui/registry.js +2 -1
- package/dist/chat/ui/intelligent-ui/render-context.d.ts +4 -1
- package/dist/chat/ui/intelligent-ui/renderers/chart.js +5 -7
- package/dist/chat/ui/intelligent-ui/renderers/choices.js +6 -2
- package/dist/chat/ui/intelligent-ui/renderers/content.js +6 -13
- package/dist/chat/ui/intelligent-ui/renderers/data.js +8 -15
- package/dist/chat/ui/intelligent-ui/renderers/inputs.d.ts +1 -0
- package/dist/chat/ui/intelligent-ui/renderers/inputs.js +16 -4
- package/dist/chat/ui/intelligent-ui/renderers/links.js +2 -1
- package/dist/chat/ui/intelligent-ui/use-ui-links.d.ts +4 -4
- package/dist/chat/ui/intelligent-ui/use-ui-links.js +29 -10
- package/dist/chat/ui/intelligent-ui/use-ui-queries.d.ts +2 -1
- package/dist/chat/ui/intelligent-ui/use-ui-queries.js +34 -21
- package/dist/chat/ui/rows/MessageRows.js +3 -4
- 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/locales/en/agent-chat.json +23 -4
- package/dist/locales/nl/agent-chat.json +23 -4
- package/dist/port-transport.js +4 -8
- package/dist/state/chats.js +15 -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 -29
- 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/UiBlockFrame.tsx +135 -5
- package/src/chat/ui/intelligent-ui/UiReply.tsx +48 -38
- package/src/chat/ui/intelligent-ui/block-local.ts +36 -1
- package/src/chat/ui/intelligent-ui/node-text.ts +60 -0
- package/src/chat/ui/intelligent-ui/registry.ts +2 -1
- package/src/chat/ui/intelligent-ui/render-context.ts +8 -2
- package/src/chat/ui/intelligent-ui/renderers/chart.tsx +5 -7
- package/src/chat/ui/intelligent-ui/renderers/choices.tsx +6 -2
- package/src/chat/ui/intelligent-ui/renderers/content.tsx +9 -14
- package/src/chat/ui/intelligent-ui/renderers/data.tsx +13 -19
- package/src/chat/ui/intelligent-ui/renderers/inputs.tsx +28 -5
- package/src/chat/ui/intelligent-ui/renderers/links.tsx +2 -1
- package/src/chat/ui/intelligent-ui/use-ui-links.ts +31 -11
- package/src/chat/ui/intelligent-ui/use-ui-queries.ts +48 -22
- 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 +4 -5
- 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/locales/en/agent-chat.json +23 -4
- package/src/locales/nl/agent-chat.json +23 -4
- package/src/port-transport.ts +4 -8
- package/src/state/chats.ts +16 -15
- 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/intelligent-ui/UiBlockFooter.d.ts +0 -15
- package/dist/chat/ui/intelligent-ui/UiBlockFooter.js +0 -50
- package/src/chat/ui/intelligent-ui/UiBlockFooter.tsx +0 -139
|
@@ -41,7 +41,11 @@ export function ChoiceRenderer({ node, children, context }: UiRendererProps<UiPr
|
|
|
41
41
|
const failed = answer === null && context.failedChoiceId === node.id;
|
|
42
42
|
const closed = waiting || unavailable || answer !== null;
|
|
43
43
|
const slow = useLater(chosen && answer.state === 'sending', SPINNER_AFTER_MS);
|
|
44
|
-
const reason = waiting
|
|
44
|
+
const reason = waiting
|
|
45
|
+
? t('blocks.choice.waiting')
|
|
46
|
+
: answer === null && unavailable
|
|
47
|
+
? ((disabled === true ? null : context.choiceReason) ?? t('blocks.choice.unavailable'))
|
|
48
|
+
: null;
|
|
45
49
|
const described = detail !== undefined && detail !== '';
|
|
46
50
|
|
|
47
51
|
const row = (
|
|
@@ -80,7 +84,7 @@ export function ChoiceRenderer({ node, children, context }: UiRendererProps<UiPr
|
|
|
80
84
|
);
|
|
81
85
|
|
|
82
86
|
return (
|
|
83
|
-
<div className=
|
|
87
|
+
<div className="flex flex-col gap-1">
|
|
84
88
|
{reason !== null ? (
|
|
85
89
|
<DisabledReason reason={reason}>{row}</DisabledReason>
|
|
86
90
|
) : described && detail.length > LONG_CONTEXT ? (
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import clsx from 'clsx';
|
|
2
|
-
import { createContext, useContext } from 'react';
|
|
2
|
+
import { createContext, useContext, useId } from 'react';
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
4
|
import type { UiProps } from '@adecore/intelligent-ui';
|
|
5
5
|
import { PreviewCard } from '@adecore/ui';
|
|
@@ -8,7 +8,7 @@ import { chatHost } from '../../../../host';
|
|
|
8
8
|
import { CodeBlock } from '../../CodeBlock';
|
|
9
9
|
import { CodeStreamingContext } from '../../code-streaming';
|
|
10
10
|
import { ImageThumb } from '../../ImageView';
|
|
11
|
-
import { uiChildrenOf, uiNodeLabel, uiNodeText } from '../node-text';
|
|
11
|
+
import { uiChildrenOf, uiNodeLabel, uiNodeText, uiUrlDomain } from '../node-text';
|
|
12
12
|
import type { UiRendererProps } from '../render-context';
|
|
13
13
|
|
|
14
14
|
// The number of every source by its node id, which the list counts in its own order.
|
|
@@ -52,25 +52,20 @@ export function ImageRenderer({ node, children, context }: UiRendererProps<UiPro
|
|
|
52
52
|
|
|
53
53
|
export function SourcesRenderer({ node, children }: UiRendererProps<UiProps<'Sources'>>) {
|
|
54
54
|
const { t } = useTranslation('agent-chat');
|
|
55
|
+
const headingId = useId();
|
|
55
56
|
const numbers = new Map(uiChildrenOf(node, 'Source').map((source, index) => [source.id, index + 1]));
|
|
56
57
|
return (
|
|
57
58
|
<SourceNumbers value={numbers}>
|
|
58
|
-
<section aria-
|
|
59
|
-
<span className="px-2 pb-1 text-xs font-medium text-text-muted">
|
|
59
|
+
<section aria-labelledby={headingId} className="flex flex-col">
|
|
60
|
+
<span id={headingId} className="px-2 pb-1 text-xs font-medium text-text-muted">
|
|
61
|
+
{t('blocks.sources')}
|
|
62
|
+
</span>
|
|
60
63
|
<ol className="chat-ui-nodes flex flex-col">{children}</ol>
|
|
61
64
|
</section>
|
|
62
65
|
</SourceNumbers>
|
|
63
66
|
);
|
|
64
67
|
}
|
|
65
68
|
|
|
66
|
-
function domainOf(url: string): string {
|
|
67
|
-
try {
|
|
68
|
-
return new URL(url).hostname.replace(/^www\./, '');
|
|
69
|
-
} catch {
|
|
70
|
-
return url;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
69
|
export interface UiSourceCitationProps {
|
|
75
70
|
/* The number of the Source, as its row in the list counts it. */
|
|
76
71
|
number: number;
|
|
@@ -106,7 +101,7 @@ export function UiSourceCitation({ number, title, url, onReveal, className }: Ui
|
|
|
106
101
|
</PreviewCard.Trigger>
|
|
107
102
|
<PreviewCard.Popup side="top" align="center" sideOffset={4} className="flex max-w-72 flex-col px-2.5 py-1.5 text-xs">
|
|
108
103
|
<span className="line-clamp-2 text-text">{title}</span>
|
|
109
|
-
<span className="truncate text-text-faint">{
|
|
104
|
+
<span className="truncate text-text-faint">{uiUrlDomain(url)}</span>
|
|
110
105
|
</PreviewCard.Popup>
|
|
111
106
|
</PreviewCard.Root>
|
|
112
107
|
);
|
|
@@ -124,7 +119,7 @@ export function SourceRenderer({ node, context }: UiRendererProps<UiProps<'Sourc
|
|
|
124
119
|
<>
|
|
125
120
|
<span className="w-5 shrink-0 text-right text-text-faint tabular-nums">{number}</span>
|
|
126
121
|
<span className="min-w-0 truncate text-text">{title}</span>
|
|
127
|
-
<span className="shrink-0 text-text-faint">{
|
|
122
|
+
<span className="shrink-0 text-text-faint">{uiUrlDomain(url)}</span>
|
|
128
123
|
</>
|
|
129
124
|
);
|
|
130
125
|
return (
|
|
@@ -5,24 +5,15 @@ import type { UiProps } from '@adecore/intelligent-ui';
|
|
|
5
5
|
import { Button, Icon, KeyValueList, Pill } from '@adecore/ui';
|
|
6
6
|
import { formatBytes, formatDuration, formatMoment, formatNumber, formatPercent, formatRounded } from '@adecore/ui/format';
|
|
7
7
|
import { useBlockLocal } from '../block-local';
|
|
8
|
-
import { uiChildrenOf } from '../node-text';
|
|
9
|
-
import type { UiLiveValue,
|
|
8
|
+
import { uiChildrenOf, uiWithUnit } from '../node-text';
|
|
9
|
+
import type { UiLiveValue, UiRendererProps } from '../render-context';
|
|
10
10
|
import { isNumericColumn, UI_TABLE_ROWS, uiTableCell, uiTableColumns, type UiTableCell, type UiTableColumn } from '../table-data';
|
|
11
11
|
import { UI_TONE_TEXT } from '../tones';
|
|
12
12
|
import { UiFallbackPart } from '../UiFallbackPart';
|
|
13
|
-
import { UiLinkChip } from './links';
|
|
14
13
|
|
|
15
14
|
/* EntityList draws a hairline between its rows from this many on. */
|
|
16
15
|
const DIVIDED_FROM = 7;
|
|
17
16
|
|
|
18
|
-
/* A number with its unit, where a percent sign hugs the number and every other unit stands apart from it. */
|
|
19
|
-
function withUnit(text: string, unit: string | undefined): string {
|
|
20
|
-
if (unit === undefined || unit === '') {
|
|
21
|
-
return text;
|
|
22
|
-
}
|
|
23
|
-
return unit === '%' ? `${text}${unit}` : `${text} ${unit}`;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
17
|
/*
|
|
27
18
|
* A number that live readings keep up to date. Keyed by its text, so a value that changed mounts
|
|
28
19
|
* again and lights up once, even when it changes again before the last light settled.
|
|
@@ -47,15 +38,15 @@ export function StatRenderer({ node, context }: UiRendererProps<UiProps<'Stat'>>
|
|
|
47
38
|
const live = context.liveValue?.(node.id, 'value');
|
|
48
39
|
const change = previous === undefined ? null : value - previous;
|
|
49
40
|
const arrow = change === null || change === 0 ? ArrowRight : change > 0 ? ArrowUp : ArrowDown;
|
|
50
|
-
const from = previous === undefined ? '' :
|
|
41
|
+
const from = previous === undefined ? '' : uiWithUnit(formatRounded(previous, 2), unit);
|
|
51
42
|
// A change from zero has no percentage.
|
|
52
43
|
const percent = change === null || previous === undefined || previous === 0 ? null : formatPercent(Math.abs((change / previous) * 100));
|
|
53
44
|
return (
|
|
54
45
|
<div className="flex min-w-0 flex-col gap-0.5 rounded-md bg-surface-hover px-2 py-1.5">
|
|
55
46
|
<span className="truncate text-xs text-text-muted">{label}</span>
|
|
56
47
|
<LiveFigure
|
|
57
|
-
text={
|
|
58
|
-
previous={typeof live?.previous === 'number' ?
|
|
48
|
+
text={uiWithUnit(formatRounded(value, 2), unit)}
|
|
49
|
+
previous={typeof live?.previous === 'number' ? uiWithUnit(formatRounded(live.previous, 2), unit) : undefined}
|
|
59
50
|
live={live}
|
|
60
51
|
className="text-lg font-semibold tabular-nums"
|
|
61
52
|
/>
|
|
@@ -89,10 +80,10 @@ export function EntryRenderer({ node, children }: UiRendererProps<UiProps<'Entry
|
|
|
89
80
|
}
|
|
90
81
|
|
|
91
82
|
function numberText(cell: Extract<UiTableCell, { kind: 'number' }>, unit: string | undefined): string {
|
|
92
|
-
return cell.as === 'bytes' ? formatBytes(cell.value) : cell.as === 'duration' ? formatDuration(cell.value) :
|
|
83
|
+
return cell.as === 'bytes' ? formatBytes(cell.value) : cell.as === 'duration' ? formatDuration(cell.value) : uiWithUnit(formatRounded(cell.value, 2), unit);
|
|
93
84
|
}
|
|
94
85
|
|
|
95
|
-
function Cell({ value, column, live
|
|
86
|
+
function Cell({ value, column, live }: { value: unknown; column: UiTableColumn; live?: UiLiveValue }) {
|
|
96
87
|
const cell = uiTableCell(value, column);
|
|
97
88
|
switch (cell.kind) {
|
|
98
89
|
case 'empty':
|
|
@@ -112,7 +103,8 @@ function Cell({ value, column, live, context }: { value: unknown; column: UiTabl
|
|
|
112
103
|
case 'date':
|
|
113
104
|
return <time dateTime={new Date(cell.at).toISOString()}>{formatMoment(cell.at)}</time>;
|
|
114
105
|
case 'file':
|
|
115
|
-
|
|
106
|
+
// A cell is no link target the host checked, so its path stays text.
|
|
107
|
+
return <span className="font-mono text-code break-all">{cell.path}</span>;
|
|
116
108
|
case 'tag':
|
|
117
109
|
return (
|
|
118
110
|
<Pill shape="tag" size="sm">
|
|
@@ -138,7 +130,10 @@ export function TableRenderer({ node, context }: UiRendererProps<UiProps<'Table'
|
|
|
138
130
|
return (
|
|
139
131
|
<div className="flex flex-col items-start gap-1.5">
|
|
140
132
|
<div className="max-h-80 w-full overflow-auto rounded-md border border-border-soft">
|
|
141
|
-
<table
|
|
133
|
+
<table
|
|
134
|
+
aria-label={t('blocks.table.label', { columns: columns.map((column) => column.title).join(', ') })}
|
|
135
|
+
className="w-full border-collapse text-xs tabular-nums"
|
|
136
|
+
>
|
|
142
137
|
<thead className="sticky top-0 z-1 bg-surface-hover">
|
|
143
138
|
<tr>
|
|
144
139
|
{columns.map((column) => (
|
|
@@ -168,7 +163,6 @@ export function TableRenderer({ node, context }: UiRendererProps<UiProps<'Table'
|
|
|
168
163
|
value={row[column.key]}
|
|
169
164
|
column={column}
|
|
170
165
|
live={isNumericColumn(column) ? context.liveValue?.(node.id, 'rows', [index, column.key]) : undefined}
|
|
171
|
-
context={context}
|
|
172
166
|
/>
|
|
173
167
|
</td>
|
|
174
168
|
))}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { createContext, useContext, type MouseEvent } from 'react';
|
|
1
|
+
import { createContext, useContext, type MouseEvent, type ReactNode } from 'react';
|
|
2
2
|
import clsx from 'clsx';
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
4
|
import type { UiBinding, UiProps, UiViewNode } from '@adecore/intelligent-ui';
|
|
5
|
-
import { Checkbox, Segmented, Slider, Switch } from '@adecore/ui';
|
|
6
|
-
import { uiChildrenOf, uiNodeLabel } from '../node-text';
|
|
5
|
+
import { Button, Checkbox, Segmented, Slider, Switch } from '@adecore/ui';
|
|
6
|
+
import { uiChildrenOf, uiNodeLabel, uiWithUnit } from '../node-text';
|
|
7
7
|
import type { UiRenderContext, UiRendererProps } from '../render-context';
|
|
8
8
|
|
|
9
9
|
type Scalar = UiProps<'Item'>['value'];
|
|
@@ -28,6 +28,16 @@ function bindingOf<Value>(node: UiViewNode<{ value: Value }>, context: UiRenderC
|
|
|
28
28
|
/* An input whose node has not closed yet stands at 60% and takes no pointer. */
|
|
29
29
|
const PENDING = 'pointer-events-none opacity-60';
|
|
30
30
|
|
|
31
|
+
/* The rows of a checklist as one named group. */
|
|
32
|
+
function ChecklistGroup({ pending, children }: { pending: boolean; children: ReactNode }) {
|
|
33
|
+
const { t } = useTranslation('agent-chat');
|
|
34
|
+
return (
|
|
35
|
+
<div role="group" aria-label={t('blocks.checklist')} className={clsx('chat-ui-nodes flex flex-col', pending && PENDING)}>
|
|
36
|
+
{children}
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
31
41
|
export function ChecklistRenderer({ node, children, context }: UiRendererProps<UiProps<'Checklist'>>) {
|
|
32
42
|
const binding = bindingOf(node, context);
|
|
33
43
|
const values = binding?.value ?? node.props.value;
|
|
@@ -36,7 +46,7 @@ export function ChecklistRenderer({ node, children, context }: UiRendererProps<U
|
|
|
36
46
|
};
|
|
37
47
|
return (
|
|
38
48
|
<ChecklistContext value={{ values, enabled: binding !== null, toggle }}>
|
|
39
|
-
<
|
|
49
|
+
<ChecklistGroup pending={!node.complete}>{children}</ChecklistGroup>
|
|
40
50
|
</ChecklistContext>
|
|
41
51
|
);
|
|
42
52
|
}
|
|
@@ -87,7 +97,8 @@ export function SliderRenderer({ node, context }: UiRendererProps<UiProps<'Slide
|
|
|
87
97
|
min={min}
|
|
88
98
|
max={max}
|
|
89
99
|
step={step}
|
|
90
|
-
unit
|
|
100
|
+
// The slider prints its unit right after the number, so the space a unit stands apart with comes from here.
|
|
101
|
+
unit={uiWithUnit('', unit)}
|
|
91
102
|
label={uiNodeLabel(node)}
|
|
92
103
|
disabled={binding === null}
|
|
93
104
|
className={clsx('px-2', !node.complete && PENDING)}
|
|
@@ -119,3 +130,15 @@ export function SegmentedRenderer({ node, context }: UiRendererProps<UiProps<'Se
|
|
|
119
130
|
</div>
|
|
120
131
|
);
|
|
121
132
|
}
|
|
133
|
+
|
|
134
|
+
/* Runs its local action once its own node closed; like an input, it closes once the block is answered. */
|
|
135
|
+
export function ButtonRenderer({ node, children, context }: UiRendererProps<UiProps<'Button'>>) {
|
|
136
|
+
const enabled = node.onAction !== undefined && node.complete && context.answer === null && node.props.disabled !== true;
|
|
137
|
+
return (
|
|
138
|
+
<div className={clsx('px-2', !node.complete && PENDING)}>
|
|
139
|
+
<Button variant="secondary" size="sm" disabled={!enabled} onClick={() => node.onAction?.()}>
|
|
140
|
+
{children}
|
|
141
|
+
</Button>
|
|
142
|
+
</div>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
@@ -5,6 +5,7 @@ import { FileDiff, GitCommitHorizontal, SquareDashed } from 'lucide-react';
|
|
|
5
5
|
import type { UiProps } from '@adecore/intelligent-ui';
|
|
6
6
|
import { FileIcon, Icon, Tooltip } from '@adecore/ui';
|
|
7
7
|
import { CHIP_IN_MESSAGE, MENTION_TONE } from '../../chips';
|
|
8
|
+
import { uiReasonText } from '../node-text';
|
|
8
9
|
import type { UiLink, UiLinkTarget, UiRenderContext, UiRendererProps } from '../render-context';
|
|
9
10
|
|
|
10
11
|
const PLAIN: UiLink = { state: 'plain' };
|
|
@@ -82,7 +83,7 @@ export function UiLinkChip({ target, context }: { target: UiLinkTarget; context:
|
|
|
82
83
|
const { t } = useTranslation('agent-chat');
|
|
83
84
|
const link = context.link?.(target) ?? PLAIN;
|
|
84
85
|
if (link.state === 'plain') {
|
|
85
|
-
const reason = link.reason ?? (context.link === undefined ? null : t('blocks.outsideProject'));
|
|
86
|
+
const reason = uiReasonText(t, link.code, link.reason) ?? (context.link === undefined ? null : t('blocks.outsideProject'));
|
|
86
87
|
// Focusable only with a reason, so a keyboard reaches the tooltip.
|
|
87
88
|
const text = (
|
|
88
89
|
<span tabIndex={reason === null ? undefined : 0} className="font-mono text-code break-all text-text-muted">
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react';
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
2
|
import type { ChatUiLinkReading, ChatUiQueryState } from '@adecore/agent-contracts';
|
|
3
3
|
import { uiLinkTargets, type UiLinkTarget } from '@adecore/intelligent-ui/links';
|
|
4
|
-
import { uiInputValues, type UiBlock, UiState } from '@adecore/intelligent-ui';
|
|
4
|
+
import { uiInputValues, UI_HOST_LIMITS, type UiBlock, UiState } from '@adecore/intelligent-ui';
|
|
5
5
|
import { chatHost } from '../../../host';
|
|
6
6
|
import type { UiRenderContext } from './render-context';
|
|
7
7
|
|
|
@@ -10,14 +10,19 @@ interface LinkState {
|
|
|
10
10
|
reading: ChatUiLinkReading;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
// Inputs that change in a run, such as a dragged slider, check their links once the run rested.
|
|
14
|
+
const LINK_SETTLE_MS = 300;
|
|
15
|
+
|
|
13
16
|
export function useUiLinks(
|
|
14
17
|
block: UiBlock,
|
|
15
18
|
state: UiState,
|
|
16
|
-
context: UiRenderContext,
|
|
19
|
+
context: Pick<UiRenderContext, 'scopeId' | 'chatId' | 'itemId' | 'phase'>,
|
|
17
20
|
reads: Readonly<Record<string, string>>,
|
|
18
21
|
frozen?: ChatUiQueryState['blocks'][string]
|
|
19
22
|
) {
|
|
20
23
|
const [checked, setChecked] = useState<Record<string, LinkState>>({});
|
|
24
|
+
// What each node was last checked as, so a change of input checks only the targets it moved.
|
|
25
|
+
const asked = useRef(new Map<string, string>());
|
|
21
26
|
const host = chatHost().intelligentUi;
|
|
22
27
|
const values = uiInputValues(block, state);
|
|
23
28
|
const scope = state.scope();
|
|
@@ -35,14 +40,25 @@ export function useUiLinks(
|
|
|
35
40
|
const signature = JSON.stringify(targets);
|
|
36
41
|
const input = JSON.stringify(values);
|
|
37
42
|
const tickets = JSON.stringify(reads);
|
|
43
|
+
const defaultInput = useMemo(() => JSON.stringify(uiInputValues(block, new UiState(block))), [block]);
|
|
38
44
|
const link = host?.link;
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
asked.current.clear();
|
|
47
|
+
}, [block.id, block.revision, context.scopeId, context.chatId, context.itemId, link]);
|
|
39
48
|
useEffect(() => {
|
|
40
49
|
if (!link || !block.revision || context.phase !== 'final') {
|
|
41
50
|
return;
|
|
42
51
|
}
|
|
43
52
|
let stopped = false;
|
|
44
53
|
const validate = async () => {
|
|
45
|
-
|
|
54
|
+
const moved = Object.entries(JSON.parse(signature) as Record<string, UiLinkTarget>)
|
|
55
|
+
.slice(0, UI_HOST_LIMITS.links)
|
|
56
|
+
.filter(([nodeId, target]) => asked.current.get(nodeId) !== JSON.stringify(target));
|
|
57
|
+
for (const [nodeId, target] of moved) {
|
|
58
|
+
const record = (reading: ChatUiLinkReading) => {
|
|
59
|
+
asked.current.set(nodeId, JSON.stringify(target));
|
|
60
|
+
setChecked((previous) => ({ ...previous, [nodeId]: { target: JSON.stringify(target), reading } }));
|
|
61
|
+
};
|
|
46
62
|
try {
|
|
47
63
|
const reading = await link(context.scopeId, {
|
|
48
64
|
chatId: context.chatId,
|
|
@@ -56,18 +72,20 @@ export function useUiLinks(
|
|
|
56
72
|
if (stopped) {
|
|
57
73
|
return;
|
|
58
74
|
}
|
|
59
|
-
|
|
75
|
+
record(reading);
|
|
60
76
|
} catch {
|
|
61
77
|
if (stopped) {
|
|
62
78
|
return;
|
|
63
79
|
}
|
|
64
|
-
|
|
80
|
+
record({ state: 'plain' });
|
|
65
81
|
}
|
|
66
82
|
}
|
|
67
83
|
};
|
|
68
|
-
|
|
84
|
+
// The first check runs at once; a later one waits until the inputs rested.
|
|
85
|
+
const timer = setTimeout(() => void validate(), asked.current.size === 0 ? 0 : LINK_SETTLE_MS);
|
|
69
86
|
return () => {
|
|
70
87
|
stopped = true;
|
|
88
|
+
clearTimeout(timer);
|
|
71
89
|
};
|
|
72
90
|
}, [block.id, block.revision, context.scopeId, context.chatId, context.itemId, context.phase, signature, input, tickets, link]);
|
|
73
91
|
const idOf = (target: UiLinkTarget) => Object.entries(targets).find(([, entry]) => JSON.stringify(entry) === JSON.stringify(target))?.[0];
|
|
@@ -79,7 +97,7 @@ export function useUiLinks(
|
|
|
79
97
|
if (checked[id]?.target === JSON.stringify(target)) {
|
|
80
98
|
return checked[id].reading;
|
|
81
99
|
}
|
|
82
|
-
if (input ===
|
|
100
|
+
if (input === defaultInput && frozen && frozen.revision === block.revision) {
|
|
83
101
|
const firstQueries = Object.fromEntries(
|
|
84
102
|
Object.entries(frozen.readings)
|
|
85
103
|
.filter(([, reading]) => reading.state === 'fresh')
|
|
@@ -115,9 +133,11 @@ export function useUiLinks(
|
|
|
115
133
|
setChecked((previous) => ({ ...previous, [nodeId]: { target: JSON.stringify(target), reading: { state: 'plain' } } }));
|
|
116
134
|
});
|
|
117
135
|
};
|
|
136
|
+
const hostOpenUrl = host?.openUrl;
|
|
137
|
+
const openUrl = useCallback((url: string) => hostOpenUrl?.(context.scopeId, url), [hostOpenUrl, context.scopeId]);
|
|
118
138
|
return {
|
|
119
|
-
link: link ? resolution :
|
|
120
|
-
openLink: link && host?.openLink ? open :
|
|
121
|
-
openUrl:
|
|
139
|
+
link: link ? resolution : undefined,
|
|
140
|
+
openLink: link && host?.openLink ? open : undefined,
|
|
141
|
+
openUrl: hostOpenUrl ? openUrl : undefined
|
|
122
142
|
};
|
|
123
143
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { useEffect, useRef, useState } from 'react';
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
2
|
import type { ChatUiQueryReading, ChatUiQueryState } from '@adecore/agent-contracts';
|
|
3
|
-
import { uiInputValues, UI_CATALOG_VERSION, UiState, type UiBlock, type UiViewNode, type UiNode } from '@adecore/intelligent-ui';
|
|
3
|
+
import { uiInputValues, UI_CATALOG_VERSION, UI_HOST_LIMITS, UiState, type UiBlock, type UiViewNode, type UiNode } from '@adecore/intelligent-ui';
|
|
4
4
|
import { chatHost } from '../../../host';
|
|
5
5
|
import type { UiLiveValue, UiLiveStatus, UiRenderContext } from './render-context';
|
|
6
6
|
|
|
@@ -10,6 +10,28 @@ interface LocalReading {
|
|
|
10
10
|
successful?: { inputs: string; reading: ChatUiQueryReading };
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
const NO_READINGS: ChatUiQueryState['blocks'][string]['readings'] = {};
|
|
14
|
+
// A changed input reads once it has rested this long, so dragging a slider reads its last value only.
|
|
15
|
+
const INPUT_SETTLE_MS = 300;
|
|
16
|
+
|
|
17
|
+
/* The reading that holds for these inputs: this view's own read of them, or the frozen first one for the inputs the block starts with. */
|
|
18
|
+
function successfulReading(
|
|
19
|
+
local: LocalReading | undefined,
|
|
20
|
+
initial: ChatUiQueryReading | undefined,
|
|
21
|
+
identity: string,
|
|
22
|
+
signature: string,
|
|
23
|
+
defaultInputs: string
|
|
24
|
+
): ChatUiQueryReading | undefined {
|
|
25
|
+
if (local?.identity === identity && local.successful?.inputs === signature) {
|
|
26
|
+
return local.successful.reading;
|
|
27
|
+
}
|
|
28
|
+
return signature === defaultInputs && initial?.state === 'fresh' ? initial : undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function settleDeadline(): number {
|
|
32
|
+
return Date.now() + INPUT_SETTLE_MS;
|
|
33
|
+
}
|
|
34
|
+
|
|
13
35
|
function documentVisible(): boolean {
|
|
14
36
|
return document.visibilityState !== 'hidden';
|
|
15
37
|
}
|
|
@@ -17,27 +39,22 @@ function documentVisible(): boolean {
|
|
|
17
39
|
export function useUiQueries(
|
|
18
40
|
block: UiBlock,
|
|
19
41
|
state: UiState,
|
|
20
|
-
context: UiRenderContext,
|
|
42
|
+
context: Pick<UiRenderContext, 'scopeId' | 'chatId' | 'itemId' | 'phase'>,
|
|
21
43
|
element: HTMLDivElement | null,
|
|
22
44
|
frozen?: ChatUiQueryState['blocks'][string]
|
|
23
45
|
) {
|
|
24
46
|
const [readings, setReadings] = useState<Record<string, LocalReading>>({});
|
|
25
47
|
const [busyInput, setBusyInput] = useState<string | null>(null);
|
|
26
|
-
const
|
|
48
|
+
const lastRead = useRef({ inputKey: '', at: 0 });
|
|
27
49
|
const names = Object.keys(block.queries);
|
|
28
50
|
const inputs = JSON.stringify(uiInputValues(block, state));
|
|
29
51
|
const identity = JSON.stringify([context.scopeId, context.chatId, context.itemId, block.id, block.revision]);
|
|
30
52
|
const inputKey = JSON.stringify([identity, inputs]);
|
|
31
53
|
const reading = busyInput === inputKey;
|
|
32
|
-
const defaultInputs = JSON.stringify(uiInputValues(block, new UiState(block)));
|
|
33
|
-
const initial = frozen && frozen.revision === block.revision ? frozen.readings :
|
|
34
|
-
const successfulFor = (name: string, signature: string): ChatUiQueryReading | undefined =>
|
|
35
|
-
|
|
36
|
-
if (local?.identity === identity && local.successful?.inputs === signature) {
|
|
37
|
-
return local.successful.reading;
|
|
38
|
-
}
|
|
39
|
-
return signature === defaultInputs && initial[name]?.state === 'fresh' ? initial[name] : undefined;
|
|
40
|
-
};
|
|
54
|
+
const defaultInputs = useMemo(() => JSON.stringify(uiInputValues(block, new UiState(block))), [block]);
|
|
55
|
+
const initial = frozen && frozen.revision === block.revision ? frozen.readings : NO_READINGS;
|
|
56
|
+
const successfulFor = (name: string, signature: string): ChatUiQueryReading | undefined =>
|
|
57
|
+
successfulReading(readings[name], initial[name], identity, signature, defaultInputs);
|
|
41
58
|
const readsFor = (values: Readonly<Record<string, unknown>>): Record<string, string> | null => {
|
|
42
59
|
const signature = JSON.stringify(values);
|
|
43
60
|
const reads: Record<string, string> = {};
|
|
@@ -57,13 +74,13 @@ export function useUiQueries(
|
|
|
57
74
|
const subscribe = chatHost().intelligentUi?.subscribe;
|
|
58
75
|
useEffect(() => {
|
|
59
76
|
const scope = state.scope();
|
|
60
|
-
for (const name of
|
|
61
|
-
const successful =
|
|
77
|
+
for (const name of Object.keys(block.queries)) {
|
|
78
|
+
const successful = successfulReading(readings[name], initial[name], identity, inputs, defaultInputs);
|
|
62
79
|
if (successful && JSON.stringify(scope[name]) !== JSON.stringify(successful.value)) {
|
|
63
80
|
state.setQuery(name, successful.value, block);
|
|
64
81
|
}
|
|
65
82
|
}
|
|
66
|
-
});
|
|
83
|
+
}, [state, block, readings, identity, inputs, defaultInputs, initial]);
|
|
67
84
|
useEffect(() => {
|
|
68
85
|
if (
|
|
69
86
|
!query ||
|
|
@@ -81,11 +98,17 @@ export function useUiQueries(
|
|
|
81
98
|
let busy = false;
|
|
82
99
|
|
|
83
100
|
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
101
|
+
// The first read of this block reads at once; a changed input waits until it rests.
|
|
102
|
+
const changed = lastRead.current.inputKey !== '' && lastRead.current.inputKey !== inputKey;
|
|
103
|
+
const settledAt = changed ? settleDeadline() : 0;
|
|
84
104
|
const refresh = async () => {
|
|
85
105
|
if (stopped || !visible || !documentVisible() || busy) {
|
|
86
106
|
return;
|
|
87
107
|
}
|
|
88
|
-
const wait =
|
|
108
|
+
const wait = Math.max(
|
|
109
|
+
settledAt - Date.now(),
|
|
110
|
+
lastRead.current.inputKey === inputKey ? UI_HOST_LIMITS.refreshMilliseconds - (Date.now() - lastRead.current.at) : 0
|
|
111
|
+
);
|
|
89
112
|
if (wait > 0) {
|
|
90
113
|
if (timer === null) {
|
|
91
114
|
if (!visible || !documentVisible()) {
|
|
@@ -99,9 +122,9 @@ export function useUiQueries(
|
|
|
99
122
|
return;
|
|
100
123
|
}
|
|
101
124
|
busy = true;
|
|
102
|
-
|
|
125
|
+
lastRead.current = { inputKey, at: Date.now() };
|
|
103
126
|
setBusyInput(inputKey);
|
|
104
|
-
for (const name of Object.keys(block.queries).slice(0,
|
|
127
|
+
for (const name of Object.keys(block.queries).slice(0, UI_HOST_LIMITS.queries)) {
|
|
105
128
|
if (!visible || !documentVisible()) {
|
|
106
129
|
break;
|
|
107
130
|
}
|
|
@@ -137,7 +160,7 @@ export function useUiQueries(
|
|
|
137
160
|
...previous,
|
|
138
161
|
[name]: {
|
|
139
162
|
identity,
|
|
140
|
-
reading: { state: 'failed', readAt: Date.now(), reason: error instanceof Error ? error.message :
|
|
163
|
+
reading: { state: 'failed', readAt: Date.now(), code: 'unreadable', reason: error instanceof Error ? error.message : undefined },
|
|
141
164
|
successful: previous[name]?.identity === identity ? previous[name].successful : undefined
|
|
142
165
|
}
|
|
143
166
|
}));
|
|
@@ -151,7 +174,7 @@ export function useUiQueries(
|
|
|
151
174
|
timer = setTimeout(() => {
|
|
152
175
|
timer = null;
|
|
153
176
|
void refresh();
|
|
154
|
-
},
|
|
177
|
+
}, UI_HOST_LIMITS.refreshMilliseconds);
|
|
155
178
|
};
|
|
156
179
|
const observer =
|
|
157
180
|
typeof IntersectionObserver === 'undefined'
|
|
@@ -176,6 +199,8 @@ export function useUiQueries(
|
|
|
176
199
|
void refresh();
|
|
177
200
|
return () => {
|
|
178
201
|
stopped = true;
|
|
202
|
+
// A read cut short by this cleanup never reaches its own reset, which would leave the choices closed.
|
|
203
|
+
setBusyInput((current) => (current === inputKey ? null : current));
|
|
179
204
|
observer?.disconnect();
|
|
180
205
|
unsubscribe?.();
|
|
181
206
|
document.removeEventListener('visibilitychange', focus);
|
|
@@ -200,7 +225,8 @@ export function useUiQueries(
|
|
|
200
225
|
.filter((reading) => reading.state === 'fresh')
|
|
201
226
|
.reduce<number | null>((at, value) => (at === null ? value.readAt : Math.min(at, value.readAt)), null),
|
|
202
227
|
source: failed ? block.queries[failed[0]]?.source : undefined,
|
|
203
|
-
reason: failed?.[1].reason
|
|
228
|
+
reason: failed?.[1].reason,
|
|
229
|
+
code: failed?.[1].code
|
|
204
230
|
}
|
|
205
231
|
: undefined;
|
|
206
232
|
return { live, reads: reads ?? {}, reading, ready, readsFor, currentReadings };
|
|
@@ -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;
|
|
@@ -248,7 +248,7 @@ export function AssistantRow({ chatId, item: derived }: { chatId: string; item:
|
|
|
248
248
|
answers={item.uiAnswers}
|
|
249
249
|
queries={item.uiQueries}
|
|
250
250
|
reply={reply}
|
|
251
|
-
context={{ scopeId: scope.id, chatId, itemId: item.id, phase: item.streaming ? 'streaming' : 'final'
|
|
251
|
+
context={{ scopeId: scope.id, chatId, itemId: item.id, phase: item.streaming ? 'streaming' : 'final' }}
|
|
252
252
|
/>
|
|
253
253
|
</div>
|
|
254
254
|
</div>
|
|
@@ -391,9 +391,8 @@ function OpenChatButton({ chatId }: { chatId: string }) {
|
|
|
391
391
|
}
|
|
392
392
|
|
|
393
393
|
/*
|
|
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.
|
|
394
|
+
* The header of a turn the agent started itself, in place of a message of the person. What the
|
|
395
|
+
* sub-agent came back with lives on its own row, which the header opens when the CLI named it.
|
|
397
396
|
*/
|
|
398
397
|
export function AgentTurnRow({ label, onOpen }: { label: string; onOpen?: () => void }) {
|
|
399
398
|
const line = 'mb-0.5 flex h-7 w-full items-center gap-2 text-left text-xs text-text-muted';
|
|
@@ -430,7 +429,7 @@ export function CompactionRow({ preTokens }: { preTokens: number | null }) {
|
|
|
430
429
|
);
|
|
431
430
|
}
|
|
432
431
|
|
|
433
|
-
const DECISION_ICON: Record<Exclude<ChatApprovalItem['decision'], 'pending'>,
|
|
432
|
+
const DECISION_ICON: Record<Exclude<ChatApprovalItem['decision'], 'pending'>, ReactNode> = {
|
|
434
433
|
allow: <Icon icon={Check} size={12} />,
|
|
435
434
|
'allow-always': <Icon icon={Check} size={12} />,
|
|
436
435
|
deny: <Icon icon={X} size={12} />,
|