@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.
- package/dist/chat/ui/EditDiff.d.ts +3 -1
- package/dist/chat/ui/EditDiff.js +25 -18
- package/dist/chat/ui/UnifiedDiff.d.ts +2 -11
- package/dist/chat/ui/UnifiedDiff.js +31 -58
- package/dist/chat/ui/diff/ApprovalReview.d.ts +5 -0
- package/dist/chat/ui/diff/ApprovalReview.js +29 -0
- package/dist/chat/ui/diff/DiffFallbackBoundary.d.ts +17 -0
- package/dist/chat/ui/diff/DiffFallbackBoundary.js +17 -0
- package/dist/chat/ui/diff/DiffOmitted.d.ts +3 -0
- package/dist/chat/ui/diff/DiffOmitted.js +8 -0
- package/dist/chat/ui/diff/DiffStack.d.ts +9 -0
- package/dist/chat/ui/diff/DiffStack.js +15 -0
- package/dist/chat/ui/diff/DiffUnavailable.d.ts +3 -0
- package/dist/chat/ui/diff/DiffUnavailable.js +8 -0
- package/dist/chat/ui/diff/EditorEditDiff.d.ts +23 -0
- package/dist/chat/ui/diff/EditorEditDiff.js +67 -0
- package/dist/chat/ui/diff/EditorPatchDiff.d.ts +16 -0
- package/dist/chat/ui/diff/EditorPatchDiff.js +20 -0
- package/dist/chat/ui/diff/LegacyDiffPool.d.ts +4 -0
- package/dist/chat/ui/diff/LegacyDiffPool.js +12 -0
- package/dist/chat/ui/diff/LegacyEditDiff.d.ts +5 -0
- package/dist/chat/ui/diff/LegacyEditDiff.js +23 -0
- package/dist/chat/ui/diff/LegacyUnifiedDiff.d.ts +2 -0
- package/dist/chat/ui/diff/LegacyUnifiedDiff.js +67 -0
- package/dist/chat/ui/diff/RawDiff.d.ts +6 -0
- package/dist/chat/ui/diff/RawDiff.js +7 -0
- package/dist/chat/ui/diff/ReviewDiff.d.ts +5 -0
- package/dist/chat/ui/diff/ReviewDiff.js +10 -0
- package/dist/chat/ui/diff/diff-props.d.ts +12 -0
- package/dist/chat/ui/diff/diff-props.js +1 -0
- package/dist/chat/ui/diff/diff-source-budget.d.ts +10 -0
- package/dist/chat/ui/diff/diff-source-budget.js +47 -0
- package/dist/chat/ui/diff/editor-edit-model.d.ts +8 -0
- package/dist/chat/ui/diff/editor-edit-model.js +19 -0
- package/dist/chat/ui/diff/review-plan.d.ts +28 -0
- package/dist/chat/ui/diff/review-plan.js +48 -0
- package/dist/chat/ui/diff/review-settle.d.ts +5 -0
- package/dist/chat/ui/diff/review-settle.js +12 -0
- package/dist/chat/ui/diff/syntax-runtime.d.ts +18 -0
- package/dist/chat/ui/diff/syntax-runtime.js +184 -0
- package/dist/chat/ui/diff/use-review-stack.d.ts +8 -0
- package/dist/chat/ui/diff/use-review-stack.js +30 -0
- package/dist/chat/ui/diff-syntax.d.ts +19 -0
- package/dist/chat/ui/diff-syntax.js +63 -0
- package/dist/chat/ui/intelligent-ui/UiBlockFrame.js +3 -3
- package/dist/chat/ui/intelligent-ui/UiFallbackPart.js +1 -1
- package/dist/chat/ui/intelligent-ui/chart-data.d.ts +22 -2
- package/dist/chat/ui/intelligent-ui/chart-data.js +83 -19
- package/dist/chat/ui/intelligent-ui/renderers/chart.d.ts +1 -1
- package/dist/chat/ui/intelligent-ui/renderers/chart.js +99 -35
- package/dist/chat/ui/intelligent-ui/renderers/choices.d.ts +1 -1
- package/dist/chat/ui/intelligent-ui/renderers/choices.js +8 -2
- package/dist/chat/ui/intelligent-ui/renderers/content.js +5 -3
- package/dist/chat/ui/intelligent-ui/renderers/inputs.js +9 -4
- package/dist/chat/ui/intelligent-ui/renderers/links.js +1 -1
- package/dist/chat/ui/intelligent-ui/renderers/text.js +5 -2
- package/dist/chat/ui/intelligent-ui/table-data.js +4 -4
- package/dist/chat/ui/intelligent-ui/use-truncated.d.ts +1 -0
- package/dist/chat/ui/intelligent-ui/use-truncated.js +28 -0
- package/dist/chat/ui/rows/WorkRows.js +16 -5
- package/dist/chat/ui/use-diff-source-tokens.d.ts +4 -0
- package/dist/chat/ui/use-diff-source-tokens.js +40 -0
- package/dist/host.d.ts +6 -1
- package/dist/lazy.d.ts +3 -1
- package/dist/lazy.js +5 -3
- package/dist/locales/en/agent-chat.json +12 -3
- package/dist/locales/en/agent-prompts.json +1 -0
- package/dist/locales/nl/agent-chat.json +12 -3
- package/dist/locales/nl/agent-prompts.json +1 -0
- package/dist/prompts/ui/ApprovalBody.js +7 -5
- package/package.json +23 -5
- package/src/chat/ui/EditDiff.tsx +37 -22
- package/src/chat/ui/UnifiedDiff.tsx +55 -88
- package/src/chat/ui/diff/ApprovalReview.tsx +53 -0
- package/src/chat/ui/diff/DiffFallbackBoundary.tsx +32 -0
- package/src/chat/ui/diff/DiffOmitted.tsx +13 -0
- package/src/chat/ui/diff/DiffStack.tsx +45 -0
- package/src/chat/ui/diff/DiffUnavailable.tsx +13 -0
- package/src/chat/ui/diff/EditorEditDiff.tsx +145 -0
- package/src/chat/ui/diff/EditorPatchDiff.tsx +60 -0
- package/src/chat/ui/diff/LegacyDiffPool.tsx +13 -0
- package/src/chat/ui/diff/LegacyEditDiff.tsx +33 -0
- package/src/chat/ui/diff/LegacyUnifiedDiff.tsx +93 -0
- package/src/chat/ui/diff/RawDiff.tsx +19 -0
- package/src/chat/ui/diff/ReviewDiff.tsx +22 -0
- package/src/chat/ui/diff/diff-props.ts +16 -0
- package/src/chat/ui/diff/diff-source-budget.ts +56 -0
- package/src/chat/ui/diff/editor-edit-model.ts +21 -0
- package/src/chat/ui/diff/review-plan.ts +69 -0
- package/src/chat/ui/diff/review-settle.ts +14 -0
- package/src/chat/ui/diff/syntax-runtime.ts +222 -0
- package/src/chat/ui/diff/use-review-stack.ts +38 -0
- package/src/chat/ui/diff-syntax.ts +82 -0
- package/src/chat/ui/intelligent-ui/UiBlockFrame.tsx +5 -5
- package/src/chat/ui/intelligent-ui/UiFallbackPart.tsx +6 -1
- package/src/chat/ui/intelligent-ui/chart-data.ts +106 -20
- package/src/chat/ui/intelligent-ui/renderers/chart.tsx +237 -72
- package/src/chat/ui/intelligent-ui/renderers/choices.tsx +9 -4
- package/src/chat/ui/intelligent-ui/renderers/content.tsx +31 -15
- package/src/chat/ui/intelligent-ui/renderers/inputs.tsx +16 -5
- package/src/chat/ui/intelligent-ui/renderers/links.tsx +3 -3
- package/src/chat/ui/intelligent-ui/renderers/text.tsx +18 -8
- package/src/chat/ui/intelligent-ui/table-data.ts +4 -4
- package/src/chat/ui/intelligent-ui/use-truncated.ts +33 -0
- package/src/chat/ui/rows/WorkRows.tsx +42 -33
- package/src/chat/ui/use-diff-source-tokens.ts +43 -0
- package/src/host.ts +10 -1
- package/src/lazy.tsx +6 -3
- package/src/locales/en/agent-chat.json +12 -3
- package/src/locales/en/agent-prompts.json +1 -0
- package/src/locales/nl/agent-chat.json +12 -3
- package/src/locales/nl/agent-prompts.json +1 -0
- package/src/prompts/ui/ApprovalBody.tsx +8 -14
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { useMemo, useState } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { Button } from '@adecore/ui';
|
|
4
|
+
import { formatNumber } from '@adecore/ui/format';
|
|
5
|
+
import { ReviewDiff } from './ReviewDiff';
|
|
6
|
+
import type { ReviewEntry } from './review-plan';
|
|
7
|
+
|
|
8
|
+
export function ApprovalReview({ entries, maxHeight }: { entries: readonly ReviewEntry[]; maxHeight: number }) {
|
|
9
|
+
const { t } = useTranslation('agent-prompts');
|
|
10
|
+
const [selected, setSelected] = useState<string | null>(null);
|
|
11
|
+
const active = entries.find((entry) => entry.key === selected) ?? entries[0];
|
|
12
|
+
const labels = useMemo(() => {
|
|
13
|
+
const totals = new Map<string, number>();
|
|
14
|
+
const counts = new Map<string, number>();
|
|
15
|
+
for (const entry of entries) {
|
|
16
|
+
totals.set(entry.path, (totals.get(entry.path) ?? 0) + 1);
|
|
17
|
+
}
|
|
18
|
+
return entries.map((entry) => {
|
|
19
|
+
const count = (counts.get(entry.path) ?? 0) + 1;
|
|
20
|
+
counts.set(entry.path, count);
|
|
21
|
+
const total = totals.get(entry.path)!;
|
|
22
|
+
const suffix = total > 1 ? ` (${formatNumber(count)}/${formatNumber(total)})` : '';
|
|
23
|
+
return { short: `${entry.path.split(/[\\/]/).at(-1)}${suffix}`, full: `${entry.path}${suffix}` };
|
|
24
|
+
});
|
|
25
|
+
}, [entries]);
|
|
26
|
+
if (active === undefined) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
return (
|
|
30
|
+
<div>
|
|
31
|
+
{entries.length > 1 && (
|
|
32
|
+
<>
|
|
33
|
+
<div role="toolbar" aria-label={t('approval.reviewChanges')} className="flex flex-wrap gap-1 border-b border-border p-1">
|
|
34
|
+
{entries.map((entry, index) => (
|
|
35
|
+
<Button
|
|
36
|
+
key={entry.key}
|
|
37
|
+
size="sm"
|
|
38
|
+
variant={active.key === entry.key ? 'secondary' : 'ghost'}
|
|
39
|
+
aria-label={labels[index]!.full}
|
|
40
|
+
aria-pressed={active.key === entry.key}
|
|
41
|
+
onClick={() => setSelected(entry.key)}
|
|
42
|
+
>
|
|
43
|
+
{labels[index]!.short}
|
|
44
|
+
</Button>
|
|
45
|
+
))}
|
|
46
|
+
</div>
|
|
47
|
+
<p className="break-all px-3 py-2 font-mono text-xs text-text-muted">{active.path}</p>
|
|
48
|
+
</>
|
|
49
|
+
)}
|
|
50
|
+
<ReviewDiff key={active.key} entry={active} maxHeight={maxHeight} />
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Component, type ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
interface Props {
|
|
4
|
+
fallback: ReactNode;
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
resetKeys: readonly unknown[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface State {
|
|
10
|
+
failed: boolean;
|
|
11
|
+
resetKeys: readonly unknown[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/* A failed view or lazy chunk must leave the containing review card usable. */
|
|
15
|
+
export class DiffFallbackBoundary extends Component<Props, State> {
|
|
16
|
+
state: State = { failed: false, resetKeys: this.props.resetKeys };
|
|
17
|
+
|
|
18
|
+
static getDerivedStateFromError(): Partial<State> {
|
|
19
|
+
return { failed: true };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
static getDerivedStateFromProps(props: Props, state: State): State | null {
|
|
23
|
+
if (state.resetKeys.length !== props.resetKeys.length || state.resetKeys.some((value, index) => !Object.is(value, props.resetKeys[index]))) {
|
|
24
|
+
return { failed: false, resetKeys: props.resetKeys };
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
render(): ReactNode {
|
|
30
|
+
return this.state.failed ? this.props.fallback : this.props.children;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { useReviewSettled } from './review-settle';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
|
|
4
|
+
export function DiffOmitted({ path }: { path: string }) {
|
|
5
|
+
useReviewSettled();
|
|
6
|
+
const { t } = useTranslation('agent-chat');
|
|
7
|
+
return (
|
|
8
|
+
<div role="status" data-diff-omitted className="px-3 py-2 text-xs text-text-muted">
|
|
9
|
+
<p className="truncate font-mono">{path}</p>
|
|
10
|
+
<p>{t('work.omitted.tooLarge')}</p>
|
|
11
|
+
</div>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { useTranslation } from 'react-i18next';
|
|
2
|
+
import { Button } from '@adecore/ui';
|
|
3
|
+
import { ReviewSettleContext } from './review-settle';
|
|
4
|
+
import { ReviewDiff } from './ReviewDiff';
|
|
5
|
+
import { reviewSize, type ReviewEntry } from './review-plan';
|
|
6
|
+
import { useReviewStack, type ReviewStack } from './use-review-stack';
|
|
7
|
+
|
|
8
|
+
export function ReviewStackView({ entries, stack }: { entries: readonly ReviewEntry[]; stack: ReviewStack }) {
|
|
9
|
+
const { t } = useTranslation('agent-chat');
|
|
10
|
+
return (
|
|
11
|
+
<div data-review-stack>
|
|
12
|
+
{entries.map((entry) => (
|
|
13
|
+
<div key={entry.key} className="border-t border-border first:border-t-0">
|
|
14
|
+
<div className="flex items-center gap-2 px-3 py-2">
|
|
15
|
+
<p className="min-w-0 grow break-all font-mono text-xs text-text-muted">{entry.path}</p>
|
|
16
|
+
{stack.slots.get(entry.key) === 'deferred' && (
|
|
17
|
+
<Button size="sm" data-review-show={entry.key} onClick={() => stack.promote(entry.key)}>
|
|
18
|
+
{t('work.showDiff')}
|
|
19
|
+
</Button>
|
|
20
|
+
)}
|
|
21
|
+
</div>
|
|
22
|
+
{stack.ready.has(entry.key) ? (
|
|
23
|
+
<ReviewSettleContext value={{ key: entry.key, settle: stack.settle }}>
|
|
24
|
+
<ReviewDiff entry={entry} />
|
|
25
|
+
</ReviewSettleContext>
|
|
26
|
+
) : stack.slots.get(entry.key) === 'editor' ? (
|
|
27
|
+
<div
|
|
28
|
+
data-review-pending={entry.key}
|
|
29
|
+
role="status"
|
|
30
|
+
className="px-3 py-2 text-xs text-text-faint"
|
|
31
|
+
style={{ height: Math.max(28, Math.min(reviewSize(entry).rows * 20, 480)) }}
|
|
32
|
+
>
|
|
33
|
+
{t('work.loadingDiff')}
|
|
34
|
+
</div>
|
|
35
|
+
) : null}
|
|
36
|
+
</div>
|
|
37
|
+
))}
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function DiffStack({ entries }: { entries: readonly ReviewEntry[] }) {
|
|
43
|
+
const stack = useReviewStack(entries);
|
|
44
|
+
return <ReviewStackView entries={entries} stack={stack} />;
|
|
45
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { useTranslation } from 'react-i18next';
|
|
2
|
+
import { useReviewSettled } from './review-settle';
|
|
3
|
+
|
|
4
|
+
export function DiffUnavailable({ path }: { path: string }) {
|
|
5
|
+
useReviewSettled();
|
|
6
|
+
const { t } = useTranslation('agent-chat');
|
|
7
|
+
return (
|
|
8
|
+
<div role="status" data-diff-unavailable className="px-3 py-2 text-xs text-text-muted">
|
|
9
|
+
<p className="truncate font-mono">{path}</p>
|
|
10
|
+
<p>{t('work.diffUnavailable')}</p>
|
|
11
|
+
</div>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { useReviewSettled } from './review-settle';
|
|
2
|
+
import { useEffect, useMemo, useState, type ReactNode } from 'react';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { getFiletypeFromFileName } from '@pierre/diffs';
|
|
5
|
+
import type { EditorEngine } from '@adecore/editor';
|
|
6
|
+
import { DiffView } from '@adecore/editor-react';
|
|
7
|
+
import { diffDocument, patchOf, type DiffDocument } from '@adecore/merge';
|
|
8
|
+
import { chatHost } from '../../../host';
|
|
9
|
+
import type { FileChange } from '../../logic/tools';
|
|
10
|
+
import { useDiffSourceTokens } from '../use-diff-source-tokens';
|
|
11
|
+
import { diffSourceBudget } from './diff-source-budget';
|
|
12
|
+
import { DiffOmitted } from './DiffOmitted';
|
|
13
|
+
import { canRenderEditorDiff, EDITOR_DIFF_LIMITS, EDITOR_DIFF_WORK } from './editor-edit-model';
|
|
14
|
+
|
|
15
|
+
type EngineLoader = () => Promise<EditorEngine>;
|
|
16
|
+
|
|
17
|
+
// Virtualized rows remount; retain the engine so their height does not return to the loading placeholder.
|
|
18
|
+
const engines = new WeakMap<EngineLoader, EditorEngine | null>();
|
|
19
|
+
|
|
20
|
+
function ReadyDiff({
|
|
21
|
+
document,
|
|
22
|
+
path,
|
|
23
|
+
engine,
|
|
24
|
+
wrap,
|
|
25
|
+
fill,
|
|
26
|
+
layout,
|
|
27
|
+
maxHeight
|
|
28
|
+
}: {
|
|
29
|
+
document: DiffDocument;
|
|
30
|
+
path: string;
|
|
31
|
+
engine: EditorEngine;
|
|
32
|
+
wrap: boolean;
|
|
33
|
+
fill: boolean;
|
|
34
|
+
layout: 'unified' | 'split';
|
|
35
|
+
maxHeight: number;
|
|
36
|
+
}) {
|
|
37
|
+
useReviewSettled();
|
|
38
|
+
const { t } = useTranslation('agent-chat');
|
|
39
|
+
const mode = chatHost().code.useMode();
|
|
40
|
+
const theme = chatHost().code.useThemes()[mode];
|
|
41
|
+
const language = getFiletypeFromFileName(path);
|
|
42
|
+
const tokens = useDiffSourceTokens(document, language, theme);
|
|
43
|
+
return (
|
|
44
|
+
<DiffView
|
|
45
|
+
engine={engine}
|
|
46
|
+
document={document}
|
|
47
|
+
tokens={tokens}
|
|
48
|
+
theme={theme}
|
|
49
|
+
maxHeight={maxHeight}
|
|
50
|
+
unfoldLimits={EDITOR_DIFF_LIMITS}
|
|
51
|
+
label={t('work.diffLabel', { path })}
|
|
52
|
+
layout={layout}
|
|
53
|
+
wrap={wrap}
|
|
54
|
+
fill={fill}
|
|
55
|
+
className="chat-diff select-text"
|
|
56
|
+
/>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function EditorDocumentDiff({
|
|
61
|
+
document,
|
|
62
|
+
path,
|
|
63
|
+
loadEngine,
|
|
64
|
+
fallback,
|
|
65
|
+
wrap = true,
|
|
66
|
+
fill = false,
|
|
67
|
+
layout = 'unified',
|
|
68
|
+
maxHeight = 480
|
|
69
|
+
}: {
|
|
70
|
+
document: DiffDocument;
|
|
71
|
+
path: string;
|
|
72
|
+
loadEngine: EngineLoader;
|
|
73
|
+
fallback: ReactNode;
|
|
74
|
+
wrap?: boolean;
|
|
75
|
+
fill?: boolean;
|
|
76
|
+
layout?: 'unified' | 'split';
|
|
77
|
+
maxHeight?: number;
|
|
78
|
+
}) {
|
|
79
|
+
const { t } = useTranslation('agent-chat');
|
|
80
|
+
const withinLimits = useMemo(() => canRenderEditorDiff(document), [document]);
|
|
81
|
+
const [loaded, setLoaded] = useState<{ load: EngineLoader; engine: EditorEngine | null }>();
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (!withinLimits || engines.has(loadEngine)) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
let active = true;
|
|
87
|
+
void Promise.resolve()
|
|
88
|
+
.then(loadEngine)
|
|
89
|
+
.then(
|
|
90
|
+
(engine) => {
|
|
91
|
+
engines.set(loadEngine, engine);
|
|
92
|
+
if (active) {
|
|
93
|
+
setLoaded({ load: loadEngine, engine });
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
() => {
|
|
97
|
+
engines.set(loadEngine, null);
|
|
98
|
+
if (active) {
|
|
99
|
+
setLoaded({ load: loadEngine, engine: null });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
);
|
|
103
|
+
return () => {
|
|
104
|
+
active = false;
|
|
105
|
+
};
|
|
106
|
+
}, [loadEngine, withinLimits]);
|
|
107
|
+
const engine = loaded?.load === loadEngine ? loaded.engine : engines.get(loadEngine);
|
|
108
|
+
if (!withinLimits) {
|
|
109
|
+
return <DiffOmitted path={path} />;
|
|
110
|
+
}
|
|
111
|
+
if (engine === undefined) {
|
|
112
|
+
return <div className="px-3 py-2 text-xs text-text-faint">{t('work.loadingDiff')}</div>;
|
|
113
|
+
}
|
|
114
|
+
return engine === null ? (
|
|
115
|
+
fallback
|
|
116
|
+
) : (
|
|
117
|
+
<ReadyDiff document={document} path={path} engine={engine} wrap={wrap} fill={fill} layout={layout} maxHeight={maxHeight} />
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export default function EditorEditDiff({
|
|
122
|
+
change,
|
|
123
|
+
loadEngine,
|
|
124
|
+
fallback,
|
|
125
|
+
maxHeight,
|
|
126
|
+
sourceChecked = false
|
|
127
|
+
}: {
|
|
128
|
+
change: FileChange;
|
|
129
|
+
sourceChecked?: boolean;
|
|
130
|
+
loadEngine: EngineLoader;
|
|
131
|
+
fallback: ReactNode;
|
|
132
|
+
maxHeight?: number;
|
|
133
|
+
}) {
|
|
134
|
+
const document = useMemo(
|
|
135
|
+
() =>
|
|
136
|
+
sourceChecked || diffSourceBudget(change.before, change.after)
|
|
137
|
+
? diffDocument(patchOf(change.before, change.after, 3, { maxWork: EDITOR_DIFF_WORK }), { old: change.before, new: change.after })
|
|
138
|
+
: undefined,
|
|
139
|
+
[change.before, change.after, sourceChecked]
|
|
140
|
+
);
|
|
141
|
+
if (document === undefined) {
|
|
142
|
+
return <DiffOmitted path={change.path} />;
|
|
143
|
+
}
|
|
144
|
+
return <EditorDocumentDiff document={document} path={change.path} loadEngine={loadEngine} fallback={fallback} maxHeight={maxHeight} />;
|
|
145
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { useMemo, type ReactNode } from 'react';
|
|
2
|
+
import type { ChatFileChange } from '@adecore/agent-contracts';
|
|
3
|
+
import type { EditorEngine } from '@adecore/editor';
|
|
4
|
+
import { diffDocument, parsePatch } from '@adecore/merge';
|
|
5
|
+
import type { DiffContents } from '../full-diff';
|
|
6
|
+
import { diffSourceBudget, patchSourceBudget } from './diff-source-budget';
|
|
7
|
+
import { DiffOmitted } from './DiffOmitted';
|
|
8
|
+
import { EditorDocumentDiff } from './EditorEditDiff';
|
|
9
|
+
|
|
10
|
+
export default function EditorPatchDiff({
|
|
11
|
+
change,
|
|
12
|
+
sourceChecked = false,
|
|
13
|
+
contents,
|
|
14
|
+
ignoreWhitespace,
|
|
15
|
+
wrap,
|
|
16
|
+
fill,
|
|
17
|
+
maxHeight,
|
|
18
|
+
layout = 'unified',
|
|
19
|
+
loadEngine,
|
|
20
|
+
fallback
|
|
21
|
+
}: {
|
|
22
|
+
change: ChatFileChange;
|
|
23
|
+
sourceChecked?: boolean;
|
|
24
|
+
contents?: DiffContents;
|
|
25
|
+
ignoreWhitespace: boolean;
|
|
26
|
+
wrap: boolean;
|
|
27
|
+
fill: boolean;
|
|
28
|
+
layout?: 'unified' | 'split';
|
|
29
|
+
maxHeight?: number;
|
|
30
|
+
loadEngine: () => Promise<EditorEngine>;
|
|
31
|
+
fallback: ReactNode;
|
|
32
|
+
}) {
|
|
33
|
+
const oldText = contents?.old;
|
|
34
|
+
const newText = contents?.new;
|
|
35
|
+
const document = useMemo(() => {
|
|
36
|
+
if (!sourceChecked && (!diffSourceBudget(oldText ?? '', newText ?? '') || !patchSourceBudget(change.diff))) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
return diffDocument(
|
|
40
|
+
parsePatch(change.diff),
|
|
41
|
+
oldText === undefined || newText === undefined ? undefined : { old: oldText, new: newText },
|
|
42
|
+
ignoreWhitespace
|
|
43
|
+
);
|
|
44
|
+
}, [change.diff, oldText, newText, ignoreWhitespace, sourceChecked]);
|
|
45
|
+
if (document === undefined) {
|
|
46
|
+
return <DiffOmitted path={change.path} />;
|
|
47
|
+
}
|
|
48
|
+
return (
|
|
49
|
+
<EditorDocumentDiff
|
|
50
|
+
document={document}
|
|
51
|
+
path={change.path}
|
|
52
|
+
loadEngine={loadEngine}
|
|
53
|
+
fallback={fallback}
|
|
54
|
+
wrap={wrap}
|
|
55
|
+
fill={fill}
|
|
56
|
+
layout={layout}
|
|
57
|
+
maxHeight={maxHeight}
|
|
58
|
+
/>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { useWorkerPool } from '@pierre/diffs/react';
|
|
3
|
+
import { chatHost } from '../../../host';
|
|
4
|
+
import DiffPool from '../DiffPool';
|
|
5
|
+
|
|
6
|
+
export function LegacyDiffPool({ children }: { children: ReactNode }) {
|
|
7
|
+
const outer = useWorkerPool();
|
|
8
|
+
const workerFactory = chatHost().code.legacyDiffWorker;
|
|
9
|
+
if (outer !== undefined || workerFactory === undefined) {
|
|
10
|
+
return children;
|
|
11
|
+
}
|
|
12
|
+
return <DiffPool workerFactory={workerFactory}>{children}</DiffPool>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { LegacyDiffPool } from './LegacyDiffPool';
|
|
2
|
+
import { useReviewSettled } from './review-settle';
|
|
3
|
+
import { useMemo } from 'react';
|
|
4
|
+
import { MultiFileDiff } from '@pierre/diffs/react';
|
|
5
|
+
import { chatHost } from '../../../host';
|
|
6
|
+
import { useDiffTheme } from '../diff-theme';
|
|
7
|
+
import type { FileChange } from '../../logic/tools';
|
|
8
|
+
|
|
9
|
+
export default function LegacyEditDiff({ change, maxHeight }: { change: FileChange; maxHeight?: number }) {
|
|
10
|
+
useReviewSettled();
|
|
11
|
+
const resolved = chatHost().code.useMode();
|
|
12
|
+
const theme = useDiffTheme();
|
|
13
|
+
const oldFile = useMemo(() => ({ name: change.path, contents: change.before }), [change.path, change.before]);
|
|
14
|
+
const newFile = useMemo(() => ({ name: change.path, contents: change.after }), [change.path, change.after]);
|
|
15
|
+
const options = useMemo(
|
|
16
|
+
() => ({
|
|
17
|
+
diffStyle: 'unified' as const,
|
|
18
|
+
theme,
|
|
19
|
+
themeType: resolved,
|
|
20
|
+
disableFileHeader: true,
|
|
21
|
+
overflow: 'wrap' as const,
|
|
22
|
+
hunkSeparators: 'simple' as const
|
|
23
|
+
}),
|
|
24
|
+
[theme, resolved]
|
|
25
|
+
);
|
|
26
|
+
return (
|
|
27
|
+
<div className="chat-diff select-text" style={maxHeight === undefined ? undefined : { maxHeight, overflow: 'auto' }}>
|
|
28
|
+
<LegacyDiffPool>
|
|
29
|
+
<MultiFileDiff oldFile={oldFile} newFile={newFile} options={options} />
|
|
30
|
+
</LegacyDiffPool>
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { LegacyDiffPool } from './LegacyDiffPool';
|
|
2
|
+
import { useReviewSettled } from './review-settle';
|
|
3
|
+
import { useMemo } from 'react';
|
|
4
|
+
import clsx from 'clsx';
|
|
5
|
+
import { FileDiff, PatchDiff } from '@pierre/diffs/react';
|
|
6
|
+
import type { ChatFileChange } from '@adecore/agent-contracts';
|
|
7
|
+
import { chatHost } from '../../../host';
|
|
8
|
+
import { useDiffTheme } from '../diff-theme';
|
|
9
|
+
import { fullFileDiff } from '../full-diff';
|
|
10
|
+
import type { UnifiedDiffProps } from './diff-props';
|
|
11
|
+
|
|
12
|
+
/*
|
|
13
|
+
* A patch the CLI reported itself. The renderer needs a file header, which a CLI that only sends hunks
|
|
14
|
+
* gets from the path we know; a diff without hunks is shown as its own lines, so nothing is swallowed.
|
|
15
|
+
*/
|
|
16
|
+
function asPatch(change: ChatFileChange): string | null {
|
|
17
|
+
if (!/^@@/m.test(change.diff)) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
if (/^\+\+\+ /m.test(change.diff)) {
|
|
21
|
+
return change.diff;
|
|
22
|
+
}
|
|
23
|
+
const from = change.kind === 'add' ? '/dev/null' : `a/${change.path}`;
|
|
24
|
+
const to = change.kind === 'delete' ? '/dev/null' : `b/${change.path}`;
|
|
25
|
+
return `--- ${from}\n+++ ${to}\n${change.diff}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/* 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. */
|
|
29
|
+
const FILL_CSS = `
|
|
30
|
+
:host { display: flex; flex-direction: column; flex-grow: 1; }
|
|
31
|
+
pre { flex-grow: 1; align-content: start; }
|
|
32
|
+
pre[data-diff-type="single"] { display: flex; flex-direction: column; }
|
|
33
|
+
pre[data-diff-type="split"][data-overflow="scroll"]:not([data-dehydrated]) { align-content: stretch; }
|
|
34
|
+
pre > [data-code] { flex-grow: 1; align-self: stretch; align-content: start; }
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
function lineClass(line: string): string {
|
|
38
|
+
if (line.startsWith('+')) {
|
|
39
|
+
return 'text-chat-added';
|
|
40
|
+
}
|
|
41
|
+
if (line.startsWith('-')) {
|
|
42
|
+
return 'text-chat-removed';
|
|
43
|
+
}
|
|
44
|
+
return 'text-text-muted';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default function LegacyUnifiedDiff({ change, overflow = 'wrap', diffStyle = 'unified', fill = false, contents, maxHeight }: UnifiedDiffProps) {
|
|
48
|
+
useReviewSettled();
|
|
49
|
+
const resolved = chatHost().code.useMode();
|
|
50
|
+
const theme = useDiffTheme();
|
|
51
|
+
const patch = useMemo(() => asPatch(change), [change]);
|
|
52
|
+
const oldText = contents?.old;
|
|
53
|
+
const newText = contents?.new;
|
|
54
|
+
const full = useMemo(
|
|
55
|
+
() => (patch === null || oldText === undefined || newText === undefined ? null : fullFileDiff(patch, change.path, { old: oldText, new: newText })),
|
|
56
|
+
[patch, change.path, oldText, newText]
|
|
57
|
+
);
|
|
58
|
+
const unfolds = full !== null;
|
|
59
|
+
const options = useMemo(
|
|
60
|
+
() => ({
|
|
61
|
+
theme,
|
|
62
|
+
themeType: resolved,
|
|
63
|
+
disableFileHeader: true,
|
|
64
|
+
diffStyle,
|
|
65
|
+
overflow,
|
|
66
|
+
hunkSeparators: unfolds ? ('line-info' as const) : ('simple' as const),
|
|
67
|
+
unsafeCSS: fill ? FILL_CSS : undefined
|
|
68
|
+
}),
|
|
69
|
+
[diffStyle, overflow, theme, resolved, fill, unfolds]
|
|
70
|
+
);
|
|
71
|
+
if (patch === null) {
|
|
72
|
+
return (
|
|
73
|
+
<pre className="max-h-64 overflow-auto px-3 py-2 font-mono text-code select-text" style={fill ? undefined : { maxHeight }}>
|
|
74
|
+
{change.diff
|
|
75
|
+
.replace(/\n$/, '')
|
|
76
|
+
.split('\n')
|
|
77
|
+
.map((line, index) => (
|
|
78
|
+
<div key={index} className={clsx(lineClass(line), 'whitespace-pre-wrap')}>
|
|
79
|
+
{line}
|
|
80
|
+
</div>
|
|
81
|
+
))}
|
|
82
|
+
</pre>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
return (
|
|
86
|
+
<div
|
|
87
|
+
className={clsx('chat-diff select-text', fill && 'flex grow flex-col')}
|
|
88
|
+
style={fill || maxHeight === undefined ? undefined : { maxHeight, overflow: 'auto' }}
|
|
89
|
+
>
|
|
90
|
+
<LegacyDiffPool>{full === null ? <PatchDiff patch={patch} options={options} /> : <FileDiff fileDiff={full} options={options} />}</LegacyDiffPool>
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import clsx from 'clsx';
|
|
2
|
+
import { useReviewSettled } from './review-settle';
|
|
3
|
+
|
|
4
|
+
export function RawDiff({ text, overflow, fill, maxHeight }: { text: string; overflow: 'wrap' | 'scroll'; fill: boolean; maxHeight?: number }) {
|
|
5
|
+
useReviewSettled();
|
|
6
|
+
return (
|
|
7
|
+
<pre
|
|
8
|
+
data-diff-raw
|
|
9
|
+
className={clsx(
|
|
10
|
+
'overflow-auto px-3 py-2 font-mono text-code text-text-muted select-text',
|
|
11
|
+
fill && 'h-full min-h-0',
|
|
12
|
+
overflow === 'wrap' ? 'whitespace-pre-wrap' : 'whitespace-pre'
|
|
13
|
+
)}
|
|
14
|
+
style={fill ? undefined : { maxHeight: maxHeight ?? 256 }}
|
|
15
|
+
>
|
|
16
|
+
{text.replace(/\n$/, '')}
|
|
17
|
+
</pre>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Suspense } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { lazyNamed } from '../../../lazy';
|
|
4
|
+
import type { ReviewEntry } from './review-plan';
|
|
5
|
+
|
|
6
|
+
const EditDiff = lazyNamed(() => import('../EditDiff'), 'default');
|
|
7
|
+
const UnifiedDiff = lazyNamed(() => import('../UnifiedDiff'), 'default');
|
|
8
|
+
|
|
9
|
+
export function ReviewDiff({ entry, maxHeight = 480 }: { entry: ReviewEntry; maxHeight?: number }) {
|
|
10
|
+
const { t } = useTranslation('agent-chat');
|
|
11
|
+
return (
|
|
12
|
+
<div data-review-entry={entry.key}>
|
|
13
|
+
<Suspense fallback={<p className="px-3 py-2 text-xs text-text-faint">{t('work.loadingDiff')}</p>}>
|
|
14
|
+
{entry.change.kind === 'edit' ? (
|
|
15
|
+
<EditDiff change={entry.change.edit} maxHeight={maxHeight} />
|
|
16
|
+
) : (
|
|
17
|
+
<UnifiedDiff change={entry.change.patch} contents={entry.change.contents} maxHeight={maxHeight} />
|
|
18
|
+
)}
|
|
19
|
+
</Suspense>
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ChatFileChange } from '@adecore/agent-contracts';
|
|
2
|
+
import type { DiffContents } from '../full-diff';
|
|
3
|
+
|
|
4
|
+
export interface UnifiedDiffProps {
|
|
5
|
+
allowEditor?: boolean;
|
|
6
|
+
maxHeight?: number;
|
|
7
|
+
ignoreWhitespace?: boolean;
|
|
8
|
+
change: ChatFileChange;
|
|
9
|
+
overflow?: 'wrap' | 'scroll';
|
|
10
|
+
/* `split` puts the old and the new side by side; the chat always stacks. */
|
|
11
|
+
diffStyle?: 'unified' | 'split';
|
|
12
|
+
/* Grow to the height of a flex column around it. */
|
|
13
|
+
fill?: boolean;
|
|
14
|
+
/* Both whole texts the patch was made between, with which a person can unfold the lines between hunks. */
|
|
15
|
+
contents?: DiffContents;
|
|
16
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
interface TextBudget {
|
|
2
|
+
readonly rows: number;
|
|
3
|
+
readonly characters: number;
|
|
4
|
+
readonly lineLength: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// Diff preparation is synchronous; cap source work before creating lines or hunks.
|
|
8
|
+
const SOURCE_BUDGET: TextBudget = { rows: 20000, characters: 2000000, lineLength: 1000000 };
|
|
9
|
+
const PATCH_BUDGET: TextBudget = { rows: 50000, characters: 4000000, lineLength: 1000001 };
|
|
10
|
+
const LEGACY_BUDGET: TextBudget = { rows: 400, characters: 100000, lineLength: 10000 };
|
|
11
|
+
|
|
12
|
+
export function textFitsBudget(text: string, budget: TextBudget): boolean {
|
|
13
|
+
if (text.length > budget.characters) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
let rows = text.length === 0 ? 0 : 1;
|
|
17
|
+
let length = 0;
|
|
18
|
+
for (let index = 0; index < text.length; index++) {
|
|
19
|
+
if (text.charCodeAt(index) === 10) {
|
|
20
|
+
if (index < text.length - 1 && ++rows > budget.rows) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
length = 0;
|
|
24
|
+
} else if (++length > budget.lineLength) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return rows <= budget.rows;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function diffSourceBudget(before: string, after: string): boolean {
|
|
32
|
+
return textFitsBudget(before, SOURCE_BUDGET) && textFitsBudget(after, SOURCE_BUDGET);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function patchSourceBudget(patch: string): boolean {
|
|
36
|
+
return textFitsBudget(patch, PATCH_BUDGET);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function legacyDiffBudget(before: string, after: string, patch = ''): boolean {
|
|
40
|
+
// Combined sources and patch must fit one small legacy render, including trailing blank lines.
|
|
41
|
+
if (before.length + after.length + patch.length > LEGACY_BUDGET.characters) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
let rows = 2;
|
|
45
|
+
for (const text of [before, after, patch]) {
|
|
46
|
+
if (!textFitsBudget(text, LEGACY_BUDGET)) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
for (let index = 0; index < text.length; index++) {
|
|
50
|
+
if (text.charCodeAt(index) === 10 && ++rows > LEGACY_BUDGET.rows) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { diffRows, type DiffDocument } from '@adecore/merge';
|
|
2
|
+
|
|
3
|
+
// Larger rendering tiers need native production measurements before this gate expands.
|
|
4
|
+
export const EDITOR_DIFF_WORK = 4_000_000;
|
|
5
|
+
export const EDITOR_DIFF_LIMITS = { rows: 2000, characters: 2000000, lineLength: 10000 };
|
|
6
|
+
|
|
7
|
+
export function canRenderEditorDiff(document: DiffDocument): boolean {
|
|
8
|
+
let rows = 0;
|
|
9
|
+
let characters = 0;
|
|
10
|
+
for (const row of diffRows(document, 'unified')) {
|
|
11
|
+
if (row.kind === 'gap') {
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
const text = row.new === undefined ? document.sides.old[row.old!.index]! : document.sides.new[row.new.index]!;
|
|
15
|
+
characters += text.length + (rows > 0 ? 1 : 0);
|
|
16
|
+
if (++rows > EDITOR_DIFF_LIMITS.rows || characters > EDITOR_DIFF_LIMITS.characters || text.length > EDITOR_DIFF_LIMITS.lineLength) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return true;
|
|
21
|
+
}
|