@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,47 @@
|
|
|
1
|
+
// Diff preparation is synchronous; cap source work before creating lines or hunks.
|
|
2
|
+
const SOURCE_BUDGET = { rows: 20000, characters: 2000000, lineLength: 1000000 };
|
|
3
|
+
const PATCH_BUDGET = { rows: 50000, characters: 4000000, lineLength: 1000001 };
|
|
4
|
+
const LEGACY_BUDGET = { rows: 400, characters: 100000, lineLength: 10000 };
|
|
5
|
+
export function textFitsBudget(text, budget) {
|
|
6
|
+
if (text.length > budget.characters) {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
let rows = text.length === 0 ? 0 : 1;
|
|
10
|
+
let length = 0;
|
|
11
|
+
for (let index = 0; index < text.length; index++) {
|
|
12
|
+
if (text.charCodeAt(index) === 10) {
|
|
13
|
+
if (index < text.length - 1 && ++rows > budget.rows) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
length = 0;
|
|
17
|
+
}
|
|
18
|
+
else if (++length > budget.lineLength) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return rows <= budget.rows;
|
|
23
|
+
}
|
|
24
|
+
export function diffSourceBudget(before, after) {
|
|
25
|
+
return textFitsBudget(before, SOURCE_BUDGET) && textFitsBudget(after, SOURCE_BUDGET);
|
|
26
|
+
}
|
|
27
|
+
export function patchSourceBudget(patch) {
|
|
28
|
+
return textFitsBudget(patch, PATCH_BUDGET);
|
|
29
|
+
}
|
|
30
|
+
export function legacyDiffBudget(before, after, patch = '') {
|
|
31
|
+
// Combined sources and patch must fit one small legacy render, including trailing blank lines.
|
|
32
|
+
if (before.length + after.length + patch.length > LEGACY_BUDGET.characters) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
let rows = 2;
|
|
36
|
+
for (const text of [before, after, patch]) {
|
|
37
|
+
if (!textFitsBudget(text, LEGACY_BUDGET)) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
for (let index = 0; index < text.length; index++) {
|
|
41
|
+
if (text.charCodeAt(index) === 10 && ++rows > LEGACY_BUDGET.rows) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type DiffDocument } from '@adecore/merge';
|
|
2
|
+
export declare const EDITOR_DIFF_WORK = 4000000;
|
|
3
|
+
export declare const EDITOR_DIFF_LIMITS: {
|
|
4
|
+
rows: number;
|
|
5
|
+
characters: number;
|
|
6
|
+
lineLength: number;
|
|
7
|
+
};
|
|
8
|
+
export declare function canRenderEditorDiff(document: DiffDocument): boolean;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { diffRows } from '@adecore/merge';
|
|
2
|
+
// Larger rendering tiers need native production measurements before this gate expands.
|
|
3
|
+
export const EDITOR_DIFF_WORK = 4_000_000;
|
|
4
|
+
export const EDITOR_DIFF_LIMITS = { rows: 2000, characters: 2000000, lineLength: 10000 };
|
|
5
|
+
export function canRenderEditorDiff(document) {
|
|
6
|
+
let rows = 0;
|
|
7
|
+
let characters = 0;
|
|
8
|
+
for (const row of diffRows(document, 'unified')) {
|
|
9
|
+
if (row.kind === 'gap') {
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
const text = row.new === undefined ? document.sides.old[row.old.index] : document.sides.new[row.new.index];
|
|
13
|
+
characters += text.length + (rows > 0 ? 1 : 0);
|
|
14
|
+
if (++rows > EDITOR_DIFF_LIMITS.rows || characters > EDITOR_DIFF_LIMITS.characters || text.length > EDITOR_DIFF_LIMITS.lineLength) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ChatFileChange } from '@adecore/agent-contracts';
|
|
2
|
+
import type { FileChange } from '../../logic/tools.js';
|
|
3
|
+
import type { DiffContents } from '../full-diff.js';
|
|
4
|
+
export interface ReviewEntry {
|
|
5
|
+
readonly key: string;
|
|
6
|
+
readonly path: string;
|
|
7
|
+
readonly change: {
|
|
8
|
+
readonly kind: 'edit';
|
|
9
|
+
readonly edit: FileChange;
|
|
10
|
+
} | {
|
|
11
|
+
readonly kind: 'patch';
|
|
12
|
+
readonly patch: ChatFileChange;
|
|
13
|
+
readonly contents?: DiffContents;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export interface ReviewBudget {
|
|
17
|
+
readonly editors: number;
|
|
18
|
+
readonly rows: number;
|
|
19
|
+
readonly characters: number;
|
|
20
|
+
}
|
|
21
|
+
export declare const REVIEW_BUDGET: ReviewBudget;
|
|
22
|
+
export type ReviewSlot = 'editor' | 'deferred';
|
|
23
|
+
export declare function reviewEntries(patches: readonly ChatFileChange[], edits: readonly FileChange[]): readonly ReviewEntry[];
|
|
24
|
+
export declare function reviewSize(entry: ReviewEntry, budget?: ReviewBudget): {
|
|
25
|
+
rows: number;
|
|
26
|
+
characters: number;
|
|
27
|
+
};
|
|
28
|
+
export declare function planReview(entries: readonly ReviewEntry[], promoted: string | null, budget?: ReviewBudget): ReadonlyMap<string, ReviewSlot>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export const REVIEW_BUDGET = { editors: 3, rows: 2000, characters: 2000000 };
|
|
2
|
+
export function reviewEntries(patches, edits) {
|
|
3
|
+
return [
|
|
4
|
+
...patches.map((patch, index) => ({ key: `patch:${index}:${patch.path}`, path: patch.path, change: { kind: 'patch', patch } })),
|
|
5
|
+
...edits.map((edit, index) => ({ key: `edit:${index}:${edit.path}`, path: edit.path, change: { kind: 'edit', edit } }))
|
|
6
|
+
];
|
|
7
|
+
}
|
|
8
|
+
export function reviewSize(entry, budget = REVIEW_BUDGET) {
|
|
9
|
+
const texts = entry.change.kind === 'edit' ? [entry.change.edit.before, entry.change.edit.after] : [entry.change.patch.diff];
|
|
10
|
+
const characters = texts.reduce((sum, text) => sum + text.length, 0);
|
|
11
|
+
if (characters > budget.characters) {
|
|
12
|
+
return { rows: budget.rows + 1, characters };
|
|
13
|
+
}
|
|
14
|
+
let rows = 0;
|
|
15
|
+
for (const text of texts) {
|
|
16
|
+
if (text.length > 0) {
|
|
17
|
+
rows++;
|
|
18
|
+
}
|
|
19
|
+
for (let index = text.indexOf('\n'); index >= 0; index = text.indexOf('\n', index + 1)) {
|
|
20
|
+
if (++rows > budget.rows) {
|
|
21
|
+
return { rows, characters };
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return { rows, characters };
|
|
26
|
+
}
|
|
27
|
+
export function planReview(entries, promoted, budget = REVIEW_BUDGET) {
|
|
28
|
+
const priority = entries.find((entry) => entry.key === promoted);
|
|
29
|
+
const ordered = priority === undefined ? entries : [priority, ...entries.filter((entry) => entry !== priority)];
|
|
30
|
+
const slots = new Map();
|
|
31
|
+
let editors = 0;
|
|
32
|
+
let rows = 0;
|
|
33
|
+
let characters = 0;
|
|
34
|
+
for (const entry of ordered) {
|
|
35
|
+
const size = reviewSize(entry, budget);
|
|
36
|
+
// The first review keeps its own omission and error handling, even when the group estimate exceeds the budget.
|
|
37
|
+
if (editors === 0 || (editors < budget.editors && rows + size.rows <= budget.rows && characters + size.characters <= budget.characters)) {
|
|
38
|
+
slots.set(entry.key, 'editor');
|
|
39
|
+
editors++;
|
|
40
|
+
rows += size.rows;
|
|
41
|
+
characters += size.characters;
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
slots.set(entry.key, 'deferred');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return slots;
|
|
48
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createContext, useContext, useEffect } from 'react';
|
|
2
|
+
export const ReviewSettleContext = createContext(null);
|
|
3
|
+
export function useReviewSettled() {
|
|
4
|
+
const scope = useContext(ReviewSettleContext);
|
|
5
|
+
const key = scope?.key;
|
|
6
|
+
const settle = scope?.settle;
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
if (key !== undefined) {
|
|
9
|
+
settle?.(key);
|
|
10
|
+
}
|
|
11
|
+
}, [key, settle]);
|
|
12
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Highlighter, ThemeRegistration } from 'shiki';
|
|
2
|
+
import { type DiffSyntaxDocument, type DiffSyntaxTokens } from '@adecore/editor/shiki-worker';
|
|
3
|
+
export interface DiffSyntaxSession {
|
|
4
|
+
request(index: number): Promise<DiffSyntaxTokens | null>;
|
|
5
|
+
cancel(): void;
|
|
6
|
+
dispose(): void;
|
|
7
|
+
}
|
|
8
|
+
export interface DiffSyntaxRuntime {
|
|
9
|
+
open(document: DiffSyntaxDocument): DiffSyntaxSession;
|
|
10
|
+
dispose(): void;
|
|
11
|
+
}
|
|
12
|
+
export interface DiffSyntaxRuntimeOptions {
|
|
13
|
+
workerFactory?: () => Worker;
|
|
14
|
+
themes: readonly ThemeRegistration[];
|
|
15
|
+
highlighter?: () => Promise<Highlighter>;
|
|
16
|
+
}
|
|
17
|
+
export declare function diffSyntaxRuntime(): DiffSyntaxRuntime;
|
|
18
|
+
export declare function createDiffSyntaxRuntime(options: DiffSyntaxRuntimeOptions): DiffSyntaxRuntime;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { createSyntaxService } from '@adecore/editor/shiki-worker';
|
|
2
|
+
import { chatHost } from '../../../host.js';
|
|
3
|
+
let nextGeneration = 0;
|
|
4
|
+
let shared;
|
|
5
|
+
export function diffSyntaxRuntime() {
|
|
6
|
+
const code = chatHost().code;
|
|
7
|
+
return (shared ??= createDiffSyntaxRuntime({ workerFactory: code.syntaxWorker, themes: code.custom, highlighter: code.highlighter }));
|
|
8
|
+
}
|
|
9
|
+
export function createDiffSyntaxRuntime(options) {
|
|
10
|
+
const sessions = new Map();
|
|
11
|
+
let worker;
|
|
12
|
+
let fallback;
|
|
13
|
+
let highlighter;
|
|
14
|
+
let started = false;
|
|
15
|
+
let stopped = false;
|
|
16
|
+
const settle = (session) => {
|
|
17
|
+
for (const pending of session.pending.values()) {
|
|
18
|
+
pending.resolve(null);
|
|
19
|
+
}
|
|
20
|
+
session.pending.clear();
|
|
21
|
+
};
|
|
22
|
+
const stopWorker = () => {
|
|
23
|
+
worker?.removeEventListener('message', onMessage);
|
|
24
|
+
worker?.removeEventListener('error', onError);
|
|
25
|
+
worker?.removeEventListener('messageerror', onError);
|
|
26
|
+
worker?.terminate();
|
|
27
|
+
worker = undefined;
|
|
28
|
+
};
|
|
29
|
+
const loadHighlighter = () => {
|
|
30
|
+
highlighter ??= import('shiki').then(({ createHighlighter }) => createHighlighter({ langs: [], themes: [] }));
|
|
31
|
+
return highlighter;
|
|
32
|
+
};
|
|
33
|
+
const startFallback = () => {
|
|
34
|
+
stopWorker();
|
|
35
|
+
if (fallback !== undefined || stopped) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
fallback = createSyntaxService({ highlighter: options.highlighter ?? loadHighlighter, send: receive });
|
|
39
|
+
dispatch({ type: 'start', themes: options.themes, maxLines: 100000 });
|
|
40
|
+
for (const [generation, session] of sessions) {
|
|
41
|
+
session.sent = false;
|
|
42
|
+
for (const index of session.pending.keys()) {
|
|
43
|
+
request(generation, session, index);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
const onError = (event) => {
|
|
48
|
+
event.preventDefault();
|
|
49
|
+
startFallback();
|
|
50
|
+
};
|
|
51
|
+
const onMessage = (event) => {
|
|
52
|
+
receive(event.data);
|
|
53
|
+
};
|
|
54
|
+
const initialize = () => {
|
|
55
|
+
if (started || stopped) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
started = true;
|
|
59
|
+
if (options.workerFactory !== undefined) {
|
|
60
|
+
try {
|
|
61
|
+
worker = options.workerFactory();
|
|
62
|
+
worker.addEventListener('message', onMessage);
|
|
63
|
+
worker.addEventListener('error', onError);
|
|
64
|
+
worker.addEventListener('messageerror', onError);
|
|
65
|
+
dispatch({ type: 'start', themes: options.themes, maxLines: 100000 });
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
startFallback();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
startFallback();
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
const dispatch = (message) => {
|
|
77
|
+
if (stopped) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (worker !== undefined) {
|
|
81
|
+
try {
|
|
82
|
+
worker.postMessage(message);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
startFallback();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else if (fallback !== undefined) {
|
|
89
|
+
void fallback.handle(message).catch(() => {
|
|
90
|
+
for (const session of sessions.values()) {
|
|
91
|
+
settle(session);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
const request = (generation, session, index, resend = false) => {
|
|
97
|
+
const document = !session.sent || resend ? session.definition : undefined;
|
|
98
|
+
session.sent = true;
|
|
99
|
+
dispatch({ type: 'request', generation, index, ...(document ? { document } : {}) });
|
|
100
|
+
};
|
|
101
|
+
const receive = (message) => {
|
|
102
|
+
if (stopped || message.type === 'ready') {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (message.type === 'error') {
|
|
106
|
+
if (worker !== undefined) {
|
|
107
|
+
startFallback();
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
for (const session of sessions.values()) {
|
|
111
|
+
settle(session);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const session = sessions.get(message.generation);
|
|
117
|
+
const pending = session?.pending.get(message.index);
|
|
118
|
+
if (session === undefined || pending === undefined) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (message.type === 'failed' && message.reason === 'missing' && !pending.resent) {
|
|
122
|
+
pending.resent = true;
|
|
123
|
+
request(message.generation, session, message.index, true);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
session.pending.delete(message.index);
|
|
127
|
+
pending.resolve(message.type === 'tokens' ? message : null);
|
|
128
|
+
};
|
|
129
|
+
return {
|
|
130
|
+
open(document) {
|
|
131
|
+
const generation = ++nextGeneration;
|
|
132
|
+
const session = { definition: document, pending: new Map(), sent: false };
|
|
133
|
+
sessions.set(generation, session);
|
|
134
|
+
return {
|
|
135
|
+
request(index) {
|
|
136
|
+
if (stopped || !sessions.has(generation)) {
|
|
137
|
+
return Promise.resolve(null);
|
|
138
|
+
}
|
|
139
|
+
const existing = session.pending.get(index);
|
|
140
|
+
if (existing !== undefined) {
|
|
141
|
+
return existing.promise;
|
|
142
|
+
}
|
|
143
|
+
let resolve;
|
|
144
|
+
const promise = new Promise((done) => {
|
|
145
|
+
resolve = done;
|
|
146
|
+
});
|
|
147
|
+
session.pending.set(index, { promise, resolve, resent: false });
|
|
148
|
+
const initialized = started;
|
|
149
|
+
initialize();
|
|
150
|
+
// Fallback initialization replays pending requests, including this first one.
|
|
151
|
+
if (initialized || worker !== undefined) {
|
|
152
|
+
request(generation, session, index);
|
|
153
|
+
}
|
|
154
|
+
return promise;
|
|
155
|
+
},
|
|
156
|
+
cancel() {
|
|
157
|
+
settle(session);
|
|
158
|
+
if (started) {
|
|
159
|
+
dispatch({ type: 'cancel', generation });
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
dispose() {
|
|
163
|
+
settle(session);
|
|
164
|
+
sessions.delete(generation);
|
|
165
|
+
if (started) {
|
|
166
|
+
dispatch({ type: 'dispose', generation });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
},
|
|
171
|
+
dispose() {
|
|
172
|
+
stopped = true;
|
|
173
|
+
for (const session of sessions.values()) {
|
|
174
|
+
settle(session);
|
|
175
|
+
}
|
|
176
|
+
sessions.clear();
|
|
177
|
+
stopWorker();
|
|
178
|
+
fallback?.dispose();
|
|
179
|
+
if (options.highlighter === undefined) {
|
|
180
|
+
void highlighter?.then((loaded) => loaded.dispose(), () => undefined);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type ReviewEntry, type ReviewSlot } from './review-plan.js';
|
|
2
|
+
export interface ReviewStack {
|
|
3
|
+
readonly slots: ReadonlyMap<string, ReviewSlot>;
|
|
4
|
+
readonly ready: ReadonlySet<string>;
|
|
5
|
+
promote(key: string): void;
|
|
6
|
+
settle(key: string): void;
|
|
7
|
+
}
|
|
8
|
+
export declare function useReviewStack(entries: readonly ReviewEntry[]): ReviewStack;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import { planReview } from './review-plan.js';
|
|
3
|
+
export function useReviewStack(entries) {
|
|
4
|
+
const [promoted, setPromoted] = useState(null);
|
|
5
|
+
const slots = useMemo(() => planReview(entries, promoted), [entries, promoted]);
|
|
6
|
+
const selected = [...slots].filter(([, slot]) => slot === 'editor').map(([key]) => key);
|
|
7
|
+
const [admitted, setAdmitted] = useState(() => selected.slice(0, 1));
|
|
8
|
+
const retained = admitted.filter((key) => selected.includes(key));
|
|
9
|
+
const live = retained.length === 0 ? selected.slice(0, 1) : retained;
|
|
10
|
+
if (live.length !== admitted.length || live.some((key, index) => key !== admitted[index])) {
|
|
11
|
+
setAdmitted(live);
|
|
12
|
+
}
|
|
13
|
+
const [settled, setSettled] = useState(() => new Set());
|
|
14
|
+
if ([...settled].some((key) => !selected.includes(key))) {
|
|
15
|
+
setSettled(new Set([...settled].filter((key) => selected.includes(key))));
|
|
16
|
+
}
|
|
17
|
+
const settle = useCallback((key) => {
|
|
18
|
+
setSettled((previous) => (previous.has(key) ? previous : new Set([...previous, key])));
|
|
19
|
+
}, []);
|
|
20
|
+
const allSettled = live.every((key) => settled.has(key));
|
|
21
|
+
const next = selected.find((key) => !live.includes(key));
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (next === undefined || !allSettled) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const frame = window.requestAnimationFrame(() => setAdmitted((previous) => [...previous, next]));
|
|
27
|
+
return () => window.cancelAnimationFrame(frame);
|
|
28
|
+
}, [next, allSettled]);
|
|
29
|
+
return { slots, ready: new Set(live), promote: setPromoted, settle };
|
|
30
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { LineToken } from '@adecore/editor';
|
|
2
|
+
import { type DiffSyntaxDocument } from '@adecore/editor/shiki-worker';
|
|
3
|
+
import type { DiffDocument } from '@adecore/merge';
|
|
4
|
+
import { type DiffSyntaxRuntime } from './diff/syntax-runtime.js';
|
|
5
|
+
export { createDiffSyntaxRuntime, diffSyntaxRuntime } from './diff/syntax-runtime.js';
|
|
6
|
+
export type { DiffSyntaxRuntime, DiffSyntaxRuntimeOptions, DiffSyntaxSession } from './diff/syntax-runtime.js';
|
|
7
|
+
export interface DiffSyntaxMapping {
|
|
8
|
+
readonly documents: Readonly<Record<'old' | 'new', DiffSyntaxDocument>>;
|
|
9
|
+
locate(side: 'old' | 'new', index: number): {
|
|
10
|
+
readonly block: number;
|
|
11
|
+
readonly line: number;
|
|
12
|
+
} | undefined;
|
|
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
|
+
export declare function diffSourceTokens(document: DiffDocument, mapping: DiffSyntaxMapping, runtime?: DiffSyntaxRuntime): DiffSourceTokenSession;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { lineTokensOf } from '@adecore/editor/shiki-worker';
|
|
2
|
+
import { diffSyntaxRuntime } from './diff/syntax-runtime.js';
|
|
3
|
+
export { createDiffSyntaxRuntime, diffSyntaxRuntime } from './diff/syntax-runtime.js';
|
|
4
|
+
/* Structurally accepts the editor's source mapping without depending on its React renderer. */
|
|
5
|
+
export function diffSourceTokens(document, mapping, runtime = diffSyntaxRuntime()) {
|
|
6
|
+
const sides = { old: runtime.open(mapping.documents.old), new: runtime.open(mapping.documents.new) };
|
|
7
|
+
const cache = { old: new Map(), new: new Map() };
|
|
8
|
+
const pending = { old: new Set(), new: new Set() };
|
|
9
|
+
const listeners = new Set();
|
|
10
|
+
let disposed = false;
|
|
11
|
+
return {
|
|
12
|
+
get(side, index) {
|
|
13
|
+
const location = mapping.locate(side, index);
|
|
14
|
+
if (disposed || location === undefined || document.sides[side][index] === undefined) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
const { block, line } = location;
|
|
18
|
+
const known = cache[side].get(block);
|
|
19
|
+
if (known !== undefined) {
|
|
20
|
+
return known[line] ?? null;
|
|
21
|
+
}
|
|
22
|
+
if (!pending[side].has(block)) {
|
|
23
|
+
pending[side].add(block);
|
|
24
|
+
const text = mapping.documents[side].blocks[block].split('\n');
|
|
25
|
+
const publish = (lines) => {
|
|
26
|
+
if (disposed) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
pending[side].delete(block);
|
|
30
|
+
cache[side].set(block, lines);
|
|
31
|
+
for (const listener of [...listeners]) {
|
|
32
|
+
listener();
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const count = text.length;
|
|
36
|
+
void sides[side].request(block).then((packet) => {
|
|
37
|
+
if (!disposed) {
|
|
38
|
+
publish(text.map((text, line) => (packet === null || packet.lines.length !== count + 1 ? null : lineTokensOf(packet, line, text))));
|
|
39
|
+
}
|
|
40
|
+
}, () => publish(text.map(() => null)));
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
},
|
|
44
|
+
subscribe(listener) {
|
|
45
|
+
if (!disposed) {
|
|
46
|
+
listeners.add(listener);
|
|
47
|
+
}
|
|
48
|
+
return () => {
|
|
49
|
+
listeners.delete(listener);
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
dispose() {
|
|
53
|
+
disposed = true;
|
|
54
|
+
listeners.clear();
|
|
55
|
+
sides.old.dispose();
|
|
56
|
+
sides.new.dispose();
|
|
57
|
+
cache.old.clear();
|
|
58
|
+
cache.new.clear();
|
|
59
|
+
pending.old.clear();
|
|
60
|
+
pending.new.clear();
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -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
|
-
|
|
20
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
39
|
-
|
|
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 {
|