dsh-diff-approval 0.36.1 → 0.37.1
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/README.md +11 -11
- package/README.zh.md +11 -11
- package/docs/images/comment-card.png +0 -0
- package/docs/images/comment-card.zh.png +0 -0
- package/docs/images/docked-sidebar.png +0 -0
- package/docs/images/docked-sidebar.zh.png +0 -0
- package/docs/images/floating-panel.png +0 -0
- package/docs/images/floating-panel.zh.png +0 -0
- package/docs/images/markdown-preview.png +0 -0
- package/docs/images/markdown-preview.zh.png +0 -0
- package/lib/client.js +9666 -866
- package/lib/index.js +158 -8
- package/lib/types/client/comment-card-seat.d.ts +24 -0
- package/lib/types/client/comment-card.d.ts +69 -0
- package/lib/types/client/locales.d.ts +10 -0
- package/lib/types/client/markdown-preview.d.ts +195 -14
- package/lib/types/client/markdown-tree-blocks.d.ts +90 -0
- package/lib/types/client/markdown-tree-match.d.ts +163 -0
- package/lib/types/client/settings.d.ts +14 -0
- package/lib/types/comment-ask.d.ts +15 -0
- package/lib/types/types.d.ts +18 -0
- package/package.json +2 -1
- package/docs/images/file-list-collapsed.png +0 -0
- package/docs/images/file-list-collapsed.zh.png +0 -0
- package/docs/images/fullscreen.png +0 -0
- package/docs/images/fullscreen.zh.png +0 -0
- package/docs/images/markdown.png +0 -0
- package/docs/images/markdown.zh.png +0 -0
- package/docs/images/pending-panel.png +0 -0
- package/docs/images/pending-panel.zh.png +0 -0
package/lib/index.js
CHANGED
|
@@ -2503,7 +2503,7 @@ var CommentAsker = class {
|
|
|
2503
2503
|
const comment = this.comments.get(commentId);
|
|
2504
2504
|
if (comment === void 0 || !inScope(comment.sessionId)) return { outcome: "missing" };
|
|
2505
2505
|
const controller = this.ctx.get("sessionController");
|
|
2506
|
-
if (typeof controller?.prompt !== "function") {
|
|
2506
|
+
if (typeof controller?.prompt !== "function" && typeof controller?.resolveAgent !== "function") {
|
|
2507
2507
|
this.ctx.logger.warn("diff-approval: no sessionController.prompt in this host, so a comment cannot be asked");
|
|
2508
2508
|
return { outcome: "no-agent" };
|
|
2509
2509
|
}
|
|
@@ -2514,6 +2514,22 @@ var CommentAsker = class {
|
|
|
2514
2514
|
this.watch(sessionId);
|
|
2515
2515
|
const requestId = randomUUID();
|
|
2516
2516
|
this.comments.recordAsk(sessionId, commentId, requestId, void 0, text);
|
|
2517
|
+
const source = {
|
|
2518
|
+
kind: "diff-comment",
|
|
2519
|
+
rpcId: requestId,
|
|
2520
|
+
commentId,
|
|
2521
|
+
path: comment.path,
|
|
2522
|
+
startLine: comment.anchor.startLine,
|
|
2523
|
+
endLine: comment.anchor.endLine
|
|
2524
|
+
};
|
|
2525
|
+
if (await this.deliverAsCard(sessionId, prompt, source)) return {
|
|
2526
|
+
outcome: "asked",
|
|
2527
|
+
requestId
|
|
2528
|
+
};
|
|
2529
|
+
if (typeof controller.prompt !== "function") return {
|
|
2530
|
+
outcome: "no-agent",
|
|
2531
|
+
requestId
|
|
2532
|
+
};
|
|
2517
2533
|
try {
|
|
2518
2534
|
await controller.prompt({
|
|
2519
2535
|
requestId,
|
|
@@ -2539,6 +2555,53 @@ var CommentAsker = class {
|
|
|
2539
2555
|
}
|
|
2540
2556
|
}
|
|
2541
2557
|
/**
|
|
2558
|
+
* Deliver one comment through the host's WAKE-UP seam, so the shell draws it as a trigger card.
|
|
2559
|
+
*
|
|
2560
|
+
* The seam is the one the host's own reminders use: resolve the live agent, publish a user message on
|
|
2561
|
+
* it, then flush. The message's `source` is what the shell's closed switch turns into a card, and this
|
|
2562
|
+
* plugin's client retitles its own source on the rendered DOM (an unknown kind falls back to a generic
|
|
2563
|
+
* title). A host without the seam — or any failure asking for it — answers false, and the caller falls
|
|
2564
|
+
* back to an ordinary prompt rather than losing the question.
|
|
2565
|
+
*
|
|
2566
|
+
* @param sessionId - the session whose live agent wakes.
|
|
2567
|
+
* @param prompt - the prompt text to deliver verbatim.
|
|
2568
|
+
* @param source - the comment's source marker, carried onto the message.
|
|
2569
|
+
* @returns whether the message was published on the live agent.
|
|
2570
|
+
*/
|
|
2571
|
+
async deliverAsCard(sessionId, prompt, source) {
|
|
2572
|
+
const controller = this.ctx.get("sessionController");
|
|
2573
|
+
if (typeof controller?.resolveAgent !== "function") return false;
|
|
2574
|
+
let resolved;
|
|
2575
|
+
try {
|
|
2576
|
+
resolved = await controller.resolveAgent(sessionId);
|
|
2577
|
+
} catch {
|
|
2578
|
+
return false;
|
|
2579
|
+
}
|
|
2580
|
+
if (resolved === void 0 || resolved.error !== void 0 || resolved.agent === void 0) return false;
|
|
2581
|
+
const agent = resolved.agent;
|
|
2582
|
+
if (typeof agent.followup !== "function") return false;
|
|
2583
|
+
try {
|
|
2584
|
+
agent.followup({
|
|
2585
|
+
id: randomUUID(),
|
|
2586
|
+
role: "user",
|
|
2587
|
+
content: [{
|
|
2588
|
+
type: "text",
|
|
2589
|
+
text: prompt
|
|
2590
|
+
}],
|
|
2591
|
+
source
|
|
2592
|
+
});
|
|
2593
|
+
} catch {
|
|
2594
|
+
return false;
|
|
2595
|
+
}
|
|
2596
|
+
const sessions = this.ctx.get("sessions");
|
|
2597
|
+
if (typeof sessions?.flush === "function" && agent.session !== void 0) try {
|
|
2598
|
+
if (!await sessions.flush(agent.session)) this.ctx.logger.warn("diff-approval: the host did not persist the comment delivery");
|
|
2599
|
+
} catch (error) {
|
|
2600
|
+
this.ctx.logger.warn(`diff-approval: flushing the comment delivery failed: ${rejectionText(error)}`);
|
|
2601
|
+
}
|
|
2602
|
+
return true;
|
|
2603
|
+
}
|
|
2604
|
+
/**
|
|
2542
2605
|
* One scope's questions, grouped by THE TRANSCRIPT each was submitted into.
|
|
2543
2606
|
*
|
|
2544
2607
|
* A lineage's threads may hold questions asked from several seats, and an answer lives in the
|
|
@@ -6548,8 +6611,17 @@ function apply(ctx, config) {
|
|
|
6548
6611
|
ok: true,
|
|
6549
6612
|
value: { outcome: "missing" }
|
|
6550
6613
|
};
|
|
6551
|
-
const
|
|
6552
|
-
|
|
6614
|
+
const chars = charSpansOf(blockTarget.block);
|
|
6615
|
+
let updatedOld;
|
|
6616
|
+
if (chars === void 0 || !charSpansStayOnOneLine(entry.oldText, entry.newText, chars)) {
|
|
6617
|
+
const accepted = contentRangeOf(entry.newText, blockTarget.block.newStart, blockTarget.block.newEnd);
|
|
6618
|
+
updatedOld = replaceContentLines(entry.oldText, blockTarget.block.oldStart, blockTarget.block.oldEnd, accepted);
|
|
6619
|
+
} else {
|
|
6620
|
+
const accepted = entry.newText.slice(chars.new.start, chars.new.end);
|
|
6621
|
+
const spliced = spliceChars(entry.oldText, chars.old.start, chars.old.end, accepted);
|
|
6622
|
+
if (spliced === void 0) return rpcError("block character span does not fit the file");
|
|
6623
|
+
updatedOld = spliced;
|
|
6624
|
+
}
|
|
6553
6625
|
store.update(blockTarget.id, { oldText: updatedOld });
|
|
6554
6626
|
const afterEntry = {
|
|
6555
6627
|
...entry,
|
|
@@ -6602,9 +6674,18 @@ function apply(ctx, config) {
|
|
|
6602
6674
|
ok: true,
|
|
6603
6675
|
value: { outcome: "missing" }
|
|
6604
6676
|
};
|
|
6605
|
-
const
|
|
6606
|
-
|
|
6607
|
-
|
|
6677
|
+
const revertChars = charSpansOf(blockTarget.block);
|
|
6678
|
+
let revertedText;
|
|
6679
|
+
if (revertChars === void 0 || !charSpansStayOnOneLine(entry.oldText, entry.newText, revertChars)) {
|
|
6680
|
+
const restored = contentRangeOf(entry.oldText, blockTarget.block.oldStart, blockTarget.block.oldEnd);
|
|
6681
|
+
revertedText = replaceContentLines(entry.newText, blockTarget.block.newStart, blockTarget.block.newEnd, restored);
|
|
6682
|
+
} else {
|
|
6683
|
+
const restored = entry.oldText.slice(revertChars.old.start, revertChars.old.end);
|
|
6684
|
+
const spliced = spliceChars(entry.newText, revertChars.new.start, revertChars.new.end, restored);
|
|
6685
|
+
if (spliced === void 0) return rpcError("block character span does not fit the file");
|
|
6686
|
+
revertedText = spliced;
|
|
6687
|
+
}
|
|
6688
|
+
const content = reencodeEol(revertedText, detectEol(entry.newText));
|
|
6608
6689
|
store.update(blockTarget.id, { newText: content });
|
|
6609
6690
|
const afterEntry = {
|
|
6610
6691
|
...entry,
|
|
@@ -6639,7 +6720,7 @@ function apply(ctx, config) {
|
|
|
6639
6720
|
}
|
|
6640
6721
|
if (undo !== void 0) pushUndo(blockTarget.sessionId, undo.before, undo.after);
|
|
6641
6722
|
persistSession();
|
|
6642
|
-
const fullyResolved = contentEqual(
|
|
6723
|
+
const fullyResolved = contentEqual(revertedText, entry.oldText);
|
|
6643
6724
|
if (fullyResolved && blockTarget.removeWhenResolved === true) {
|
|
6644
6725
|
const resolvedDrop = droppingComments({
|
|
6645
6726
|
id: entry.id,
|
|
@@ -7233,6 +7314,8 @@ function blockTargetOf(payload) {
|
|
|
7233
7314
|
newStart,
|
|
7234
7315
|
newEnd
|
|
7235
7316
|
].every((value) => typeof value === "number" && Number.isFinite(value))) return void 0;
|
|
7317
|
+
const oldChars = charSpanOf(block.oldChars);
|
|
7318
|
+
const newChars = charSpanOf(block.newChars);
|
|
7236
7319
|
const removeWhenResolved = payload.removeWhenResolved;
|
|
7237
7320
|
return {
|
|
7238
7321
|
...target,
|
|
@@ -7240,11 +7323,78 @@ function blockTargetOf(payload) {
|
|
|
7240
7323
|
oldStart,
|
|
7241
7324
|
oldEnd,
|
|
7242
7325
|
newStart,
|
|
7243
|
-
newEnd
|
|
7326
|
+
newEnd,
|
|
7327
|
+
...oldChars !== void 0 && newChars !== void 0 ? {
|
|
7328
|
+
oldChars,
|
|
7329
|
+
newChars
|
|
7330
|
+
} : {}
|
|
7244
7331
|
},
|
|
7245
7332
|
removeWhenResolved: typeof removeWhenResolved === "boolean" ? removeWhenResolved : void 0
|
|
7246
7333
|
};
|
|
7247
7334
|
}
|
|
7335
|
+
/** Narrow one wire character span: two finite, non-negative, ordered offsets. */
|
|
7336
|
+
function charSpanOf(value) {
|
|
7337
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
7338
|
+
const { start, end } = value;
|
|
7339
|
+
if (typeof start !== "number" || typeof end !== "number") return void 0;
|
|
7340
|
+
if (!Number.isInteger(start) || !Number.isInteger(end)) return void 0;
|
|
7341
|
+
if (start < 0 || end < start) return void 0;
|
|
7342
|
+
return {
|
|
7343
|
+
start,
|
|
7344
|
+
end
|
|
7345
|
+
};
|
|
7346
|
+
}
|
|
7347
|
+
/**
|
|
7348
|
+
* The pair of character spans a block names, when it names both.
|
|
7349
|
+
*
|
|
7350
|
+
* A block carries either both spans (a preview's sub-line change) or neither (every line-based caller); one
|
|
7351
|
+
* without the other is meaningless and is treated as absent.
|
|
7352
|
+
*
|
|
7353
|
+
* @param block - the incoming block range.
|
|
7354
|
+
* @returns the two spans, or undefined for a line-based block.
|
|
7355
|
+
*/
|
|
7356
|
+
function charSpansOf(block) {
|
|
7357
|
+
if (block.oldChars === void 0 || block.newChars === void 0) return void 0;
|
|
7358
|
+
return {
|
|
7359
|
+
old: block.oldChars,
|
|
7360
|
+
new: block.newChars
|
|
7361
|
+
};
|
|
7362
|
+
}
|
|
7363
|
+
/**
|
|
7364
|
+
* Whether character spans really stay inside ONE line on both sides.
|
|
7365
|
+
*
|
|
7366
|
+
* A character-level keep/revert is only safe within a line. A caller can (and an older renderer did) send a
|
|
7367
|
+
* span that crosses a newline — splicing that accepted text into the middle of a line corrupts the baseline,
|
|
7368
|
+
* duplicating or swallowing lines and making the block impossible to keep afterwards. Such a span falls back
|
|
7369
|
+
* to its line range, which the block always carries.
|
|
7370
|
+
*
|
|
7371
|
+
* @param oldText - the entry's baseline.
|
|
7372
|
+
* @param newText - the entry's current content.
|
|
7373
|
+
* @param spans - the character spans to check.
|
|
7374
|
+
* @returns true when both spanned slices are single-line (or empty).
|
|
7375
|
+
*/
|
|
7376
|
+
function charSpansStayOnOneLine(oldText, newText, spans) {
|
|
7377
|
+
const oldSlice = oldText.slice(spans.old.start, spans.old.end);
|
|
7378
|
+
const newSlice = newText.slice(spans.new.start, spans.new.end);
|
|
7379
|
+
return !oldSlice.includes("\n") && !newSlice.includes("\n");
|
|
7380
|
+
}
|
|
7381
|
+
/**
|
|
7382
|
+
* Replace `[start, end)` of `text` with `insert`, refusing a span that does not fit.
|
|
7383
|
+
*
|
|
7384
|
+
* The character-level counterpart of `replaceContentLines` (which works in whole lines): a Markdown preview
|
|
7385
|
+
* block can be smaller than a line — a changed table cell beside an unchanged one — and keeping or reverting
|
|
7386
|
+
* just that change must not touch its neighbours on the same line.
|
|
7387
|
+
*
|
|
7388
|
+
* @param text - the file the span indexes.
|
|
7389
|
+
* @param start - first character to replace, 0-based.
|
|
7390
|
+
* @param end - offset just past the last character to replace.
|
|
7391
|
+
* @param insert - what takes its place.
|
|
7392
|
+
* @returns the new text, or undefined when the span is out of range.
|
|
7393
|
+
*/
|
|
7394
|
+
function spliceChars(text, start, end, insert) {
|
|
7395
|
+
if (start > end || end > text.length) return void 0;
|
|
7396
|
+
return text.slice(0, start) + insert + text.slice(end);
|
|
7397
|
+
}
|
|
7248
7398
|
/** Narrow a wire payload to one keep/revert target. */
|
|
7249
7399
|
function targetOf(payload) {
|
|
7250
7400
|
const sessionId = sessionOf(payload);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The session-scoped seat that publishes the shown conversation to the comment-card bridge.
|
|
3
|
+
*
|
|
4
|
+
* It renders nothing: the shell hands every session-scope slot component `sessionId` and `useChat`, and
|
|
5
|
+
* the published snapshot is what lets the DOM side join a rendered trigger card's node key back to the
|
|
6
|
+
* comment it delivered. Both props are read defensively, so a shell that hands only one of them still
|
|
7
|
+
* leaves the other facts intact.
|
|
8
|
+
*
|
|
9
|
+
* @module dsh-diff-approval/client/comment-card-seat
|
|
10
|
+
*/
|
|
11
|
+
import type { ReactNode } from 'react';
|
|
12
|
+
/** Props the shell injects into a session-scoped seat. */
|
|
13
|
+
export interface CommentCardSeatProps {
|
|
14
|
+
/** The session this seat's page is showing. */
|
|
15
|
+
sessionId?: unknown;
|
|
16
|
+
/** The chat snapshot selector, when this shell merges it onto session-scope slots. */
|
|
17
|
+
useChat?: unknown;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Publish the shown conversation's snapshot for the comment-card titles.
|
|
21
|
+
* @param props - the shell-injected session id and chat hook.
|
|
22
|
+
* @returns null; this seat is invisible by design.
|
|
23
|
+
*/
|
|
24
|
+
export declare function CommentCardSeat(props: CommentCardSeatProps): ReactNode;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Title the shell's trigger card for a COMMENT delivered from the review panel.
|
|
3
|
+
*
|
|
4
|
+
* A comment asked of an idle agent is delivered through the host's wake-up seam (see
|
|
5
|
+
* `src/comment-ask.ts`), so the shell draws that turn's wake message as its own `turn-trigger` card —
|
|
6
|
+
* the same card a scheduled reminder or a shortcut gets. The card's title comes from a CLOSED switch
|
|
7
|
+
* over `source.kind` inside the shell, and a kind it does not know falls back to the generic "execution
|
|
8
|
+
* requested"; there is no seam to teach it a new kind without taking over the renderer for EVERY wake
|
|
9
|
+
* source. So the title is corrected here, in the DOM, joined through the node key the shell already
|
|
10
|
+
* puts on the card.
|
|
11
|
+
*
|
|
12
|
+
* Only cards whose source kind is this plugin's own (`diff-comment`) are touched: a reminder, a goal
|
|
13
|
+
* or another plugin's delivery keeps exactly the title it had.
|
|
14
|
+
*
|
|
15
|
+
* @module dsh-diff-approval/client/comment-card
|
|
16
|
+
*/
|
|
17
|
+
/** One delivered comment, as the chat snapshot exposes it. */
|
|
18
|
+
export interface CommentCardMeta {
|
|
19
|
+
/** The reviewed file, as the comment recorded it. */
|
|
20
|
+
path: string;
|
|
21
|
+
/** First new-file line of the comment's anchor. */
|
|
22
|
+
startLine: number;
|
|
23
|
+
/** Last new-file line of the comment's anchor. */
|
|
24
|
+
endLine: number;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Build the node key → comment map from one chat snapshot.
|
|
28
|
+
* @param snapshot - the chat snapshot handed to the seat.
|
|
29
|
+
* @returns the delivered comments, by the node key the shell puts on their card.
|
|
30
|
+
*/
|
|
31
|
+
export declare function buildCommentCards(snapshot: unknown): Map<string, CommentCardMeta>;
|
|
32
|
+
/**
|
|
33
|
+
* Publish the shown conversation's delivered comments.
|
|
34
|
+
* @param snapshot - the chat snapshot as the seat received it.
|
|
35
|
+
*/
|
|
36
|
+
export declare function publishCommentCards(snapshot: unknown): void;
|
|
37
|
+
/** Forget the published comments (the seat unmounted). */
|
|
38
|
+
export declare function withdrawCommentCards(): void;
|
|
39
|
+
/** Subscribe a non-React reader to publishes. */
|
|
40
|
+
export declare function subscribeCommentCards(listener: () => void): () => void;
|
|
41
|
+
/** The comment a delivered card came from, or undefined for every other wake source. */
|
|
42
|
+
export declare function publishedCommentCard(nodeKey: string): CommentCardMeta | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* The label a card's title carries: the file's base name and the comment's lines.
|
|
45
|
+
* @param meta - the delivered comment.
|
|
46
|
+
* @returns `README.md:40`, or `README.md:40-42` for a multi-line anchor.
|
|
47
|
+
*/
|
|
48
|
+
export declare function commentCardLabel(meta: CommentCardMeta): string;
|
|
49
|
+
/** The translator this decoration needs (the plugin's own namespace). */
|
|
50
|
+
export type CommentCardTranslate = (key: 'commentCard.title') => string;
|
|
51
|
+
/**
|
|
52
|
+
* Title every rendered trigger card that belongs to one of our delivered comments.
|
|
53
|
+
*
|
|
54
|
+
* Separate from {@link attachCommentCardTitles} so the DOM pass is a plain, testable call.
|
|
55
|
+
* @param t - translator for the plugin's namespace.
|
|
56
|
+
*/
|
|
57
|
+
export declare function titleCommentCards(t: CommentCardTranslate): void;
|
|
58
|
+
/**
|
|
59
|
+
* Title delivered comments' cards, and keep them titled as the chat re-renders.
|
|
60
|
+
*
|
|
61
|
+
* Returns early without a `document`, like every DOM bridge in this plugin: the published bundle is
|
|
62
|
+
* exercised in headless harnesses where those globals do not exist, and a decoration must never be the
|
|
63
|
+
* reason a plugin fails to load.
|
|
64
|
+
* @param options - the plugin's translator.
|
|
65
|
+
* @returns a disposer that stops observing (the cards unmount with the view).
|
|
66
|
+
*/
|
|
67
|
+
export declare function attachCommentCardTitles(options: {
|
|
68
|
+
t: CommentCardTranslate;
|
|
69
|
+
}): () => void;
|
|
@@ -57,6 +57,11 @@ export declare const zh: {
|
|
|
57
57
|
'panel.diffLineHeightDesc': string;
|
|
58
58
|
'panel.mdPreview': string;
|
|
59
59
|
'panel.mdPreviewDesc': string;
|
|
60
|
+
'panel.mdStructure': string;
|
|
61
|
+
'panel.mdStructureDesc': string;
|
|
62
|
+
'panel.sourceDiff': string;
|
|
63
|
+
'panel.invisibleContent': string;
|
|
64
|
+
'commentCard.title': string;
|
|
60
65
|
'panel.codeFont': string;
|
|
61
66
|
'panel.codeFontDesc': string;
|
|
62
67
|
'panel.mdMaxWidth': string;
|
|
@@ -327,6 +332,11 @@ export declare const en: {
|
|
|
327
332
|
'panel.diffLineHeightDesc': string;
|
|
328
333
|
'panel.mdPreview': string;
|
|
329
334
|
'panel.mdPreviewDesc': string;
|
|
335
|
+
'panel.mdStructure': string;
|
|
336
|
+
'panel.mdStructureDesc': string;
|
|
337
|
+
'panel.sourceDiff': string;
|
|
338
|
+
'panel.invisibleContent': string;
|
|
339
|
+
'commentCard.title': string;
|
|
330
340
|
'panel.codeFont': string;
|
|
331
341
|
'panel.codeFontDesc': string;
|
|
332
342
|
'panel.mdMaxWidth': string;
|
|
@@ -1,20 +1,201 @@
|
|
|
1
1
|
/** Self-contained Markdown preview-diff renderer.
|
|
2
2
|
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* `
|
|
6
|
-
*
|
|
7
|
-
*
|
|
3
|
+
* Two styles, chosen by the caller:
|
|
4
|
+
*
|
|
5
|
+
* - `blocks` (the original): the source diff is cut into contiguous add/del/context runs and each run is
|
|
6
|
+
* parsed as a document of its own, then wrapped in a `<div>`. Simple, and wrong for any structure that
|
|
7
|
+
* spans a run boundary — a reworded list item becomes four lists, a changed table row falls out of the
|
|
8
|
+
* table and renders as text, a changed fenced line stops being code.
|
|
9
|
+
* - `structure` (the default): the whole document is rendered ONCE, exactly as the author wrote it, and
|
|
10
|
+
* the diff is mapped onto the elements that came from the changed lines. Nothing is re-parsed, no
|
|
11
|
+
* wrapper is inserted, so the document keeps its own layout. A deletion and the addition that replaces
|
|
12
|
+
* it are shown TOGETHER inside the same element (`~~old~~ new`), and a deleted element with no
|
|
13
|
+
* counterpart is emitted as a struck ghost beside its sibling — inside the same list, table or column.
|
|
14
|
+
*
|
|
15
|
+
* Both styles emit the same contract the panel's anchoring already reads: `data-md-block` / `data-md-kind` on
|
|
16
|
+
* every element that carries a difference BLOCK (what the toolbar numbers, frames and keeps), `data-md-unit`
|
|
17
|
+
* on every element that carries a character-level change inside one (what is highlighted), plus `mdAdd` /
|
|
18
|
+
* `mdDel` for the tint and `mdWordAdd` / `mdWordDel` for the changed characters inside one element.
|
|
19
|
+
*/
|
|
20
|
+
/** How the preview expresses the diff. See the module comment. */
|
|
21
|
+
export type MarkdownPreviewStyle = 'structure' | 'blocks';
|
|
22
|
+
/** An inclusive span of 0-based source lines. */
|
|
23
|
+
interface LineSpan {
|
|
24
|
+
start: number;
|
|
25
|
+
end: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* One character-level change the preview HIGHLIGHTS.
|
|
29
|
+
*
|
|
30
|
+
* The source view's blocks are lines, but a rendered document puts several changes on one line: a table row
|
|
31
|
+
* holds a reworded cell AND a retargeted link, and each is a separate run to highlight. The preview cuts each
|
|
32
|
+
* line block into these units for the drawn marks. What the reader COUNTS and keeps, though, is a BLOCK
|
|
33
|
+
* (`PreviewBlock`): one pure-addition hunk, one table row, or one invisible change — which may hold several
|
|
34
|
+
* units (see `previewBlocks`).
|
|
35
|
+
*/
|
|
36
|
+
export interface PreviewUnit {
|
|
37
|
+
/** Index in the preview's own unit numbering: the `data-md-unit` value (its block is `data-md-block`). */
|
|
38
|
+
index: number;
|
|
39
|
+
/** The source-line block this unit was cut from. */
|
|
40
|
+
lineBlock: number;
|
|
41
|
+
/** The line block's own 1-based inclusive line ranges, carried along for the source view. */
|
|
42
|
+
oldLines: {
|
|
43
|
+
start: number;
|
|
44
|
+
end: number;
|
|
45
|
+
};
|
|
46
|
+
newLines: {
|
|
47
|
+
start: number;
|
|
48
|
+
end: number;
|
|
49
|
+
};
|
|
50
|
+
/** The change's absolute character span per side, `[start, end)`; `start === end` is an insertion. */
|
|
51
|
+
oldChars: {
|
|
52
|
+
start: number;
|
|
53
|
+
end: number;
|
|
54
|
+
};
|
|
55
|
+
newChars: {
|
|
56
|
+
start: number;
|
|
57
|
+
end: number;
|
|
58
|
+
};
|
|
59
|
+
/** The changed source itself on each side, for matching the runs that were drawn. */
|
|
60
|
+
oldText: string;
|
|
61
|
+
newText: string;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Cut the file's change blocks into the preview's units.
|
|
65
|
+
*
|
|
66
|
+
* A line block is worked through LINE BY LINE: its changed old lines and changed new lines are paired in
|
|
67
|
+
* order (the k-th removed line with the k-th added one, the same reading the side-by-side view uses), and each
|
|
68
|
+
* pair is diffed again character by character — every run of changed characters becomes a unit, and runs a
|
|
69
|
+
* few characters apart on that line are folded into one so a reworded sentence stays one change rather than
|
|
70
|
+
* three. A changed line with no counterpart is a pure deletion or addition and is one unit itself over its
|
|
71
|
+
* whole line.
|
|
72
|
+
*
|
|
73
|
+
* Working per line is what keeps a deleted paragraph from being swallowed by the retargeted link above it: a
|
|
74
|
+
* character diff run over a whole multi-line block would fold the newline into the change and report the link
|
|
75
|
+
* and the paragraph as one unit, and a unit is a hover target and a Keep/Revert action.
|
|
76
|
+
*
|
|
77
|
+
* @param oldText - the file's tracked baseline.
|
|
78
|
+
* @param newText - the file's current content.
|
|
79
|
+
* @returns the units, in document order.
|
|
8
80
|
*/
|
|
81
|
+
/** localStorage key that picks the preview's diff pipeline: `1` = tree-first, `0` = legacy line-hunk path. */
|
|
82
|
+
export declare const TREE_DIFF_SETTING = "diff-approval:tree-diff";
|
|
83
|
+
/** The pipeline the preview uses when nothing is stored: the tree-first path. */
|
|
84
|
+
export declare const TREE_DIFF_DEFAULT = true;
|
|
85
|
+
/**
|
|
86
|
+
* Whether the tree-first diff is active.
|
|
87
|
+
*
|
|
88
|
+
* The browser default is the TREE path (`TREE_DIFF_DEFAULT`); `localStorage` can force either pipeline (1/0).
|
|
89
|
+
* Inside the unit-test environment the default is the LEGACY path instead, so the old behavior's regression
|
|
90
|
+
* specs keep asserting the old behavior; the tree path's own specs opt in explicitly. End-to-end tests run the
|
|
91
|
+
* real browser default and a dedicated e2e spec pins the tree path there.
|
|
92
|
+
*/
|
|
93
|
+
export declare function treeDiffEnabled(): boolean;
|
|
94
|
+
export declare function previewUnits(oldText: string, newText: string): PreviewUnit[];
|
|
95
|
+
/** A change the rendering cannot show: the toolbar lists it as source, with its own line numbers. */
|
|
96
|
+
export interface PreviewInvisibleDiff {
|
|
97
|
+
/** The removed source line(s), 1-based inclusive. */
|
|
98
|
+
oldLines?: {
|
|
99
|
+
start: number;
|
|
100
|
+
end: number;
|
|
101
|
+
};
|
|
102
|
+
/** The added source line(s), 1-based inclusive. */
|
|
103
|
+
newLines?: {
|
|
104
|
+
start: number;
|
|
105
|
+
end: number;
|
|
106
|
+
};
|
|
107
|
+
/** The removed source itself. */
|
|
108
|
+
oldText?: string;
|
|
109
|
+
/** The added source itself. */
|
|
110
|
+
newText?: string;
|
|
111
|
+
/** The grammar the source is highlighted with: YAML for front matter, Markdown otherwise. */
|
|
112
|
+
grammar: 'markdown' | 'yaml';
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* One DIFFERENCE BLOCK: what the preview numbers, navigates, frames, and keeps or reverts.
|
|
116
|
+
*
|
|
117
|
+
* A block is NOT the same thing as a unit. A unit is a character-level change inside one line — the thing the
|
|
118
|
+
* preview highlights — while a block is the thing the reader counts and acts on. One block can hold several
|
|
119
|
+
* units (a changed table row's cells, a whole added paragraph's lines) or none at all (a whole new row, a
|
|
120
|
+
* deletion the rendering only reports as source). The rules:
|
|
121
|
+
*
|
|
122
|
+
* - a pure addition or pure deletion hunk (a new paragraph, a new list, a new table, the whole of a new
|
|
123
|
+
* file) is ONE block, however many units its lines were cut into;
|
|
124
|
+
* - a mixed hunk (an edit) numbers one block per unit, except a table row, which is one block for the row;
|
|
125
|
+
* a table row with no counterpart in the other version is one block too, tinted as a row;
|
|
126
|
+
* - a change that renders nothing (front matter, an HTML comment, a link definition) is a block of its own;
|
|
127
|
+
* inside a merged pure block it joins that block instead (its source is listed in the block's tabs).
|
|
128
|
+
*/
|
|
129
|
+
export interface PreviewBlock {
|
|
130
|
+
/** Document order: the preview's `data-md-block` value and the toolbar's number. */
|
|
131
|
+
index: number;
|
|
132
|
+
/** The source-line hunk this block came from. */
|
|
133
|
+
lineBlock: number;
|
|
134
|
+
/**
|
|
135
|
+
* The highlighted units that belong to this block. A whole-row block still lists its one whole-line unit
|
|
136
|
+
* (the unit→block map needs it), but the renderer draws the ROW rather than that unit's characters.
|
|
137
|
+
*/
|
|
138
|
+
units: PreviewUnit[];
|
|
139
|
+
/** Keep/revert range on the old side, 1-based; an absent side is empty (`oldStart > oldEnd`). */
|
|
140
|
+
oldStart: number;
|
|
141
|
+
oldEnd: number;
|
|
142
|
+
/** Keep/revert range on the new side, 1-based; an absent side is empty (`newStart > newEnd`). */
|
|
143
|
+
newStart: number;
|
|
144
|
+
newEnd: number;
|
|
145
|
+
/** The character span used when this block is exactly one unit and has no invisible part. */
|
|
146
|
+
oldChars?: {
|
|
147
|
+
start: number;
|
|
148
|
+
end: number;
|
|
149
|
+
};
|
|
150
|
+
newChars?: {
|
|
151
|
+
start: number;
|
|
152
|
+
end: number;
|
|
153
|
+
};
|
|
154
|
+
/** The lines that actually changed (0-based, inclusive), for drawing the block: no insertion widening. */
|
|
155
|
+
oldChanged?: LineSpan;
|
|
156
|
+
newChanged?: LineSpan;
|
|
157
|
+
/** Whether a whole pure-addition/deletion hunk was merged into this block. */
|
|
158
|
+
merged: boolean;
|
|
159
|
+
/** A table row with no counterpart on the other side: tint the row, not its cells. */
|
|
160
|
+
wholeRow?: 'add' | 'del';
|
|
161
|
+
/** Changes inside this block that the rendering cannot show, in source order. */
|
|
162
|
+
invisible: PreviewInvisibleDiff[];
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* The preview's DIFFERENCE BLOCKS: the unit the reader counts, navigates, frames and keeps/reverts.
|
|
166
|
+
*
|
|
167
|
+
* @param oldText - the file's tracked baseline.
|
|
168
|
+
* @param newText - the file's current content.
|
|
169
|
+
* @returns the blocks, in document order.
|
|
170
|
+
*/
|
|
171
|
+
export declare function previewBlocks(oldText: string, newText: string): PreviewBlock[];
|
|
9
172
|
/**
|
|
10
173
|
* Render the Markdown preview as sanitized HTML.
|
|
11
174
|
* @param oldText - the file's tracked baseline (rendered as "before").
|
|
12
|
-
* @param newText - the file's current content
|
|
13
|
-
* @param mode - 'single' merges
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* @
|
|
18
|
-
*
|
|
19
|
-
|
|
20
|
-
|
|
175
|
+
* @param newText - the file's current content.
|
|
176
|
+
* @param mode - 'single' merges both sides into one reading of the document; 'double' shows the before and
|
|
177
|
+
* after renderings as two whole documents.
|
|
178
|
+
* @param style - 'structure' (default) renders each document once and maps the diff onto its elements;
|
|
179
|
+
* 'blocks' is the original fragment-per-run renderer, kept behind the setting.
|
|
180
|
+
* @param labels - user-facing words the rendered markup must carry (none by default; the tests render bare).
|
|
181
|
+
* @returns sanitized HTML for the preview body. Local images keep their src so a later pass can resolve
|
|
182
|
+
* them to data URIs (see the preview image resolver).
|
|
183
|
+
*/
|
|
184
|
+
export declare function renderMarkdownPreview(oldText: string, newText: string, mode: 'single' | 'double', style?: MarkdownPreviewStyle, labels?: {
|
|
185
|
+
readonly invisible?: string;
|
|
186
|
+
}): string;
|
|
187
|
+
/** The tree path's units, adapted to the panel's `PreviewUnit` shape. */
|
|
188
|
+
export declare function treePreviewUnits(oldText: string, newText: string): PreviewUnit[];
|
|
189
|
+
/** The tree path's blocks, adapted to the panel's `PreviewBlock` shape. */
|
|
190
|
+
export declare function treePreviewBlocks(oldText: string, newText: string): PreviewBlock[];
|
|
191
|
+
/** The paragraph-level blocks, adapted to the panel's `PreviewBlock` shape. */
|
|
192
|
+
export declare function flowPreviewBlocks(oldText: string, newText: string): PreviewBlock[];
|
|
193
|
+
/** Render the paragraph-level single column: old above new, tinted rows grouped into hunk blocks. */
|
|
194
|
+
export declare function renderFlowMerged(oldText: string, newText: string): string;
|
|
195
|
+
/**
|
|
196
|
+
* The same flow model, rendered as TWO aligned columns: old left, new right. Every difference block is one grid
|
|
197
|
+
* row whose two cells are the red (old) and green (new) halves of the change, so a matched block's colour reads
|
|
198
|
+
* as one continuous band across the gutter. Unchanged rows are context grid rows (same content on both sides).
|
|
199
|
+
*/
|
|
200
|
+
export declare function renderFlowDouble(oldText: string, newText: string): string;
|
|
201
|
+
export {};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { type MarkdownTreeNode } from './markdown-tree-match.ts';
|
|
2
|
+
/** One character-level change: what the preview highlights. Mirrors `PreviewUnit` in markdown-preview.ts. */
|
|
3
|
+
export interface TreePreviewUnit {
|
|
4
|
+
index: number;
|
|
5
|
+
/** Kept for source-view compatibility; the tree path has no line hunks, so this is the block's index. */
|
|
6
|
+
lineBlock: number;
|
|
7
|
+
oldLines: {
|
|
8
|
+
start: number;
|
|
9
|
+
end: number;
|
|
10
|
+
};
|
|
11
|
+
newLines: {
|
|
12
|
+
start: number;
|
|
13
|
+
end: number;
|
|
14
|
+
};
|
|
15
|
+
oldChars: {
|
|
16
|
+
start: number;
|
|
17
|
+
end: number;
|
|
18
|
+
};
|
|
19
|
+
newChars: {
|
|
20
|
+
start: number;
|
|
21
|
+
end: number;
|
|
22
|
+
};
|
|
23
|
+
oldText: string;
|
|
24
|
+
newText: string;
|
|
25
|
+
}
|
|
26
|
+
/** A change the rendering cannot show: mirrors `PreviewInvisibleDiff` in markdown-preview.ts. */
|
|
27
|
+
export interface TreePreviewInvisibleDiff {
|
|
28
|
+
oldLines?: {
|
|
29
|
+
start: number;
|
|
30
|
+
end: number;
|
|
31
|
+
};
|
|
32
|
+
newLines?: {
|
|
33
|
+
start: number;
|
|
34
|
+
end: number;
|
|
35
|
+
};
|
|
36
|
+
oldText?: string;
|
|
37
|
+
newText?: string;
|
|
38
|
+
grammar: 'markdown' | 'yaml';
|
|
39
|
+
}
|
|
40
|
+
/** One difference block: mirrors `PreviewBlock` in markdown-preview.ts, plus the tree nodes it came from. */
|
|
41
|
+
export interface TreePreviewBlock {
|
|
42
|
+
index: number;
|
|
43
|
+
lineBlock: number;
|
|
44
|
+
units: TreePreviewUnit[];
|
|
45
|
+
oldStart: number;
|
|
46
|
+
oldEnd: number;
|
|
47
|
+
newStart: number;
|
|
48
|
+
newEnd: number;
|
|
49
|
+
oldChars?: {
|
|
50
|
+
start: number;
|
|
51
|
+
end: number;
|
|
52
|
+
};
|
|
53
|
+
newChars?: {
|
|
54
|
+
start: number;
|
|
55
|
+
end: number;
|
|
56
|
+
};
|
|
57
|
+
merged: boolean;
|
|
58
|
+
wholeRow?: 'add' | 'del';
|
|
59
|
+
invisible: TreePreviewInvisibleDiff[];
|
|
60
|
+
/** The block's own kind, for the renderer and tests (`paragraph`, `list_item`, `table_row`, `code`, …). */
|
|
61
|
+
kind: string;
|
|
62
|
+
/** A `table_cell` block's column: which cell of its row it covers. */
|
|
63
|
+
cellColumn?: number;
|
|
64
|
+
/**
|
|
65
|
+
* For an INLINE-CHILD block (a text run, a changed link/strong/image inside a paragraph/cell/item): the
|
|
66
|
+
* container pair whose rendered element hosts the change. The block never owns the container element
|
|
67
|
+
* itself, so blocks cannot nest inside each other.
|
|
68
|
+
*/
|
|
69
|
+
inlineContainer?: {
|
|
70
|
+
old?: MarkdownTreeNode;
|
|
71
|
+
new?: MarkdownTreeNode;
|
|
72
|
+
};
|
|
73
|
+
/** The old-side nodes this block covers (empty for a pure addition). */
|
|
74
|
+
oldNodes: MarkdownTreeNode[];
|
|
75
|
+
/** The new-side nodes this block covers (empty for a pure deletion). */
|
|
76
|
+
newNodes: MarkdownTreeNode[];
|
|
77
|
+
}
|
|
78
|
+
/** The tree path's whole model: units in reading order, and blocks in document order. */
|
|
79
|
+
export interface TreePreviewModel {
|
|
80
|
+
blocks: TreePreviewBlock[];
|
|
81
|
+
units: TreePreviewUnit[];
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Compute the tree path's preview model for one document pair.
|
|
85
|
+
*
|
|
86
|
+
* @param oldText - the baseline.
|
|
87
|
+
* @param newText - the current content.
|
|
88
|
+
* @returns the blocks (document order) and their units, with 1-based line ranges for keep/revert.
|
|
89
|
+
*/
|
|
90
|
+
export declare function treePreviewModel(oldText: string, newText: string): TreePreviewModel;
|