@adecore/agents-react 0.21.1 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/dist/agents/model-requirement.d.ts +6 -0
  2. package/dist/agents/model-requirement.js +26 -0
  3. package/dist/agents/status-look.js +2 -3
  4. package/dist/chat/chat-client.d.ts +3 -2
  5. package/dist/chat/chat-client.js +4 -2
  6. package/dist/chat/drafts.d.ts +2 -1
  7. package/dist/chat/drafts.js +21 -17
  8. package/dist/chat/logic/ansi.js +5 -1
  9. package/dist/chat/logic/fork.js +8 -9
  10. package/dist/chat/logic/limit.js +17 -14
  11. package/dist/chat/logic/picker-entries.d.ts +10 -0
  12. package/dist/chat/logic/picker-entries.js +19 -0
  13. package/dist/chat/logic/resume-compaction.js +2 -11
  14. package/dist/chat/logic/scrubber.js +9 -18
  15. package/dist/chat/logic/timeline.d.ts +5 -0
  16. package/dist/chat/logic/timeline.js +25 -11
  17. package/dist/chat/logic/tool-catalog.js +3 -4
  18. package/dist/chat/mentions.js +5 -6
  19. package/dist/chat/prompt-hold.d.ts +8 -0
  20. package/dist/chat/prompt-hold.js +16 -0
  21. package/dist/chat/quote.d.ts +4 -0
  22. package/dist/chat/quote.js +19 -0
  23. package/dist/chat/runtime-modes.js +2 -4
  24. package/dist/chat/stash.d.ts +8 -2
  25. package/dist/chat/stash.js +22 -8
  26. package/dist/chat/subagent-list.d.ts +7 -1
  27. package/dist/chat/subagent-list.js +13 -1
  28. package/dist/chat/thumbnails.js +3 -4
  29. package/dist/chat/timeline-scroll.d.ts +6 -2
  30. package/dist/chat/timeline-scroll.js +45 -46
  31. package/dist/chat/ui/BookmarkSubmenu.js +1 -5
  32. package/dist/chat/ui/ChatActivity.js +22 -8
  33. package/dist/chat/ui/CodeBlock.js +2 -3
  34. package/dist/chat/ui/Composer.js +160 -88
  35. package/dist/chat/ui/ComposerInput.d.ts +3 -1
  36. package/dist/chat/ui/ComposerInput.js +13 -4
  37. package/dist/chat/ui/Markdown.js +11 -11
  38. package/dist/chat/ui/Pickers.js +31 -27
  39. package/dist/chat/ui/PromptComposer.d.ts +8 -1
  40. package/dist/chat/ui/PromptComposer.js +18 -6
  41. package/dist/chat/ui/QuoteButton.js +4 -5
  42. package/dist/chat/ui/RunSettings.js +8 -2
  43. package/dist/chat/ui/Scrubber.js +2 -3
  44. package/dist/chat/ui/SubagentControls.d.ts +4 -2
  45. package/dist/chat/ui/SubagentControls.js +13 -11
  46. package/dist/chat/ui/SubagentInfo.d.ts +1 -1
  47. package/dist/chat/ui/SubagentInfo.js +9 -15
  48. package/dist/chat/ui/SubagentStopButton.js +2 -1
  49. package/dist/chat/ui/Timeline.js +39 -21
  50. package/dist/chat/ui/TimelineMenu.js +3 -9
  51. package/dist/chat/ui/UnifiedDiff.js +3 -5
  52. package/dist/chat/ui/VisualFrame.js +3 -4
  53. package/dist/chat/ui/composer/keys.js +19 -11
  54. package/dist/chat/ui/composer/menu-aria.d.ts +10 -0
  55. package/dist/chat/ui/composer/menu-aria.js +18 -0
  56. package/dist/chat/ui/intelligent-ui/render-context.d.ts +1 -1
  57. package/dist/chat/ui/quote-selection.d.ts +5 -2
  58. package/dist/chat/ui/quote-selection.js +3 -2
  59. package/dist/chat/ui/rows/MessageRows.js +26 -8
  60. package/dist/chat/ui/rows/Rows.js +1 -1
  61. package/dist/chat/ui/rows/WorkRows.d.ts +7 -4
  62. package/dist/chat/ui/rows/WorkRows.js +22 -17
  63. package/dist/chat/visual-viewport.d.ts +1 -1
  64. package/dist/chat/visual-viewport.js +9 -8
  65. package/dist/host.d.ts +1 -0
  66. package/dist/host.js +1 -0
  67. package/dist/locales/en/agent-chat.json +30 -7
  68. package/dist/locales/nl/agent-chat.json +30 -7
  69. package/dist/port-transport.js +4 -8
  70. package/dist/state/chats.d.ts +2 -1
  71. package/dist/state/chats.js +29 -15
  72. package/dist/state/usage.js +44 -49
  73. package/dist/storage.js +7 -7
  74. package/dist/theme.css +6 -14
  75. package/dist/usage/LimitsList.js +1 -2
  76. package/dist/usage/UsageBreakdown.js +1 -3
  77. package/dist/usage/UsageDialog.js +2 -4
  78. package/dist/usage/format.js +6 -6
  79. package/dist/usage/money.js +1 -5
  80. package/dist/usage/summary.js +2 -4
  81. package/package.json +24 -4
  82. package/src/agents/model-requirement.ts +36 -0
  83. package/src/agents/status-look.ts +2 -3
  84. package/src/chat/chat-client.ts +7 -3
  85. package/src/chat/drafts.ts +33 -21
  86. package/src/chat/logic/ansi.ts +5 -1
  87. package/src/chat/logic/fork.ts +8 -9
  88. package/src/chat/logic/limit.ts +17 -16
  89. package/src/chat/logic/picker-entries.ts +25 -0
  90. package/src/chat/logic/resume-compaction.ts +2 -11
  91. package/src/chat/logic/scrubber.ts +13 -17
  92. package/src/chat/logic/timeline.ts +39 -13
  93. package/src/chat/logic/tool-catalog.ts +7 -6
  94. package/src/chat/mentions.ts +5 -6
  95. package/src/chat/prompt-hold.ts +27 -0
  96. package/src/chat/quote.ts +20 -0
  97. package/src/chat/runtime-modes.ts +2 -4
  98. package/src/chat/stash.ts +32 -8
  99. package/src/chat/subagent-list.ts +19 -5
  100. package/src/chat/thumbnails.ts +3 -4
  101. package/src/chat/timeline-scroll.ts +58 -53
  102. package/src/chat/ui/BookmarkSubmenu.tsx +1 -5
  103. package/src/chat/ui/ChatActivity.tsx +35 -9
  104. package/src/chat/ui/CodeBlock.tsx +2 -3
  105. package/src/chat/ui/Composer.tsx +263 -138
  106. package/src/chat/ui/ComposerInput.tsx +18 -3
  107. package/src/chat/ui/Markdown.tsx +13 -17
  108. package/src/chat/ui/Pickers.tsx +56 -39
  109. package/src/chat/ui/PromptComposer.tsx +37 -8
  110. package/src/chat/ui/QuoteButton.tsx +5 -5
  111. package/src/chat/ui/RunSettings.tsx +9 -2
  112. package/src/chat/ui/Scrubber.tsx +2 -3
  113. package/src/chat/ui/SubagentControls.tsx +23 -22
  114. package/src/chat/ui/SubagentInfo.tsx +24 -42
  115. package/src/chat/ui/SubagentStopButton.tsx +2 -1
  116. package/src/chat/ui/SubagentTimeline.tsx +1 -2
  117. package/src/chat/ui/Timeline.tsx +44 -24
  118. package/src/chat/ui/TimelineMenu.tsx +3 -9
  119. package/src/chat/ui/UnifiedDiff.tsx +3 -5
  120. package/src/chat/ui/VisualFrame.tsx +3 -4
  121. package/src/chat/ui/composer/keys.ts +21 -11
  122. package/src/chat/ui/composer/menu-aria.ts +29 -0
  123. package/src/chat/ui/intelligent-ui/render-context.ts +2 -2
  124. package/src/chat/ui/quote-selection.ts +6 -4
  125. package/src/chat/ui/rehype-chips.ts +1 -1
  126. package/src/chat/ui/rehype-fade.ts +1 -1
  127. package/src/chat/ui/remark-html-as-text.ts +1 -1
  128. package/src/chat/ui/rows/MessageRows.tsx +47 -9
  129. package/src/chat/ui/rows/Rows.tsx +3 -1
  130. package/src/chat/ui/rows/WorkRows.tsx +101 -70
  131. package/src/chat/visual-viewport.ts +10 -8
  132. package/src/host.ts +4 -3
  133. package/src/locales/en/agent-chat.json +30 -7
  134. package/src/locales/nl/agent-chat.json +30 -7
  135. package/src/port-transport.ts +4 -8
  136. package/src/state/chats.ts +42 -16
  137. package/src/state/usage.ts +49 -60
  138. package/src/storage.ts +7 -7
  139. package/src/theme.css +6 -14
  140. package/src/usage/LimitsList.tsx +2 -4
  141. package/src/usage/UsageBreakdown.tsx +2 -5
  142. package/src/usage/UsageDialog.tsx +2 -4
  143. package/src/usage/format.ts +6 -6
  144. package/src/usage/money.ts +1 -5
  145. package/src/usage/summary.ts +2 -4
@@ -6,9 +6,8 @@ import { chatHost } from '../../host.js';
6
6
  import { useDiffTheme } from './diff-theme.js';
7
7
  import { fullFileDiff } from './full-diff.js';
8
8
  /*
9
- * A patch the CLI reported itself (Codex writes unified diffs). The renderer needs a file header
10
- * to know what it is looking at, and a CLI that only sends hunks gets one from the path we know.
11
- * A diff without hunks at all is shown as its own lines, so nothing is ever swallowed.
9
+ * A patch the CLI reported itself. The renderer needs a file header, which a CLI that only sends hunks
10
+ * gets from the path we know; a diff without hunks is shown as its own lines, so nothing is swallowed.
12
11
  */
13
12
  function asPatch(change) {
14
13
  if (!/^@@/m.test(change.diff)) {
@@ -21,8 +20,7 @@ function asPatch(change) {
21
20
  const to = change.kind === 'delete' ? '/dev/null' : `b/${change.path}`;
22
21
  return `--- ${from}\n+++ ${to}\n${change.diff}`;
23
22
  }
24
- /* Lets a diff with a view of its own run to the bottom of it, so the scrollbar of a short one sits
25
- there instead of under its last line; the lines themselves keep their height. */
23
+ /* Lets a diff with a view of its own run to its bottom, so a short one's scrollbar sits there and not under its last line. */
26
24
  const FILL_CSS = `
27
25
  :host { display: flex; flex-direction: column; flex-grow: 1; }
28
26
  pre { flex-grow: 1; align-content: start; }
@@ -19,10 +19,9 @@ function personActsIn(frame) {
19
19
  return frame.ownerDocument.activeElement === frame && navigator.userActivation?.isActive !== false;
20
20
  }
21
21
  /*
22
- * One visual's page in a frame on the thread's own ground. The frame loads the app's sandbox host
23
- * page and the bridge hands it the page and the theme. The frame never gets `allow-same-origin`,
24
- * which keeps the page on an opaque origin even when the host page is served on the app's own. Its
25
- * box keeps one height while the page loads and when it fails, so nothing under it moves.
22
+ * One visual's page in a frame that loads the app's sandbox host page; the bridge hands it the page and
23
+ * the theme. The frame never gets `allow-same-origin`, which keeps the page on an opaque origin even
24
+ * when the host page is served on the app's own. Its box keeps one height while it loads or fails.
26
25
  */
27
26
  export function VisualFrame({ chatId, visual, fill = false, className }) {
28
27
  const { t } = useTranslation('agent-chat');
@@ -30,24 +30,32 @@ export function enterAction(keys, { inOpenFence, list, column }) {
30
30
  }
31
31
  return list.empty ? 'leave-list' : 'continue-list';
32
32
  }
33
- /* Whether `pos` sits inside a fenced code block that has an opening fence and no closing one yet. */
34
- export function inOpenFence(tree, pos) {
33
+ /* The innermost fenced code block around `pos`, if any. */
34
+ function enclosingFence(tree, pos) {
35
35
  for (let node = tree.resolveInner(pos, -1); node !== null; node = node.parent) {
36
36
  if (node.name === 'FencedCode') {
37
- return pos > node.from && node.getChildren('CodeMark').length < 2;
37
+ return node;
38
38
  }
39
39
  }
40
- return false;
40
+ return null;
41
+ }
42
+ /* A fence nobody has closed yet has its opening mark only. */
43
+ function isOpenFence(fence) {
44
+ return fence.getChildren('CodeMark').length < 2;
45
+ }
46
+ /* Whether `pos` sits inside a fenced code block that has an opening fence and no closing one yet. */
47
+ export function inOpenFence(tree, pos) {
48
+ const fence = enclosingFence(tree, pos);
49
+ return fence !== null && pos > fence.from && isOpenFence(fence);
41
50
  }
42
51
  /* Whether `pos` sits in the body of a fenced code block, open or closed: below its opening line and before its end. */
43
52
  export function inFenceBody(tree, text, pos) {
44
- for (let node = tree.resolveInner(pos, -1); node !== null; node = node.parent) {
45
- if (node.name === 'FencedCode') {
46
- const openingEnd = text.indexOf('\n', node.from);
47
- return openingEnd !== -1 && pos > openingEnd && (pos < node.to || node.getChildren('CodeMark').length < 2);
48
- }
53
+ const fence = enclosingFence(tree, pos);
54
+ if (fence === null) {
55
+ return false;
49
56
  }
50
- return false;
57
+ const openingEnd = text.indexOf('\n', fence.from);
58
+ return openingEnd !== -1 && pos > openingEnd && (pos < fence.to || isOpenFence(fence));
51
59
  }
52
60
  /* The spaces Tab inserts at `column`, up to the next stop of four, the way a code editor lines up. */
53
61
  export function tabSpaces(column) {
@@ -63,7 +71,7 @@ export function inCode(tree, text, pos) {
63
71
  let blockFrom = null;
64
72
  for (let node = tree.resolveInner(pos, -1); node !== null; node = node.parent) {
65
73
  if (node.name === 'FencedCode') {
66
- return pos > node.from && (pos < node.to || node.getChildren('CodeMark').length < 2);
74
+ return pos > node.from && (pos < node.to || isOpenFence(node));
67
75
  }
68
76
  if (node.name === 'InlineCode' && pos > node.from && pos < node.to) {
69
77
  return true;
@@ -0,0 +1,10 @@
1
+ export interface ComposerMenuAriaInput {
2
+ label: string;
3
+ listId: string;
4
+ open: boolean;
5
+ activeIndex: number;
6
+ count: number;
7
+ }
8
+ export declare function clampMenuIndex(index: number, count: number): number;
9
+ export declare function optionIdOf(listId: string, index: number): string;
10
+ export declare function composerMenuAria({ label, listId, open, activeIndex, count }: ComposerMenuAriaInput): Record<string, string>;
@@ -0,0 +1,18 @@
1
+ /* The index that still points at an option once the list has shrunk under a stale one. */
2
+ export function clampMenuIndex(index, count) {
3
+ return Math.min(index, Math.max(count - 1, 0));
4
+ }
5
+ export function optionIdOf(listId, index) {
6
+ return `${listId}-${index}`;
7
+ }
8
+ /*
9
+ * The attributes the editor's content element takes while a suggestion menu stands beside it. The
10
+ * element keeps its `textbox` role; ARIA 1.2 lets it name the option the arrow keys are on.
11
+ */
12
+ export function composerMenuAria({ label, listId, open, activeIndex, count }) {
13
+ return {
14
+ 'aria-label': label,
15
+ 'aria-autocomplete': 'list',
16
+ ...(open && count > 0 ? { 'aria-controls': listId, 'aria-activedescendant': optionIdOf(listId, activeIndex) } : {})
17
+ };
18
+ }
@@ -1,7 +1,7 @@
1
1
  import { type ReactNode } from 'react';
2
2
  import type { UiComponentName, UiProps, UiViewNode } from '@adecore/intelligent-ui';
3
- export type { UiLinkTarget } from '@adecore/intelligent-ui/links';
4
3
  import type { UiLinkTarget } from '@adecore/intelligent-ui/links';
4
+ export type { UiLinkTarget };
5
5
  export interface UiLink {
6
6
  state: 'chip' | 'plain';
7
7
  label?: string;
@@ -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,21 +193,20 @@ 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
- * The header of a turn the agent started itself, in place of the message of the person that is
190
- * missing. It is one line: what the sub-agent came back with lives on that agent's own row, behind
191
- * "Show result". When the CLI named the sub-agent it woke up about, the header opens that row.
208
+ * The header of a turn the agent started itself, in place of a message of the person. What the
209
+ * sub-agent came back with lives on its own row, which the header opens when the CLI named it.
192
210
  */
193
211
  export function AgentTurnRow({ label, onOpen }) {
194
212
  const line = 'mb-0.5 flex h-7 w-full items-center gap-2 text-left text-xs text-text-muted';
@@ -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,20 +1,22 @@
1
+ import { type ReactNode } from 'react';
1
2
  import type { ChatCheckpointDiff, ChatToolItem, ChatTurnItem } from '@adecore/agent-contracts';
3
+ import type { BaseMoved } from '../../logic/timeline.js';
2
4
  export declare function ToggleLine({ icon, label, detail, open, onToggle, failed, live, trailing, className }: {
3
- icon: React.ReactNode;
5
+ icon: ReactNode;
4
6
  label: string;
5
7
  detail?: string;
6
8
  open: boolean;
7
9
  onToggle(): void;
8
10
  failed?: boolean;
9
11
  live?: boolean;
10
- trailing?: React.ReactNode;
12
+ trailing?: ReactNode;
11
13
  className?: string;
12
14
  }): import("react").JSX.Element;
13
15
  export declare function WorkRow({ tool, nested, detail, icon, label, failed }: {
14
16
  tool: ChatToolItem;
15
17
  nested?: boolean;
16
18
  detail?: string;
17
- icon?: React.ReactNode;
19
+ icon?: ReactNode;
18
20
  label?: string;
19
21
  failed?: boolean;
20
22
  }): import("react").JSX.Element;
@@ -38,10 +40,11 @@ export declare function TurnFoldRow({ turn, label, work, expanded, onToggle }: {
38
40
  expanded: boolean;
39
41
  onToggle(): void;
40
42
  }): import("react").JSX.Element;
41
- export declare function ChangedFilesRow({ tools, diff, checkpoint, chatId, turnId }: {
43
+ export declare function ChangedFilesRow({ tools, diff, checkpoint, baseMoved, chatId, turnId }: {
42
44
  tools: ChatToolItem[];
43
45
  diff: ChatCheckpointDiff | null;
44
46
  checkpoint: boolean;
47
+ baseMoved?: BaseMoved | undefined;
45
48
  chatId: string;
46
49
  turnId: string;
47
50
  }): 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
- function ToolBody({ tool }) {
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(Suspense, { fallback: _jsx("div", { className: "px-3 py-2 text-xs text-text-faint", children: t('work.loadingDiff') }), children: patches.map((change, index) => (_jsx(UnifiedDiff, { change: change }, index))) })) : changes.length > 0 ? (_jsx(Suspense, { fallback: _jsx("div", { className: "px-3 py-2 text-xs text-text-faint", children: t('work.loadingDiff') }), 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 }) }))] }));
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
- /* One settled tool call: a line, and its input and output behind it. An image the call looked at
40
- is drawn under the line, because a picture says more about that read than its path does.
41
- `icon`, `label` and `failed` let a row that knows the call better name it in its own words. */
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,23 +85,22 @@ 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(Suspense, { fallback: _jsx("div", { className: "px-3 py-2 text-xs text-text-faint", children: t('work.loadingDiff') }), children: _jsx(UnifiedDiff, { change: file }) }));
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
- function ChangedFilesCard({ count, truncated, added, deleted, revealNonce = 0, children }) {
94
+ function ChangedFilesCard({ count, truncated, added, deleted, revealNonce = 0, baseMoved, children }) {
87
95
  const { t } = useTranslation('agent-chat');
88
96
  const [expanded, setExpanded] = useState(false);
89
- // A jump from outside opens the card, also when the jump put the row on screen and so mounted it; the flash is gone by the time the row is scrolled to again.
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);
93
101
  setExpanded(true);
94
102
  }
95
- 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] }));
96
104
  }
97
105
  /*
98
106
  * The files a settled turn changed, as a card; each file opens its diff in place. The host's
@@ -100,7 +108,7 @@ function ChangedFilesCard({ count, truncated, added, deleted, revealNonce = 0, c
100
108
  * unified diffs the CLI reported, then the before and after of its own edits. A turn that has a
101
109
  * checkpoint but no stored diff asks the host for one, which is also how a running turn gets it.
102
110
  */
103
- export function ChangedFilesRow({ tools, diff, checkpoint, chatId, turnId }) {
111
+ export function ChangedFilesRow({ tools, diff, checkpoint, baseMoved, chatId, turnId }) {
104
112
  const [open, setOpen] = useState({});
105
113
  const [fetched, setFetched] = useState(null);
106
114
  const actions = useChatActions();
@@ -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) => (_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": file.path, onClick: () => setOpen((o) => ({ ...o, [file.path]: !o[file.path] })), children: [_jsx(Icon, { icon: ChevronRight, size: 12, className: clsx('shrink-0 text-text-faint transition-transform', open[file.path] && 'rotate-90') }), _jsx("span", { className: "min-w-0 truncate font-mono text-text", children: file.path }), _jsx("span", { className: "grow" }), _jsxs("span", { className: "text-chat-added tabular-nums", children: ["+", file.added] }), _jsxs("span", { className: "text-chat-removed tabular-nums", children: ["-", file.deleted] })] }), open[file.path] && (_jsx("div", { className: "border-t border-border", 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))) }));
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
- /** Keeps one small frame in a full-height timeline row, without rendering React on scroll. */
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 outer = this.scroller.getBoundingClientRect();
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 scale = outer.height / this.scroller.offsetHeight || 1;
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;
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",