@adecore/agents-react 0.22.0 → 0.23.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 (113) hide show
  1. package/dist/chat/ui/EditDiff.d.ts +3 -1
  2. package/dist/chat/ui/EditDiff.js +25 -18
  3. package/dist/chat/ui/UnifiedDiff.d.ts +2 -11
  4. package/dist/chat/ui/UnifiedDiff.js +31 -58
  5. package/dist/chat/ui/diff/ApprovalReview.d.ts +5 -0
  6. package/dist/chat/ui/diff/ApprovalReview.js +29 -0
  7. package/dist/chat/ui/diff/DiffFallbackBoundary.d.ts +17 -0
  8. package/dist/chat/ui/diff/DiffFallbackBoundary.js +17 -0
  9. package/dist/chat/ui/diff/DiffOmitted.d.ts +3 -0
  10. package/dist/chat/ui/diff/DiffOmitted.js +8 -0
  11. package/dist/chat/ui/diff/DiffStack.d.ts +9 -0
  12. package/dist/chat/ui/diff/DiffStack.js +15 -0
  13. package/dist/chat/ui/diff/DiffUnavailable.d.ts +3 -0
  14. package/dist/chat/ui/diff/DiffUnavailable.js +8 -0
  15. package/dist/chat/ui/diff/EditorEditDiff.d.ts +23 -0
  16. package/dist/chat/ui/diff/EditorEditDiff.js +67 -0
  17. package/dist/chat/ui/diff/EditorPatchDiff.d.ts +16 -0
  18. package/dist/chat/ui/diff/EditorPatchDiff.js +20 -0
  19. package/dist/chat/ui/diff/LegacyDiffPool.d.ts +4 -0
  20. package/dist/chat/ui/diff/LegacyDiffPool.js +12 -0
  21. package/dist/chat/ui/diff/LegacyEditDiff.d.ts +5 -0
  22. package/dist/chat/ui/diff/LegacyEditDiff.js +23 -0
  23. package/dist/chat/ui/diff/LegacyUnifiedDiff.d.ts +2 -0
  24. package/dist/chat/ui/diff/LegacyUnifiedDiff.js +67 -0
  25. package/dist/chat/ui/diff/RawDiff.d.ts +6 -0
  26. package/dist/chat/ui/diff/RawDiff.js +7 -0
  27. package/dist/chat/ui/diff/ReviewDiff.d.ts +5 -0
  28. package/dist/chat/ui/diff/ReviewDiff.js +10 -0
  29. package/dist/chat/ui/diff/diff-props.d.ts +12 -0
  30. package/dist/chat/ui/diff/diff-props.js +1 -0
  31. package/dist/chat/ui/diff/diff-source-budget.d.ts +10 -0
  32. package/dist/chat/ui/diff/diff-source-budget.js +47 -0
  33. package/dist/chat/ui/diff/editor-edit-model.d.ts +8 -0
  34. package/dist/chat/ui/diff/editor-edit-model.js +19 -0
  35. package/dist/chat/ui/diff/review-plan.d.ts +28 -0
  36. package/dist/chat/ui/diff/review-plan.js +48 -0
  37. package/dist/chat/ui/diff/review-settle.d.ts +5 -0
  38. package/dist/chat/ui/diff/review-settle.js +12 -0
  39. package/dist/chat/ui/diff/syntax-runtime.d.ts +18 -0
  40. package/dist/chat/ui/diff/syntax-runtime.js +184 -0
  41. package/dist/chat/ui/diff/use-review-stack.d.ts +8 -0
  42. package/dist/chat/ui/diff/use-review-stack.js +30 -0
  43. package/dist/chat/ui/diff-syntax.d.ts +19 -0
  44. package/dist/chat/ui/diff-syntax.js +63 -0
  45. package/dist/chat/ui/intelligent-ui/UiBlockFrame.js +3 -3
  46. package/dist/chat/ui/intelligent-ui/UiFallbackPart.js +1 -1
  47. package/dist/chat/ui/intelligent-ui/chart-data.d.ts +22 -2
  48. package/dist/chat/ui/intelligent-ui/chart-data.js +83 -19
  49. package/dist/chat/ui/intelligent-ui/renderers/chart.d.ts +1 -1
  50. package/dist/chat/ui/intelligent-ui/renderers/chart.js +99 -35
  51. package/dist/chat/ui/intelligent-ui/renderers/choices.d.ts +1 -1
  52. package/dist/chat/ui/intelligent-ui/renderers/choices.js +8 -2
  53. package/dist/chat/ui/intelligent-ui/renderers/content.js +5 -3
  54. package/dist/chat/ui/intelligent-ui/renderers/inputs.js +9 -4
  55. package/dist/chat/ui/intelligent-ui/renderers/links.js +1 -1
  56. package/dist/chat/ui/intelligent-ui/renderers/text.js +5 -2
  57. package/dist/chat/ui/intelligent-ui/table-data.js +4 -4
  58. package/dist/chat/ui/intelligent-ui/use-truncated.d.ts +1 -0
  59. package/dist/chat/ui/intelligent-ui/use-truncated.js +28 -0
  60. package/dist/chat/ui/rows/WorkRows.js +16 -5
  61. package/dist/chat/ui/use-diff-source-tokens.d.ts +4 -0
  62. package/dist/chat/ui/use-diff-source-tokens.js +40 -0
  63. package/dist/host.d.ts +6 -1
  64. package/dist/lazy.d.ts +3 -1
  65. package/dist/lazy.js +5 -3
  66. package/dist/locales/en/agent-chat.json +12 -3
  67. package/dist/locales/en/agent-prompts.json +1 -0
  68. package/dist/locales/nl/agent-chat.json +12 -3
  69. package/dist/locales/nl/agent-prompts.json +1 -0
  70. package/dist/prompts/ui/ApprovalBody.js +7 -5
  71. package/package.json +23 -5
  72. package/src/chat/ui/EditDiff.tsx +37 -22
  73. package/src/chat/ui/UnifiedDiff.tsx +55 -88
  74. package/src/chat/ui/diff/ApprovalReview.tsx +53 -0
  75. package/src/chat/ui/diff/DiffFallbackBoundary.tsx +32 -0
  76. package/src/chat/ui/diff/DiffOmitted.tsx +13 -0
  77. package/src/chat/ui/diff/DiffStack.tsx +45 -0
  78. package/src/chat/ui/diff/DiffUnavailable.tsx +13 -0
  79. package/src/chat/ui/diff/EditorEditDiff.tsx +145 -0
  80. package/src/chat/ui/diff/EditorPatchDiff.tsx +60 -0
  81. package/src/chat/ui/diff/LegacyDiffPool.tsx +13 -0
  82. package/src/chat/ui/diff/LegacyEditDiff.tsx +33 -0
  83. package/src/chat/ui/diff/LegacyUnifiedDiff.tsx +93 -0
  84. package/src/chat/ui/diff/RawDiff.tsx +19 -0
  85. package/src/chat/ui/diff/ReviewDiff.tsx +22 -0
  86. package/src/chat/ui/diff/diff-props.ts +16 -0
  87. package/src/chat/ui/diff/diff-source-budget.ts +56 -0
  88. package/src/chat/ui/diff/editor-edit-model.ts +21 -0
  89. package/src/chat/ui/diff/review-plan.ts +69 -0
  90. package/src/chat/ui/diff/review-settle.ts +14 -0
  91. package/src/chat/ui/diff/syntax-runtime.ts +222 -0
  92. package/src/chat/ui/diff/use-review-stack.ts +38 -0
  93. package/src/chat/ui/diff-syntax.ts +82 -0
  94. package/src/chat/ui/intelligent-ui/UiBlockFrame.tsx +5 -5
  95. package/src/chat/ui/intelligent-ui/UiFallbackPart.tsx +6 -1
  96. package/src/chat/ui/intelligent-ui/chart-data.ts +106 -20
  97. package/src/chat/ui/intelligent-ui/renderers/chart.tsx +237 -72
  98. package/src/chat/ui/intelligent-ui/renderers/choices.tsx +9 -4
  99. package/src/chat/ui/intelligent-ui/renderers/content.tsx +31 -15
  100. package/src/chat/ui/intelligent-ui/renderers/inputs.tsx +16 -5
  101. package/src/chat/ui/intelligent-ui/renderers/links.tsx +3 -3
  102. package/src/chat/ui/intelligent-ui/renderers/text.tsx +18 -8
  103. package/src/chat/ui/intelligent-ui/table-data.ts +4 -4
  104. package/src/chat/ui/intelligent-ui/use-truncated.ts +33 -0
  105. package/src/chat/ui/rows/WorkRows.tsx +42 -33
  106. package/src/chat/ui/use-diff-source-tokens.ts +43 -0
  107. package/src/host.ts +10 -1
  108. package/src/lazy.tsx +6 -3
  109. package/src/locales/en/agent-chat.json +12 -3
  110. package/src/locales/en/agent-prompts.json +1 -0
  111. package/src/locales/nl/agent-chat.json +12 -3
  112. package/src/locales/nl/agent-prompts.json +1 -0
  113. package/src/prompts/ui/ApprovalBody.tsx +8 -14
@@ -115,11 +115,11 @@ function LinkRow({ target, context, described, children }: { target: UiLinkTarge
115
115
  return <UiLinkChip target={target} context={context} />;
116
116
  }
117
117
  return (
118
- <div className="flex min-w-0 items-baseline gap-2 px-2 text-sm">
119
- <span className="shrink-0">
118
+ <div className="flex min-w-0 flex-wrap items-baseline gap-x-2 px-2 text-sm">
119
+ <span className="max-w-full min-w-0">
120
120
  <UiLinkChip target={target} context={context} />
121
121
  </span>
122
- <span className="min-w-0 text-text-muted">{children}</span>
122
+ <span className="min-w-0 wrap-break-word text-text-muted">{children}</span>
123
123
  </div>
124
124
  );
125
125
  }
@@ -31,20 +31,27 @@ function isLive(context: UiRenderContext): boolean {
31
31
  return context.phase === 'streaming' || (context.live !== undefined && context.live !== null && context.live.state !== 'refused');
32
32
  }
33
33
 
34
+ /* A badge longer than this is a sentence, which a tag cannot wrap; it stands under the title as text instead. */
35
+ const BADGE_AS_TAG = 32;
36
+
34
37
  /* The head of the block when it is the first node, a subheading without an icon further down. */
35
38
  export function SummaryRenderer({ node, children }: UiRendererProps<UiProps<'Summary'>>) {
36
39
  const headId = useContext(UiHeadContext);
37
40
  const head = headId === undefined || headId === node.id;
38
41
  const { tone, badge } = node.props;
42
+ const tag = badge !== undefined && badge !== '' && badge.length <= BADGE_AS_TAG;
39
43
  return (
40
44
  <div className="flex items-start gap-2 px-2">
41
45
  {head && tone !== undefined && <Icon icon={UI_TONE_ICONS[tone]} size={16} className={clsx('mt-0.75 shrink-0', UI_TONE_TEXT[tone])} />}
42
- <div className="min-w-0 grow text-sm font-semibold text-balance text-text">{children}</div>
43
- {badge !== undefined && badge !== '' && (
44
- <Pill shape="tag" tone={tone === undefined ? 'muted' : UI_TONE_PILL[tone]} className="shrink-0">
45
- {badge}
46
- </Pill>
47
- )}
46
+ <div className="flex min-w-0 grow flex-wrap items-start gap-x-2 gap-y-1">
47
+ <div className="max-w-full min-w-0 grow basis-48 text-sm font-semibold text-balance wrap-break-word text-text">{children}</div>
48
+ {tag && (
49
+ <Pill shape="tag" tone={tone === undefined ? 'muted' : UI_TONE_PILL[tone]}>
50
+ {badge}
51
+ </Pill>
52
+ )}
53
+ {!tag && badge !== undefined && badge !== '' && <p className="w-full text-xs wrap-anywhere text-text-muted">{badge}</p>}
54
+ </div>
48
55
  </div>
49
56
  );
50
57
  }
@@ -117,8 +124,11 @@ export function StepRenderer({ node, children, context }: UiRendererProps<UiProp
117
124
  )}
118
125
  </span>
119
126
  <span className="sr-only">{t(`blocks.step.${state}`)}</span>
120
- <span className={clsx('min-w-0 grow', quiet ? 'text-text-faint' : 'text-text')}>{children}</span>
121
- {detail !== undefined && detail !== '' && <span className="shrink-0 text-xs text-text-faint tabular-nums">{detail}</span>}
127
+ {/* A short detail stays beside the title; one that does not fit beside it takes the line under it. */}
128
+ <span className="flex min-w-0 grow flex-wrap items-baseline justify-between gap-x-2">
129
+ <span className={clsx('max-w-full min-w-0 grow basis-48 wrap-break-word', quiet ? 'text-text-faint' : 'text-text')}>{children}</span>
130
+ {detail !== undefined && detail !== '' && <span className="max-w-full text-xs wrap-anywhere text-text-faint tabular-nums">{detail}</span>}
131
+ </span>
122
132
  </li>
123
133
  );
124
134
  }
@@ -21,8 +21,8 @@ export type UiTableCell =
21
21
 
22
22
  /* Rows shown before a person asks for all of them. */
23
23
  export const UI_TABLE_ROWS = 50;
24
- /* Columns a table without Column children takes from its rows. */
25
- const DERIVED_COLUMNS = 8;
24
+ /* The furthest a JavaScript Date reaches from the epoch either way, in milliseconds. */
25
+ const DATE_RANGE = 8.64e15;
26
26
  const NUMERIC_KINDS: ReadonlySet<UiColumnKind> = new Set(['number', 'bytes', 'duration']);
27
27
 
28
28
  export function isNumericColumn(column: UiTableColumn): boolean {
@@ -51,7 +51,7 @@ export function uiTableColumns(node: UiViewNode<UiProps<'Table'>>): UiTableColum
51
51
  const keys: string[] = [];
52
52
  for (const row of rows) {
53
53
  for (const key of Object.keys(row)) {
54
- if (!keys.includes(key) && keys.length < DERIVED_COLUMNS) {
54
+ if (!keys.includes(key)) {
55
55
  keys.push(key);
56
56
  }
57
57
  }
@@ -92,7 +92,7 @@ export function uiTableCell(value: unknown, column: UiTableColumn): UiTableCell
92
92
  }
93
93
  case 'date': {
94
94
  const at = typeof value === 'number' ? value : typeof value === 'string' ? Date.parse(value) : Number.NaN;
95
- return Number.isFinite(at) ? { kind: 'date', at } : { kind: 'text', text: textOf(value) };
95
+ return Number.isFinite(at) && Math.abs(at) <= DATE_RANGE ? { kind: 'date', at } : { kind: 'text', text: textOf(value) };
96
96
  }
97
97
  case 'file':
98
98
  return typeof value === 'string' ? { kind: 'file', path: value } : { kind: 'text', text: textOf(value) };
@@ -0,0 +1,33 @@
1
+ import { useCallback, useState } from 'react';
2
+
3
+ function cut(element: HTMLElement): boolean {
4
+ return element.scrollWidth > element.clientWidth + 1;
5
+ }
6
+
7
+ /*
8
+ * Whether an element's text is cut short where it stands, for a tooltip or a list of values that shows
9
+ * it whole exactly then. `within` asks the same of every `.truncate` element inside it instead. Measured
10
+ * again when the box resizes or the text changes, since neither moves the other.
11
+ */
12
+ export function useTruncated<T extends HTMLElement>(within = false): [(element: T | null) => (() => void) | undefined, boolean] {
13
+ const [truncated, setTruncated] = useState(false);
14
+ const measure = useCallback(
15
+ (element: T | null) => {
16
+ if (element === null || typeof ResizeObserver === 'undefined') {
17
+ return undefined;
18
+ }
19
+ const check = () => setTruncated(within ? [...element.querySelectorAll<HTMLElement>('.truncate')].some(cut) : cut(element));
20
+ check();
21
+ const resize = new ResizeObserver(check);
22
+ resize.observe(element);
23
+ const text = new MutationObserver(check);
24
+ text.observe(element, { characterData: true, childList: true, subtree: true });
25
+ return () => {
26
+ resize.disconnect();
27
+ text.disconnect();
28
+ };
29
+ },
30
+ [within]
31
+ );
32
+ return [measure, truncated];
33
+ }
@@ -17,9 +17,11 @@ import { formatClockDuration, formatElapsedShort } from '@adecore/ui/format';
17
17
  import { Icon, Spinner, useTickingText } from '@adecore/ui';
18
18
  import { ROW_GUTTER, toolIcon } from '../icons';
19
19
  import { AnsiOutput } from '../AnsiOutput';
20
+ import { DiffStack, ReviewStackView } from '../diff/DiffStack';
21
+ import { reviewEntries, type ReviewEntry } from '../diff/review-plan';
22
+ import { useReviewStack } from '../diff/use-review-stack';
20
23
 
21
- // The diff renderers carry shiki; they only load once a thread shows a file change.
22
- const EditDiff = lazyNamed(() => import('../EditDiff'), 'default');
24
+ // A checkpoint opens its renderer only when a file is expanded.
23
25
  const UnifiedDiff = lazyNamed(() => import('../UnifiedDiff'), 'default');
24
26
 
25
27
  const OUTPUT_LIMIT = 4000;
@@ -118,18 +120,8 @@ function ToolBody({ tool }: { tool: ChatToolItem }) {
118
120
  const changes = patches.length > 0 ? [] : fileChanges(tool.name, tool.input);
119
121
  return (
120
122
  <div className="mt-1.5 mb-2 ml-6 overflow-hidden rounded-md border border-border bg-surface-raised">
121
- {patches.length > 0 ? (
122
- <DiffSuspense>
123
- {patches.map((change, index) => (
124
- <UnifiedDiff key={index} change={change} />
125
- ))}
126
- </DiffSuspense>
127
- ) : changes.length > 0 ? (
128
- <DiffSuspense>
129
- {changes.map((change, index) => (
130
- <EditDiff key={index} change={change} />
131
- ))}
132
- </DiffSuspense>
123
+ {patches.length + changes.length > 0 ? (
124
+ <DiffStack entries={reviewEntries(patches, changes)} />
133
125
  ) : (
134
126
  <pre className="max-h-64 overflow-auto px-3 py-2 font-mono text-code text-text-muted select-text">
135
127
  {clip(JSON.stringify(tool.input, null, 2) ?? '')}
@@ -416,6 +408,36 @@ export function ChangedFilesRow({
416
408
  return <ProviderChangedFiles tools={tools} open={open} setOpen={setOpen} revealNonce={revealNonce} />;
417
409
  }
418
410
 
411
+ interface FileReviews {
412
+ readonly path: string;
413
+ readonly count: number;
414
+ readonly reviews: readonly ReviewEntry[];
415
+ }
416
+
417
+ function ProviderReviewFiles({
418
+ reviews,
419
+ open,
420
+ setOpen
421
+ }: {
422
+ reviews: readonly FileReviews[];
423
+ open: Record<string, boolean>;
424
+ setOpen: Dispatch<SetStateAction<Record<string, boolean>>>;
425
+ }) {
426
+ const { t } = useTranslation('agent-chat');
427
+ const stack = useReviewStack(reviews.filter(({ path }) => open[path] === true).flatMap(({ reviews }) => reviews));
428
+ return reviews.map(({ path, count, reviews }) => (
429
+ <ChangedFileLine
430
+ key={path}
431
+ path={path}
432
+ open={open[path] === true}
433
+ onToggle={() => setOpen((previous) => ({ ...previous, [path]: !previous[path] }))}
434
+ trailing={<span className="text-text-faint">{t('work.edits', { count })}</span>}
435
+ >
436
+ <ReviewStackView entries={reviews} stack={stack} />
437
+ </ChangedFileLine>
438
+ ));
439
+ }
440
+
419
441
  /* The fallback card, built from what the CLI itself reported about its edits. */
420
442
  function ProviderChangedFiles({
421
443
  tools,
@@ -428,7 +450,6 @@ function ProviderChangedFiles({
428
450
  revealNonce: number;
429
451
  setOpen: Dispatch<SetStateAction<Record<string, boolean>>>;
430
452
  }) {
431
- const { t } = useTranslation('agent-chat');
432
453
  const byPath = new Map<string, { edits: FileChange[]; patches: ChatFileChange[] }>();
433
454
  const entryFor = (path: string) => {
434
455
  const existing = byPath.get(path);
@@ -451,26 +472,14 @@ function ProviderChangedFiles({
451
472
  entryFor(change.path || tool.id).edits.push(change);
452
473
  }
453
474
  }
475
+ const reviews = [...byPath].map(([path, entry]) => ({
476
+ path,
477
+ count: entry.patches.length + entry.edits.length,
478
+ reviews: reviewEntries(entry.patches, entry.edits)
479
+ }));
454
480
  return (
455
481
  <ChangedFilesCard count={byPath.size} revealNonce={revealNonce}>
456
- {[...byPath].map(([path, entry]) => (
457
- <ChangedFileLine
458
- key={path}
459
- path={path}
460
- open={open[path] === true}
461
- onToggle={() => setOpen((o) => ({ ...o, [path]: !o[path] }))}
462
- trailing={<span className="text-text-faint">{t('work.edits', { count: entry.edits.length + entry.patches.length })}</span>}
463
- >
464
- <DiffSuspense>
465
- {entry.patches.map((change, index) => (
466
- <UnifiedDiff key={`patch-${index}`} change={change} />
467
- ))}
468
- {entry.edits.map((change, index) => (
469
- <EditDiff key={`edit-${index}`} change={change} />
470
- ))}
471
- </DiffSuspense>
472
- </ChangedFileLine>
473
- ))}
482
+ <ProviderReviewFiles reviews={reviews} open={open} setOpen={setOpen} />
474
483
  </ChangedFilesCard>
475
484
  );
476
485
  }
@@ -0,0 +1,43 @@
1
+ import { useLayoutEffect, useMemo } from 'react';
2
+ import { diffSyntaxDocument, type DiffSourceTokens } from '@adecore/editor-react';
3
+ import type { DiffDocument } from '@adecore/merge';
4
+ import { diffSourceTokens, type DiffSourceTokenSession, type DiffSyntaxRuntime } from './diff-syntax';
5
+
6
+ function createSourceBinding(document: DiffDocument, language: string, theme: string, runtime?: DiffSyntaxRuntime) {
7
+ const mapping = diffSyntaxDocument(document, language, theme);
8
+ const listeners = new Set<() => void>();
9
+ let source: DiffSourceTokenSession | undefined;
10
+ const current = (): DiffSourceTokenSession => {
11
+ if (source === undefined) {
12
+ source = diffSourceTokens(document, mapping, runtime);
13
+ source.subscribe(() => {
14
+ for (const listener of [...listeners]) {
15
+ listener();
16
+ }
17
+ });
18
+ }
19
+ return source;
20
+ };
21
+ return {
22
+ tokens: {
23
+ get: (side, index) => (mapping.locate(side, index) === undefined ? null : current().get(side, index)),
24
+ subscribe(listener) {
25
+ listeners.add(listener);
26
+ return () => {
27
+ listeners.delete(listener);
28
+ };
29
+ }
30
+ } satisfies DiffSourceTokens,
31
+ release() {
32
+ // StrictMode replays effects with the same memo; the next read opens fresh sessions.
33
+ source?.dispose();
34
+ source = undefined;
35
+ }
36
+ };
37
+ }
38
+
39
+ export function useDiffSourceTokens(document: DiffDocument, language: string, theme: string, runtime?: DiffSyntaxRuntime): DiffSourceTokens {
40
+ const binding = useMemo(() => createSourceBinding(document, language, theme, runtime), [document, language, theme, runtime]);
41
+ useLayoutEffect(() => () => binding.release(), [binding]);
42
+ return binding.tokens;
43
+ }
package/src/host.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import type { ComponentProps, ComponentType, ReactNode, RefObject } from 'react';
2
2
  import type { Extension } from '@codemirror/state';
3
3
  import type { EditorView } from '@codemirror/view';
4
- import type { ThemeRegistration } from 'shiki';
4
+ import type { Highlighter, ThemeRegistration } from 'shiki';
5
+ import type { EditorEngine } from '@adecore/editor';
5
6
  import type {
6
7
  AgentKind,
7
8
  ChatCheckpointDiff,
@@ -244,6 +245,14 @@ export interface ChatHost {
244
245
  useThemes(): { light: string; dark: string };
245
246
  /* Themes the app ships itself, found by name; the rest Shiki looks up by id. */
246
247
  custom: ReadonlyArray<ThemeRegistration & { name: string }>;
248
+ /* One syntax worker shared by chat diffs and standalone review panes; omitted, tokenize on the page. */
249
+ syntaxWorker?: () => Worker;
250
+ /* Starts legacy highlighting with its first diff; an enclosing DiffPool takes precedence. */
251
+ legacyDiffWorker?: () => Worker;
252
+ /* Borrow the page highlighter for fallback; the host retains ownership. */
253
+ highlighter?: () => Promise<Highlighter>;
254
+ /* Opt in to bounded editor reviews; failures show a status, omitted keeps the legacy renderer. */
255
+ editor?: () => Promise<EditorEngine>;
247
256
  };
248
257
  /* How a reply appears while it streams: a word, a block or the whole of it at a time. */
249
258
  useStreaming(): 'words' | 'blocks' | 'whole';
package/src/lazy.tsx CHANGED
@@ -35,7 +35,8 @@ export function onLazyOpenError(listener: (error: unknown) => void): () => void
35
35
  */
36
36
  export function lazyNamed<Module extends Record<Name, ComponentType<any>>, Name extends keyof Module>(
37
37
  load: () => Promise<Module>,
38
- name: Name
38
+ name: Name,
39
+ options?: { readonly prefetch?: boolean }
39
40
  ): ComponentType<ComponentProps<Module[Name]>> {
40
41
  let loaded: ComponentType<any> | null = null;
41
42
  const open = async (): Promise<ComponentType<any>> => {
@@ -43,8 +44,10 @@ export function lazyNamed<Module extends Record<Name, ComponentType<any>>, Name
43
44
  loaded ??= component;
44
45
  return component;
45
46
  };
46
- loaders.push(open);
47
- register?.(open);
47
+ if (options?.prefetch !== false) {
48
+ loaders.push(open);
49
+ register?.(open);
50
+ }
48
51
  // Only a render loads through here; a prefetch calls `open` itself, so its failures report nothing.
49
52
  const Lazy = lazy(async () => {
50
53
  try {
@@ -157,7 +157,10 @@
157
157
  "work": {
158
158
  "moreCharacters_one": "[{{count}} more character]",
159
159
  "moreCharacters_other": "[{{count}} more characters]",
160
+ "diffLabel": "Changes in {{path}}",
161
+ "showDiff": "Show diff",
160
162
  "loadingDiff": "Loading diff",
163
+ "diffUnavailable": "Could not load this diff",
161
164
  "runningFor": "running for {{elapsed}}",
162
165
  "forksAfter_one": "A fork goes on after this turn",
163
166
  "forksAfter_other": "{{count}} forks go on after this turn",
@@ -527,7 +530,13 @@
527
530
  "chart": {
528
531
  "values": "Values",
529
532
  "summary": "{{count}} values, highest {{label}} at {{value}}",
530
- "summarySeries": "{{count}} categories, highest {{series}} for {{label}} at {{value}}"
533
+ "summarySeries": "{{count}} categories, highest {{series}} for {{label}} at {{value}}",
534
+ "rowsLeftOut": "The first {{shown}} of {{total}} rows are drawn.",
535
+ "seriesLeftOut_one": "{{series}} is not drawn.",
536
+ "seriesLeftOut_other": "{{series}} are not drawn.",
537
+ "sideBySide": "Negative values cannot be stacked, so the series stand side by side.",
538
+ "showValues": "Show all values",
539
+ "hideValues": "Hide values"
531
540
  },
532
541
  "image": {
533
542
  "label": "Image",
@@ -551,8 +560,8 @@
551
560
  "answered": "Answered with “{{label}}”",
552
561
  "chose": "Chose “{{label}}”",
553
562
  "choseOlder": "Chose “{{label}}” on the reply of {{time}}",
554
- "fixes_one": "{{count}} fix",
555
- "fixes_other": "{{count}} fixes",
563
+ "notes_one": "{{count}} note",
564
+ "notes_other": "{{count}} notes",
556
565
  "shownAsText": "Shown as text",
557
566
  "unknownComponent": "This version cannot draw {{component}}",
558
567
  "renderFailed": "Could not draw this part",
@@ -18,6 +18,7 @@
18
18
  "allChanges_one": "Allow applies to the {{count}} change in this request.",
19
19
  "allChanges_other": "Allow applies to all {{count}} changes in this request.",
20
20
  "loadingDiff": "Loading diff…",
21
+ "reviewChanges": "Changes to review",
21
22
  "viewDiff": "View full diff",
22
23
  "collapseDiff": "Collapse diff",
23
24
  "details": "Details",
@@ -157,7 +157,10 @@
157
157
  "work": {
158
158
  "moreCharacters_one": "[nog {{count}} teken]",
159
159
  "moreCharacters_other": "[nog {{count}} tekens]",
160
+ "diffLabel": "Wijzigingen in {{path}}",
161
+ "showDiff": "Diff tonen",
160
162
  "loadingDiff": "Diff laden",
163
+ "diffUnavailable": "Deze diff kon niet worden geladen",
161
164
  "runningFor": "loopt al {{elapsed}}",
162
165
  "forksAfter_one": "Een fork gaat verder na deze turn",
163
166
  "forksAfter_other": "{{count}} forks gaan verder na deze turn",
@@ -527,7 +530,13 @@
527
530
  "chart": {
528
531
  "values": "Waarden",
529
532
  "summary": "{{count}} waarden, hoogste {{label}} met {{value}}",
530
- "summarySeries": "{{count}} categorieën, hoogste {{series}} bij {{label}} met {{value}}"
533
+ "summarySeries": "{{count}} categorieën, hoogste {{series}} bij {{label}} met {{value}}",
534
+ "rowsLeftOut": "De eerste {{shown}} van {{total}} rijen zijn getekend.",
535
+ "seriesLeftOut_one": "{{series}} is niet getekend.",
536
+ "seriesLeftOut_other": "{{series}} zijn niet getekend.",
537
+ "sideBySide": "Negatieve waarden kunnen niet gestapeld worden, dus de reeksen staan naast elkaar.",
538
+ "showValues": "Alle waarden tonen",
539
+ "hideValues": "Waarden verbergen"
531
540
  },
532
541
  "image": {
533
542
  "label": "Afbeelding",
@@ -551,8 +560,8 @@
551
560
  "answered": "Beantwoord met “{{label}}”",
552
561
  "chose": "Gekozen: “{{label}}”",
553
562
  "choseOlder": "Gekozen: “{{label}}”, bij het antwoord van {{time}}",
554
- "fixes_one": "{{count}} correctie",
555
- "fixes_other": "{{count}} correcties",
563
+ "notes_one": "{{count}} opmerking",
564
+ "notes_other": "{{count}} opmerkingen",
556
565
  "shownAsText": "Als tekst getoond",
557
566
  "unknownComponent": "Deze versie kan {{component}} niet tekenen",
558
567
  "renderFailed": "Dit deel kon niet getekend worden",
@@ -18,6 +18,7 @@
18
18
  "allChanges_one": "Toestaan geldt voor de {{count}} wijziging in dit verzoek.",
19
19
  "allChanges_other": "Toestaan geldt voor alle {{count}} wijzigingen in dit verzoek.",
20
20
  "loadingDiff": "Diff laden…",
21
+ "reviewChanges": "Wijzigingen beoordelen",
21
22
  "viewDiff": "Volledige diff tonen",
22
23
  "collapseDiff": "Diff inklappen",
23
24
  "details": "Details",
@@ -1,17 +1,15 @@
1
- import { Suspense, useState } from 'react';
1
+ import { useState } from 'react';
2
2
  import clsx from 'clsx';
3
3
  import { useTranslation } from 'react-i18next';
4
4
  import type { ChatApprovalItem } from '@adecore/agent-contracts';
5
5
  import { approvalChanges, fileChanges } from '../../chat/logic/tools';
6
6
  import { chatHost } from '../../host';
7
- import { lazyNamed } from '../../lazy';
7
+ import { ApprovalReview } from '../../chat/ui/diff/ApprovalReview';
8
+ import { reviewEntries } from '../../chat/ui/diff/review-plan';
8
9
  import { isPrimaryKey } from '../logic/keys';
9
10
  import type { PromptDraft } from '../logic/prompts';
10
11
  import { Button, TextMenu } from '@adecore/ui';
11
12
 
12
- const EditDiff = lazyNamed(() => import('../../chat/ui/EditDiff'), 'default');
13
- const UnifiedDiff = lazyNamed(() => import('../../chat/ui/UnifiedDiff'), 'default');
14
-
15
13
  /* A command in the sunken box a person reads before allowing it, with the folder it runs in when that is known. */
16
14
  export function CommandBox({ command, cwd }: { command: string; cwd?: string }) {
17
15
  return (
@@ -27,6 +25,9 @@ function ApprovalDetails({ item }: { item: ChatApprovalItem }) {
27
25
  const [expanded, setExpanded] = useState(false);
28
26
  const patches = approvalChanges(item.input);
29
27
  const edits = patches.length ? [] : fileChanges(item.toolName, item.input);
28
+ const entries = reviewEntries(patches, edits);
29
+ const editorPreview = chatHost().code.editor !== undefined;
30
+ const maxHeight = expanded ? 480 : 176;
30
31
  const input = typeof item.input === 'object' && item.input !== null ? (item.input as Record<string, unknown>) : {};
31
32
  return (
32
33
  <>
@@ -36,15 +37,8 @@ function ApprovalDetails({ item }: { item: ChatApprovalItem }) {
36
37
  )}
37
38
  {patches.length + edits.length > 0 ? (
38
39
  <div className="overflow-hidden rounded-xl bg-surface-sunken">
39
- <div className={clsx('overflow-auto', !expanded && 'max-h-44')}>
40
- <Suspense fallback={<p className="p-3 text-xs text-text-muted">{t('approval.loadingDiff')}</p>}>
41
- {patches.map((change, i) => (
42
- <UnifiedDiff key={i} change={change} />
43
- ))}
44
- {edits.map((change, i) => (
45
- <EditDiff key={i} change={change} />
46
- ))}
47
- </Suspense>
40
+ <div className={clsx(editorPreview ? 'overflow-hidden' : 'overflow-auto', !editorPreview && !expanded && 'max-h-44')}>
41
+ <ApprovalReview key={item.requestId} entries={entries} maxHeight={maxHeight} />
48
42
  </div>
49
43
  <div className="flex items-center border-t border-border p-1">
50
44
  <Button size="sm" aria-expanded={expanded} onClick={() => setExpanded(!expanded)}>