@adecore/agents-react 0.20.1 → 0.21.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/dist/chat/logic/generated-image.d.ts +27 -0
  2. package/dist/chat/logic/generated-image.js +32 -0
  3. package/dist/chat/logic/timeline-target.d.ts +13 -1
  4. package/dist/chat/logic/timeline-target.js +26 -0
  5. package/dist/chat/logic/timeline.js +16 -10
  6. package/dist/chat/subagent-conversation.d.ts +2 -1
  7. package/dist/chat/subagent-conversation.js +7 -1
  8. package/dist/chat/timeline-flash.d.ts +3 -1
  9. package/dist/chat/timeline-flash.js +2 -2
  10. package/dist/chat/ui/ImageView.d.ts +8 -2
  11. package/dist/chat/ui/ImageView.js +12 -6
  12. package/dist/chat/ui/SubagentTimeline.js +6 -2
  13. package/dist/chat/ui/Timeline.js +67 -7
  14. package/dist/chat/ui/intelligent-ui/UiBlockFrame.d.ts +23 -0
  15. package/dist/chat/ui/intelligent-ui/UiBlockFrame.js +103 -0
  16. package/dist/chat/ui/intelligent-ui/UiFallbackPart.d.ts +10 -0
  17. package/dist/chat/ui/intelligent-ui/UiFallbackPart.js +11 -0
  18. package/dist/chat/ui/intelligent-ui/UiReply.d.ts +15 -0
  19. package/dist/chat/ui/intelligent-ui/UiReply.js +329 -0
  20. package/dist/chat/ui/intelligent-ui/block-local.d.ts +11 -0
  21. package/dist/chat/ui/intelligent-ui/block-local.js +42 -0
  22. package/dist/chat/ui/intelligent-ui/chart-data.d.ts +16 -0
  23. package/dist/chat/ui/intelligent-ui/chart-data.js +46 -0
  24. package/dist/chat/ui/intelligent-ui/node-text.d.ts +15 -0
  25. package/dist/chat/ui/intelligent-ui/node-text.js +106 -0
  26. package/dist/chat/ui/intelligent-ui/registry.d.ts +2 -0
  27. package/dist/chat/ui/intelligent-ui/registry.js +51 -0
  28. package/dist/chat/ui/intelligent-ui/render-context.d.ts +53 -0
  29. package/dist/chat/ui/intelligent-ui/render-context.js +3 -0
  30. package/dist/chat/ui/intelligent-ui/renderers/chart.d.ts +3 -0
  31. package/dist/chat/ui/intelligent-ui/renderers/chart.js +123 -0
  32. package/dist/chat/ui/intelligent-ui/renderers/choices.d.ts +4 -0
  33. package/dist/chat/ui/intelligent-ui/renderers/choices.js +45 -0
  34. package/dist/chat/ui/intelligent-ui/renderers/content.d.ts +14 -0
  35. package/dist/chat/ui/intelligent-ui/renderers/content.js +59 -0
  36. package/dist/chat/ui/intelligent-ui/renderers/data.d.ts +7 -0
  37. package/dist/chat/ui/intelligent-ui/renderers/data.js +80 -0
  38. package/dist/chat/ui/intelligent-ui/renderers/inputs.d.ts +8 -0
  39. package/dist/chat/ui/intelligent-ui/renderers/inputs.js +72 -0
  40. package/dist/chat/ui/intelligent-ui/renderers/links.d.ts +10 -0
  41. package/dist/chat/ui/intelligent-ui/renderers/links.js +87 -0
  42. package/dist/chat/ui/intelligent-ui/renderers/structure.d.ts +6 -0
  43. package/dist/chat/ui/intelligent-ui/renderers/structure.js +32 -0
  44. package/dist/chat/ui/intelligent-ui/renderers/text.d.ts +8 -0
  45. package/dist/chat/ui/intelligent-ui/renderers/text.js +61 -0
  46. package/dist/chat/ui/intelligent-ui/reply-parts.d.ts +9 -0
  47. package/dist/chat/ui/intelligent-ui/reply-parts.js +18 -0
  48. package/dist/chat/ui/intelligent-ui/table-data.d.ts +31 -0
  49. package/dist/chat/ui/intelligent-ui/table-data.js +79 -0
  50. package/dist/chat/ui/intelligent-ui/tones.d.ts +7 -0
  51. package/dist/chat/ui/intelligent-ui/tones.js +31 -0
  52. package/dist/chat/ui/intelligent-ui/use-later.d.ts +1 -0
  53. package/dist/chat/ui/intelligent-ui/use-later.js +16 -0
  54. package/dist/chat/ui/intelligent-ui/use-ui-links.d.ts +9 -0
  55. package/dist/chat/ui/intelligent-ui/use-ui-links.js +126 -0
  56. package/dist/chat/ui/intelligent-ui/use-ui-queries.d.ts +21 -0
  57. package/dist/chat/ui/intelligent-ui/use-ui-queries.js +272 -0
  58. package/dist/chat/ui/reply-context.d.ts +8 -0
  59. package/dist/chat/ui/reply-context.js +1 -0
  60. package/dist/chat/ui/rows/GeneratedImageRow.d.ts +5 -0
  61. package/dist/chat/ui/rows/GeneratedImageRow.js +121 -0
  62. package/dist/chat/ui/rows/MessageRows.js +21 -4
  63. package/dist/chat/ui/rows/Rows.js +4 -2
  64. package/dist/chat/ui/rows/SubagentRow.js +5 -0
  65. package/dist/chat/ui/rows/WorkRows.d.ts +4 -1
  66. package/dist/chat/ui/rows/WorkRows.js +4 -3
  67. package/dist/host.d.ts +11 -1
  68. package/dist/locales/en/agent-chat.json +101 -0
  69. package/dist/locales/nl/agent-chat.json +101 -0
  70. package/dist/state/chats.js +13 -0
  71. package/dist/theme.css +19 -0
  72. package/package.json +128 -7
  73. package/src/chat/logic/generated-image.ts +48 -0
  74. package/src/chat/logic/timeline-target.ts +41 -1
  75. package/src/chat/logic/timeline.ts +19 -10
  76. package/src/chat/subagent-conversation.ts +13 -2
  77. package/src/chat/timeline-flash.ts +5 -3
  78. package/src/chat/ui/ImageView.tsx +51 -8
  79. package/src/chat/ui/SubagentTimeline.tsx +24 -1
  80. package/src/chat/ui/Timeline.tsx +97 -21
  81. package/src/chat/ui/intelligent-ui/UiBlockFrame.tsx +219 -0
  82. package/src/chat/ui/intelligent-ui/UiFallbackPart.tsx +21 -0
  83. package/src/chat/ui/intelligent-ui/UiReply.tsx +541 -0
  84. package/src/chat/ui/intelligent-ui/block-local.ts +51 -0
  85. package/src/chat/ui/intelligent-ui/chart-data.ts +67 -0
  86. package/src/chat/ui/intelligent-ui/node-text.ts +119 -0
  87. package/src/chat/ui/intelligent-ui/registry.ts +53 -0
  88. package/src/chat/ui/intelligent-ui/render-context.ts +84 -0
  89. package/src/chat/ui/intelligent-ui/renderers/chart.tsx +319 -0
  90. package/src/chat/ui/intelligent-ui/renderers/choices.tsx +102 -0
  91. package/src/chat/ui/intelligent-ui/renderers/content.tsx +140 -0
  92. package/src/chat/ui/intelligent-ui/renderers/data.tsx +181 -0
  93. package/src/chat/ui/intelligent-ui/renderers/inputs.tsx +144 -0
  94. package/src/chat/ui/intelligent-ui/renderers/links.tsx +158 -0
  95. package/src/chat/ui/intelligent-ui/renderers/structure.tsx +84 -0
  96. package/src/chat/ui/intelligent-ui/renderers/text.tsx +124 -0
  97. package/src/chat/ui/intelligent-ui/reply-parts.ts +22 -0
  98. package/src/chat/ui/intelligent-ui/table-data.ts +104 -0
  99. package/src/chat/ui/intelligent-ui/tones.ts +37 -0
  100. package/src/chat/ui/intelligent-ui/use-later.ts +17 -0
  101. package/src/chat/ui/intelligent-ui/use-ui-links.ts +143 -0
  102. package/src/chat/ui/intelligent-ui/use-ui-queries.ts +308 -0
  103. package/src/chat/ui/reply-context.ts +7 -0
  104. package/src/chat/ui/rows/GeneratedImageRow.tsx +244 -0
  105. package/src/chat/ui/rows/MessageRows.tsx +60 -4
  106. package/src/chat/ui/rows/Rows.tsx +4 -2
  107. package/src/chat/ui/rows/SubagentRow.tsx +5 -0
  108. package/src/chat/ui/rows/WorkRows.tsx +20 -5
  109. package/src/host.ts +27 -1
  110. package/src/locales/en/agent-chat.json +101 -0
  111. package/src/locales/nl/agent-chat.json +101 -0
  112. package/src/state/chats.ts +13 -0
  113. package/src/theme.css +19 -0
@@ -0,0 +1,87 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import clsx from 'clsx';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { FileDiff, GitCommitHorizontal, SquareDashed } from 'lucide-react';
5
+ import { FileIcon, Icon, Tooltip } from '@adecore/ui';
6
+ import { CHIP_IN_MESSAGE, MENTION_TONE } from '../../chips.js';
7
+ import { uiReasonText } from '../node-text.js';
8
+ const PLAIN = { state: 'plain' };
9
+ function baseName(path) {
10
+ const trimmed = path.replace(/\/+$/, '');
11
+ return trimmed.slice(trimmed.lastIndexOf('/') + 1) || path;
12
+ }
13
+ function plainText(target, link) {
14
+ switch (target.type) {
15
+ case 'File':
16
+ return target.line === undefined ? target.path : `${target.path}:${target.line}`;
17
+ case 'Diff':
18
+ return target.path;
19
+ case 'Commit':
20
+ return target.sha.slice(0, 7);
21
+ case 'Node':
22
+ return link.label ?? target.id;
23
+ }
24
+ }
25
+ function chipFace(target, link) {
26
+ switch (target.type) {
27
+ case 'File':
28
+ return {
29
+ face: (_jsxs(_Fragment, { children: [_jsx(FileIcon, { path: target.path, size: 14 }), _jsx("span", { className: "truncate", children: baseName(target.path) }), target.line !== undefined && _jsxs("span", { className: "opacity-70", children: [":", target.line] })] })),
30
+ hint: target.line === undefined ? target.path : `${target.path}:${target.line}`
31
+ };
32
+ case 'Diff':
33
+ return {
34
+ face: (_jsxs(_Fragment, { children: [_jsx(Icon, { icon: FileDiff, size: 12 }), _jsx("span", { className: "truncate", children: baseName(target.path) })] })),
35
+ hint: target.path
36
+ };
37
+ case 'Commit':
38
+ return {
39
+ face: (_jsxs(_Fragment, { children: [_jsx(Icon, { icon: GitCommitHorizontal, size: 12 }), _jsx("span", { className: "font-mono", children: target.sha.slice(0, 7) })] })),
40
+ hint: link.label ?? target.sha
41
+ };
42
+ case 'Node':
43
+ return {
44
+ face: (_jsxs(_Fragment, { children: [_jsx(Icon, { icon: SquareDashed, size: 12 }), _jsx("span", { className: "truncate", children: link.label ?? target.id })] })),
45
+ hint: null
46
+ };
47
+ }
48
+ }
49
+ /*
50
+ * A resource of the host as a chip, the shape a mention has in a message. The host says whether it
51
+ * can open the target; one it cannot, outside the project or gone, stays mono text with the reason
52
+ * in a tooltip. A chip fades in, since a link reads as text until the host checked it.
53
+ */
54
+ export function UiLinkChip({ target, context }) {
55
+ const { t } = useTranslation('agent-chat');
56
+ const link = context.link?.(target) ?? PLAIN;
57
+ if (link.state === 'plain') {
58
+ const reason = uiReasonText(t, link.code, link.reason) ?? (context.link === undefined ? null : t('blocks.outsideProject'));
59
+ // Focusable only with a reason, so a keyboard reaches the tooltip.
60
+ const text = (_jsx("span", { tabIndex: reason === null ? undefined : 0, className: "font-mono text-code break-all text-text-muted", children: plainText(target, link) }));
61
+ return reason === null ? text : _jsx(Tooltip, { label: reason, children: text });
62
+ }
63
+ const { face, hint } = chipFace(target, link);
64
+ const { openLink } = context;
65
+ const chip = openLink === undefined ? (_jsx("span", { className: clsx(CHIP_IN_MESSAGE, MENTION_TONE, 'transition-opacity duration-120 starting:opacity-0'), children: face })) : (_jsx("button", { type: "button", className: clsx(CHIP_IN_MESSAGE, MENTION_TONE, 'transition-opacity duration-120 hover:brightness-95 starting:opacity-0'), onClick: () => openLink(target), children: face }));
66
+ return hint === null ? chip : _jsx(Tooltip, { label: hint, children: chip });
67
+ }
68
+ /* A link with children is a row: the chip and what the agent says about it. */
69
+ function LinkRow({ target, context, described, children }) {
70
+ if (!described) {
71
+ return _jsx(UiLinkChip, { target: target, context: context });
72
+ }
73
+ return (_jsxs("div", { className: "flex min-w-0 items-baseline gap-2 px-2 text-sm", children: [_jsx("span", { className: "shrink-0", children: _jsx(UiLinkChip, { target: target, context: context }) }), _jsx("span", { className: "min-w-0 text-text-muted", children: children })] }));
74
+ }
75
+ export function FileRenderer({ node, children, context }) {
76
+ const { path, line } = node.props;
77
+ return (_jsx(LinkRow, { target: { type: 'File', path, line }, context: context, described: node.children.length > 0, children: children }));
78
+ }
79
+ export function DiffRenderer({ node, children, context }) {
80
+ return (_jsx(LinkRow, { target: { type: 'Diff', path: node.props.path }, context: context, described: node.children.length > 0, children: children }));
81
+ }
82
+ export function CommitRenderer({ node, children, context }) {
83
+ return (_jsx(LinkRow, { target: { type: 'Commit', sha: node.props.sha }, context: context, described: node.children.length > 0, children: children }));
84
+ }
85
+ export function NodeRenderer({ node, children, context }) {
86
+ return (_jsx(LinkRow, { target: { type: 'Node', id: node.props.id }, context: context, described: node.children.length > 0, children: children }));
87
+ }
@@ -0,0 +1,6 @@
1
+ import type { UiProps } from '@adecore/intelligent-ui';
2
+ import type { UiRendererProps } from '../render-context.js';
3
+ export declare function TabsRenderer({ node, children, context }: UiRendererProps<UiProps<'Tabs'>>): import("react").JSX.Element;
4
+ export declare function TabRenderer({ node, children }: UiRendererProps<UiProps<'Tab'>>): import("react").JSX.Element;
5
+ export declare function SectionsRenderer({ node, children, context }: UiRendererProps<UiProps<'Sections'>>): import("react").JSX.Element;
6
+ export declare function SectionRenderer({ node, children, context }: UiRendererProps<UiProps<'Section'>>): import("react").JSX.Element;
@@ -0,0 +1,32 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { createContext, useContext, useId } from 'react';
3
+ import clsx from 'clsx';
4
+ import { ChevronRight } from 'lucide-react';
5
+ import { Icon, Tabs } from '@adecore/ui';
6
+ import { useBlockLocal } from '../block-local.js';
7
+ import { uiChildrenOf } from '../node-text.js';
8
+ const SectionsContext = createContext({ touched: false, touch: () => undefined });
9
+ /* A tab that arrives while the block streams is not picked: the one that was open stays open. */
10
+ export function TabsRenderer({ node, children, context }) {
11
+ const tabs = uiChildrenOf(node, 'Tab');
12
+ const [picked, setPicked] = useBlockLocal(context, node.id, () => null);
13
+ const value = picked !== null && tabs.some((tab) => tab.id === picked) ? picked : (tabs[0]?.id ?? null);
14
+ return (_jsxs(Tabs.Root, { value: value, onValueChange: (next) => setPicked(String(next)), className: "flex flex-col gap-3", children: [_jsx(Tabs.List, { className: "px-2", children: tabs.map((tab) => (_jsx(Tabs.Tab, { value: tab.id, "data-ui-reveal": tab.id, children: tab.props.title }, tab.id))) }), children] }));
15
+ }
16
+ export function TabRenderer({ node, children }) {
17
+ return (_jsx(Tabs.Panel, { value: node.id, className: "chat-ui-nodes flex flex-col gap-3", children: children }));
18
+ }
19
+ export function SectionsRenderer({ node, children, context }) {
20
+ const [touched, setTouched] = useBlockLocal(context, node.id, () => false);
21
+ return (_jsx(SectionsContext, { value: { touched, touch: () => setTouched(true) }, children: _jsx("div", { className: "chat-ui-nodes flex flex-col divide-y divide-border-soft", children: children }) }));
22
+ }
23
+ /* Arrives open until the person folds a section of the set themselves; the content starts under the title. */
24
+ export function SectionRenderer({ node, children, context }) {
25
+ const fold = useContext(SectionsContext);
26
+ const [open, setOpen] = useBlockLocal(context, node.id, () => !fold.touched);
27
+ const contentId = useId();
28
+ return (_jsxs("section", { children: [_jsxs("button", { type: "button", "data-ui-reveal": node.id, "aria-expanded": open, "aria-controls": contentId, className: "flex h-8 w-full items-center gap-2 rounded-md px-2 text-left text-sm font-medium text-text hover:bg-surface-hover", onClick: () => {
29
+ fold.touch();
30
+ setOpen(!open);
31
+ }, children: [_jsx(Icon, { icon: ChevronRight, size: 14, className: clsx('shrink-0 text-text-faint transition-transform duration-150 motion-reduce:transition-none', open && 'rotate-90') }), _jsx("span", { className: "min-w-0 truncate", children: node.props.title })] }), _jsx("div", { id: contentId, hidden: !open, className: "chat-ui-nodes flex flex-col gap-3 pt-1 pr-2 pb-2 pl-7.5", children: children })] }));
32
+ }
@@ -0,0 +1,8 @@
1
+ import type { UiProps } from '@adecore/intelligent-ui';
2
+ import { type UiRendererProps } from '../render-context.js';
3
+ export declare function SummaryRenderer({ node, children }: UiRendererProps<UiProps<'Summary'>>): import("react").JSX.Element;
4
+ export declare function CalloutRenderer({ node, children }: UiRendererProps<UiProps<'Callout'>>): import("react").JSX.Element;
5
+ export declare function TagRenderer({ node, children }: UiRendererProps<UiProps<'Tag'>>): import("react").JSX.Element;
6
+ export declare function ProgressRenderer({ node, children, context }: UiRendererProps<UiProps<'Progress'>>): import("react").JSX.Element;
7
+ export declare function StepsRenderer({ children }: UiRendererProps<UiProps<'Steps'>>): import("react").JSX.Element;
8
+ export declare function StepRenderer({ node, children, context }: UiRendererProps<UiProps<'Step'>>): import("react").JSX.Element;
@@ -0,0 +1,61 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useContext } from 'react';
3
+ import clsx from 'clsx';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { Circle, CircleCheck, CircleDot, CircleMinus, CircleX } from 'lucide-react';
6
+ import { Icon, Meter, Pill, Spinner } from '@adecore/ui';
7
+ import { formatNumber, formatPercent } from '@adecore/ui/format';
8
+ import { UiHeadContext } from '../render-context.js';
9
+ import { uiNodeLabel } from '../node-text.js';
10
+ import { UI_TONE_ICONS, UI_TONE_PILL, UI_TONE_SURFACE, UI_TONE_TEXT } from '../tones.js';
11
+ const STEP_ICONS = {
12
+ done: CircleCheck,
13
+ pending: Circle,
14
+ failed: CircleX,
15
+ skipped: CircleMinus
16
+ };
17
+ const STEP_TONES = {
18
+ done: 'text-status-idle',
19
+ running: 'text-accent',
20
+ pending: 'text-text-faint',
21
+ failed: 'text-status-error',
22
+ skipped: 'text-text-faint'
23
+ };
24
+ /* Whether the block still changes: while it streams, or while it reads live data. Only then does anything spin. */
25
+ function isLive(context) {
26
+ return context.phase === 'streaming' || (context.live !== undefined && context.live !== null && context.live.state !== 'refused');
27
+ }
28
+ /* The head of the block when it is the first node, a subheading without an icon further down. */
29
+ export function SummaryRenderer({ node, children }) {
30
+ const headId = useContext(UiHeadContext);
31
+ const head = headId === undefined || headId === node.id;
32
+ const { tone, badge } = node.props;
33
+ return (_jsxs("div", { className: "flex items-start gap-2 px-2", children: [head && tone !== undefined && _jsx(Icon, { icon: UI_TONE_ICONS[tone], size: 16, className: clsx('mt-0.75 shrink-0', UI_TONE_TEXT[tone]) }), _jsx("div", { className: "min-w-0 grow text-sm font-semibold text-balance text-text", children: children }), badge !== undefined && badge !== '' && (_jsx(Pill, { shape: "tag", tone: tone === undefined ? 'muted' : UI_TONE_PILL[tone], className: "shrink-0", children: badge }))] }));
34
+ }
35
+ export function CalloutRenderer({ node, children }) {
36
+ const { tone, title } = node.props;
37
+ return (_jsxs("div", { className: clsx('flex gap-2 rounded-md p-2 text-sm', UI_TONE_SURFACE[tone]), children: [_jsx(Icon, { icon: UI_TONE_ICONS[tone], size: 16, className: clsx('mt-0.75 shrink-0', UI_TONE_TEXT[tone]) }), _jsxs("div", { className: "min-w-0 grow", children: [title !== undefined && title !== '' && _jsx("div", { className: "font-medium text-text", children: title }), _jsx("div", { className: "text-text-muted", children: children })] })] }));
38
+ }
39
+ export function TagRenderer({ node, children }) {
40
+ const { tone } = node.props;
41
+ return (_jsx(Pill, { shape: "tag", size: "sm", tone: tone === undefined ? 'muted' : UI_TONE_PILL[tone], children: children }));
42
+ }
43
+ /* Without a value the progress is indeterminate; without a max the value is a percentage. */
44
+ export function ProgressRenderer({ node, children, context }) {
45
+ const { t } = useTranslation('agent-chat');
46
+ const { value, max } = node.props;
47
+ const limit = max ?? 100;
48
+ const reading = value === undefined ? null : limit === 100 ? formatPercent(value) : t('blocks.progress.of', { value: formatNumber(value), max: formatNumber(limit) });
49
+ const label = uiNodeLabel(node);
50
+ return (_jsxs("div", { className: "flex flex-col gap-1 px-2", children: [_jsxs("div", { className: "flex items-baseline justify-between gap-3 text-xs", children: [_jsx("span", { className: "min-w-0 text-text", children: children }), reading !== null ? (_jsx("span", { className: "shrink-0 text-text-muted tabular-nums", children: reading })) : (isLive(context) && _jsx(Spinner, { size: 12, className: "shrink-0 self-center text-text-faint" }))] }), _jsx(Meter, { value: value ?? null, max: limit, label: label === '' ? t('blocks.progress.label') : label, valueText: reading ?? undefined })] }));
51
+ }
52
+ export function StepsRenderer({ children }) {
53
+ return _jsx("ol", { className: "chat-ui-nodes flex flex-col px-2", children: children });
54
+ }
55
+ /* A running step spins only while the block is live; in a quiet block it is a still dot. */
56
+ export function StepRenderer({ node, children, context }) {
57
+ const { t } = useTranslation('agent-chat');
58
+ const { state, detail } = node.props;
59
+ const quiet = state === 'pending' || state === 'skipped';
60
+ return (_jsxs("li", { className: "flex min-h-7 items-center gap-2 text-sm", children: [_jsx("span", { className: clsx('grid size-4 shrink-0 place-items-center', STEP_TONES[state]), children: state === 'running' ? (isLive(context) ? (_jsx(Spinner, { size: 14 })) : (_jsx(Icon, { icon: CircleDot, size: 14 }))) : (_jsx(Icon, { icon: STEP_ICONS[state], size: 14 })) }), _jsx("span", { className: "sr-only", children: t(`blocks.step.${state}`) }), _jsx("span", { className: clsx('min-w-0 grow', quiet ? 'text-text-faint' : 'text-text'), children: children }), detail !== undefined && detail !== '' && _jsx("span", { className: "shrink-0 text-xs text-text-faint tabular-nums", children: detail })] }));
61
+ }
@@ -0,0 +1,9 @@
1
+ import type { UiBlock } from '@adecore/intelligent-ui';
2
+ export type UiReplyPart = {
3
+ kind: 'text';
4
+ text: string;
5
+ } | {
6
+ kind: 'ui';
7
+ block: UiBlock;
8
+ };
9
+ export declare function uiReplyParts(text: string, blocks: readonly UiBlock[], streaming?: boolean): UiReplyPart[];
@@ -0,0 +1,18 @@
1
+ export function uiReplyParts(text, blocks, streaming = false) {
2
+ const parts = [];
3
+ let position = 0;
4
+ for (const block of [...blocks].sort((left, right) => left.start - right.start)) {
5
+ if (!Number.isInteger(block.start) || !Number.isInteger(block.end) || block.start < position || block.end < block.start || block.end > text.length) {
6
+ continue;
7
+ }
8
+ if (block.start > position) {
9
+ parts.push({ kind: 'text', text: text.slice(position, block.start) });
10
+ }
11
+ parts.push({ kind: 'ui', block });
12
+ position = streaming && !block.complete ? text.length : block.end;
13
+ }
14
+ if (position < text.length) {
15
+ parts.push({ kind: 'text', text: text.slice(position) });
16
+ }
17
+ return parts;
18
+ }
@@ -0,0 +1,31 @@
1
+ import type { UiProps, UiViewNode } from '@adecore/intelligent-ui';
2
+ export type UiColumnKind = NonNullable<UiProps<'Column'>['as']>;
3
+ export interface UiTableColumn {
4
+ key: string;
5
+ title: string;
6
+ unit?: string;
7
+ as: UiColumnKind;
8
+ }
9
+ export type UiTableCell = {
10
+ kind: 'empty';
11
+ } | {
12
+ kind: 'text';
13
+ text: string;
14
+ } | {
15
+ kind: 'number';
16
+ as: 'number' | 'bytes' | 'duration';
17
+ value: number;
18
+ } | {
19
+ kind: 'date';
20
+ at: number;
21
+ } | {
22
+ kind: 'file';
23
+ path: string;
24
+ } | {
25
+ kind: 'tag';
26
+ text: string;
27
+ };
28
+ export declare const UI_TABLE_ROWS = 50;
29
+ export declare function isNumericColumn(column: UiTableColumn): boolean;
30
+ export declare function uiTableColumns(node: UiViewNode<UiProps<'Table'>>): UiTableColumn[];
31
+ export declare function uiTableCell(value: unknown, column: UiTableColumn): UiTableCell;
@@ -0,0 +1,79 @@
1
+ import { uiChildrenOf } from './node-text.js';
2
+ /* Rows shown before a person asks for all of them. */
3
+ export const UI_TABLE_ROWS = 50;
4
+ /* Columns a table without Column children takes from its rows. */
5
+ const DERIVED_COLUMNS = 8;
6
+ const NUMERIC_KINDS = new Set(['number', 'bytes', 'duration']);
7
+ export function isNumericColumn(column) {
8
+ return NUMERIC_KINDS.has(column.as);
9
+ }
10
+ /*
11
+ * The columns a table can draw: its Column children whose key some row has, each key once. A table
12
+ * without columns takes the keys of its rows, as numbers when every value there is one. Empty when
13
+ * nothing is usable, which draws the fallback.
14
+ */
15
+ export function uiTableColumns(node) {
16
+ const rows = node.props.rows;
17
+ const present = (key) => rows.some((row) => Object.hasOwn(row, key));
18
+ const declared = uiChildrenOf(node, 'Column');
19
+ if (declared.length > 0) {
20
+ const seen = new Set();
21
+ return declared.flatMap(({ props }) => {
22
+ if (seen.has(props.key) || (rows.length > 0 && !present(props.key))) {
23
+ return [];
24
+ }
25
+ seen.add(props.key);
26
+ return [{ key: props.key, title: props.title ?? props.key, unit: props.unit, as: props.as ?? 'text' }];
27
+ });
28
+ }
29
+ const keys = [];
30
+ for (const row of rows) {
31
+ for (const key of Object.keys(row)) {
32
+ if (!keys.includes(key) && keys.length < DERIVED_COLUMNS) {
33
+ keys.push(key);
34
+ }
35
+ }
36
+ }
37
+ return keys.map((key) => {
38
+ const values = rows.map((row) => row[key]).filter((value) => value !== undefined && value !== null);
39
+ const numeric = values.length > 0 && values.every((value) => typeof value === 'number' && Number.isFinite(value));
40
+ return { key, title: key, as: numeric ? 'number' : 'text' };
41
+ });
42
+ }
43
+ function textOf(value) {
44
+ return typeof value === 'object' ? JSON.stringify(value) : String(value);
45
+ }
46
+ function numberOf(value) {
47
+ if (typeof value === 'number') {
48
+ return Number.isFinite(value) ? value : null;
49
+ }
50
+ if (typeof value === 'string' && value.trim() !== '') {
51
+ const parsed = Number(value);
52
+ return Number.isFinite(parsed) ? parsed : null;
53
+ }
54
+ return null;
55
+ }
56
+ /* A duration is in milliseconds and a date is milliseconds since the epoch or an ISO string; anything else reads as text. */
57
+ export function uiTableCell(value, column) {
58
+ if (value === undefined || value === null || value === '') {
59
+ return { kind: 'empty' };
60
+ }
61
+ switch (column.as) {
62
+ case 'number':
63
+ case 'bytes':
64
+ case 'duration': {
65
+ const number = numberOf(value);
66
+ return number === null ? { kind: 'text', text: textOf(value) } : { kind: 'number', as: column.as, value: number };
67
+ }
68
+ case 'date': {
69
+ const at = typeof value === 'number' ? value : typeof value === 'string' ? Date.parse(value) : Number.NaN;
70
+ return Number.isFinite(at) ? { kind: 'date', at } : { kind: 'text', text: textOf(value) };
71
+ }
72
+ case 'file':
73
+ return typeof value === 'string' ? { kind: 'file', path: value } : { kind: 'text', text: textOf(value) };
74
+ case 'tag':
75
+ return { kind: 'tag', text: textOf(value) };
76
+ case 'text':
77
+ return { kind: 'text', text: textOf(value) };
78
+ }
79
+ }
@@ -0,0 +1,7 @@
1
+ import { type LucideIcon } from 'lucide-react';
2
+ import type { UiTone } from '@adecore/intelligent-ui';
3
+ import type { PillProps } from '@adecore/ui';
4
+ export declare const UI_TONE_ICONS: Readonly<Record<UiTone, LucideIcon>>;
5
+ export declare const UI_TONE_TEXT: Readonly<Record<UiTone, string>>;
6
+ export declare const UI_TONE_SURFACE: Readonly<Record<UiTone, string>>;
7
+ export declare const UI_TONE_PILL: Readonly<Record<UiTone, NonNullable<PillProps['tone']>>>;
@@ -0,0 +1,31 @@
1
+ import { CircleCheck, Info, MessageCircle, OctagonAlert, TriangleAlert } from 'lucide-react';
2
+ /* Every tone has an icon of its own, so color is never the only thing that tells two apart. */
3
+ export const UI_TONE_ICONS = {
4
+ neutral: MessageCircle,
5
+ info: Info,
6
+ success: CircleCheck,
7
+ warning: TriangleAlert,
8
+ danger: OctagonAlert
9
+ };
10
+ export const UI_TONE_TEXT = {
11
+ neutral: 'text-text-muted',
12
+ info: 'text-status-running',
13
+ success: 'text-positive',
14
+ warning: 'text-status-needs-you',
15
+ danger: 'text-status-error'
16
+ };
17
+ /* The tone mixed over the ground, for a surface such as a callout. */
18
+ export const UI_TONE_SURFACE = {
19
+ neutral: 'bg-text/7',
20
+ info: 'bg-status-running/10',
21
+ success: 'bg-positive/10',
22
+ warning: 'bg-status-needs-you/12',
23
+ danger: 'bg-status-error/10'
24
+ };
25
+ export const UI_TONE_PILL = {
26
+ neutral: 'muted',
27
+ info: 'accent',
28
+ success: 'idle',
29
+ warning: 'needsYou',
30
+ danger: 'error'
31
+ };
@@ -0,0 +1 @@
1
+ export declare function useLater(on: boolean, ms: number): boolean;
@@ -0,0 +1,16 @@
1
+ import { useEffect, useState } from 'react';
2
+ /* True once `on` has held for `ms`, for a spinner that only shows when waiting takes long. */
3
+ export function useLater(on, ms) {
4
+ const [late, setLate] = useState(false);
5
+ useEffect(() => {
6
+ if (!on) {
7
+ return;
8
+ }
9
+ const timer = setTimeout(() => setLate(true), ms);
10
+ return () => {
11
+ clearTimeout(timer);
12
+ setLate(false);
13
+ };
14
+ }, [on, ms]);
15
+ return on && late;
16
+ }
@@ -0,0 +1,9 @@
1
+ import type { ChatUiLinkReading, ChatUiQueryState } from '@adecore/agent-contracts';
2
+ import { type UiLinkTarget } from '@adecore/intelligent-ui/links';
3
+ import { type UiBlock, UiState } from '@adecore/intelligent-ui';
4
+ import type { UiRenderContext } from './render-context.js';
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
+ openLink: ((target: UiLinkTarget) => void) | undefined;
8
+ openUrl: ((url: string) => void | undefined) | undefined;
9
+ };
@@ -0,0 +1,126 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import { uiLinkTargets } from '@adecore/intelligent-ui/links';
3
+ import { uiInputValues, UI_HOST_LIMITS, UiState } from '@adecore/intelligent-ui';
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;
7
+ export function useUiLinks(block, state, context, reads, frozen) {
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());
11
+ const host = chatHost().intelligentUi;
12
+ const values = uiInputValues(block, state);
13
+ const scope = state.scope();
14
+ const queryValues = Object.fromEntries(Object.keys(block.queries)
15
+ .filter((name) => Object.hasOwn(scope, name))
16
+ .map((name) => [name, scope[name]]));
17
+ let targets = {};
18
+ try {
19
+ if (block.complete) {
20
+ targets = uiLinkTargets(block, values, queryValues);
21
+ }
22
+ }
23
+ catch { }
24
+ const signature = JSON.stringify(targets);
25
+ const input = JSON.stringify(values);
26
+ const tickets = JSON.stringify(reads);
27
+ const defaultInput = useMemo(() => JSON.stringify(uiInputValues(block, new UiState(block))), [block]);
28
+ const link = host?.link;
29
+ useEffect(() => {
30
+ asked.current.clear();
31
+ }, [block.id, block.revision, context.scopeId, context.chatId, context.itemId, link]);
32
+ useEffect(() => {
33
+ if (!link || !block.revision || context.phase !== 'final') {
34
+ return;
35
+ }
36
+ let stopped = false;
37
+ const validate = async () => {
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
+ };
46
+ try {
47
+ const reading = await link(context.scopeId, {
48
+ chatId: context.chatId,
49
+ itemId: context.itemId,
50
+ blockId: block.id,
51
+ revision: block.revision,
52
+ nodeId,
53
+ values: JSON.parse(input),
54
+ reads: JSON.parse(tickets)
55
+ });
56
+ if (stopped) {
57
+ return;
58
+ }
59
+ record(reading);
60
+ }
61
+ catch {
62
+ if (stopped) {
63
+ return;
64
+ }
65
+ record({ state: 'plain' });
66
+ }
67
+ }
68
+ };
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);
71
+ return () => {
72
+ stopped = true;
73
+ clearTimeout(timer);
74
+ };
75
+ }, [block.id, block.revision, context.scopeId, context.chatId, context.itemId, context.phase, signature, input, tickets, link]);
76
+ const idOf = (target) => Object.entries(targets).find(([, entry]) => JSON.stringify(entry) === JSON.stringify(target))?.[0];
77
+ const resolution = (target) => {
78
+ const id = idOf(target);
79
+ if (id === undefined) {
80
+ return { state: 'plain' };
81
+ }
82
+ if (checked[id]?.target === JSON.stringify(target)) {
83
+ return checked[id].reading;
84
+ }
85
+ if (input === defaultInput && frozen && frozen.revision === block.revision) {
86
+ const firstQueries = Object.fromEntries(Object.entries(frozen.readings)
87
+ .filter(([, reading]) => reading.state === 'fresh')
88
+ .map(([name, reading]) => [name, reading.value]));
89
+ if (JSON.stringify(queryValues) === JSON.stringify(firstQueries)) {
90
+ return frozen.links?.[id] ?? { state: 'plain' };
91
+ }
92
+ }
93
+ return { state: 'plain' };
94
+ };
95
+ const open = (target) => {
96
+ const nodeId = idOf(target);
97
+ if (!nodeId || !link || !block.revision) {
98
+ return;
99
+ }
100
+ void link(context.scopeId, {
101
+ chatId: context.chatId,
102
+ itemId: context.itemId,
103
+ blockId: block.id,
104
+ revision: block.revision,
105
+ nodeId,
106
+ values,
107
+ reads: { ...reads }
108
+ })
109
+ .then((reading) => {
110
+ setChecked((previous) => ({ ...previous, [nodeId]: { target: JSON.stringify(target), reading } }));
111
+ if (reading.state === 'chip' && reading.target) {
112
+ host?.openLink?.(context.scopeId, reading);
113
+ }
114
+ })
115
+ .catch(() => {
116
+ setChecked((previous) => ({ ...previous, [nodeId]: { target: JSON.stringify(target), reading: { state: 'plain' } } }));
117
+ });
118
+ };
119
+ const hostOpenUrl = host?.openUrl;
120
+ const openUrl = useCallback((url) => hostOpenUrl?.(context.scopeId, url), [hostOpenUrl, context.scopeId]);
121
+ return {
122
+ link: link ? resolution : undefined,
123
+ openLink: link && host?.openLink ? open : undefined,
124
+ openUrl: hostOpenUrl ? openUrl : undefined
125
+ };
126
+ }
@@ -0,0 +1,21 @@
1
+ import type { ChatUiQueryReading, ChatUiQueryState } from '@adecore/agent-contracts';
2
+ import { UiState, type UiBlock, type UiViewNode } from '@adecore/intelligent-ui';
3
+ import type { UiLiveValue, UiLiveStatus, UiRenderContext } from './render-context.js';
4
+ export declare function useUiQueries(block: UiBlock, state: UiState, context: Pick<UiRenderContext, 'scopeId' | 'chatId' | 'itemId' | 'phase'>, element: HTMLDivElement | null, frozen?: ChatUiQueryState['blocks'][string]): {
5
+ live: UiLiveStatus | undefined;
6
+ reads: Record<string, string>;
7
+ reading: boolean;
8
+ ready: boolean;
9
+ readsFor: (values: Readonly<Record<string, unknown>>) => Record<string, string> | null;
10
+ currentReadings: {
11
+ [k: string]: {
12
+ state: "failed" | "fresh" | "refused";
13
+ value?: unknown;
14
+ readId?: string | undefined;
15
+ readAt: number;
16
+ code?: string | undefined;
17
+ reason?: string | undefined;
18
+ };
19
+ };
20
+ };
21
+ export declare function useUiLiveValues(block: UiBlock, nodes: readonly UiViewNode[], readings: Readonly<Record<string, ChatUiQueryReading>>): ((id: string, prop: string, path?: readonly [number, string]) => UiLiveValue | undefined) | undefined;