@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
@@ -16,8 +16,10 @@ import { formatClockDuration, formatElapsedShort } from '@adecore/ui/format';
16
16
  import { Icon, Spinner, useTickingText } from '@adecore/ui';
17
17
  import { ROW_GUTTER, toolIcon } from '../icons.js';
18
18
  import { AnsiOutput } from '../AnsiOutput.js';
19
- // The diff renderers carry shiki; they only load once a thread shows a file change.
20
- const EditDiff = lazyNamed(() => import('../EditDiff.js'), 'default');
19
+ import { DiffStack, ReviewStackView } from '../diff/DiffStack.js';
20
+ import { reviewEntries } from '../diff/review-plan.js';
21
+ import { useReviewStack } from '../diff/use-review-stack.js';
22
+ // A checkpoint opens its renderer only when a file is expanded.
21
23
  const UnifiedDiff = lazyNamed(() => import('../UnifiedDiff.js'), 'default');
22
24
  const OUTPUT_LIMIT = 4000;
23
25
  function clip(text) {
@@ -42,7 +44,7 @@ function ChangedFileLine({ path, open, onToggle, trailing, children }) {
42
44
  function ToolBody({ tool }) {
43
45
  const patches = unifiedChanges(tool);
44
46
  const changes = patches.length > 0 ? [] : fileChanges(tool.name, tool.input);
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 }) }))] }));
47
+ return (_jsxs("div", { className: "mt-1.5 mb-2 ml-6 overflow-hidden rounded-md border border-border bg-surface-raised", children: [patches.length + changes.length > 0 ? (_jsx(DiffStack, { entries: reviewEntries(patches, changes) })) : (_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 }) }))] }));
46
48
  }
47
49
  /*
48
50
  * One settled tool call: a line, and its input and output behind it. An image the call looked at is
@@ -139,9 +141,13 @@ export function ChangedFilesRow({ tools, diff, checkpoint, baseMoved, chatId, tu
139
141
  }
140
142
  return _jsx(ProviderChangedFiles, { tools: tools, open: open, setOpen: setOpen, revealNonce: revealNonce });
141
143
  }
144
+ function ProviderReviewFiles({ reviews, open, setOpen }) {
145
+ const { t } = useTranslation('agent-chat');
146
+ const stack = useReviewStack(reviews.filter(({ path }) => open[path] === true).flatMap(({ reviews }) => reviews));
147
+ return reviews.map(({ path, count, reviews }) => (_jsx(ChangedFileLine, { path: path, open: open[path] === true, onToggle: () => setOpen((previous) => ({ ...previous, [path]: !previous[path] })), trailing: _jsx("span", { className: "text-text-faint", children: t('work.edits', { count }) }), children: _jsx(ReviewStackView, { entries: reviews, stack: stack }) }, path)));
148
+ }
142
149
  /* The fallback card, built from what the CLI itself reported about its edits. */
143
150
  function ProviderChangedFiles({ tools, open, setOpen, revealNonce }) {
144
- const { t } = useTranslation('agent-chat');
145
151
  const byPath = new Map();
146
152
  const entryFor = (path) => {
147
153
  const existing = byPath.get(path);
@@ -164,7 +170,12 @@ function ProviderChangedFiles({ tools, open, setOpen, revealNonce }) {
164
170
  entryFor(change.path || tool.id).edits.push(change);
165
171
  }
166
172
  }
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))) }));
173
+ const reviews = [...byPath].map(([path, entry]) => ({
174
+ path,
175
+ count: entry.patches.length + entry.edits.length,
176
+ reviews: reviewEntries(entry.patches, entry.edits)
177
+ }));
178
+ return (_jsx(ChangedFilesCard, { count: byPath.size, revealNonce: revealNonce, children: _jsx(ProviderReviewFiles, { reviews: reviews, open: open, setOpen: setOpen }) }));
168
179
  }
169
180
  /* "Working for 00:14". */
170
181
  export function WorkingRow({ startedAt }) {
@@ -0,0 +1,4 @@
1
+ import { type DiffSourceTokens } from '@adecore/editor-react';
2
+ import type { DiffDocument } from '@adecore/merge';
3
+ import { type DiffSyntaxRuntime } from './diff-syntax.js';
4
+ export declare function useDiffSourceTokens(document: DiffDocument, language: string, theme: string, runtime?: DiffSyntaxRuntime): DiffSourceTokens;
@@ -0,0 +1,40 @@
1
+ import { useLayoutEffect, useMemo } from 'react';
2
+ import { diffSyntaxDocument } from '@adecore/editor-react';
3
+ import { diffSourceTokens } from './diff-syntax.js';
4
+ function createSourceBinding(document, language, theme, runtime) {
5
+ const mapping = diffSyntaxDocument(document, language, theme);
6
+ const listeners = new Set();
7
+ let source;
8
+ const current = () => {
9
+ if (source === undefined) {
10
+ source = diffSourceTokens(document, mapping, runtime);
11
+ source.subscribe(() => {
12
+ for (const listener of [...listeners]) {
13
+ listener();
14
+ }
15
+ });
16
+ }
17
+ return source;
18
+ };
19
+ return {
20
+ tokens: {
21
+ get: (side, index) => (mapping.locate(side, index) === undefined ? null : current().get(side, index)),
22
+ subscribe(listener) {
23
+ listeners.add(listener);
24
+ return () => {
25
+ listeners.delete(listener);
26
+ };
27
+ }
28
+ },
29
+ release() {
30
+ // StrictMode replays effects with the same memo; the next read opens fresh sessions.
31
+ source?.dispose();
32
+ source = undefined;
33
+ }
34
+ };
35
+ }
36
+ export function useDiffSourceTokens(document, language, theme, runtime) {
37
+ const binding = useMemo(() => createSourceBinding(document, language, theme, runtime), [document, language, theme, runtime]);
38
+ useLayoutEffect(() => () => binding.release(), [binding]);
39
+ return binding.tokens;
40
+ }
package/dist/host.d.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 { AgentKind, ChatCheckpointDiff, ChatConfigurePayload, ChatItem, ChatSubagentItem, ChatUiChoicePayload, ChatUiQueryPayload, ChatUiQueryReading, ChatUiLinkPayload, ChatUiLinkReading } from '@adecore/agent-contracts';
6
7
  import type { TimelineRow } from './chat/logic/timeline.js';
7
8
  import type { FindReveal } from './chat/ui/find-reveal.js';
@@ -163,6 +164,10 @@ export interface ChatHost {
163
164
  custom: ReadonlyArray<ThemeRegistration & {
164
165
  name: string;
165
166
  }>;
167
+ syntaxWorker?: () => Worker;
168
+ legacyDiffWorker?: () => Worker;
169
+ highlighter?: () => Promise<Highlighter>;
170
+ editor?: () => Promise<EditorEngine>;
166
171
  };
167
172
  useStreaming(): 'words' | 'blocks' | 'whole';
168
173
  useSendDelivery(): 'queue' | 'steer';
package/dist/lazy.d.ts CHANGED
@@ -2,5 +2,7 @@ import { type ComponentProps, type ComponentType } from 'react';
2
2
  type Loader = () => Promise<unknown>;
3
3
  export declare function setLazyPrefetch(next: (load: Loader) => void): void;
4
4
  export declare function onLazyOpenError(listener: (error: unknown) => void): () => void;
5
- export declare function lazyNamed<Module extends Record<Name, ComponentType<any>>, Name extends keyof Module>(load: () => Promise<Module>, name: Name): ComponentType<ComponentProps<Module[Name]>>;
5
+ export declare function lazyNamed<Module extends Record<Name, ComponentType<any>>, Name extends keyof Module>(load: () => Promise<Module>, name: Name, options?: {
6
+ readonly prefetch?: boolean;
7
+ }): ComponentType<ComponentProps<Module[Name]>>;
6
8
  export {};
package/dist/lazy.js CHANGED
@@ -28,15 +28,17 @@ export function onLazyOpenError(listener) {
28
28
  * is here the component is drawn straight away, since `React.lazy` suspends for a tick even on a
29
29
  * module the prefetcher already fetched.
30
30
  */
31
- export function lazyNamed(load, name) {
31
+ export function lazyNamed(load, name, options) {
32
32
  let loaded = null;
33
33
  const open = async () => {
34
34
  const component = (await load())[name];
35
35
  loaded ??= component;
36
36
  return component;
37
37
  };
38
- loaders.push(open);
39
- register?.(open);
38
+ if (options?.prefetch !== false) {
39
+ loaders.push(open);
40
+ register?.(open);
41
+ }
40
42
  // Only a render loads through here; a prefetch calls `open` itself, so its failures report nothing.
41
43
  const Lazy = lazy(async () => {
42
44
  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,14 +1,13 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Suspense, useState } from 'react';
2
+ import { useState } from 'react';
3
3
  import clsx from 'clsx';
4
4
  import { useTranslation } from 'react-i18next';
5
5
  import { approvalChanges, fileChanges } from '../../chat/logic/tools.js';
6
6
  import { chatHost } from '../../host.js';
7
- import { lazyNamed } from '../../lazy.js';
7
+ import { ApprovalReview } from '../../chat/ui/diff/ApprovalReview.js';
8
+ import { reviewEntries } from '../../chat/ui/diff/review-plan.js';
8
9
  import { isPrimaryKey } from '../logic/keys.js';
9
10
  import { Button, TextMenu } from '@adecore/ui';
10
- const EditDiff = lazyNamed(() => import('../../chat/ui/EditDiff.js'), 'default');
11
- const UnifiedDiff = lazyNamed(() => import('../../chat/ui/UnifiedDiff.js'), 'default');
12
11
  /* A command in the sunken box a person reads before allowing it, with the folder it runs in when that is known. */
13
12
  export function CommandBox({ command, cwd }) {
14
13
  return (_jsxs(TextMenu, { className: "rounded-xl bg-surface-sunken p-3", children: [cwd !== undefined && _jsx("p", { className: "mb-2 break-all font-mono text-xs text-text-muted select-text", children: cwd }), _jsx("pre", { className: "whitespace-pre-wrap break-words font-mono text-code text-text select-text", children: command })] }));
@@ -18,8 +17,11 @@ function ApprovalDetails({ item }) {
18
17
  const [expanded, setExpanded] = useState(false);
19
18
  const patches = approvalChanges(item.input);
20
19
  const edits = patches.length ? [] : fileChanges(item.toolName, item.input);
20
+ const entries = reviewEntries(patches, edits);
21
+ const editorPreview = chatHost().code.editor !== undefined;
22
+ const maxHeight = expanded ? 480 : 176;
21
23
  const input = typeof item.input === 'object' && item.input !== null ? item.input : {};
22
- return (_jsxs(_Fragment, { children: [item.description && _jsx("p", { className: "text-sm text-text-muted", children: item.description }), patches.length + edits.length > 1 && (_jsx("p", { className: "text-xs text-text-muted", children: t('approval.allChanges', { count: patches.length + edits.length }) })), patches.length + edits.length > 0 ? (_jsxs("div", { className: "overflow-hidden rounded-xl bg-surface-sunken", children: [_jsx("div", { className: clsx('overflow-auto', !expanded && 'max-h-44'), children: _jsxs(Suspense, { fallback: _jsx("p", { className: "p-3 text-xs text-text-muted", children: t('approval.loadingDiff') }), children: [patches.map((change, i) => (_jsx(UnifiedDiff, { change: change }, i))), edits.map((change, i) => (_jsx(EditDiff, { change: change }, i)))] }) }), _jsx("div", { className: "flex items-center border-t border-border p-1", children: _jsx(Button, { size: "sm", "aria-expanded": expanded, onClick: () => setExpanded(!expanded), children: expanded ? t('approval.collapseDiff') : t('approval.viewDiff') }) })] })) : typeof input.command === 'string' ? (_jsx(CommandBox, { command: input.command, cwd: typeof input.cwd === 'string' ? input.cwd : undefined })) : (_jsxs("details", { children: [_jsx("summary", { className: "flex h-7 cursor-pointer items-center text-xs text-text-muted", children: t('approval.details') }), _jsx(TextMenu, { children: _jsx("pre", { className: "max-h-64 overflow-auto whitespace-pre-wrap break-words rounded-xl bg-surface-sunken p-3 font-mono text-code text-text select-text", children: JSON.stringify(item.input, null, 2) }) })] }))] }));
24
+ return (_jsxs(_Fragment, { children: [item.description && _jsx("p", { className: "text-sm text-text-muted", children: item.description }), patches.length + edits.length > 1 && (_jsx("p", { className: "text-xs text-text-muted", children: t('approval.allChanges', { count: patches.length + edits.length }) })), patches.length + edits.length > 0 ? (_jsxs("div", { className: "overflow-hidden rounded-xl bg-surface-sunken", children: [_jsx("div", { className: clsx(editorPreview ? 'overflow-hidden' : 'overflow-auto', !editorPreview && !expanded && 'max-h-44'), children: _jsx(ApprovalReview, { entries: entries, maxHeight: maxHeight }, item.requestId) }), _jsx("div", { className: "flex items-center border-t border-border p-1", children: _jsx(Button, { size: "sm", "aria-expanded": expanded, onClick: () => setExpanded(!expanded), children: expanded ? t('approval.collapseDiff') : t('approval.viewDiff') }) })] })) : typeof input.command === 'string' ? (_jsx(CommandBox, { command: input.command, cwd: typeof input.cwd === 'string' ? input.cwd : undefined })) : (_jsxs("details", { children: [_jsx("summary", { className: "flex h-7 cursor-pointer items-center text-xs text-text-muted", children: t('approval.details') }), _jsx(TextMenu, { children: _jsx("pre", { className: "max-h-64 overflow-auto whitespace-pre-wrap break-words rounded-xl bg-surface-sunken p-3 font-mono text-code text-text select-text", children: JSON.stringify(item.input, null, 2) }) })] }))] }));
23
25
  }
24
26
  /* A chat's permission request: the diff, the command or the raw input, and the reason a Deny may carry. */
25
27
  export function ApprovalBody({ item, draft, onDraft, denyReason, locked }) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adecore/agents-react",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "description": "React agent chat threads, composers, approvals, provider accounts and usage views.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -514,6 +514,11 @@
514
514
  "types": "./dist/chat/ui/composer/menu-aria.d.ts",
515
515
  "default": "./dist/chat/ui/composer/menu-aria.js"
516
516
  },
517
+ "./chat/ui/diff-syntax": {
518
+ "source": "./src/chat/ui/diff-syntax.ts",
519
+ "types": "./dist/chat/ui/diff-syntax.d.ts",
520
+ "default": "./dist/chat/ui/diff-syntax.js"
521
+ },
517
522
  "./chat/ui/diff-theme": {
518
523
  "source": "./src/chat/ui/diff-theme.ts",
519
524
  "types": "./dist/chat/ui/diff-theme.d.ts",
@@ -639,6 +644,11 @@
639
644
  "types": "./dist/chat/ui/intelligent-ui/use-later.d.ts",
640
645
  "default": "./dist/chat/ui/intelligent-ui/use-later.js"
641
646
  },
647
+ "./chat/ui/intelligent-ui/use-truncated": {
648
+ "source": "./src/chat/ui/intelligent-ui/use-truncated.ts",
649
+ "types": "./dist/chat/ui/intelligent-ui/use-truncated.d.ts",
650
+ "default": "./dist/chat/ui/intelligent-ui/use-truncated.js"
651
+ },
642
652
  "./chat/ui/intelligent-ui/use-ui-links": {
643
653
  "source": "./src/chat/ui/intelligent-ui/use-ui-links.ts",
644
654
  "types": "./dist/chat/ui/intelligent-ui/use-ui-links.d.ts",
@@ -1022,6 +1032,11 @@
1022
1032
  "source": "./src/chat/visual-viewport.ts",
1023
1033
  "types": "./dist/chat/visual-viewport.d.ts",
1024
1034
  "default": "./dist/chat/visual-viewport.js"
1035
+ },
1036
+ "./chat/ui/use-diff-source-tokens": {
1037
+ "source": "./src/chat/ui/use-diff-source-tokens.ts",
1038
+ "types": "./dist/chat/ui/use-diff-source-tokens.d.ts",
1039
+ "default": "./dist/chat/ui/use-diff-source-tokens.js"
1025
1040
  }
1026
1041
  },
1027
1042
  "scripts": {
@@ -1031,8 +1046,10 @@
1031
1046
  "test:visuals": "bun --config=integration.bunfig.toml --conditions=source test ./src/chat/visual-viewport.integration.test.ts"
1032
1047
  },
1033
1048
  "dependencies": {
1034
- "@adecore/agent-contracts": "0.22.0",
1035
- "@adecore/intelligent-ui": "0.22.0",
1049
+ "@adecore/agent-contracts": "0.23.0",
1050
+ "@adecore/intelligent-ui": "0.23.0",
1051
+ "@adecore/editor": "0.23.0",
1052
+ "@adecore/merge": "0.23.0",
1036
1053
  "@codemirror/commands": "^6.11.1",
1037
1054
  "@codemirror/language": "^6.12.4",
1038
1055
  "@codemirror/state": "^6.7.6",
@@ -1050,10 +1067,11 @@
1050
1067
  "shiki": "^4.4.3",
1051
1068
  "simple-icons": "^16.32.0",
1052
1069
  "zod": "^4.6.5",
1053
- "zustand": "^5.0.15"
1070
+ "zustand": "^5.0.15",
1071
+ "@adecore/editor-react": "0.23.0"
1054
1072
  },
1055
1073
  "peerDependencies": {
1056
- "@adecore/ui": "^0.22.0",
1074
+ "@adecore/ui": "^0.23.0",
1057
1075
  "i18next": "^26.4.2",
1058
1076
  "react": "^19.3.0",
1059
1077
  "react-dom": "^19.3.0",
@@ -1,30 +1,45 @@
1
- import { useMemo } from 'react';
2
- import { MultiFileDiff } from '@pierre/diffs/react';
1
+ import { Suspense, useMemo } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { lazyNamed } from '../../lazy';
4
+ import { diffSourceBudget, legacyDiffBudget } from './diff/diff-source-budget';
5
+ import { DiffOmitted } from './diff/DiffOmitted';
6
+ import { DiffUnavailable } from './diff/DiffUnavailable';
7
+ import { DiffFallbackBoundary } from './diff/DiffFallbackBoundary';
3
8
  import { chatHost } from '../../host';
4
- import { useDiffTheme } from './diff-theme';
5
9
 
6
10
  import type { FileChange } from '../logic/tools';
7
11
 
8
- /* One hunk of a file edit, highlighted in a worker. The header is ours, so the library's is off. */
9
- export default function EditDiff({ change }: { change: FileChange }) {
10
- const resolved = chatHost().code.useMode();
11
- const theme = useDiffTheme();
12
- const oldFile = useMemo(() => ({ name: change.path, contents: change.before }), [change.path, change.before]);
13
- const newFile = useMemo(() => ({ name: change.path, contents: change.after }), [change.path, change.after]);
14
- const options = useMemo(
15
- () => ({
16
- diffStyle: 'unified' as const,
17
- theme,
18
- themeType: resolved,
19
- disableFileHeader: true,
20
- overflow: 'wrap' as const,
21
- hunkSeparators: 'simple' as const
22
- }),
23
- [theme, resolved]
12
+ const LegacyEditDiff = lazyNamed(() => import('./diff/LegacyEditDiff'), 'default', { prefetch: false });
13
+ const EditorEditDiff = lazyNamed(() => import('./diff/EditorEditDiff'), 'default');
14
+
15
+ export default function EditDiff({ change, allowEditor = true, maxHeight }: { change: FileChange; allowEditor?: boolean; maxHeight?: number }) {
16
+ const { t } = useTranslation('agent-chat');
17
+ const loadEngine = chatHost().code.editor;
18
+ const sourceAllowed = useMemo(() => diffSourceBudget(change.before, change.after), [change.before, change.after]);
19
+ const legacyMode = !allowEditor || loadEngine === undefined;
20
+ const legacyAllowed = useMemo(() => legacyMode && legacyDiffBudget(change.before, change.after), [legacyMode, change.before, change.after]);
21
+ const omitted = <DiffOmitted path={change.path} />;
22
+ const fallback = legacyAllowed ? (
23
+ <DiffFallbackBoundary fallback={omitted} resetKeys={[change.path, change.before, change.after]}>
24
+ <Suspense fallback={<div className="px-3 py-2 text-xs text-text-faint">{t('work.loadingDiff')}</div>}>
25
+ <LegacyEditDiff change={change} maxHeight={maxHeight} />
26
+ </Suspense>
27
+ </DiffFallbackBoundary>
28
+ ) : (
29
+ omitted
24
30
  );
31
+ if (!sourceAllowed) {
32
+ return omitted;
33
+ }
34
+ if (legacyMode) {
35
+ return fallback;
36
+ }
37
+ const unavailable = <DiffUnavailable path={change.path} />;
25
38
  return (
26
- <div className="chat-diff select-text">
27
- <MultiFileDiff oldFile={oldFile} newFile={newFile} options={options} />
28
- </div>
39
+ <DiffFallbackBoundary fallback={unavailable} resetKeys={[change.path, change.before, change.after, loadEngine]}>
40
+ <Suspense fallback={<div className="px-3 py-2 text-xs text-text-faint">{t('work.loadingDiff')}</div>}>
41
+ <EditorEditDiff change={change} loadEngine={loadEngine!} fallback={unavailable} maxHeight={maxHeight} sourceChecked />
42
+ </Suspense>
43
+ </DiffFallbackBoundary>
29
44
  );
30
45
  }
@@ -1,97 +1,64 @@
1
- import { useMemo } from 'react';
2
- import clsx from 'clsx';
3
- import { FileDiff, PatchDiff } from '@pierre/diffs/react';
4
- import type { ChatFileChange } from '@adecore/agent-contracts';
1
+ import { Suspense, useMemo } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { lazyNamed } from '../../lazy';
4
+ import { diffSourceBudget, legacyDiffBudget, patchSourceBudget } from './diff/diff-source-budget';
5
+ import { DiffOmitted } from './diff/DiffOmitted';
6
+ import { DiffUnavailable } from './diff/DiffUnavailable';
7
+ import { RawDiff } from './diff/RawDiff';
8
+ import { DiffFallbackBoundary } from './diff/DiffFallbackBoundary';
5
9
  import { chatHost } from '../../host';
6
- import { useDiffTheme } from './diff-theme';
7
- import { fullFileDiff, type DiffContents } from './full-diff';
10
+ import type { UnifiedDiffProps } from './diff/diff-props';
8
11
 
9
- /*
10
- * A patch the CLI reported itself. The renderer needs a file header, which a CLI that only sends hunks
11
- * gets from the path we know; a diff without hunks is shown as its own lines, so nothing is swallowed.
12
- */
13
- function asPatch(change: ChatFileChange): string | null {
14
- if (!/^@@/m.test(change.diff)) {
15
- return null;
16
- }
17
- if (/^\+\+\+ /m.test(change.diff)) {
18
- return change.diff;
19
- }
20
- const from = change.kind === 'add' ? '/dev/null' : `a/${change.path}`;
21
- const to = change.kind === 'delete' ? '/dev/null' : `b/${change.path}`;
22
- return `--- ${from}\n+++ ${to}\n${change.diff}`;
23
- }
12
+ const LegacyUnifiedDiff = lazyNamed(() => import('./diff/LegacyUnifiedDiff'), 'default', { prefetch: false });
13
+ const EditorPatchDiff = lazyNamed(() => import('./diff/EditorPatchDiff'), 'default');
24
14
 
25
- /* 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
- const FILL_CSS = `
27
- :host { display: flex; flex-direction: column; flex-grow: 1; }
28
- pre { flex-grow: 1; align-content: start; }
29
- pre[data-diff-type="single"] { display: flex; flex-direction: column; }
30
- pre[data-diff-type="split"][data-overflow="scroll"]:not([data-dehydrated]) { align-content: stretch; }
31
- pre > [data-code] { flex-grow: 1; align-self: stretch; align-content: start; }
32
- `;
33
-
34
- function lineClass(line: string): string {
35
- if (line.startsWith('+')) {
36
- return 'text-chat-added';
15
+ export default function UnifiedDiff(props: UnifiedDiffProps) {
16
+ const { t } = useTranslation('agent-chat');
17
+ const { change, contents, allowEditor = true, ignoreWhitespace = false, overflow = 'wrap', diffStyle = 'unified', fill = false, maxHeight } = props;
18
+ const loadEngine = chatHost().code.editor;
19
+ const oldText = contents?.old ?? '';
20
+ const newText = contents?.new ?? '';
21
+ const sourceAllowed = useMemo(() => diffSourceBudget(oldText, newText) && patchSourceBudget(change.diff), [oldText, newText, change.diff]);
22
+ const legacyMode = !allowEditor || loadEngine === undefined;
23
+ const hasHunks = /^@@/m.test(change.diff);
24
+ const legacyAllowed = useMemo(() => legacyMode && legacyDiffBudget(oldText, newText, change.diff), [legacyMode, oldText, newText, change.diff]);
25
+ const rawAllowed = useMemo(() => hasHunks || legacyDiffBudget('', '', change.diff), [hasHunks, change.diff]);
26
+ const omitted = <DiffOmitted path={change.path} />;
27
+ const fallback = legacyAllowed ? (
28
+ <DiffFallbackBoundary fallback={omitted} resetKeys={[change.path, change.diff, oldText, newText, diffStyle]}>
29
+ <Suspense fallback={<div className="px-3 py-2 text-xs text-text-faint">{t('work.loadingDiff')}</div>}>
30
+ <LegacyUnifiedDiff {...props} />
31
+ </Suspense>
32
+ </DiffFallbackBoundary>
33
+ ) : (
34
+ omitted
35
+ );
36
+ if (!sourceAllowed) {
37
+ return omitted;
37
38
  }
38
- if (line.startsWith('-')) {
39
- return 'text-chat-removed';
39
+ if (!hasHunks) {
40
+ return rawAllowed ? <RawDiff text={change.diff} overflow={overflow} fill={fill} maxHeight={maxHeight} /> : omitted;
40
41
  }
41
- return 'text-text-muted';
42
- }
43
-
44
- interface UnifiedDiffProps {
45
- change: ChatFileChange;
46
- overflow?: 'wrap' | 'scroll';
47
- /* `split` puts the old and the new side by side; the chat always stacks. */
48
- diffStyle?: 'unified' | 'split';
49
- /* Grow to the height of a flex column around it. */
50
- fill?: boolean;
51
- /* Both whole texts the patch was made between, with which a person can unfold the lines between hunks. */
52
- contents?: DiffContents;
53
- }
54
-
55
- export default function UnifiedDiff({ change, overflow = 'wrap', diffStyle = 'unified', fill = false, contents }: UnifiedDiffProps) {
56
- const resolved = chatHost().code.useMode();
57
- const theme = useDiffTheme();
58
- const patch = useMemo(() => asPatch(change), [change]);
59
- const oldText = contents?.old;
60
- const newText = contents?.new;
61
- const full = useMemo(
62
- () => (patch === null || oldText === undefined || newText === undefined ? null : fullFileDiff(patch, change.path, { old: oldText, new: newText })),
63
- [patch, change.path, oldText, newText]
64
- );
65
- const unfolds = full !== null;
66
- const options = useMemo(
67
- () => ({
68
- theme,
69
- themeType: resolved,
70
- disableFileHeader: true,
71
- diffStyle,
72
- overflow,
73
- hunkSeparators: unfolds ? ('line-info' as const) : ('simple' as const),
74
- unsafeCSS: fill ? FILL_CSS : undefined
75
- }),
76
- [diffStyle, overflow, theme, resolved, fill, unfolds]
77
- );
78
- if (patch === null) {
79
- return (
80
- <pre className="max-h-64 overflow-auto px-3 py-2 font-mono text-code select-text">
81
- {change.diff
82
- .replace(/\n$/, '')
83
- .split('\n')
84
- .map((line, index) => (
85
- <div key={index} className={clsx(lineClass(line), 'whitespace-pre-wrap')}>
86
- {line}
87
- </div>
88
- ))}
89
- </pre>
90
- );
42
+ if (legacyMode) {
43
+ return fallback;
91
44
  }
45
+ const unavailable = <DiffUnavailable path={change.path} />;
92
46
  return (
93
- <div className={clsx('chat-diff select-text', fill && 'flex grow flex-col')}>
94
- {full === null ? <PatchDiff patch={patch} options={options} /> : <FileDiff fileDiff={full} options={options} />}
95
- </div>
47
+ <DiffFallbackBoundary fallback={unavailable} resetKeys={[change.path, change.diff, oldText, newText, ignoreWhitespace, diffStyle, loadEngine]}>
48
+ <Suspense fallback={<div className="px-3 py-2 text-xs text-text-faint">{t('work.loadingDiff')}</div>}>
49
+ <EditorPatchDiff
50
+ change={change}
51
+ sourceChecked
52
+ contents={contents}
53
+ ignoreWhitespace={ignoreWhitespace}
54
+ wrap={overflow === 'wrap'}
55
+ fill={fill}
56
+ maxHeight={maxHeight}
57
+ layout={diffStyle}
58
+ loadEngine={loadEngine!}
59
+ fallback={unavailable}
60
+ />
61
+ </Suspense>
62
+ </DiffFallbackBoundary>
96
63
  );
97
64
  }