@cueloop/client 0.1.0-alpha.85 → 0.1.0-alpha.87

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/src/view-diff.ts CHANGED
@@ -31,6 +31,10 @@ export interface DiffRow {
31
31
  newLine?: number;
32
32
  }
33
33
 
34
+ function isCodeRow(row: DiffRow | undefined): row is DiffRow {
35
+ return row !== undefined && row.kind !== "file" && row.kind !== "hunk";
36
+ }
37
+
34
38
  export function diffRows(patchText: string): DiffRow[] {
35
39
  const rows: DiffRow[] = [];
36
40
  const patches = parsePatchFiles(patchText);
@@ -168,6 +172,73 @@ export function marksByRows(
168
172
  return marksByIndex;
169
173
  }
170
174
 
175
+ function addResolvedMark(
176
+ marksByIndex: Map<number, Mark[]>,
177
+ annotation: Annotation,
178
+ blocks: Block[],
179
+ startBlockIndex: number,
180
+ endBlockIndex: number,
181
+ focusedId?: string,
182
+ ): void {
183
+ const startAnchor = annotation.reviewComment?.startAnchor ?? annotation.anchor;
184
+ const span: TextSpan = {
185
+ start: {
186
+ blockIndex: startBlockIndex,
187
+ char: Math.min(startAnchor.start ?? 0, blocks[startBlockIndex]!.text.length),
188
+ },
189
+ end: {
190
+ blockIndex: endBlockIndex,
191
+ char: Math.min(
192
+ annotation.anchor.end ?? annotation.anchor.quote.length,
193
+ blocks[endBlockIndex]!.text.length,
194
+ ),
195
+ },
196
+ };
197
+
198
+ for (let rowIndex = startBlockIndex; rowIndex <= endBlockIndex; rowIndex++) {
199
+ const range = spanRangeInBlock(span, rowIndex, blocks[rowIndex]!.text.length);
200
+
201
+ const marksEmptyLine =
202
+ range === null &&
203
+ startBlockIndex === endBlockIndex &&
204
+ rowIndex === startBlockIndex &&
205
+ blocks[rowIndex]!.text.length === 0;
206
+
207
+ if (!range && !marksEmptyLine) continue;
208
+ const marks = marksByIndex.get(rowIndex) ?? [];
209
+
210
+ marks.push({
211
+ start: range?.start ?? 0,
212
+ end: range?.end ?? 0,
213
+ role: annotation.id === focusedId ? "mark-focus" : "mark-comment",
214
+ annotationId: annotation.id,
215
+ span,
216
+ });
217
+ marksByIndex.set(rowIndex, marks);
218
+ }
219
+ }
220
+
221
+ function addReviewFindingMark(
222
+ marksByIndex: Map<number, Mark[]>,
223
+ annotation: Annotation,
224
+ rows: DiffRow[],
225
+ blocks: Block[],
226
+ focusedId?: string,
227
+ ): boolean {
228
+ const finding = annotation.reviewComment;
229
+
230
+ if (!finding || isAddressed(annotation)) return false;
231
+ const end = resolveReviewAnchorRow(rows, annotation.anchor, finding.path, finding.side);
232
+ const start = finding.startAnchor
233
+ ? resolveReviewAnchorRow(rows, finding.startAnchor, finding.path, finding.side)
234
+ : end;
235
+
236
+ if (!start || !end || start.rowIndex > end.rowIndex) return false;
237
+ addResolvedMark(marksByIndex, annotation, blocks, start.rowIndex, end.rowIndex, focusedId);
238
+
239
+ return true;
240
+ }
241
+
171
242
  /**
172
243
  * A whole file rendered as context rows, so the diff sheet can show and annotate a plain file the
173
244
  * same way it does a diff: every line is unchanged, numbered on both sides (the file view collapses
@@ -225,7 +296,76 @@ export function fileTargetMarks(
225
296
 
226
297
  if (!range) continue;
227
298
  const base = range.start;
228
- const fileMarks = marksByRows(fileAnnotations, rows.slice(range.start, range.end), focusedId);
299
+ const fileRows = rows.slice(range.start, range.end);
300
+ const reviewRootIds = new Set(
301
+ fileAnnotations.flatMap((annotation) =>
302
+ annotation.reviewComment === undefined ? [] : [annotation.id],
303
+ ),
304
+ );
305
+ const ordinaryAnnotations = fileAnnotations.filter(
306
+ (annotation) =>
307
+ annotation.reviewComment === undefined &&
308
+ (annotation.replyTo === undefined || !reviewRootIds.has(annotation.replyTo)),
309
+ );
310
+ const fileMarks = marksByRows(ordinaryAnnotations, fileRows, focusedId);
311
+ const blocks = diffRowBlocks(fileRows);
312
+ const resolvedIds = new Set(
313
+ [...fileMarks.values()]
314
+ .flat()
315
+ .flatMap((mark) => (mark.annotationId === undefined ? [] : [mark.annotationId])),
316
+ );
317
+
318
+ for (const annotation of fileAnnotations) {
319
+ const finding = annotation.reviewComment;
320
+
321
+ if (!finding || isAddressed(annotation)) continue;
322
+ if (addReviewFindingMark(fileMarks, annotation, fileRows, blocks, focusedId)) {
323
+ resolvedIds.add(annotation.id);
324
+ continue;
325
+ }
326
+ const relativeRow = fileRows.findIndex(
327
+ (row) =>
328
+ (finding.side === "RIGHT" ? row.newLine : row.oldLine) === finding.line &&
329
+ row.kind !== "file" &&
330
+ row.kind !== "hunk",
331
+ );
332
+ const fallbackRow = relativeRow === -1 ? 0 : relativeRow;
333
+ const mark: Mark = {
334
+ start: 0,
335
+ end: 0,
336
+ role: annotation.id === focusedId ? "mark-focus" : "mark-comment",
337
+ annotationId: annotation.id,
338
+ outdated: true,
339
+ span: {
340
+ start: { blockIndex: fallbackRow, char: 0 },
341
+ end: { blockIndex: fallbackRow, char: 0 },
342
+ },
343
+ };
344
+
345
+ fileMarks.set(fallbackRow, [...(fileMarks.get(fallbackRow) ?? []), mark]);
346
+ resolvedIds.add(annotation.id);
347
+ }
348
+
349
+ for (const annotation of fileAnnotations) {
350
+ if (!annotation.replyTo || resolvedIds.has(annotation.id) || isAddressed(annotation))
351
+ continue;
352
+ const parent = [...fileMarks.entries()].find(([, marks]) =>
353
+ marks.some((mark) => mark.annotationId === annotation.replyTo),
354
+ );
355
+
356
+ if (!parent) continue;
357
+ const [fallbackRow, parentMarks] = parent;
358
+ const parentMark = parentMarks.find((mark) => mark.annotationId === annotation.replyTo)!;
359
+ const replyMark: Mark = {
360
+ ...parentMark,
361
+ role: annotation.id === focusedId ? "mark-focus" : "mark-comment",
362
+ annotationId: annotation.id,
363
+ outdated: parentMark.outdated,
364
+ };
365
+
366
+ fileMarks.set(fallbackRow, [...parentMarks, replyMark]);
367
+ resolvedIds.add(annotation.id);
368
+ }
229
369
 
230
370
  for (const [relativeRow, marks] of fileMarks) {
231
371
  result.set(
@@ -279,8 +419,17 @@ export function changesMarks(
279
419
  const artifactNotes = session.annotations.filter(
280
420
  (annotation) => annotationTarget(annotation).kind === "artifact",
281
421
  );
422
+ const marks = marksByRows(artifactNotes, rows, focusedId);
423
+
424
+ // Agent PR findings carry a file target so repeated text cannot bind across files.
425
+ // Plain diff comments still use the artifact target for backward compatibility.
426
+ if (session.artifact.meta.pr !== undefined) {
427
+ for (const [row, fileMarks] of fileTargetMarks(session.annotations, rows, focusedId)) {
428
+ marks.set(row, [...(marks.get(row) ?? []), ...fileMarks]);
429
+ }
430
+ }
282
431
 
283
- return marksByRows(artifactNotes, rows, focusedId);
432
+ return marks;
284
433
  }
285
434
 
286
435
  return fileTargetMarks(session.annotations, rows, focusedId);
@@ -308,22 +457,63 @@ export function commentCountsByFile(session: Thread, rows: DiffRow[]): Map<strin
308
457
  /** Quote-primary anchor for a diff row: neighbors as context selectors. */
309
458
  export function diffRowAnchor(rows: DiffRow[], rowIndex: number) {
310
459
  const row = rows[rowIndex]!;
460
+ const context = diffRowAnchorContext(rows, rowIndex);
461
+
462
+ return {
463
+ quote: diffRowText(row),
464
+ ...context,
465
+ blockIndex: rowIndex,
466
+ endBlockIndex: rowIndex,
467
+ start: 0,
468
+ end: diffRowText(row).length,
469
+ };
470
+ }
471
+
472
+ function diffRowAnchorContext(
473
+ rows: DiffRow[],
474
+ rowIndex: number,
475
+ ): Pick<Annotation["anchor"], "prefix" | "suffix"> {
311
476
  const prev = rows[rowIndex - 1];
312
477
  const next = rows[rowIndex + 1];
313
478
 
314
479
  return {
315
- quote: row.text,
316
- prefix:
317
- prev && (prev.kind === "ctx" || prev.kind === "add" || prev.kind === "del")
318
- ? prev.text.slice(-24)
319
- : "",
320
- suffix:
321
- next && (next.kind === "ctx" || next.kind === "add" || next.kind === "del")
322
- ? next.text.slice(0, 24)
323
- : "",
480
+ prefix: isCodeRow(prev) ? diffRowText(prev).slice(-24) : "",
481
+ suffix: isCodeRow(next) ? diffRowText(next).slice(0, 24) : "",
324
482
  };
325
483
  }
326
484
 
485
+ /** Resolve a publishable PR finding exactly; fuzzy matches must remain visibly outdated. */
486
+ export function resolveReviewAnchorRow(
487
+ rows: DiffRow[],
488
+ anchor: Annotation["anchor"],
489
+ path: string,
490
+ side: "LEFT" | "RIGHT",
491
+ ): { row: DiffRow; rowIndex: number } | undefined {
492
+ const candidates = rows.flatMap((row, rowIndex) => {
493
+ if (
494
+ row.file !== path ||
495
+ diffRowText(row) !== anchor.quote ||
496
+ (side === "RIGHT" ? row.newLine === undefined : row.oldLine === undefined)
497
+ )
498
+ return [];
499
+ const context = diffRowAnchorContext(rows, rowIndex);
500
+
501
+ return [
502
+ {
503
+ row,
504
+ rowIndex,
505
+ score: Number(context.prefix === anchor.prefix) + Number(context.suffix === anchor.suffix),
506
+ },
507
+ ];
508
+ });
509
+
510
+ if (candidates.length === 0) return undefined;
511
+ const bestScore = Math.max(...candidates.map((candidate) => candidate.score));
512
+ const best = candidates.filter((candidate) => candidate.score === bestScore);
513
+
514
+ return best.length === 1 ? best[0] : undefined;
515
+ }
516
+
327
517
  /** Location label for the rail and feedback: file:newLine (or old for del). */
328
518
  export function diffRowLocation(row: DiffRow): string {
329
519
  const line = row.newLine ?? row.oldLine;
package/src/view-plan.ts CHANGED
@@ -361,6 +361,8 @@ export interface Mark {
361
361
  annotationId?: string;
362
362
  /** The whole anchored stretch in display coordinates; one thread per span. */
363
363
  span?: TextSpan;
364
+ /** The recorded PR line remains visible, but its quote no longer resolves. */
365
+ outdated?: boolean;
364
366
  }
365
367
 
366
368
  /**