@adecore/agents-react 0.22.1 → 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/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 +3 -0
- package/dist/locales/en/agent-prompts.json +1 -0
- package/dist/locales/nl/agent-chat.json +3 -0
- package/dist/locales/nl/agent-prompts.json +1 -0
- package/dist/prompts/ui/ApprovalBody.js +7 -5
- package/package.json +18 -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/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 +3 -0
- package/src/locales/en/agent-prompts.json +1 -0
- package/src/locales/nl/agent-chat.json +3 -0
- package/src/locales/nl/agent-prompts.json +1 -0
- package/src/prompts/ui/ApprovalBody.tsx +8 -14
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import type { Highlighter, ThemeRegistration } from 'shiki';
|
|
2
|
+
import {
|
|
3
|
+
createSyntaxService,
|
|
4
|
+
type DiffSyntaxDocument,
|
|
5
|
+
type DiffSyntaxInput,
|
|
6
|
+
type DiffSyntaxOutput,
|
|
7
|
+
type DiffSyntaxService,
|
|
8
|
+
type DiffSyntaxTokens
|
|
9
|
+
} from '@adecore/editor/shiki-worker';
|
|
10
|
+
import { chatHost } from '../../../host';
|
|
11
|
+
|
|
12
|
+
interface Pending {
|
|
13
|
+
promise: Promise<DiffSyntaxTokens | null>;
|
|
14
|
+
resolve: (value: DiffSyntaxTokens | null) => void;
|
|
15
|
+
resent: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface Session {
|
|
19
|
+
definition: DiffSyntaxDocument;
|
|
20
|
+
pending: Map<number, Pending>;
|
|
21
|
+
sent: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface DiffSyntaxSession {
|
|
25
|
+
request(index: number): Promise<DiffSyntaxTokens | null>;
|
|
26
|
+
cancel(): void;
|
|
27
|
+
dispose(): void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface DiffSyntaxRuntime {
|
|
31
|
+
open(document: DiffSyntaxDocument): DiffSyntaxSession;
|
|
32
|
+
dispose(): void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface DiffSyntaxRuntimeOptions {
|
|
36
|
+
workerFactory?: () => Worker;
|
|
37
|
+
themes: readonly ThemeRegistration[];
|
|
38
|
+
highlighter?: () => Promise<Highlighter>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let nextGeneration = 0;
|
|
42
|
+
let shared: DiffSyntaxRuntime | undefined;
|
|
43
|
+
|
|
44
|
+
export function diffSyntaxRuntime(): DiffSyntaxRuntime {
|
|
45
|
+
const code = chatHost().code;
|
|
46
|
+
return (shared ??= createDiffSyntaxRuntime({ workerFactory: code.syntaxWorker, themes: code.custom, highlighter: code.highlighter }));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function createDiffSyntaxRuntime(options: DiffSyntaxRuntimeOptions): DiffSyntaxRuntime {
|
|
50
|
+
const sessions = new Map<number, Session>();
|
|
51
|
+
let worker: Worker | undefined;
|
|
52
|
+
let fallback: DiffSyntaxService | undefined;
|
|
53
|
+
let highlighter: Promise<Highlighter> | undefined;
|
|
54
|
+
let started = false;
|
|
55
|
+
let stopped = false;
|
|
56
|
+
const settle = (session: Session): void => {
|
|
57
|
+
for (const pending of session.pending.values()) {
|
|
58
|
+
pending.resolve(null);
|
|
59
|
+
}
|
|
60
|
+
session.pending.clear();
|
|
61
|
+
};
|
|
62
|
+
const stopWorker = (): void => {
|
|
63
|
+
worker?.removeEventListener('message', onMessage);
|
|
64
|
+
worker?.removeEventListener('error', onError);
|
|
65
|
+
worker?.removeEventListener('messageerror', onError);
|
|
66
|
+
worker?.terminate();
|
|
67
|
+
worker = undefined;
|
|
68
|
+
};
|
|
69
|
+
const loadHighlighter = (): Promise<Highlighter> => {
|
|
70
|
+
highlighter ??= import('shiki').then(({ createHighlighter }) => createHighlighter({ langs: [], themes: [] }));
|
|
71
|
+
return highlighter;
|
|
72
|
+
};
|
|
73
|
+
const startFallback = (): void => {
|
|
74
|
+
stopWorker();
|
|
75
|
+
if (fallback !== undefined || stopped) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
fallback = createSyntaxService({ highlighter: options.highlighter ?? loadHighlighter, send: receive });
|
|
79
|
+
dispatch({ type: 'start', themes: options.themes, maxLines: 100000 });
|
|
80
|
+
for (const [generation, session] of sessions) {
|
|
81
|
+
session.sent = false;
|
|
82
|
+
for (const index of session.pending.keys()) {
|
|
83
|
+
request(generation, session, index);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
const onError = (event: Event): void => {
|
|
88
|
+
event.preventDefault();
|
|
89
|
+
startFallback();
|
|
90
|
+
};
|
|
91
|
+
const onMessage = (event: MessageEvent<DiffSyntaxOutput>): void => {
|
|
92
|
+
receive(event.data);
|
|
93
|
+
};
|
|
94
|
+
const initialize = (): void => {
|
|
95
|
+
if (started || stopped) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
started = true;
|
|
99
|
+
if (options.workerFactory !== undefined) {
|
|
100
|
+
try {
|
|
101
|
+
worker = options.workerFactory();
|
|
102
|
+
worker.addEventListener('message', onMessage);
|
|
103
|
+
worker.addEventListener('error', onError);
|
|
104
|
+
worker.addEventListener('messageerror', onError);
|
|
105
|
+
dispatch({ type: 'start', themes: options.themes, maxLines: 100000 });
|
|
106
|
+
return;
|
|
107
|
+
} catch {
|
|
108
|
+
startFallback();
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
startFallback();
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
const dispatch = (message: DiffSyntaxInput): void => {
|
|
115
|
+
if (stopped) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (worker !== undefined) {
|
|
119
|
+
try {
|
|
120
|
+
worker.postMessage(message);
|
|
121
|
+
} catch {
|
|
122
|
+
startFallback();
|
|
123
|
+
}
|
|
124
|
+
} else if (fallback !== undefined) {
|
|
125
|
+
void fallback.handle(message).catch(() => {
|
|
126
|
+
for (const session of sessions.values()) {
|
|
127
|
+
settle(session);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
const request = (generation: number, session: Session, index: number, resend = false): void => {
|
|
133
|
+
const document = !session.sent || resend ? session.definition : undefined;
|
|
134
|
+
session.sent = true;
|
|
135
|
+
dispatch({ type: 'request', generation, index, ...(document ? { document } : {}) });
|
|
136
|
+
};
|
|
137
|
+
const receive = (message: DiffSyntaxOutput): void => {
|
|
138
|
+
if (stopped || message.type === 'ready') {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (message.type === 'error') {
|
|
142
|
+
if (worker !== undefined) {
|
|
143
|
+
startFallback();
|
|
144
|
+
} else {
|
|
145
|
+
for (const session of sessions.values()) {
|
|
146
|
+
settle(session);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const session = sessions.get(message.generation);
|
|
152
|
+
const pending = session?.pending.get(message.index);
|
|
153
|
+
if (session === undefined || pending === undefined) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (message.type === 'failed' && message.reason === 'missing' && !pending.resent) {
|
|
157
|
+
pending.resent = true;
|
|
158
|
+
request(message.generation, session, message.index, true);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
session.pending.delete(message.index);
|
|
162
|
+
pending.resolve(message.type === 'tokens' ? message : null);
|
|
163
|
+
};
|
|
164
|
+
return {
|
|
165
|
+
open(document) {
|
|
166
|
+
const generation = ++nextGeneration;
|
|
167
|
+
const session: Session = { definition: document, pending: new Map(), sent: false };
|
|
168
|
+
sessions.set(generation, session);
|
|
169
|
+
return {
|
|
170
|
+
request(index) {
|
|
171
|
+
if (stopped || !sessions.has(generation)) {
|
|
172
|
+
return Promise.resolve(null);
|
|
173
|
+
}
|
|
174
|
+
const existing = session.pending.get(index);
|
|
175
|
+
if (existing !== undefined) {
|
|
176
|
+
return existing.promise;
|
|
177
|
+
}
|
|
178
|
+
let resolve!: Pending['resolve'];
|
|
179
|
+
const promise = new Promise<DiffSyntaxTokens | null>((done) => {
|
|
180
|
+
resolve = done;
|
|
181
|
+
});
|
|
182
|
+
session.pending.set(index, { promise, resolve, resent: false });
|
|
183
|
+
const initialized = started;
|
|
184
|
+
initialize();
|
|
185
|
+
// Fallback initialization replays pending requests, including this first one.
|
|
186
|
+
if (initialized || worker !== undefined) {
|
|
187
|
+
request(generation, session, index);
|
|
188
|
+
}
|
|
189
|
+
return promise;
|
|
190
|
+
},
|
|
191
|
+
cancel() {
|
|
192
|
+
settle(session);
|
|
193
|
+
if (started) {
|
|
194
|
+
dispatch({ type: 'cancel', generation });
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
dispose() {
|
|
198
|
+
settle(session);
|
|
199
|
+
sessions.delete(generation);
|
|
200
|
+
if (started) {
|
|
201
|
+
dispatch({ type: 'dispose', generation });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
},
|
|
206
|
+
dispose() {
|
|
207
|
+
stopped = true;
|
|
208
|
+
for (const session of sessions.values()) {
|
|
209
|
+
settle(session);
|
|
210
|
+
}
|
|
211
|
+
sessions.clear();
|
|
212
|
+
stopWorker();
|
|
213
|
+
fallback?.dispose();
|
|
214
|
+
if (options.highlighter === undefined) {
|
|
215
|
+
void highlighter?.then(
|
|
216
|
+
(loaded) => loaded.dispose(),
|
|
217
|
+
() => undefined
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import { planReview, type ReviewEntry, type ReviewSlot } from './review-plan';
|
|
3
|
+
|
|
4
|
+
export interface ReviewStack {
|
|
5
|
+
readonly slots: ReadonlyMap<string, ReviewSlot>;
|
|
6
|
+
readonly ready: ReadonlySet<string>;
|
|
7
|
+
promote(key: string): void;
|
|
8
|
+
settle(key: string): void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function useReviewStack(entries: readonly ReviewEntry[]): ReviewStack {
|
|
12
|
+
const [promoted, setPromoted] = useState<string | null>(null);
|
|
13
|
+
const slots = useMemo(() => planReview(entries, promoted), [entries, promoted]);
|
|
14
|
+
const selected = [...slots].filter(([, slot]) => slot === 'editor').map(([key]) => key);
|
|
15
|
+
const [admitted, setAdmitted] = useState<string[]>(() => selected.slice(0, 1));
|
|
16
|
+
const retained = admitted.filter((key) => selected.includes(key));
|
|
17
|
+
const live = retained.length === 0 ? selected.slice(0, 1) : retained;
|
|
18
|
+
if (live.length !== admitted.length || live.some((key, index) => key !== admitted[index])) {
|
|
19
|
+
setAdmitted(live);
|
|
20
|
+
}
|
|
21
|
+
const [settled, setSettled] = useState<ReadonlySet<string>>(() => new Set());
|
|
22
|
+
if ([...settled].some((key) => !selected.includes(key))) {
|
|
23
|
+
setSettled(new Set([...settled].filter((key) => selected.includes(key))));
|
|
24
|
+
}
|
|
25
|
+
const settle = useCallback((key: string) => {
|
|
26
|
+
setSettled((previous) => (previous.has(key) ? previous : new Set([...previous, key])));
|
|
27
|
+
}, []);
|
|
28
|
+
const allSettled = live.every((key) => settled.has(key));
|
|
29
|
+
const next = selected.find((key) => !live.includes(key));
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (next === undefined || !allSettled) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const frame = window.requestAnimationFrame(() => setAdmitted((previous) => [...previous, next]));
|
|
35
|
+
return () => window.cancelAnimationFrame(frame);
|
|
36
|
+
}, [next, allSettled]);
|
|
37
|
+
return { slots, ready: new Set(live), promote: setPromoted, settle };
|
|
38
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { LineToken } from '@adecore/editor';
|
|
2
|
+
import { lineTokensOf, type DiffSyntaxDocument } from '@adecore/editor/shiki-worker';
|
|
3
|
+
import type { DiffDocument } from '@adecore/merge';
|
|
4
|
+
import { diffSyntaxRuntime, type DiffSyntaxRuntime } from './diff/syntax-runtime';
|
|
5
|
+
|
|
6
|
+
export { createDiffSyntaxRuntime, diffSyntaxRuntime } from './diff/syntax-runtime';
|
|
7
|
+
export type { DiffSyntaxRuntime, DiffSyntaxRuntimeOptions, DiffSyntaxSession } from './diff/syntax-runtime';
|
|
8
|
+
|
|
9
|
+
export interface DiffSyntaxMapping {
|
|
10
|
+
readonly documents: Readonly<Record<'old' | 'new', DiffSyntaxDocument>>;
|
|
11
|
+
locate(side: 'old' | 'new', index: number): { readonly block: number; readonly line: number } | undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface DiffSourceTokenSession {
|
|
15
|
+
get(side: 'old' | 'new', index: number): readonly LineToken[] | null | undefined;
|
|
16
|
+
subscribe(listener: () => void): () => void;
|
|
17
|
+
dispose(): void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/* Structurally accepts the editor's source mapping without depending on its React renderer. */
|
|
21
|
+
export function diffSourceTokens(document: DiffDocument, mapping: DiffSyntaxMapping, runtime: DiffSyntaxRuntime = diffSyntaxRuntime()): DiffSourceTokenSession {
|
|
22
|
+
const sides = { old: runtime.open(mapping.documents.old), new: runtime.open(mapping.documents.new) };
|
|
23
|
+
const cache = { old: new Map<number, ReadonlyArray<readonly LineToken[] | null>>(), new: new Map<number, ReadonlyArray<readonly LineToken[] | null>>() };
|
|
24
|
+
const pending = { old: new Set<number>(), new: new Set<number>() };
|
|
25
|
+
const listeners = new Set<() => void>();
|
|
26
|
+
let disposed = false;
|
|
27
|
+
return {
|
|
28
|
+
get(side, index) {
|
|
29
|
+
const location = mapping.locate(side, index);
|
|
30
|
+
if (disposed || location === undefined || document.sides[side][index] === undefined) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
const { block, line } = location;
|
|
34
|
+
const known = cache[side].get(block);
|
|
35
|
+
if (known !== undefined) {
|
|
36
|
+
return known[line] ?? null;
|
|
37
|
+
}
|
|
38
|
+
if (!pending[side].has(block)) {
|
|
39
|
+
pending[side].add(block);
|
|
40
|
+
const text = mapping.documents[side].blocks[block]!.split('\n');
|
|
41
|
+
const publish = (lines: ReadonlyArray<readonly LineToken[] | null>): void => {
|
|
42
|
+
if (disposed) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
pending[side].delete(block);
|
|
46
|
+
cache[side].set(block, lines);
|
|
47
|
+
for (const listener of [...listeners]) {
|
|
48
|
+
listener();
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
const count = text.length;
|
|
52
|
+
void sides[side].request(block).then(
|
|
53
|
+
(packet) => {
|
|
54
|
+
if (!disposed) {
|
|
55
|
+
publish(text.map((text, line) => (packet === null || packet.lines.length !== count + 1 ? null : lineTokensOf(packet, line, text))));
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
() => publish(text.map(() => null))
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
},
|
|
63
|
+
subscribe(listener) {
|
|
64
|
+
if (!disposed) {
|
|
65
|
+
listeners.add(listener);
|
|
66
|
+
}
|
|
67
|
+
return () => {
|
|
68
|
+
listeners.delete(listener);
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
dispose() {
|
|
72
|
+
disposed = true;
|
|
73
|
+
listeners.clear();
|
|
74
|
+
sides.old.dispose();
|
|
75
|
+
sides.new.dispose();
|
|
76
|
+
cache.old.clear();
|
|
77
|
+
cache.new.clear();
|
|
78
|
+
pending.old.clear();
|
|
79
|
+
pending.new.clear();
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -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
|
-
//
|
|
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
|
-
<
|
|
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
|
-
{
|
|
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
|
-
|
|
47
|
-
|
|
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",
|
|
@@ -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",
|
|
@@ -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 {
|
|
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 {
|
|
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
|
-
<
|
|
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)}>
|