@adecore/agents-react 0.21.2 → 0.22.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 (104) hide show
  1. package/dist/agents/model-requirement.d.ts +6 -0
  2. package/dist/agents/model-requirement.js +26 -0
  3. package/dist/chat/chat-client.d.ts +2 -1
  4. package/dist/chat/chat-client.js +2 -0
  5. package/dist/chat/drafts.d.ts +2 -1
  6. package/dist/chat/drafts.js +15 -5
  7. package/dist/chat/logic/picker-entries.d.ts +10 -0
  8. package/dist/chat/logic/picker-entries.js +19 -0
  9. package/dist/chat/logic/timeline.d.ts +5 -0
  10. package/dist/chat/logic/timeline.js +10 -1
  11. package/dist/chat/prompt-hold.d.ts +8 -0
  12. package/dist/chat/prompt-hold.js +16 -0
  13. package/dist/chat/quote.d.ts +4 -0
  14. package/dist/chat/quote.js +19 -0
  15. package/dist/chat/stash.d.ts +8 -2
  16. package/dist/chat/stash.js +21 -5
  17. package/dist/chat/subagent-list.d.ts +7 -1
  18. package/dist/chat/subagent-list.js +13 -1
  19. package/dist/chat/timeline-scroll.d.ts +6 -2
  20. package/dist/chat/timeline-scroll.js +12 -7
  21. package/dist/chat/ui/ChatActivity.js +22 -8
  22. package/dist/chat/ui/Composer.js +103 -19
  23. package/dist/chat/ui/ComposerInput.d.ts +3 -1
  24. package/dist/chat/ui/ComposerInput.js +13 -4
  25. package/dist/chat/ui/Pickers.js +26 -18
  26. package/dist/chat/ui/PromptComposer.d.ts +8 -1
  27. package/dist/chat/ui/PromptComposer.js +18 -6
  28. package/dist/chat/ui/QuoteButton.js +2 -2
  29. package/dist/chat/ui/SubagentControls.d.ts +4 -2
  30. package/dist/chat/ui/SubagentControls.js +12 -8
  31. package/dist/chat/ui/SubagentInfo.d.ts +1 -1
  32. package/dist/chat/ui/SubagentInfo.js +9 -15
  33. package/dist/chat/ui/SubagentStopButton.js +2 -1
  34. package/dist/chat/ui/Timeline.js +24 -3
  35. package/dist/chat/ui/composer/menu-aria.d.ts +10 -0
  36. package/dist/chat/ui/composer/menu-aria.js +18 -0
  37. package/dist/chat/ui/intelligent-ui/UiBlockFrame.js +3 -3
  38. package/dist/chat/ui/intelligent-ui/UiFallbackPart.js +1 -1
  39. package/dist/chat/ui/intelligent-ui/chart-data.d.ts +22 -2
  40. package/dist/chat/ui/intelligent-ui/chart-data.js +83 -19
  41. package/dist/chat/ui/intelligent-ui/renderers/chart.d.ts +1 -1
  42. package/dist/chat/ui/intelligent-ui/renderers/chart.js +99 -35
  43. package/dist/chat/ui/intelligent-ui/renderers/choices.d.ts +1 -1
  44. package/dist/chat/ui/intelligent-ui/renderers/choices.js +8 -2
  45. package/dist/chat/ui/intelligent-ui/renderers/content.js +5 -3
  46. package/dist/chat/ui/intelligent-ui/renderers/inputs.js +9 -4
  47. package/dist/chat/ui/intelligent-ui/renderers/links.js +1 -1
  48. package/dist/chat/ui/intelligent-ui/renderers/text.js +5 -2
  49. package/dist/chat/ui/intelligent-ui/table-data.js +4 -4
  50. package/dist/chat/ui/intelligent-ui/use-truncated.d.ts +1 -0
  51. package/dist/chat/ui/intelligent-ui/use-truncated.js +28 -0
  52. package/dist/chat/ui/quote-selection.d.ts +5 -2
  53. package/dist/chat/ui/quote-selection.js +3 -2
  54. package/dist/chat/ui/rows/MessageRows.js +24 -5
  55. package/dist/chat/ui/rows/Rows.js +1 -1
  56. package/dist/chat/ui/rows/WorkRows.d.ts +3 -1
  57. package/dist/chat/ui/rows/WorkRows.js +4 -4
  58. package/dist/host.d.ts +1 -0
  59. package/dist/host.js +1 -0
  60. package/dist/locales/en/agent-chat.json +39 -10
  61. package/dist/locales/nl/agent-chat.json +39 -10
  62. package/dist/state/chats.d.ts +2 -1
  63. package/dist/state/chats.js +19 -1
  64. package/package.json +29 -4
  65. package/src/agents/model-requirement.ts +36 -0
  66. package/src/chat/chat-client.ts +5 -0
  67. package/src/chat/drafts.ts +27 -7
  68. package/src/chat/logic/picker-entries.ts +25 -0
  69. package/src/chat/logic/timeline.ts +17 -2
  70. package/src/chat/prompt-hold.ts +27 -0
  71. package/src/chat/quote.ts +20 -0
  72. package/src/chat/stash.ts +31 -5
  73. package/src/chat/subagent-list.ts +19 -5
  74. package/src/chat/timeline-scroll.ts +24 -10
  75. package/src/chat/ui/ChatActivity.tsx +35 -9
  76. package/src/chat/ui/Composer.tsx +197 -52
  77. package/src/chat/ui/ComposerInput.tsx +18 -3
  78. package/src/chat/ui/Pickers.tsx +51 -30
  79. package/src/chat/ui/PromptComposer.tsx +37 -8
  80. package/src/chat/ui/QuoteButton.tsx +3 -2
  81. package/src/chat/ui/SubagentControls.tsx +22 -19
  82. package/src/chat/ui/SubagentInfo.tsx +24 -42
  83. package/src/chat/ui/SubagentStopButton.tsx +2 -1
  84. package/src/chat/ui/Timeline.tsx +26 -4
  85. package/src/chat/ui/composer/menu-aria.ts +29 -0
  86. package/src/chat/ui/intelligent-ui/UiBlockFrame.tsx +5 -5
  87. package/src/chat/ui/intelligent-ui/UiFallbackPart.tsx +6 -1
  88. package/src/chat/ui/intelligent-ui/chart-data.ts +106 -20
  89. package/src/chat/ui/intelligent-ui/renderers/chart.tsx +237 -72
  90. package/src/chat/ui/intelligent-ui/renderers/choices.tsx +9 -4
  91. package/src/chat/ui/intelligent-ui/renderers/content.tsx +31 -15
  92. package/src/chat/ui/intelligent-ui/renderers/inputs.tsx +16 -5
  93. package/src/chat/ui/intelligent-ui/renderers/links.tsx +3 -3
  94. package/src/chat/ui/intelligent-ui/renderers/text.tsx +18 -8
  95. package/src/chat/ui/intelligent-ui/table-data.ts +4 -4
  96. package/src/chat/ui/intelligent-ui/use-truncated.ts +33 -0
  97. package/src/chat/ui/quote-selection.ts +6 -4
  98. package/src/chat/ui/rows/MessageRows.tsx +44 -5
  99. package/src/chat/ui/rows/Rows.tsx +3 -1
  100. package/src/chat/ui/rows/WorkRows.tsx +15 -1
  101. package/src/host.ts +3 -0
  102. package/src/locales/en/agent-chat.json +39 -10
  103. package/src/locales/nl/agent-chat.json +39 -10
  104. package/src/state/chats.ts +31 -2
@@ -3,7 +3,9 @@ import { useId, useState } from 'react';
3
3
  import clsx from 'clsx';
4
4
  import { useTranslation } from 'react-i18next';
5
5
  import { ArrowRight, Check } from 'lucide-react';
6
+ import { uiChoiceLabel } from '@adecore/intelligent-ui';
6
7
  import { DisabledReason, Icon, Spinner, Tooltip } from '@adecore/ui';
8
+ import { UiFallbackPart } from '../UiFallbackPart.js';
7
9
  import { useLater } from '../use-later.js';
8
10
  /* A context this long may run past its two lines, so the row offers all of it in a tooltip. */
9
11
  const LONG_CONTEXT = 120;
@@ -17,7 +19,8 @@ export function ChoicesRenderer({ children, context }) {
17
19
  /*
18
20
  * A row that shows what it sends: the label, and under it the context the agent receives. It stays
19
21
  * closed while the reply streams, once the block is answered and without a host to send it, and
20
- * remains focusable then so a screen reader hears why.
22
+ * remains focusable then so a screen reader hears why. A finished Choice the host would refuse for
23
+ * its label is no row at all, but its text.
21
24
  */
22
25
  export function ChoiceRenderer({ node, children, context }) {
23
26
  const { t } = useTranslation('agent-chat');
@@ -30,6 +33,9 @@ export function ChoiceRenderer({ node, children, context }) {
30
33
  const failed = answer === null && context.failedChoiceId === node.id;
31
34
  const closed = waiting || unavailable || answer !== null;
32
35
  const slow = useLater(chosen && answer.state === 'sending', SPINNER_AFTER_MS);
36
+ if (!waiting && uiChoiceLabel(node) === null) {
37
+ return _jsx(UiFallbackPart, { fallback: node.fallback, problem: { kind: 'failed' } });
38
+ }
33
39
  const reason = waiting
34
40
  ? t('blocks.choice.waiting')
35
41
  : answer === null && unavailable
@@ -40,6 +46,6 @@ export function ChoiceRenderer({ node, children, context }) {
40
46
  if (!closed) {
41
47
  onChoose?.(node.id);
42
48
  }
43
- }, children: [_jsx(Icon, { icon: ArrowRight, size: 14, className: clsx('mt-1 shrink-0', primary === true ? 'text-accent' : 'text-text-faint') }), _jsxs("span", { className: "flex min-w-0 grow flex-col", children: [_jsx("span", { className: "text-sm font-semibold text-text", children: children }), described && (_jsx("span", { id: descriptionId, className: "line-clamp-2 text-xs text-text-muted", children: detail }))] }), chosen && (_jsxs("span", { className: "mt-0.5 flex shrink-0 items-center gap-1 text-xs text-accent", children: [slow ? _jsx(Spinner, { size: 12 }) : _jsx(Icon, { icon: Check, size: 12 }), answer.state === 'queued' ? t('blocks.choice.queued') : t('blocks.choice.sent')] }))] }));
49
+ }, children: [_jsx(Icon, { icon: ArrowRight, size: 14, className: clsx('mt-1 shrink-0', primary === true ? 'text-accent' : 'text-text-faint') }), _jsxs("span", { className: "flex min-w-0 grow flex-col", children: [_jsx("span", { className: "text-sm font-semibold wrap-anywhere text-text", children: children }), described && (_jsx("span", { id: descriptionId, className: "line-clamp-2 text-xs wrap-anywhere text-text-muted", children: detail }))] }), chosen && (_jsxs("span", { className: "mt-0.5 flex shrink-0 items-center gap-1 text-xs text-accent", children: [slow ? _jsx(Spinner, { size: 12 }) : _jsx(Icon, { icon: Check, size: 12 }), answer.state === 'queued' ? t('blocks.choice.queued') : t('blocks.choice.sent')] }))] }));
44
50
  return (_jsxs("div", { className: "flex flex-col gap-1", children: [reason !== null ? (_jsx(DisabledReason, { reason: reason, children: row })) : described && detail.length > LONG_CONTEXT ? (_jsx(Tooltip, { label: detail, children: row })) : (row), failed && (_jsx("p", { role: "alert", className: "px-2 text-xs text-status-error", children: t('blocks.choice.failed') }))] }));
45
51
  }
@@ -2,13 +2,14 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
2
2
  import clsx from 'clsx';
3
3
  import { createContext, useContext, useId } from 'react';
4
4
  import { useTranslation } from 'react-i18next';
5
- import { PreviewCard } from '@adecore/ui';
5
+ import { PreviewCard, Tooltip } from '@adecore/ui';
6
6
  import { formatNumber } from '@adecore/ui/format';
7
7
  import { chatHost } from '../../../../host.js';
8
8
  import { CodeBlock } from '../../CodeBlock.js';
9
9
  import { CodeStreamingContext } from '../../code-streaming.js';
10
10
  import { ImageThumb } from '../../ImageView.js';
11
11
  import { uiChildrenOf, uiNodeLabel, uiNodeText, uiUrlDomain } from '../node-text.js';
12
+ import { useTruncated } from '../use-truncated.js';
12
13
  // The number of every source by its node id, which the list counts in its own order.
13
14
  const SourceNumbers = createContext(new Map());
14
15
  /* Plain text in the same measure until the node closes, so highlighting never makes it jump. */
@@ -54,6 +55,7 @@ export function SourceRenderer({ node, context }) {
54
55
  const number = useContext(SourceNumbers).get(node.id);
55
56
  const { title, url } = node.props;
56
57
  const { openUrl } = context;
57
- const face = (_jsxs(_Fragment, { children: [_jsx("span", { className: "w-5 shrink-0 text-right text-text-faint tabular-nums", children: number }), _jsx("span", { className: "min-w-0 truncate text-text", children: title }), _jsx("span", { className: "shrink-0 text-text-faint", children: uiUrlDomain(url) })] }));
58
- return (_jsx("li", { "data-ui-source": node.id, tabIndex: -1, className: "chat-ui-source scroll-my-2 rounded-md", children: openUrl === undefined ? (_jsx("span", { className: "flex h-8 items-center gap-2 px-2 text-xs", children: face })) : (_jsx("button", { type: "button", className: "flex h-8 w-full items-center gap-2 rounded-md px-2 text-left text-xs hover:bg-surface-hover", onClick: () => openUrl(url), children: face })) }));
58
+ const [measureDomain, domainCut] = useTruncated();
59
+ const face = (_jsxs(_Fragment, { children: [_jsx("span", { className: "w-5 shrink-0 self-start text-right text-text-faint tabular-nums", children: number }), _jsx("span", { className: "min-w-0 grow wrap-anywhere text-text", children: title }), _jsx("span", { ref: measureDomain, className: "max-w-1/2 min-w-0 shrink-0 self-start truncate text-text-faint", children: uiUrlDomain(url) })] }));
60
+ return (_jsx("li", { "data-ui-source": node.id, tabIndex: -1, className: "chat-ui-source scroll-my-2 rounded-md", children: _jsx(Tooltip, { label: _jsxs(_Fragment, { children: [_jsx("span", { className: "block", children: title }), _jsx("span", { className: "block text-text-muted", children: url })] }), children: openUrl === undefined ? (_jsx("span", { tabIndex: domainCut ? 0 : undefined, className: "flex min-h-8 items-start gap-2 rounded-md px-2 py-2 text-xs", children: face })) : (_jsx("button", { type: "button", className: "flex min-h-8 w-full items-start gap-2 rounded-md px-2 py-2 text-left text-xs hover:bg-surface-hover", onClick: () => openUrl(url), children: face })) }) }));
59
61
  }
@@ -2,8 +2,9 @@ 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 { Button, Checkbox, Segmented, Slider, Switch } from '@adecore/ui';
5
+ import { Button, Checkbox, Segmented, Slider, Switch, Tooltip } from '@adecore/ui';
6
6
  import { uiChildrenOf, uiNodeLabel, uiWithUnit } from '../node-text.js';
7
+ import { useTruncated } from '../use-truncated.js';
7
8
  const ChecklistContext = createContext({ values: [], enabled: false, toggle: () => undefined });
8
9
  /*
9
10
  * The binding of an input, or null for a read-only node. An input works once its own node closed and
@@ -51,6 +52,8 @@ export function SliderRenderer({ node, context }) {
51
52
  // The slider prints its unit right after the number, so the space a unit stands apart with comes from here.
52
53
  unit: uiWithUnit('', unit), label: uiNodeLabel(node), disabled: binding === null, className: clsx('px-2', !node.complete && PENDING) }));
53
54
  }
55
+ /* A label longer than this reads as a sentence, which wraps badly beside another; the options then stand in a column. */
56
+ const SIDE_BY_SIDE_LABEL = 24;
54
57
  /* Left aligned, never the full width. Each Option is a segment by its place, since a value may be any scalar. */
55
58
  export function SegmentedRenderer({ node, context }) {
56
59
  const { t } = useTranslation('agent-chat');
@@ -58,15 +61,17 @@ export function SegmentedRenderer({ node, context }) {
58
61
  const current = binding?.value ?? node.props.value;
59
62
  const options = uiChildrenOf(node, 'Option').map((option, index) => ({ id: String(index), label: uiNodeLabel(option), value: option.props.value }));
60
63
  const picked = options.find((option) => option.value === current)?.id ?? '';
61
- return (_jsx("div", { className: clsx('px-2', !node.complete && PENDING), children: _jsx(Segmented, { value: picked, options: options, label: t('blocks.options'), disabled: binding === null, onValueChange: (id) => {
64
+ return (_jsx("div", { className: clsx('px-2', !node.complete && PENDING), children: _jsx(Segmented, { value: picked, options: options, label: t('blocks.options'), orientation: options.some((option) => option.label.length > SIDE_BY_SIDE_LABEL) ? 'vertical' : 'horizontal', disabled: binding === null, onValueChange: (id) => {
62
65
  const option = options[Number(id)];
63
66
  if (option !== undefined) {
64
67
  binding?.onValueChange(option.value);
65
68
  }
66
69
  } }) }));
67
70
  }
68
- /* Runs its local action once its own node closed; like an input, it closes once the block is answered. */
71
+ /* Runs its local action once its own node closed; like an input, it closes once the block is answered. A label cut to the button's one line reads whole in its tooltip. */
69
72
  export function ButtonRenderer({ node, children, context }) {
70
73
  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 }) }));
74
+ const label = uiNodeLabel(node);
75
+ const [measure, truncated] = useTruncated();
76
+ return (_jsx("div", { className: clsx('px-2', !node.complete && PENDING), children: _jsx(Tooltip, { label: label, disabled: !truncated, children: _jsx(Button, { variant: "secondary", size: "sm", className: "max-w-full", disabled: !enabled, onClick: () => node.onAction?.(), children: _jsx("span", { ref: measure, className: "min-w-0 truncate", children: children }) }) }) }));
72
77
  }
@@ -70,7 +70,7 @@ function LinkRow({ target, context, described, children }) {
70
70
  if (!described) {
71
71
  return _jsx(UiLinkChip, { target: target, context: context });
72
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 })] }));
73
+ return (_jsxs("div", { className: "flex min-w-0 flex-wrap items-baseline gap-x-2 px-2 text-sm", children: [_jsx("span", { className: "max-w-full min-w-0", children: _jsx(UiLinkChip, { target: target, context: context }) }), _jsx("span", { className: "min-w-0 wrap-break-word text-text-muted", children: children })] }));
74
74
  }
75
75
  export function FileRenderer({ node, children, context }) {
76
76
  const { path, line } = node.props;
@@ -25,12 +25,15 @@ const STEP_TONES = {
25
25
  function isLive(context) {
26
26
  return context.phase === 'streaming' || (context.live !== undefined && context.live !== null && context.live.state !== 'refused');
27
27
  }
28
+ /* A badge longer than this is a sentence, which a tag cannot wrap; it stands under the title as text instead. */
29
+ const BADGE_AS_TAG = 32;
28
30
  /* The head of the block when it is the first node, a subheading without an icon further down. */
29
31
  export function SummaryRenderer({ node, children }) {
30
32
  const headId = useContext(UiHeadContext);
31
33
  const head = headId === undefined || headId === node.id;
32
34
  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 }))] }));
35
+ const tag = badge !== undefined && badge !== '' && badge.length <= BADGE_AS_TAG;
36
+ 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]) }), _jsxs("div", { className: "flex min-w-0 grow flex-wrap items-start gap-x-2 gap-y-1", children: [_jsx("div", { className: "max-w-full min-w-0 grow basis-48 text-sm font-semibold text-balance wrap-break-word text-text", children: children }), tag && (_jsx(Pill, { shape: "tag", tone: tone === undefined ? 'muted' : UI_TONE_PILL[tone], children: badge })), !tag && badge !== undefined && badge !== '' && _jsx("p", { className: "w-full text-xs wrap-anywhere text-text-muted", children: badge })] })] }));
34
37
  }
35
38
  export function CalloutRenderer({ node, children }) {
36
39
  const { tone, title } = node.props;
@@ -57,5 +60,5 @@ export function StepRenderer({ node, children, context }) {
57
60
  const { t } = useTranslation('agent-chat');
58
61
  const { state, detail } = node.props;
59
62
  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 })] }));
63
+ 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}`) }), _jsxs("span", { className: "flex min-w-0 grow flex-wrap items-baseline justify-between gap-x-2", children: [_jsx("span", { className: clsx('max-w-full min-w-0 grow basis-48 wrap-break-word', quiet ? 'text-text-faint' : 'text-text'), children: children }), detail !== undefined && detail !== '' && _jsx("span", { className: "max-w-full text-xs wrap-anywhere text-text-faint tabular-nums", children: detail })] })] }));
61
64
  }
@@ -1,8 +1,8 @@
1
1
  import { uiChildrenOf } from './node-text.js';
2
2
  /* Rows shown before a person asks for all of them. */
3
3
  export const UI_TABLE_ROWS = 50;
4
- /* Columns a table without Column children takes from its rows. */
5
- const DERIVED_COLUMNS = 8;
4
+ /* The furthest a JavaScript Date reaches from the epoch either way, in milliseconds. */
5
+ const DATE_RANGE = 8.64e15;
6
6
  const NUMERIC_KINDS = new Set(['number', 'bytes', 'duration']);
7
7
  export function isNumericColumn(column) {
8
8
  return NUMERIC_KINDS.has(column.as);
@@ -29,7 +29,7 @@ export function uiTableColumns(node) {
29
29
  const keys = [];
30
30
  for (const row of rows) {
31
31
  for (const key of Object.keys(row)) {
32
- if (!keys.includes(key) && keys.length < DERIVED_COLUMNS) {
32
+ if (!keys.includes(key)) {
33
33
  keys.push(key);
34
34
  }
35
35
  }
@@ -67,7 +67,7 @@ export function uiTableCell(value, column) {
67
67
  }
68
68
  case 'date': {
69
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) };
70
+ return Number.isFinite(at) && Math.abs(at) <= DATE_RANGE ? { kind: 'date', at } : { kind: 'text', text: textOf(value) };
71
71
  }
72
72
  case 'file':
73
73
  return typeof value === 'string' ? { kind: 'file', path: value } : { kind: 'text', text: textOf(value) };
@@ -0,0 +1 @@
1
+ export declare function useTruncated<T extends HTMLElement>(within?: boolean): [(element: T | null) => (() => void) | undefined, boolean];
@@ -0,0 +1,28 @@
1
+ import { useCallback, useState } from 'react';
2
+ function cut(element) {
3
+ return element.scrollWidth > element.clientWidth + 1;
4
+ }
5
+ /*
6
+ * Whether an element's text is cut short where it stands, for a tooltip or a list of values that shows
7
+ * it whole exactly then. `within` asks the same of every `.truncate` element inside it instead. Measured
8
+ * again when the box resizes or the text changes, since neither moves the other.
9
+ */
10
+ export function useTruncated(within = false) {
11
+ const [truncated, setTruncated] = useState(false);
12
+ const measure = useCallback((element) => {
13
+ if (element === null || typeof ResizeObserver === 'undefined') {
14
+ return undefined;
15
+ }
16
+ const check = () => setTruncated(within ? [...element.querySelectorAll('.truncate')].some(cut) : cut(element));
17
+ check();
18
+ const resize = new ResizeObserver(check);
19
+ resize.observe(element);
20
+ const text = new MutationObserver(check);
21
+ text.observe(element, { characterData: true, childList: true, subtree: true });
22
+ return () => {
23
+ resize.disconnect();
24
+ text.disconnect();
25
+ };
26
+ }, [within]);
27
+ return [measure, truncated];
28
+ }
@@ -1,3 +1,6 @@
1
- export type QuoteTaker = (quote: string) => void;
1
+ export type QuoteTaker = (quote: string, itemId: string | null) => void;
2
2
  export declare const QuoteTakerContext: import("react").Context<((take: QuoteTaker) => () => void) | null>;
3
- export declare function selectedAnswerQuote(selection: Selection | null, thread: HTMLElement | null): string | null;
3
+ export declare function selectedAnswerQuote(selection: Selection | null, thread: HTMLElement | null): {
4
+ text: string;
5
+ itemId: string | null;
6
+ } | null;
@@ -28,7 +28,7 @@ function selectsOutside(range, answer) {
28
28
  }
29
29
  return false;
30
30
  }
31
- /* The quote the selection makes, when it lies in one answer of this thread. */
31
+ /* The quote the selection makes, when it lies in one answer of this thread, and the reply it was taken from. */
32
32
  export function selectedAnswerQuote(selection, thread) {
33
33
  if (thread === null || selection === null || selection.isCollapsed || selection.rangeCount === 0) {
34
34
  return null;
@@ -42,5 +42,6 @@ export function selectedAnswerQuote(selection, thread) {
42
42
  if (!inside && selectsOutside(range, answer)) {
43
43
  return null;
44
44
  }
45
- return quoteOf(selection.toString());
45
+ const text = quoteOf(selection.toString());
46
+ return text === null ? null : { text, itemId: answer.closest('[data-find-item]')?.getAttribute('data-find-item') ?? null };
46
47
  }
@@ -3,10 +3,13 @@ import { useContext, useMemo, useState } from 'react';
3
3
  import clsx from 'clsx';
4
4
  import { useTranslation } from 'react-i18next';
5
5
  import { Bot, Brain, Check, ChevronDown, CircleAlert, File, Info, MessageCircleQuestionMark, Reply, TriangleAlert, X } from 'lucide-react';
6
+ import { WAITING_NOTE_PREFIX } from '@adecore/agent-contracts';
6
7
  import { fileBadge, formatBytes, isImageAttachment } from '../../attachments.js';
7
8
  import { useChatRow, useCurrentItem } from '../../../state/chats.js';
8
9
  import { chatHost } from '../../../host.js';
9
10
  import { useChatScope } from '../../../scope.js';
11
+ import { splitQuote } from '../../quote.js';
12
+ import { jumpToTimelineItem } from '../../timeline-scroll.js';
10
13
  import { useProviders } from '../../../state/providers.js';
11
14
  import { ChatReferenceChip } from '../ChatReferenceChip.js';
12
15
  import { ImageThumb } from '../ImageView.js';
@@ -55,8 +58,24 @@ export function UserRow({ chatId, item }) {
55
58
  const long = item.text.split('\n').length > USER_FOLD_LINES || item.text.length > USER_FOLD_CHARS;
56
59
  const attachments = item.attachments ?? [];
57
60
  const chats = item.chats ?? [];
61
+ const [missing, setMissing] = useState(false);
62
+ const { keyOf } = useChatScope();
58
63
  useOpenForFind(item.id, 'text', setOpen);
59
- return (_jsxs("div", { "data-find-item": item.id, className: "flex flex-col items-end", children: [_jsx(MessageHeading, { children: t('rows.user.heading') }), attachments.length > 0 && (_jsx("div", { className: "mb-1.5 flex max-w-[80%] flex-wrap justify-end gap-1.5", children: attachments.map((attachment) => isImageAttachment(attachment.mime) ? (_jsx(AttachmentThumb, { chatId: chatId, attachment: attachment }, attachment.id)) : (_jsx(AttachmentLink, { chatId: chatId, attachment: attachment }, attachment.id))) })), chats.length > 0 && (_jsx("div", { className: "mb-1.5 flex max-w-[80%] flex-wrap justify-end gap-1.5", children: chats.map((id) => (_jsx(ChatReferenceChip, { chatId: id }, id))) })), item.uiChoice !== undefined && _jsx(ChoiceLine, { chatId: chatId, choice: item.uiChoice }), item.text !== '' && (_jsxs("div", { className: "relative max-w-[80%] rounded-2xl bg-surface-active px-3.5 py-2.5 text-sm wrap-anywhere text-text select-text", children: [_jsx("div", { "data-find-field": "text", className: clsx(long && !open && FOLD), children: _jsx(MessageMarkdown, { text: item.text, mentions: item.mentions, skills: item.skills }) }), long && (_jsxs("button", { className: "mt-1 flex items-center gap-1 text-xs text-text-muted hover:text-text", onClick: () => setOpen((o) => !o), children: [_jsx(Icon, { icon: ChevronDown, size: 12, className: clsx('transition-transform', open && 'rotate-180') }), ' ', open ? t('rows.user.showLess') : t('rows.user.showAll')] }))] }))] }));
64
+ // A quote only leads somewhere when the message still opens with the blockquote it was sent with.
65
+ const quoted = item.quoteSource === undefined ? null : splitQuote(item.text);
66
+ const source = item.quoteSource?.itemId;
67
+ function goToSource() {
68
+ if (source !== undefined) {
69
+ setMissing(false);
70
+ jumpToTimelineItem(keyOf(chatId), source, { flash: true, onMissing: () => setMissing(true) });
71
+ }
72
+ }
73
+ return (_jsxs("div", { "data-find-item": item.id, className: "flex flex-col items-end", children: [_jsx(MessageHeading, { children: t('rows.user.heading') }), attachments.length > 0 && (_jsx("div", { className: "mb-1.5 flex max-w-[80%] flex-wrap justify-end gap-1.5", children: attachments.map((attachment) => isImageAttachment(attachment.mime) ? (_jsx(AttachmentThumb, { chatId: chatId, attachment: attachment }, attachment.id)) : (_jsx(AttachmentLink, { chatId: chatId, attachment: attachment }, attachment.id))) })), chats.length > 0 && (_jsx("div", { className: "mb-1.5 flex max-w-[80%] flex-wrap justify-end gap-1.5", children: chats.map((id) => (_jsx(ChatReferenceChip, { chatId: id }, id))) })), item.uiChoice !== undefined && _jsx(ChoiceLine, { chatId: chatId, choice: item.uiChoice }), item.text !== '' && (_jsxs("div", { className: "relative max-w-[80%] rounded-2xl bg-surface-active px-3.5 py-2.5 text-sm wrap-anywhere text-text select-text", children: [_jsx("div", { "data-find-field": "text", className: clsx(long && !open && FOLD), children: quoted !== null && source !== undefined ? (_jsxs(_Fragment, { children: [_jsx(Tooltip, { label: t('composer.quote.goToSource'), children: _jsx("div", { role: "button", tabIndex: 0, className: "mb-2 cursor-pointer rounded-sm border-l-2 border-border-strong pl-2.5 text-text-muted hover:bg-text/5 hover:text-text focus-visible:outline-2 focus-visible:outline-accent", onClick: goToSource, onKeyDown: (e) => {
74
+ if (e.key === 'Enter' || e.key === ' ') {
75
+ e.preventDefault();
76
+ goToSource();
77
+ }
78
+ }, children: _jsx(MessageMarkdown, { text: quoted.quote }) }) }), missing && _jsx("div", { className: "mb-2 text-xs text-text-faint", children: t('rows.user.quoteGone') }), quoted.rest !== '' && _jsx(MessageMarkdown, { text: quoted.rest, mentions: item.mentions, skills: item.skills })] })) : (_jsx(MessageMarkdown, { text: item.text, mentions: item.mentions, skills: item.skills })) }), long && (_jsxs("button", { className: "mt-1 flex items-center gap-1 text-xs text-text-muted hover:text-text", onClick: () => setOpen((o) => !o), children: [_jsx(Icon, { icon: ChevronDown, size: 12, className: clsx('transition-transform', open && 'rotate-180') }), ' ', open ? t('rows.user.showLess') : t('rows.user.showAll')] }))] }))] }));
60
79
  }
61
80
  /*
62
81
  * The choice a message answers. The label stays out of the message, which holds exactly what the agent
@@ -174,16 +193,16 @@ export function NoteRow({ id, level, text, from }) {
174
193
  const head = breakAt === -1 ? text : text.slice(0, breakAt);
175
194
  const rest = breakAt === -1 ? '' : text.slice(breakAt + 1).trim();
176
195
  useOpenForFind(id, 'output', setOpen);
177
- return (_jsxs("div", { "data-find-item": id, className: clsx('-mx-1 mb-0.5 flex min-h-7 items-start gap-2 px-1 py-1 text-xs', level === 'error' ? 'text-status-error' : level === 'warning' ? 'text-status-needs-you' : 'text-text-faint'), children: [_jsx("span", { className: ROW_GUTTER, children: NOTE_ICON[level] }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-x-2", children: [_jsx("span", { "data-find-field": "summary", className: "select-text", children: head }), rest !== '' && (_jsx("button", { className: "text-text-muted hover:text-text", onClick: () => setOpen(!open), children: open ? t('rows.note.hide') : t('rows.note.show') })), from !== undefined && _jsx(OpenChatButton, { chatId: from })] }), open && rest !== '' && (_jsx("div", { "data-find-field": "output", className: "mt-1 text-text-muted select-text", children: _jsx(MessageMarkdown, { text: rest }) }))] })] }));
196
+ return (_jsxs("div", { "data-find-item": id, className: clsx('-mx-1 mb-0.5 flex min-h-7 items-start gap-2 px-1 py-1 text-xs', level === 'error' ? 'text-status-error' : level === 'warning' ? 'text-status-needs-you' : 'text-text-faint'), children: [_jsx("span", { className: ROW_GUTTER, children: NOTE_ICON[level] }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-x-2", children: [_jsx("span", { "data-find-field": "summary", className: "select-text", children: head }), rest !== '' && (_jsx("button", { className: "text-text-muted hover:text-text", onClick: () => setOpen(!open), children: open ? t('rows.note.hide') : t('rows.note.show') })), from !== undefined && _jsx(OpenChatButton, { chatId: from, label: id.startsWith(WAITING_NOTE_PREFIX) ? 'openWaiting' : 'openFork' })] }), open && rest !== '' && (_jsx("div", { "data-find-field": "output", className: "mt-1 text-text-muted select-text", children: _jsx(MessageMarkdown, { text: rest }) }))] })] }));
178
197
  }
179
- /* "Open fork" on a note a fork sent, while that fork is still in the project. */
180
- function OpenChatButton({ chatId }) {
198
+ /* "Open fork" on a note a fork sent, or "Open to answer" on one about a waiting child, while that chat is still in the project. */
199
+ function OpenChatButton({ chatId, label }) {
181
200
  const { t } = useTranslation('agent-chat');
182
201
  const place = chatHost().useChatPlace(chatId);
183
202
  if (place.title === null) {
184
203
  return null;
185
204
  }
186
- return (_jsx("button", { className: "text-text-muted hover:text-text", onClick: place.go, children: t('rows.note.openFork') }));
205
+ return (_jsx("button", { className: "text-text-muted hover:text-text", onClick: place.go, children: t(`rows.note.${label}`) }));
187
206
  }
188
207
  /*
189
208
  * The header of a turn the agent started itself, in place of a message of the person. What the
@@ -47,7 +47,7 @@ export function Row({ row, chatId, toggleGroup, toggleTurn, toggleSubagent, open
47
47
  case 'forks':
48
48
  return _jsx(ForksRow, { chatId: chatId, turnId: row.turnId });
49
49
  case 'changed-files':
50
- return _jsx(ChangedFilesRow, { chatId: chatId, turnId: row.turnId, tools: row.tools, diff: row.diff, checkpoint: row.checkpoint });
50
+ return (_jsx(ChangedFilesRow, { chatId: chatId, turnId: row.turnId, tools: row.tools, diff: row.diff, checkpoint: row.checkpoint, baseMoved: row.baseMoved }));
51
51
  case 'approval':
52
52
  return _jsx(ApprovalHistoryRow, { item: row.item });
53
53
  case 'question':
@@ -1,5 +1,6 @@
1
1
  import { type ReactNode } from 'react';
2
2
  import type { ChatCheckpointDiff, ChatToolItem, ChatTurnItem } from '@adecore/agent-contracts';
3
+ import type { BaseMoved } from '../../logic/timeline.js';
3
4
  export declare function ToggleLine({ icon, label, detail, open, onToggle, failed, live, trailing, className }: {
4
5
  icon: ReactNode;
5
6
  label: string;
@@ -39,10 +40,11 @@ export declare function TurnFoldRow({ turn, label, work, expanded, onToggle }: {
39
40
  expanded: boolean;
40
41
  onToggle(): void;
41
42
  }): import("react").JSX.Element;
42
- export declare function ChangedFilesRow({ tools, diff, checkpoint, chatId, turnId }: {
43
+ export declare function ChangedFilesRow({ tools, diff, checkpoint, baseMoved, chatId, turnId }: {
43
44
  tools: ChatToolItem[];
44
45
  diff: ChatCheckpointDiff | null;
45
46
  checkpoint: boolean;
47
+ baseMoved?: BaseMoved | undefined;
46
48
  chatId: string;
47
49
  turnId: string;
48
50
  }): import("react").JSX.Element;
@@ -91,7 +91,7 @@ function CheckpointFileBody({ file }) {
91
91
  return (_jsx(DiffSuspense, { children: _jsx(UnifiedDiff, { change: file }) }));
92
92
  }
93
93
  /* The frame of a changed files card: a header that folds the list of files away. */
94
- function ChangedFilesCard({ count, truncated, added, deleted, revealNonce = 0, children }) {
94
+ function ChangedFilesCard({ count, truncated, added, deleted, revealNonce = 0, baseMoved, children }) {
95
95
  const { t } = useTranslation('agent-chat');
96
96
  const [expanded, setExpanded] = useState(false);
97
97
  // A jump opens the card, also one that mounted it by scrolling it on screen; the flash is gone by the next visit.
@@ -100,7 +100,7 @@ function ChangedFilesCard({ count, truncated, added, deleted, revealNonce = 0, c
100
100
  setSeenReveal(revealNonce);
101
101
  setExpanded(true);
102
102
  }
103
- return (_jsxs("div", { className: "mb-3 overflow-hidden rounded-lg border border-border bg-surface-raised", children: [_jsxs("button", { className: "flex w-full items-center gap-2 px-3 py-2 text-left text-xs text-text-muted hover:bg-surface-hover", "aria-expanded": expanded, onClick: () => setExpanded((value) => !value), children: [_jsx(Icon, { icon: FileDiff, size: 12, className: "shrink-0" }), _jsx("span", { className: "shrink-0 font-medium text-text", children: t('work.changedFiles', { count }) }), truncated && _jsx("span", { className: "min-w-0 truncate text-text-faint", children: t('work.andMore') }), _jsx("span", { className: "grow" }), added !== undefined && _jsxs("span", { className: "text-chat-added tabular-nums", children: ["+", added] }), deleted !== undefined && _jsxs("span", { className: "text-chat-removed tabular-nums", children: ["-", deleted] }), _jsx(Icon, { icon: ChevronRight, size: 12, className: clsx('shrink-0 text-text-faint transition-transform', expanded && 'rotate-90') })] }), expanded && children] }));
103
+ return (_jsxs("div", { className: "mb-3 overflow-hidden rounded-lg border border-border bg-surface-raised", children: [_jsxs("button", { className: "flex w-full items-center gap-2 px-3 py-2 text-left text-xs text-text-muted hover:bg-surface-hover", "aria-expanded": expanded, onClick: () => setExpanded((value) => !value), children: [_jsx(Icon, { icon: FileDiff, size: 12, className: "shrink-0" }), _jsx("span", { className: "shrink-0 font-medium text-text", children: baseMoved === undefined ? t('work.changedFiles', { count }) : t('work.folderChanges', { count }) }), truncated && _jsx("span", { className: "min-w-0 truncate text-text-faint", children: t('work.andMore') }), _jsx("span", { className: "grow" }), added !== undefined && _jsxs("span", { className: "text-chat-added tabular-nums", children: ["+", added] }), deleted !== undefined && _jsxs("span", { className: "text-chat-removed tabular-nums", children: ["-", deleted] }), _jsx(Icon, { icon: ChevronRight, size: 12, className: clsx('shrink-0 text-text-faint transition-transform', expanded && 'rotate-90') })] }), baseMoved !== undefined && (_jsx("div", { className: "pr-3 pb-2 pl-8 text-xs text-text-faint", children: t('work.baseMoved', { from: baseMoved.from.slice(0, 7), to: baseMoved.to.slice(0, 7) }) })), expanded && children] }));
104
104
  }
105
105
  /*
106
106
  * The files a settled turn changed, as a card; each file opens its diff in place. The host's
@@ -108,7 +108,7 @@ function ChangedFilesCard({ count, truncated, added, deleted, revealNonce = 0, c
108
108
  * unified diffs the CLI reported, then the before and after of its own edits. A turn that has a
109
109
  * checkpoint but no stored diff asks the host for one, which is also how a running turn gets it.
110
110
  */
111
- export function ChangedFilesRow({ tools, diff, checkpoint, chatId, turnId }) {
111
+ export function ChangedFilesRow({ tools, diff, checkpoint, baseMoved, chatId, turnId }) {
112
112
  const [open, setOpen] = useState({});
113
113
  const [fetched, setFetched] = useState(null);
114
114
  const actions = useChatActions();
@@ -135,7 +135,7 @@ export function ChangedFilesRow({ tools, diff, checkpoint, chatId, turnId }) {
135
135
  const revealNonce = flash !== null && flash.opens && flash.key === keyOf(chatId) && flash.rowId === `files-${turnId}` ? flash.nonce : 0;
136
136
  if (checkpointDiff !== null && checkpointDiff.files.length > 0) {
137
137
  const files = checkpointDiff.files;
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))) }));
138
+ return (_jsx(ChangedFilesCard, { revealNonce: revealNonce, baseMoved: baseMoved, 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))) }));
139
139
  }
140
140
  return _jsx(ProviderChangedFiles, { tools: tools, open: open, setOpen: setOpen, revealNonce: revealNonce });
141
141
  }
package/dist/host.d.ts CHANGED
@@ -194,6 +194,7 @@ export interface ChatHost {
194
194
  name: string;
195
195
  mark: ReactNode;
196
196
  } | null;
197
+ useMachineLabel(scopeId: string): string | null;
197
198
  useChatPlace(chatId: string): ChatPlace;
198
199
  useContextSources(chatId: string): ReadonlyArray<{
199
200
  title: string;
package/dist/host.js CHANGED
@@ -29,6 +29,7 @@ const DEFAULT_HOST = {
29
29
  openLogin: null,
30
30
  useLoginBlocked: () => null,
31
31
  useProjectLook: () => null,
32
+ useMachineLabel: () => null,
32
33
  useChatPlace: () => NOWHERE,
33
34
  useContextSources: () => NO_SOURCES,
34
35
  useHidesFolder: () => false,
@@ -91,7 +91,8 @@
91
91
  "user": {
92
92
  "heading": "You",
93
93
  "showAll": "Show all",
94
- "showLess": "Show less"
94
+ "showLess": "Show less",
95
+ "quoteGone": "The reply this quote came from is no longer in this chat."
95
96
  },
96
97
  "report": {
97
98
  "heading": "Report"
@@ -107,7 +108,8 @@
107
108
  "note": {
108
109
  "hide": "Hide",
109
110
  "show": "Show",
110
- "openFork": "Open fork"
111
+ "openFork": "Open fork",
112
+ "openWaiting": "Open to answer"
111
113
  },
112
114
  "compaction": {
113
115
  "plain": "Context compacted",
@@ -165,6 +167,9 @@
165
167
  },
166
168
  "changedFiles_one": "{{count}} changed file",
167
169
  "changedFiles_other": "{{count}} changed files",
170
+ "folderChanges_one": "{{count}} change in the folder this turn",
171
+ "folderChanges_other": "{{count}} changes in the folder this turn",
172
+ "baseMoved": "The branch moved during this turn ({{from}} to {{to}}), so this includes what came in with it.",
168
173
  "andMore": "and more, truncated",
169
174
  "edits_one": "{{count}} edit",
170
175
  "edits_other": "{{count}} edits",
@@ -249,6 +254,17 @@
249
254
  },
250
255
  "composer": {
251
256
  "slotFailed": "This control failed to render",
257
+ "inputLabel": "Message",
258
+ "menu": {
259
+ "commands": "Commands",
260
+ "skills": "Skills",
261
+ "mentions": "Mentions"
262
+ },
263
+ "held": {
264
+ "question": "A question is waiting. It opens when you pause typing.",
265
+ "approval": "An approval is waiting. It opens when you pause typing.",
266
+ "show": "Show now"
267
+ },
252
268
  "commands": {
253
269
  "model": "Switch the model",
254
270
  "compact": "Compact the context",
@@ -294,7 +310,8 @@
294
310
  },
295
311
  "quote": {
296
312
  "add": "Quote",
297
- "remove": "Remove quote"
313
+ "remove": "Remove quote",
314
+ "goToSource": "Go to the reply"
298
315
  },
299
316
  "removeAttachment": "Remove {{name}}",
300
317
  "attach": "Attach files",
@@ -319,6 +336,9 @@
319
336
  "monitors_one": "{{count}} monitor",
320
337
  "monitors_other": "{{count}} monitors",
321
338
  "pausedUntil": "paused until {{time}}",
339
+ "progress": "{{done}} of {{total}} sub-agents done",
340
+ "needsYou_one": "{{count}} needs you",
341
+ "needsYou_other": "{{count}} need you",
322
342
  "took": {
323
343
  "done": "done in {{duration}}",
324
344
  "failed": "failed after {{duration}}",
@@ -332,8 +352,10 @@
332
352
  "subagents": {
333
353
  "stop": {
334
354
  "task": "Stop this task",
355
+ "native": "Stop this sub-agent",
335
356
  "mark": "Mark as stopped. The CLI cannot stop one sub-agent, so it may keep working until the chat's process ends."
336
357
  },
358
+ "stale": "This sub-agent belongs to a process that has ended.",
337
359
  "title": "Sub-agents",
338
360
  "backToMain": "Back to the main agent",
339
361
  "loading": "Loading the conversation…",
@@ -344,9 +366,7 @@
344
366
  "stopFailed": "The sub-agent could not be stopped",
345
367
  "info": {
346
368
  "label": "About this sub-agent",
347
- "tokens": "{{tokens}} tokens",
348
- "showTask": "Show its task",
349
- "hideTask": "Hide its task"
369
+ "tokens": "{{tokens}} tokens"
350
370
  }
351
371
  },
352
372
  "modes": {
@@ -382,7 +402,10 @@
382
402
  "noMatch": "No models match",
383
403
  "legacyGroup_one": "Legacy models ({{count}})",
384
404
  "legacyGroup_other": "Legacy models ({{count}})",
385
- "legacy": "Legacy"
405
+ "legacy": "Legacy",
406
+ "requires": "Requires {{cli}} {{version}} or later on {{machine}}",
407
+ "requiresHere": "Requires {{cli}} {{version}} or later",
408
+ "providerSettings": "Provider settings"
386
409
  },
387
410
  "stash": {
388
411
  "title": "Stashed prompts",
@@ -504,7 +527,13 @@
504
527
  "chart": {
505
528
  "values": "Values",
506
529
  "summary": "{{count}} values, highest {{label}} at {{value}}",
507
- "summarySeries": "{{count}} categories, highest {{series}} for {{label}} at {{value}}"
530
+ "summarySeries": "{{count}} categories, highest {{series}} for {{label}} at {{value}}",
531
+ "rowsLeftOut": "The first {{shown}} of {{total}} rows are drawn.",
532
+ "seriesLeftOut_one": "{{series}} is not drawn.",
533
+ "seriesLeftOut_other": "{{series}} are not drawn.",
534
+ "sideBySide": "Negative values cannot be stacked, so the series stand side by side.",
535
+ "showValues": "Show all values",
536
+ "hideValues": "Hide values"
508
537
  },
509
538
  "image": {
510
539
  "label": "Image",
@@ -528,8 +557,8 @@
528
557
  "answered": "Answered with “{{label}}”",
529
558
  "chose": "Chose “{{label}}”",
530
559
  "choseOlder": "Chose “{{label}}” on the reply of {{time}}",
531
- "fixes_one": "{{count}} fix",
532
- "fixes_other": "{{count}} fixes",
560
+ "notes_one": "{{count}} note",
561
+ "notes_other": "{{count}} notes",
533
562
  "shownAsText": "Shown as text",
534
563
  "unknownComponent": "This version cannot draw {{component}}",
535
564
  "renderFailed": "Could not draw this part",