@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
|
@@ -5,20 +5,12 @@ import { ArrowDown, ArrowRight, ArrowUp } from 'lucide-react';
|
|
|
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.js';
|
|
8
|
-
import { uiChildrenOf } from '../node-text.js';
|
|
8
|
+
import { uiChildrenOf, uiWithUnit } from '../node-text.js';
|
|
9
9
|
import { isNumericColumn, UI_TABLE_ROWS, uiTableCell, uiTableColumns } from '../table-data.js';
|
|
10
10
|
import { UI_TONE_TEXT } from '../tones.js';
|
|
11
11
|
import { UiFallbackPart } from '../UiFallbackPart.js';
|
|
12
|
-
import { UiLinkChip } from './links.js';
|
|
13
12
|
/* EntityList draws a hairline between its rows from this many on. */
|
|
14
13
|
const DIVIDED_FROM = 7;
|
|
15
|
-
/* A number with its unit, where a percent sign hugs the number and every other unit stands apart from it. */
|
|
16
|
-
function withUnit(text, unit) {
|
|
17
|
-
if (unit === undefined || unit === '') {
|
|
18
|
-
return text;
|
|
19
|
-
}
|
|
20
|
-
return unit === '%' ? `${text}${unit}` : `${text} ${unit}`;
|
|
21
|
-
}
|
|
22
14
|
/*
|
|
23
15
|
* A number that live readings keep up to date. Keyed by its text, so a value that changed mounts
|
|
24
16
|
* again and lights up once, even when it changes again before the last light settled.
|
|
@@ -37,10 +29,10 @@ export function StatRenderer({ node, context }) {
|
|
|
37
29
|
const live = context.liveValue?.(node.id, 'value');
|
|
38
30
|
const change = previous === undefined ? null : value - previous;
|
|
39
31
|
const arrow = change === null || change === 0 ? ArrowRight : change > 0 ? ArrowUp : ArrowDown;
|
|
40
|
-
const from = previous === undefined ? '' :
|
|
32
|
+
const from = previous === undefined ? '' : uiWithUnit(formatRounded(previous, 2), unit);
|
|
41
33
|
// A change from zero has no percentage.
|
|
42
34
|
const percent = change === null || previous === undefined || previous === 0 ? null : formatPercent(Math.abs((change / previous) * 100));
|
|
43
|
-
return (_jsxs("div", { className: "flex min-w-0 flex-col gap-0.5 rounded-md bg-surface-hover px-2 py-1.5", children: [_jsx("span", { className: "truncate text-xs text-text-muted", children: label }), _jsx(LiveFigure, { text:
|
|
35
|
+
return (_jsxs("div", { className: "flex min-w-0 flex-col gap-0.5 rounded-md bg-surface-hover px-2 py-1.5", children: [_jsx("span", { className: "truncate text-xs text-text-muted", children: label }), _jsx(LiveFigure, { text: uiWithUnit(formatRounded(value, 2), unit), previous: typeof live?.previous === 'number' ? uiWithUnit(formatRounded(live.previous, 2), unit) : undefined, live: live, className: "text-lg font-semibold tabular-nums" }), change !== null && (_jsxs("span", { className: clsx('flex items-center gap-1 text-xs tabular-nums', tone === undefined ? 'text-text-faint' : UI_TONE_TEXT[tone]), children: [_jsx(Icon, { icon: arrow, size: 12, className: "shrink-0" }), _jsx("span", { className: "min-w-0 truncate", children: percent === null ? t('blocks.stat.from', { previous: from }) : t('blocks.stat.change', { change: percent, previous: from }) })] }))] }));
|
|
44
36
|
}
|
|
45
37
|
export function EntityListRenderer({ node, children }) {
|
|
46
38
|
return (_jsx(KeyValueList.Root, { divided: uiChildrenOf(node, 'Entry').length >= DIVIDED_FROM, className: "chat-ui-nodes px-2", children: children }));
|
|
@@ -49,9 +41,9 @@ export function EntryRenderer({ node, children }) {
|
|
|
49
41
|
return (_jsxs(KeyValueList.Item, { children: [_jsx(KeyValueList.Name, { children: node.props.label }), _jsx(KeyValueList.Value, { children: children })] }));
|
|
50
42
|
}
|
|
51
43
|
function numberText(cell, unit) {
|
|
52
|
-
return cell.as === 'bytes' ? formatBytes(cell.value) : cell.as === 'duration' ? formatDuration(cell.value) :
|
|
44
|
+
return cell.as === 'bytes' ? formatBytes(cell.value) : cell.as === 'duration' ? formatDuration(cell.value) : uiWithUnit(formatRounded(cell.value, 2), unit);
|
|
53
45
|
}
|
|
54
|
-
function Cell({ value, column, live
|
|
46
|
+
function Cell({ value, column, live }) {
|
|
55
47
|
const cell = uiTableCell(value, column);
|
|
56
48
|
switch (cell.kind) {
|
|
57
49
|
case 'empty':
|
|
@@ -65,7 +57,8 @@ function Cell({ value, column, live, context }) {
|
|
|
65
57
|
case 'date':
|
|
66
58
|
return _jsx("time", { dateTime: new Date(cell.at).toISOString(), children: formatMoment(cell.at) });
|
|
67
59
|
case 'file':
|
|
68
|
-
|
|
60
|
+
// A cell is no link target the host checked, so its path stays text.
|
|
61
|
+
return _jsx("span", { className: "font-mono text-code break-all", children: cell.path });
|
|
69
62
|
case 'tag':
|
|
70
63
|
return (_jsx(Pill, { shape: "tag", size: "sm", children: cell.text }));
|
|
71
64
|
}
|
|
@@ -83,5 +76,5 @@ export function TableRenderer({ node, context }) {
|
|
|
83
76
|
}
|
|
84
77
|
const { rows } = node.props;
|
|
85
78
|
const shown = all ? rows : rows.slice(0, UI_TABLE_ROWS);
|
|
86
|
-
return (_jsxs("div", { className: "flex flex-col items-start gap-1.5", children: [_jsx("div", { className: "max-h-80 w-full overflow-auto rounded-md border border-border-soft", children: _jsxs("table", { className: "w-full border-collapse text-xs tabular-nums", children: [_jsx("thead", { className: "sticky top-0 z-1 bg-surface-hover", children: _jsx("tr", { children: columns.map((column) => (_jsx("th", { scope: "col", className: clsx('h-8 px-2 font-medium whitespace-nowrap text-text-muted', isNumericColumn(column) ? 'text-right' : 'text-left'), children: column.title }, column.key))) }) }), _jsx("tbody", { children: shown.map((row, index) => (_jsx("tr", { className: "border-t border-border-soft", children: columns.map((column) => (_jsx("td", { className: clsx('h-8 px-2 text-text', isNumericColumn(column) ? 'text-right whitespace-nowrap' : 'text-left'), children: _jsx(Cell, { value: row[column.key], column: column, live: isNumericColumn(column) ? context.liveValue?.(node.id, 'rows', [index, column.key]) : undefined
|
|
79
|
+
return (_jsxs("div", { className: "flex flex-col items-start gap-1.5", children: [_jsx("div", { className: "max-h-80 w-full overflow-auto rounded-md border border-border-soft", children: _jsxs("table", { "aria-label": t('blocks.table.label', { columns: columns.map((column) => column.title).join(', ') }), className: "w-full border-collapse text-xs tabular-nums", children: [_jsx("thead", { className: "sticky top-0 z-1 bg-surface-hover", children: _jsx("tr", { children: columns.map((column) => (_jsx("th", { scope: "col", className: clsx('h-8 px-2 font-medium whitespace-nowrap text-text-muted', isNumericColumn(column) ? 'text-right' : 'text-left'), children: column.title }, column.key))) }) }), _jsx("tbody", { children: shown.map((row, index) => (_jsx("tr", { className: "border-t border-border-soft", children: columns.map((column) => (_jsx("td", { className: clsx('h-8 px-2 text-text', isNumericColumn(column) ? 'text-right whitespace-nowrap' : 'text-left'), children: _jsx(Cell, { value: row[column.key], column: column, live: isNumericColumn(column) ? context.liveValue?.(node.id, 'rows', [index, column.key]) : undefined }) }, column.key))) }, index))) })] }) }), !all && rows.length > UI_TABLE_ROWS && (_jsx(Button, { size: "xs", variant: "secondary", onClick: () => setAll(true), children: t('blocks.table.showAll', { total: formatNumber(rows.length) }) }))] }));
|
|
87
80
|
}
|
|
@@ -5,3 +5,4 @@ export declare function ItemRenderer({ node, children }: UiRendererProps<UiProps
|
|
|
5
5
|
export declare function SwitchRenderer({ node, children, context }: UiRendererProps<UiProps<'Switch'>>): import("react").JSX.Element;
|
|
6
6
|
export declare function SliderRenderer({ node, context }: UiRendererProps<UiProps<'Slider'>>): import("react").JSX.Element;
|
|
7
7
|
export declare function SegmentedRenderer({ node, context }: UiRendererProps<UiProps<'Segmented'>>): import("react").JSX.Element;
|
|
8
|
+
export declare function ButtonRenderer({ node, children, context }: UiRendererProps<UiProps<'Button'>>): import("react").JSX.Element;
|
|
@@ -2,8 +2,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { createContext, useContext } from 'react';
|
|
3
3
|
import clsx from 'clsx';
|
|
4
4
|
import { useTranslation } from 'react-i18next';
|
|
5
|
-
import { Checkbox, Segmented, Slider, Switch } from '@adecore/ui';
|
|
6
|
-
import { uiChildrenOf, uiNodeLabel } from '../node-text.js';
|
|
5
|
+
import { Button, Checkbox, Segmented, Slider, Switch } from '@adecore/ui';
|
|
6
|
+
import { uiChildrenOf, uiNodeLabel, uiWithUnit } from '../node-text.js';
|
|
7
7
|
const ChecklistContext = createContext({ values: [], enabled: false, toggle: () => undefined });
|
|
8
8
|
/*
|
|
9
9
|
* The binding of an input, or null for a read-only node. An input works once its own node closed and
|
|
@@ -15,13 +15,18 @@ function bindingOf(node, context) {
|
|
|
15
15
|
}
|
|
16
16
|
/* An input whose node has not closed yet stands at 60% and takes no pointer. */
|
|
17
17
|
const PENDING = 'pointer-events-none opacity-60';
|
|
18
|
+
/* The rows of a checklist as one named group. */
|
|
19
|
+
function ChecklistGroup({ pending, children }) {
|
|
20
|
+
const { t } = useTranslation('agent-chat');
|
|
21
|
+
return (_jsx("div", { role: "group", "aria-label": t('blocks.checklist'), className: clsx('chat-ui-nodes flex flex-col', pending && PENDING), children: children }));
|
|
22
|
+
}
|
|
18
23
|
export function ChecklistRenderer({ node, children, context }) {
|
|
19
24
|
const binding = bindingOf(node, context);
|
|
20
25
|
const values = binding?.value ?? node.props.value;
|
|
21
26
|
const toggle = (value) => {
|
|
22
27
|
binding?.onValueChange(values.includes(value) ? values.filter((entry) => entry !== value) : [...values, value]);
|
|
23
28
|
};
|
|
24
|
-
return (_jsx(ChecklistContext, { value: { values, enabled: binding !== null, toggle }, children: _jsx(
|
|
29
|
+
return (_jsx(ChecklistContext, { value: { values, enabled: binding !== null, toggle }, children: _jsx(ChecklistGroup, { pending: !node.complete, children: children }) }));
|
|
25
30
|
}
|
|
26
31
|
/* The whole row is the label; a link inside it keeps its own click and checks nothing. */
|
|
27
32
|
export function ItemRenderer({ node, children }) {
|
|
@@ -42,7 +47,9 @@ export function SwitchRenderer({ node, children, context }) {
|
|
|
42
47
|
export function SliderRenderer({ node, context }) {
|
|
43
48
|
const binding = bindingOf(node, context);
|
|
44
49
|
const { value, min, max, step, unit } = node.props;
|
|
45
|
-
return (_jsx(Slider, { value: binding?.value ?? value, onValueChange: (next) => binding?.onValueChange(next), min: min, max: max, step: step,
|
|
50
|
+
return (_jsx(Slider, { value: binding?.value ?? value, onValueChange: (next) => binding?.onValueChange(next), min: min, max: max, step: step,
|
|
51
|
+
// The slider prints its unit right after the number, so the space a unit stands apart with comes from here.
|
|
52
|
+
unit: uiWithUnit('', unit), label: uiNodeLabel(node), disabled: binding === null, className: clsx('px-2', !node.complete && PENDING) }));
|
|
46
53
|
}
|
|
47
54
|
/* Left aligned, never the full width. Each Option is a segment by its place, since a value may be any scalar. */
|
|
48
55
|
export function SegmentedRenderer({ node, context }) {
|
|
@@ -58,3 +65,8 @@ export function SegmentedRenderer({ node, context }) {
|
|
|
58
65
|
}
|
|
59
66
|
} }) }));
|
|
60
67
|
}
|
|
68
|
+
/* Runs its local action once its own node closed; like an input, it closes once the block is answered. */
|
|
69
|
+
export function ButtonRenderer({ node, children, context }) {
|
|
70
|
+
const enabled = node.onAction !== undefined && node.complete && context.answer === null && node.props.disabled !== true;
|
|
71
|
+
return (_jsx("div", { className: clsx('px-2', !node.complete && PENDING), children: _jsx(Button, { variant: "secondary", size: "sm", disabled: !enabled, onClick: () => node.onAction?.(), children: children }) }));
|
|
72
|
+
}
|
|
@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
|
|
|
4
4
|
import { FileDiff, GitCommitHorizontal, SquareDashed } from 'lucide-react';
|
|
5
5
|
import { FileIcon, Icon, Tooltip } from '@adecore/ui';
|
|
6
6
|
import { CHIP_IN_MESSAGE, MENTION_TONE } from '../../chips.js';
|
|
7
|
+
import { uiReasonText } from '../node-text.js';
|
|
7
8
|
const PLAIN = { state: 'plain' };
|
|
8
9
|
function baseName(path) {
|
|
9
10
|
const trimmed = path.replace(/\/+$/, '');
|
|
@@ -54,7 +55,7 @@ export function UiLinkChip({ target, context }) {
|
|
|
54
55
|
const { t } = useTranslation('agent-chat');
|
|
55
56
|
const link = context.link?.(target) ?? PLAIN;
|
|
56
57
|
if (link.state === 'plain') {
|
|
57
|
-
const reason = link.reason ?? (context.link === undefined ? null : t('blocks.outsideProject'));
|
|
58
|
+
const reason = uiReasonText(t, link.code, link.reason) ?? (context.link === undefined ? null : t('blocks.outsideProject'));
|
|
58
59
|
// Focusable only with a reason, so a keyboard reaches the tooltip.
|
|
59
60
|
const text = (_jsx("span", { tabIndex: reason === null ? undefined : 0, className: "font-mono text-code break-all text-text-muted", children: plainText(target, link) }));
|
|
60
61
|
return reason === null ? text : _jsx(Tooltip, { label: reason, children: text });
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type { ChatUiQueryState } from '@adecore/agent-contracts';
|
|
1
|
+
import type { ChatUiLinkReading, ChatUiQueryState } from '@adecore/agent-contracts';
|
|
2
2
|
import { type UiLinkTarget } from '@adecore/intelligent-ui/links';
|
|
3
3
|
import { type UiBlock, UiState } from '@adecore/intelligent-ui';
|
|
4
4
|
import type { UiRenderContext } from './render-context.js';
|
|
5
|
-
export declare function useUiLinks(block: UiBlock, state: UiState, context: UiRenderContext, reads: Readonly<Record<string, string>>, frozen?: ChatUiQueryState['blocks'][string]): {
|
|
6
|
-
link: ((target: UiLinkTarget) =>
|
|
5
|
+
export declare function useUiLinks(block: UiBlock, state: UiState, context: Pick<UiRenderContext, 'scopeId' | 'chatId' | 'itemId' | 'phase'>, reads: Readonly<Record<string, string>>, frozen?: ChatUiQueryState['blocks'][string]): {
|
|
6
|
+
link: ((target: UiLinkTarget) => ChatUiLinkReading) | undefined;
|
|
7
7
|
openLink: ((target: UiLinkTarget) => void) | undefined;
|
|
8
|
-
openUrl: ((url: string) => void) | undefined;
|
|
8
|
+
openUrl: ((url: string) => void | undefined) | undefined;
|
|
9
9
|
};
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react';
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
2
|
import { uiLinkTargets } from '@adecore/intelligent-ui/links';
|
|
3
|
-
import { uiInputValues, UiState } from '@adecore/intelligent-ui';
|
|
3
|
+
import { uiInputValues, UI_HOST_LIMITS, UiState } from '@adecore/intelligent-ui';
|
|
4
4
|
import { chatHost } from '../../../host.js';
|
|
5
|
+
// Inputs that change in a run, such as a dragged slider, check their links once the run rested.
|
|
6
|
+
const LINK_SETTLE_MS = 300;
|
|
5
7
|
export function useUiLinks(block, state, context, reads, frozen) {
|
|
6
8
|
const [checked, setChecked] = useState({});
|
|
9
|
+
// What each node was last checked as, so a change of input checks only the targets it moved.
|
|
10
|
+
const asked = useRef(new Map());
|
|
7
11
|
const host = chatHost().intelligentUi;
|
|
8
12
|
const values = uiInputValues(block, state);
|
|
9
13
|
const scope = state.scope();
|
|
@@ -20,14 +24,25 @@ export function useUiLinks(block, state, context, reads, frozen) {
|
|
|
20
24
|
const signature = JSON.stringify(targets);
|
|
21
25
|
const input = JSON.stringify(values);
|
|
22
26
|
const tickets = JSON.stringify(reads);
|
|
27
|
+
const defaultInput = useMemo(() => JSON.stringify(uiInputValues(block, new UiState(block))), [block]);
|
|
23
28
|
const link = host?.link;
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
asked.current.clear();
|
|
31
|
+
}, [block.id, block.revision, context.scopeId, context.chatId, context.itemId, link]);
|
|
24
32
|
useEffect(() => {
|
|
25
33
|
if (!link || !block.revision || context.phase !== 'final') {
|
|
26
34
|
return;
|
|
27
35
|
}
|
|
28
36
|
let stopped = false;
|
|
29
37
|
const validate = async () => {
|
|
30
|
-
|
|
38
|
+
const moved = Object.entries(JSON.parse(signature))
|
|
39
|
+
.slice(0, UI_HOST_LIMITS.links)
|
|
40
|
+
.filter(([nodeId, target]) => asked.current.get(nodeId) !== JSON.stringify(target));
|
|
41
|
+
for (const [nodeId, target] of moved) {
|
|
42
|
+
const record = (reading) => {
|
|
43
|
+
asked.current.set(nodeId, JSON.stringify(target));
|
|
44
|
+
setChecked((previous) => ({ ...previous, [nodeId]: { target: JSON.stringify(target), reading } }));
|
|
45
|
+
};
|
|
31
46
|
try {
|
|
32
47
|
const reading = await link(context.scopeId, {
|
|
33
48
|
chatId: context.chatId,
|
|
@@ -41,19 +56,21 @@ export function useUiLinks(block, state, context, reads, frozen) {
|
|
|
41
56
|
if (stopped) {
|
|
42
57
|
return;
|
|
43
58
|
}
|
|
44
|
-
|
|
59
|
+
record(reading);
|
|
45
60
|
}
|
|
46
61
|
catch {
|
|
47
62
|
if (stopped) {
|
|
48
63
|
return;
|
|
49
64
|
}
|
|
50
|
-
|
|
65
|
+
record({ state: 'plain' });
|
|
51
66
|
}
|
|
52
67
|
}
|
|
53
68
|
};
|
|
54
|
-
|
|
69
|
+
// The first check runs at once; a later one waits until the inputs rested.
|
|
70
|
+
const timer = setTimeout(() => void validate(), asked.current.size === 0 ? 0 : LINK_SETTLE_MS);
|
|
55
71
|
return () => {
|
|
56
72
|
stopped = true;
|
|
73
|
+
clearTimeout(timer);
|
|
57
74
|
};
|
|
58
75
|
}, [block.id, block.revision, context.scopeId, context.chatId, context.itemId, context.phase, signature, input, tickets, link]);
|
|
59
76
|
const idOf = (target) => Object.entries(targets).find(([, entry]) => JSON.stringify(entry) === JSON.stringify(target))?.[0];
|
|
@@ -65,7 +82,7 @@ export function useUiLinks(block, state, context, reads, frozen) {
|
|
|
65
82
|
if (checked[id]?.target === JSON.stringify(target)) {
|
|
66
83
|
return checked[id].reading;
|
|
67
84
|
}
|
|
68
|
-
if (input ===
|
|
85
|
+
if (input === defaultInput && frozen && frozen.revision === block.revision) {
|
|
69
86
|
const firstQueries = Object.fromEntries(Object.entries(frozen.readings)
|
|
70
87
|
.filter(([, reading]) => reading.state === 'fresh')
|
|
71
88
|
.map(([name, reading]) => [name, reading.value]));
|
|
@@ -99,9 +116,11 @@ export function useUiLinks(block, state, context, reads, frozen) {
|
|
|
99
116
|
setChecked((previous) => ({ ...previous, [nodeId]: { target: JSON.stringify(target), reading: { state: 'plain' } } }));
|
|
100
117
|
});
|
|
101
118
|
};
|
|
119
|
+
const hostOpenUrl = host?.openUrl;
|
|
120
|
+
const openUrl = useCallback((url) => hostOpenUrl?.(context.scopeId, url), [hostOpenUrl, context.scopeId]);
|
|
102
121
|
return {
|
|
103
|
-
link: link ? resolution :
|
|
104
|
-
openLink: link && host?.openLink ? open :
|
|
105
|
-
openUrl:
|
|
122
|
+
link: link ? resolution : undefined,
|
|
123
|
+
openLink: link && host?.openLink ? open : undefined,
|
|
124
|
+
openUrl: hostOpenUrl ? openUrl : undefined
|
|
106
125
|
};
|
|
107
126
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ChatUiQueryReading, ChatUiQueryState } from '@adecore/agent-contracts';
|
|
2
2
|
import { UiState, type UiBlock, type UiViewNode } from '@adecore/intelligent-ui';
|
|
3
3
|
import type { UiLiveValue, UiLiveStatus, UiRenderContext } from './render-context.js';
|
|
4
|
-
export declare function useUiQueries(block: UiBlock, state: UiState, context: UiRenderContext, element: HTMLDivElement | null, frozen?: ChatUiQueryState['blocks'][string]): {
|
|
4
|
+
export declare function useUiQueries(block: UiBlock, state: UiState, context: Pick<UiRenderContext, 'scopeId' | 'chatId' | 'itemId' | 'phase'>, element: HTMLDivElement | null, frozen?: ChatUiQueryState['blocks'][string]): {
|
|
5
5
|
live: UiLiveStatus | undefined;
|
|
6
6
|
reads: Record<string, string>;
|
|
7
7
|
reading: boolean;
|
|
@@ -13,6 +13,7 @@ export declare function useUiQueries(block: UiBlock, state: UiState, context: Ui
|
|
|
13
13
|
value?: unknown;
|
|
14
14
|
readId?: string | undefined;
|
|
15
15
|
readAt: number;
|
|
16
|
+
code?: string | undefined;
|
|
16
17
|
reason?: string | undefined;
|
|
17
18
|
};
|
|
18
19
|
};
|
|
@@ -1,27 +1,34 @@
|
|
|
1
|
-
import { useEffect, useRef, useState } from 'react';
|
|
2
|
-
import { uiInputValues, UI_CATALOG_VERSION, UiState } from '@adecore/intelligent-ui';
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { uiInputValues, UI_CATALOG_VERSION, UI_HOST_LIMITS, UiState } from '@adecore/intelligent-ui';
|
|
3
3
|
import { chatHost } from '../../../host.js';
|
|
4
|
+
const NO_READINGS = {};
|
|
5
|
+
// A changed input reads once it has rested this long, so dragging a slider reads its last value only.
|
|
6
|
+
const INPUT_SETTLE_MS = 300;
|
|
7
|
+
/* 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. */
|
|
8
|
+
function successfulReading(local, initial, identity, signature, defaultInputs) {
|
|
9
|
+
if (local?.identity === identity && local.successful?.inputs === signature) {
|
|
10
|
+
return local.successful.reading;
|
|
11
|
+
}
|
|
12
|
+
return signature === defaultInputs && initial?.state === 'fresh' ? initial : undefined;
|
|
13
|
+
}
|
|
14
|
+
function settleDeadline() {
|
|
15
|
+
return Date.now() + INPUT_SETTLE_MS;
|
|
16
|
+
}
|
|
4
17
|
function documentVisible() {
|
|
5
18
|
return document.visibilityState !== 'hidden';
|
|
6
19
|
}
|
|
7
20
|
export function useUiQueries(block, state, context, element, frozen) {
|
|
8
21
|
const [readings, setReadings] = useState({});
|
|
9
22
|
const [busyInput, setBusyInput] = useState(null);
|
|
10
|
-
const
|
|
23
|
+
const lastRead = useRef({ inputKey: '', at: 0 });
|
|
11
24
|
const names = Object.keys(block.queries);
|
|
12
25
|
const inputs = JSON.stringify(uiInputValues(block, state));
|
|
13
26
|
const identity = JSON.stringify([context.scopeId, context.chatId, context.itemId, block.id, block.revision]);
|
|
14
27
|
const inputKey = JSON.stringify([identity, inputs]);
|
|
15
28
|
const reading = busyInput === inputKey;
|
|
16
|
-
const defaultInputs = JSON.stringify(uiInputValues(block, new UiState(block)));
|
|
17
|
-
const initial = frozen && frozen.revision === block.revision ? frozen.readings :
|
|
18
|
-
const successfulFor = (name, signature) =>
|
|
19
|
-
const local = readings[name];
|
|
20
|
-
if (local?.identity === identity && local.successful?.inputs === signature) {
|
|
21
|
-
return local.successful.reading;
|
|
22
|
-
}
|
|
23
|
-
return signature === defaultInputs && initial[name]?.state === 'fresh' ? initial[name] : undefined;
|
|
24
|
-
};
|
|
29
|
+
const defaultInputs = useMemo(() => JSON.stringify(uiInputValues(block, new UiState(block))), [block]);
|
|
30
|
+
const initial = frozen && frozen.revision === block.revision ? frozen.readings : NO_READINGS;
|
|
31
|
+
const successfulFor = (name, signature) => successfulReading(readings[name], initial[name], identity, signature, defaultInputs);
|
|
25
32
|
const readsFor = (values) => {
|
|
26
33
|
const signature = JSON.stringify(values);
|
|
27
34
|
const reads = {};
|
|
@@ -41,13 +48,13 @@ export function useUiQueries(block, state, context, element, frozen) {
|
|
|
41
48
|
const subscribe = chatHost().intelligentUi?.subscribe;
|
|
42
49
|
useEffect(() => {
|
|
43
50
|
const scope = state.scope();
|
|
44
|
-
for (const name of
|
|
45
|
-
const successful =
|
|
51
|
+
for (const name of Object.keys(block.queries)) {
|
|
52
|
+
const successful = successfulReading(readings[name], initial[name], identity, inputs, defaultInputs);
|
|
46
53
|
if (successful && JSON.stringify(scope[name]) !== JSON.stringify(successful.value)) {
|
|
47
54
|
state.setQuery(name, successful.value, block);
|
|
48
55
|
}
|
|
49
56
|
}
|
|
50
|
-
});
|
|
57
|
+
}, [state, block, readings, identity, inputs, defaultInputs, initial]);
|
|
51
58
|
useEffect(() => {
|
|
52
59
|
if (!query ||
|
|
53
60
|
!element ||
|
|
@@ -62,11 +69,14 @@ export function useUiQueries(block, state, context, element, frozen) {
|
|
|
62
69
|
let visible = typeof IntersectionObserver === 'undefined';
|
|
63
70
|
let busy = false;
|
|
64
71
|
let timer = null;
|
|
72
|
+
// The first read of this block reads at once; a changed input waits until it rests.
|
|
73
|
+
const changed = lastRead.current.inputKey !== '' && lastRead.current.inputKey !== inputKey;
|
|
74
|
+
const settledAt = changed ? settleDeadline() : 0;
|
|
65
75
|
const refresh = async () => {
|
|
66
76
|
if (stopped || !visible || !documentVisible() || busy) {
|
|
67
77
|
return;
|
|
68
78
|
}
|
|
69
|
-
const wait =
|
|
79
|
+
const wait = Math.max(settledAt - Date.now(), lastRead.current.inputKey === inputKey ? UI_HOST_LIMITS.refreshMilliseconds - (Date.now() - lastRead.current.at) : 0);
|
|
70
80
|
if (wait > 0) {
|
|
71
81
|
if (timer === null) {
|
|
72
82
|
if (!visible || !documentVisible()) {
|
|
@@ -80,9 +90,9 @@ export function useUiQueries(block, state, context, element, frozen) {
|
|
|
80
90
|
return;
|
|
81
91
|
}
|
|
82
92
|
busy = true;
|
|
83
|
-
|
|
93
|
+
lastRead.current = { inputKey, at: Date.now() };
|
|
84
94
|
setBusyInput(inputKey);
|
|
85
|
-
for (const name of Object.keys(block.queries).slice(0,
|
|
95
|
+
for (const name of Object.keys(block.queries).slice(0, UI_HOST_LIMITS.queries)) {
|
|
86
96
|
if (!visible || !documentVisible()) {
|
|
87
97
|
break;
|
|
88
98
|
}
|
|
@@ -118,7 +128,7 @@ export function useUiQueries(block, state, context, element, frozen) {
|
|
|
118
128
|
...previous,
|
|
119
129
|
[name]: {
|
|
120
130
|
identity,
|
|
121
|
-
reading: { state: 'failed', readAt: Date.now(), reason: error instanceof Error ? error.message :
|
|
131
|
+
reading: { state: 'failed', readAt: Date.now(), code: 'unreadable', reason: error instanceof Error ? error.message : undefined },
|
|
122
132
|
successful: previous[name]?.identity === identity ? previous[name].successful : undefined
|
|
123
133
|
}
|
|
124
134
|
}));
|
|
@@ -132,7 +142,7 @@ export function useUiQueries(block, state, context, element, frozen) {
|
|
|
132
142
|
timer = setTimeout(() => {
|
|
133
143
|
timer = null;
|
|
134
144
|
void refresh();
|
|
135
|
-
},
|
|
145
|
+
}, UI_HOST_LIMITS.refreshMilliseconds);
|
|
136
146
|
};
|
|
137
147
|
const observer = typeof IntersectionObserver === 'undefined'
|
|
138
148
|
? null
|
|
@@ -157,6 +167,8 @@ export function useUiQueries(block, state, context, element, frozen) {
|
|
|
157
167
|
void refresh();
|
|
158
168
|
return () => {
|
|
159
169
|
stopped = true;
|
|
170
|
+
// A read cut short by this cleanup never reaches its own reset, which would leave the choices closed.
|
|
171
|
+
setBusyInput((current) => (current === inputKey ? null : current));
|
|
160
172
|
observer?.disconnect();
|
|
161
173
|
unsubscribe?.();
|
|
162
174
|
document.removeEventListener('visibilitychange', focus);
|
|
@@ -179,7 +191,8 @@ export function useUiQueries(block, state, context, element, frozen) {
|
|
|
179
191
|
.filter((reading) => reading.state === 'fresh')
|
|
180
192
|
.reduce((at, value) => (at === null ? value.readAt : Math.min(at, value.readAt)), null),
|
|
181
193
|
source: failed ? block.queries[failed[0]]?.source : undefined,
|
|
182
|
-
reason: failed?.[1].reason
|
|
194
|
+
reason: failed?.[1].reason,
|
|
195
|
+
code: failed?.[1].code
|
|
183
196
|
}
|
|
184
197
|
: undefined;
|
|
185
198
|
return { live, reads: reads ?? {}, reading, ready, readsFor, currentReadings };
|
|
@@ -129,7 +129,7 @@ export function AssistantRow({ chatId, item: derived }) {
|
|
|
129
129
|
const live = mode === 'words' && item.streaming;
|
|
130
130
|
const reveal = useRevealedText(item.text, live);
|
|
131
131
|
if (item.ui?.length && !item.parentToolUseId && !(mode === 'whole' && item.streaming)) {
|
|
132
|
-
return (_jsxs("div", { "data-find-item": item.id, className: "-mx-1 px-1 pb-2", children: [_jsx(ReplyHeading, { chatId: chatId }), _jsx("div", { "data-find-field": "text", "data-quote-answer": true, className: "contents", children: _jsx(UiReply, { text: item.text, blocks: item.ui, answers: item.uiAnswers, queries: item.uiQueries, reply: reply, context: { scopeId: scope.id, chatId, itemId: item.id, phase: item.streaming ? 'streaming' : 'final'
|
|
132
|
+
return (_jsxs("div", { "data-find-item": item.id, className: "-mx-1 px-1 pb-2", children: [_jsx(ReplyHeading, { chatId: chatId }), _jsx("div", { "data-find-field": "text", "data-quote-answer": true, className: "contents", children: _jsx(UiReply, { text: item.text, blocks: item.ui, answers: item.uiAnswers, queries: item.uiQueries, reply: reply, context: { scopeId: scope.id, chatId, itemId: item.id, phase: item.streaming ? 'streaming' : 'final' } }) })] }));
|
|
133
133
|
}
|
|
134
134
|
if (mode === 'blocks') {
|
|
135
135
|
const settled = item.streaming ? settledBlocksText(item.text) : item.text;
|
|
@@ -186,9 +186,8 @@ function OpenChatButton({ chatId }) {
|
|
|
186
186
|
return (_jsx("button", { className: "text-text-muted hover:text-text", onClick: place.go, children: t('rows.note.openFork') }));
|
|
187
187
|
}
|
|
188
188
|
/*
|
|
189
|
-
* The header of a turn the agent started itself, in place of
|
|
190
|
-
*
|
|
191
|
-
* "Show result". When the CLI named the sub-agent it woke up about, the header opens that row.
|
|
189
|
+
* The header of a turn the agent started itself, in place of a message of the person. What the
|
|
190
|
+
* sub-agent came back with lives on its own row, which the header opens when the CLI named it.
|
|
192
191
|
*/
|
|
193
192
|
export function AgentTurnRow({ label, onOpen }) {
|
|
194
193
|
const line = 'mb-0.5 flex h-7 w-full items-center gap-2 text-left text-xs text-text-muted';
|
|
@@ -1,20 +1,21 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
1
2
|
import type { ChatCheckpointDiff, ChatToolItem, ChatTurnItem } from '@adecore/agent-contracts';
|
|
2
3
|
export declare function ToggleLine({ icon, label, detail, open, onToggle, failed, live, trailing, className }: {
|
|
3
|
-
icon:
|
|
4
|
+
icon: ReactNode;
|
|
4
5
|
label: string;
|
|
5
6
|
detail?: string;
|
|
6
7
|
open: boolean;
|
|
7
8
|
onToggle(): void;
|
|
8
9
|
failed?: boolean;
|
|
9
10
|
live?: boolean;
|
|
10
|
-
trailing?:
|
|
11
|
+
trailing?: ReactNode;
|
|
11
12
|
className?: string;
|
|
12
13
|
}): import("react").JSX.Element;
|
|
13
14
|
export declare function WorkRow({ tool, nested, detail, icon, label, failed }: {
|
|
14
15
|
tool: ChatToolItem;
|
|
15
16
|
nested?: boolean;
|
|
16
17
|
detail?: string;
|
|
17
|
-
icon?:
|
|
18
|
+
icon?: ReactNode;
|
|
18
19
|
label?: string;
|
|
19
20
|
failed?: boolean;
|
|
20
21
|
}): import("react").JSX.Element;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Suspense, useEffect, useState } from 'react';
|
|
3
3
|
import clsx from 'clsx';
|
|
4
4
|
import i18next from 'i18next';
|
|
@@ -30,15 +30,24 @@ export function ToggleLine({ icon, label, detail, open, onToggle, failed, live,
|
|
|
30
30
|
// Fix the width so long commands reach the truncation inside the negative margins.
|
|
31
31
|
'-mx-1 mb-0.5 flex h-7 w-[calc(100%+8px)] items-center gap-2 rounded-md px-1 text-left text-xs text-text-muted hover:bg-surface-hover', failed && 'text-status-error', className), onClick: onToggle, children: [_jsx("span", { className: clsx(ROW_GUTTER, live && 'text-accent'), children: icon }), _jsx("span", { className: clsx('shrink-0', live && 'chat-live-text'), children: label }), detail && (_jsx("span", { "data-find-field": "summary", className: "min-w-0 truncate font-mono text-text-faint", children: detail })), _jsx("span", { className: "grow" }), trailing, failed && _jsx(Icon, { icon: X, size: 12, className: "shrink-0" }), _jsx(Icon, { icon: ChevronRight, size: 12, className: clsx('shrink-0 text-text-faint transition-transform', open && 'rotate-90') })] }));
|
|
32
32
|
}
|
|
33
|
-
|
|
33
|
+
/* The diff renderers load lazily, so a diff waits on them under a line that says so. */
|
|
34
|
+
function DiffSuspense({ children }) {
|
|
34
35
|
const { t } = useTranslation('agent-chat');
|
|
36
|
+
return _jsx(Suspense, { fallback: _jsx("div", { className: "px-3 py-2 text-xs text-text-faint", children: t('work.loadingDiff') }), children: children });
|
|
37
|
+
}
|
|
38
|
+
/* A file in a changed files card, which opens its diff under it. */
|
|
39
|
+
function ChangedFileLine({ path, open, onToggle, trailing, children }) {
|
|
40
|
+
return (_jsxs("div", { className: "border-t border-border", children: [_jsxs("button", { className: "flex w-full items-center gap-2 px-3 py-1.5 text-left text-xs text-text-muted hover:bg-surface-hover", "data-file-path": path, onClick: onToggle, children: [_jsx(Icon, { icon: ChevronRight, size: 12, className: clsx('shrink-0 text-text-faint transition-transform', open && 'rotate-90') }), _jsx("span", { className: "min-w-0 truncate font-mono text-text", children: path }), _jsx("span", { className: "grow" }), trailing] }), open && _jsx("div", { className: "border-t border-border", children: children })] }));
|
|
41
|
+
}
|
|
42
|
+
function ToolBody({ tool }) {
|
|
35
43
|
const patches = unifiedChanges(tool);
|
|
36
44
|
const changes = patches.length > 0 ? [] : fileChanges(tool.name, tool.input);
|
|
37
|
-
return (_jsxs("div", { className: "mt-1.5 mb-2 ml-6 overflow-hidden rounded-md border border-border bg-surface-raised", children: [patches.length > 0 ? (_jsx(
|
|
45
|
+
return (_jsxs("div", { className: "mt-1.5 mb-2 ml-6 overflow-hidden rounded-md border border-border bg-surface-raised", children: [patches.length > 0 ? (_jsx(DiffSuspense, { children: patches.map((change, index) => (_jsx(UnifiedDiff, { change: change }, index))) })) : changes.length > 0 ? (_jsx(DiffSuspense, { children: changes.map((change, index) => (_jsx(EditDiff, { change: change }, index))) })) : (_jsx("pre", { className: "max-h-64 overflow-auto px-3 py-2 font-mono text-code text-text-muted select-text", children: clip(JSON.stringify(tool.input, null, 2) ?? '') })), tool.output !== null && tool.output !== '' && patches.length === 0 && (_jsx("pre", { "data-find-field": "output", className: clsx('max-h-64 overflow-auto whitespace-pre-wrap border-t border-border px-3 py-2 font-mono text-code select-text', tool.state === 'error' ? 'text-chat-removed' : 'text-chat-output'), children: _jsx(AnsiOutput, { text: tool.output, limit: OUTPUT_LIMIT }) }))] }));
|
|
38
46
|
}
|
|
39
|
-
/*
|
|
40
|
-
|
|
41
|
-
|
|
47
|
+
/*
|
|
48
|
+
* One settled tool call: a line, and its input and output behind it. An image the call looked at is
|
|
49
|
+
* drawn under the line. `icon`, `label` and `failed` let a row that knows the call better name it.
|
|
50
|
+
*/
|
|
42
51
|
export function WorkRow({ tool, nested, detail, icon, label, failed }) {
|
|
43
52
|
const [open, setOpen] = useState(false);
|
|
44
53
|
const image = tool.state === 'done' ? readImagePath(tool.name, tool.input) : null;
|
|
@@ -76,17 +85,16 @@ function omittedLabel(reason) {
|
|
|
76
85
|
}
|
|
77
86
|
/* One file of a turn's checkpoint diff: its patch, or the reason there is none. */
|
|
78
87
|
function CheckpointFileBody({ file }) {
|
|
79
|
-
const { t } = useTranslation('agent-chat');
|
|
80
88
|
if (file.omitted) {
|
|
81
89
|
return _jsx("div", { className: "px-3 py-2 text-xs text-text-faint", children: omittedLabel(file.omitted) });
|
|
82
90
|
}
|
|
83
|
-
return (_jsx(
|
|
91
|
+
return (_jsx(DiffSuspense, { children: _jsx(UnifiedDiff, { change: file }) }));
|
|
84
92
|
}
|
|
85
93
|
/* The frame of a changed files card: a header that folds the list of files away. */
|
|
86
94
|
function ChangedFilesCard({ count, truncated, added, deleted, revealNonce = 0, children }) {
|
|
87
95
|
const { t } = useTranslation('agent-chat');
|
|
88
96
|
const [expanded, setExpanded] = useState(false);
|
|
89
|
-
// A jump
|
|
97
|
+
// A jump opens the card, also one that mounted it by scrolling it on screen; the flash is gone by the next visit.
|
|
90
98
|
const [seenReveal, setSeenReveal] = useState(0);
|
|
91
99
|
if (revealNonce > 0 && revealNonce !== seenReveal) {
|
|
92
100
|
setSeenReveal(revealNonce);
|
|
@@ -127,7 +135,7 @@ export function ChangedFilesRow({ tools, diff, checkpoint, chatId, turnId }) {
|
|
|
127
135
|
const revealNonce = flash !== null && flash.opens && flash.key === keyOf(chatId) && flash.rowId === `files-${turnId}` ? flash.nonce : 0;
|
|
128
136
|
if (checkpointDiff !== null && checkpointDiff.files.length > 0) {
|
|
129
137
|
const files = checkpointDiff.files;
|
|
130
|
-
return (_jsx(ChangedFilesCard, { revealNonce: revealNonce, count: files.length, truncated: checkpointDiff.truncated, added: files.reduce((sum, file) => sum + file.added, 0), deleted: files.reduce((sum, file) => sum + file.deleted, 0), children: files.map((file) => (
|
|
138
|
+
return (_jsx(ChangedFilesCard, { revealNonce: revealNonce, count: files.length, truncated: checkpointDiff.truncated, added: files.reduce((sum, file) => sum + file.added, 0), deleted: files.reduce((sum, file) => sum + file.deleted, 0), children: files.map((file) => (_jsx(ChangedFileLine, { path: file.path, open: open[file.path] === true, onToggle: () => setOpen((o) => ({ ...o, [file.path]: !o[file.path] })), trailing: _jsxs(_Fragment, { children: [_jsxs("span", { className: "text-chat-added tabular-nums", children: ["+", file.added] }), _jsxs("span", { className: "text-chat-removed tabular-nums", children: ["-", file.deleted] })] }), children: _jsx(CheckpointFileBody, { file: file }) }, file.path))) }));
|
|
131
139
|
}
|
|
132
140
|
return _jsx(ProviderChangedFiles, { tools: tools, open: open, setOpen: setOpen, revealNonce: revealNonce });
|
|
133
141
|
}
|
|
@@ -156,10 +164,7 @@ function ProviderChangedFiles({ tools, open, setOpen, revealNonce }) {
|
|
|
156
164
|
entryFor(change.path || tool.id).edits.push(change);
|
|
157
165
|
}
|
|
158
166
|
}
|
|
159
|
-
return (_jsx(ChangedFilesCard, { count: byPath.size, revealNonce: revealNonce, children: [...byPath].map(([path, entry]) => {
|
|
160
|
-
const count = entry.edits.length + entry.patches.length;
|
|
161
|
-
return (_jsxs("div", { className: "border-t border-border", children: [_jsxs("button", { className: "flex w-full items-center gap-2 px-3 py-1.5 text-left text-xs text-text-muted hover:bg-surface-hover", "data-file-path": path, onClick: () => setOpen((o) => ({ ...o, [path]: !o[path] })), children: [_jsx(Icon, { icon: ChevronRight, size: 12, className: clsx('shrink-0 text-text-faint transition-transform', open[path] && 'rotate-90') }), _jsx("span", { className: "min-w-0 truncate font-mono text-text", children: path }), _jsx("span", { className: "grow" }), _jsx("span", { className: "text-text-faint", children: t('work.edits', { count }) })] }), open[path] && (_jsx("div", { className: "border-t border-border", children: _jsxs(Suspense, { fallback: _jsx("div", { className: "px-3 py-2 text-xs text-text-faint", children: t('work.loadingDiff') }), children: [entry.patches.map((change, index) => (_jsx(UnifiedDiff, { change: change }, `patch-${index}`))), entry.edits.map((change, index) => (_jsx(EditDiff, { change: change }, `edit-${index}`)))] }) }))] }, path));
|
|
162
|
-
}) }));
|
|
167
|
+
return (_jsx(ChangedFilesCard, { count: byPath.size, revealNonce: revealNonce, children: [...byPath].map(([path, entry]) => (_jsx(ChangedFileLine, { path: path, open: open[path] === true, onToggle: () => setOpen((o) => ({ ...o, [path]: !o[path] })), trailing: _jsx("span", { className: "text-text-faint", children: t('work.edits', { count: entry.edits.length + entry.patches.length }) }), children: _jsxs(DiffSuspense, { children: [entry.patches.map((change, index) => (_jsx(UnifiedDiff, { change: change }, `patch-${index}`))), entry.edits.map((change, index) => (_jsx(EditDiff, { change: change }, `edit-${index}`)))] }) }, path))) }));
|
|
163
168
|
}
|
|
164
169
|
/* "Working for 00:14". */
|
|
165
170
|
export function WorkingRow({ startedAt }) {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { VisualScrollRequest } from '@adecore/agent-contracts/visual';
|
|
2
2
|
import type { VisualBridge } from './visual-bridge.js';
|
|
3
|
-
/** Keeps one small frame in a full-height timeline row, without rendering React on scroll. */
|
|
4
3
|
export declare class VisualViewportController {
|
|
5
4
|
private readonly box;
|
|
6
5
|
private readonly viewport;
|
|
@@ -18,5 +17,6 @@ export declare class VisualViewportController {
|
|
|
18
17
|
refresh(): void;
|
|
19
18
|
scroll(request: VisualScrollRequest): void;
|
|
20
19
|
dispose(): void;
|
|
20
|
+
private offsetInScroller;
|
|
21
21
|
private schedule;
|
|
22
22
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { visualViewportGeometry } from './logic/visual-viewport.js';
|
|
2
|
-
|
|
2
|
+
/* Keeps one small frame in a full-height timeline row, without rendering React on scroll. */
|
|
3
3
|
export class VisualViewportController {
|
|
4
4
|
box;
|
|
5
5
|
viewport;
|
|
@@ -35,11 +35,7 @@ export class VisualViewportController {
|
|
|
35
35
|
if (this.disposed) {
|
|
36
36
|
return;
|
|
37
37
|
}
|
|
38
|
-
const
|
|
39
|
-
const box = this.box.getBoundingClientRect();
|
|
40
|
-
// A chat on a canvas can be scaled; messages inside its frame use unscaled CSS pixels.
|
|
41
|
-
const scale = outer.height / this.scroller.offsetHeight || 1;
|
|
42
|
-
const top = (box.top - outer.top) / scale - this.scroller.clientTop;
|
|
38
|
+
const top = this.offsetInScroller(this.scroller.getBoundingClientRect(), this.box.getBoundingClientRect());
|
|
43
39
|
const available = this.scroller.clientHeight;
|
|
44
40
|
const geometry = visualViewportGeometry(this.box.offsetHeight, available, top);
|
|
45
41
|
this.viewport.style.height = `${geometry.height}px`;
|
|
@@ -61,8 +57,7 @@ export class VisualViewportController {
|
|
|
61
57
|
return;
|
|
62
58
|
}
|
|
63
59
|
this.pendingDelta = 0;
|
|
64
|
-
const
|
|
65
|
-
const start = this.scroller.scrollTop + (box.top - outer.top) / scale - this.scroller.clientTop;
|
|
60
|
+
const start = this.scroller.scrollTop + this.offsetInScroller(outer, box);
|
|
66
61
|
const top = 'to' in request ? start + request.to : request.edge === 'start' ? 0 : this.scroller.scrollHeight;
|
|
67
62
|
this.scroller.scrollTo({ top, behavior: 'instant' });
|
|
68
63
|
this.refresh();
|
|
@@ -75,6 +70,12 @@ export class VisualViewportController {
|
|
|
75
70
|
this.scroller.removeEventListener('scroll', this.onScroll);
|
|
76
71
|
this.bridge.setViewport(null);
|
|
77
72
|
}
|
|
73
|
+
/* The box's top below the scroller's visible top, in the scroller's own pixels. */
|
|
74
|
+
offsetInScroller(outer, box) {
|
|
75
|
+
// A chat on a canvas can be scaled; messages inside its frame use unscaled CSS pixels.
|
|
76
|
+
const scale = outer.height / this.scroller.offsetHeight || 1;
|
|
77
|
+
return (box.top - outer.top) / scale - this.scroller.clientTop;
|
|
78
|
+
}
|
|
78
79
|
schedule() {
|
|
79
80
|
if (this.frame || this.disposed) {
|
|
80
81
|
return;
|