dsh-diff-approval 0.32.0 → 0.33.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/lib/client.js +94 -47
- package/lib/types/client/produced-diff.d.ts +19 -8
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -19810,16 +19810,27 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
|
|
|
19810
19810
|
* `[data-produced-files-row] button` 0.1.5's produced-file card `title` = the path
|
|
19811
19811
|
* `[data-presented-files-row] button` 0.1.7's presented-file card `title` = the path
|
|
19812
19812
|
* `[data-changed-files] button` 0.1.7's changed-files row `aria-describedby` = the path
|
|
19813
|
-
* `button[title]` a message's file LINK (0.1.7) `title` = the path
|
|
19814
19813
|
* `button[data-ref-chip]` a `@file` chip in a message `title` = the token
|
|
19815
|
-
* `
|
|
19816
|
-
*
|
|
19817
|
-
*
|
|
19818
|
-
*
|
|
19819
|
-
*
|
|
19820
|
-
*
|
|
19821
|
-
*
|
|
19822
|
-
*
|
|
19814
|
+
* `button._fileLink_…` / `._fileMention_…` a message's file LINK `title` = the path
|
|
19815
|
+
*
|
|
19816
|
+
* A PRESS IS ADMITTED BY THE SHELL'S OWN MARK, NEVER BY THE SHAPE OF ITS TEXT. The link row's classes are
|
|
19817
|
+
* the shell's CSS-module names — measured on the live shell as `_fileMention_1ypvv_85 _fileLink_1ypvv_59`,
|
|
19818
|
+
* carrying `title="src/client/produced-diff.ts"` — and that name survives the hash and line suffix the
|
|
19819
|
+
* module adds per build, so the marker is the `_fileLink_` / `_fileMention_` substring rather than the
|
|
19820
|
+
* whole class.
|
|
19821
|
+
*
|
|
19822
|
+
* This replaced two blanket rules, `button[title]` and every `a[href]`, which admitted a press by how its
|
|
19823
|
+
* text was spelled and were measured wrong twice on 2026-10-09: a model picker's button carries a tooltip
|
|
19824
|
+
* naming the model, and a version-like name (`…v3.2`) ends in the ".extension" the text filter accepted, so
|
|
19825
|
+
* pressing 修改模型 raised this menu over the shell's own control; an in-app route link (`/settings`) holds a
|
|
19826
|
+
* separator and was admitted the same way. A file press is one the shell marks as one.
|
|
19827
|
+
*
|
|
19828
|
+
* Everything else is left entirely alone: a link to a URL, an in-page anchor, a button whose tooltip names
|
|
19829
|
+
* something that is not a file, a file LINK whose file this panel does not hold, a modified press
|
|
19830
|
+
* (Ctrl/Cmd/Shift/Alt, or a middle click — gestures the harness may give its own meaning later), and any
|
|
19831
|
+
* press inside this plugin's own surface, whose buttons carry paths too and belong to the panel that is
|
|
19832
|
+
* already showing the file. A file-list ROW is the one press taken over whether or not the panel holds its
|
|
19833
|
+
* file (see `ROW_SELECTOR`); "we do not hold it" then decides what the menu offers, not whether it appears.
|
|
19823
19834
|
*
|
|
19824
19835
|
* The name of the module (and of the event it raises) dates from when the produced-file card was the
|
|
19825
19836
|
* only press of this kind; `data-presented-files-row` is that same list under 0.1.7's name for it.
|
|
@@ -19862,13 +19873,24 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
|
|
|
19862
19873
|
*
|
|
19863
19874
|
* A press this list misses is not a crash — it is one menu the reader does not get.
|
|
19864
19875
|
*/
|
|
19876
|
+
/**
|
|
19877
|
+
* The shell's OWN mark on a file link, as the name part of its CSS-module classes: measured on the live
|
|
19878
|
+
* shell as `class="_fileMention_1ypvv_85 _fileLink_1ypvv_59"` on the `<button title="src/client/
|
|
19879
|
+
* produced-diff.ts">` a message draws for a file. The hash and the line number after the name are per
|
|
19880
|
+
* build, so the marker is the `_name_` substring and not the whole class.
|
|
19881
|
+
*
|
|
19882
|
+
* A CSS-module class name is the shell telling this module what the element IS, which is why this replaced
|
|
19883
|
+
* the text-shape rules (see the module doc). It is a name, not an API: if the shell ever renames or
|
|
19884
|
+
* minifies these classes, this marker stops matching and file links fall back to the shell's own press —
|
|
19885
|
+
* fewer menus, never a menu over the wrong control, which is the direction a wrong guess must fail in.
|
|
19886
|
+
*/
|
|
19887
|
+
const MARKED_LINK_SELECTOR = "[class*=\"_fileLink_\"], [class*=\"_fileMention_\"]";
|
|
19865
19888
|
const PRESS_SELECTOR = [
|
|
19866
19889
|
"[data-produced-files-row] button",
|
|
19867
19890
|
"[data-presented-files-row] button",
|
|
19868
19891
|
"[data-changed-files] button",
|
|
19869
|
-
"button[title]",
|
|
19870
19892
|
"button[data-ref-chip]",
|
|
19871
|
-
|
|
19893
|
+
MARKED_LINK_SELECTOR
|
|
19872
19894
|
].join(", ");
|
|
19873
19895
|
/**
|
|
19874
19896
|
* A shell press that looks like one of the above and is NOT one: the review pane's file picker.
|
|
@@ -19912,41 +19934,65 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
|
|
|
19912
19934
|
return stripSuffix(text.replace(/^@/, "").replace(/^"|"$/g, "").trim());
|
|
19913
19935
|
}
|
|
19914
19936
|
/**
|
|
19915
|
-
* Whether this text
|
|
19937
|
+
* Whether this text is spelled as something that is not a file at all: an in-page anchor, a query, or
|
|
19938
|
+
* another scheme's URL. A Windows drive letter is a file despite the colon it starts with.
|
|
19939
|
+
*
|
|
19940
|
+
* @param value - the candidate text.
|
|
19941
|
+
* @returns whether it is spelled as a non-file.
|
|
19942
|
+
*/
|
|
19943
|
+
function isNonFileSpelling(value) {
|
|
19944
|
+
if (value === "" || value.startsWith("#") || value.startsWith("?")) return true;
|
|
19945
|
+
return /^[A-Za-z][A-Za-z0-9+.-]*:/.test(value) && !/^[A-Za-z]:[\\/]/.test(value);
|
|
19946
|
+
}
|
|
19947
|
+
/**
|
|
19948
|
+
* Whether this text has the SHAPE of a path: a `.extension` tail or a separator.
|
|
19916
19949
|
*
|
|
19917
|
-
*
|
|
19918
|
-
* a file
|
|
19919
|
-
*
|
|
19920
|
-
* through untouched rather than towards taking one over.
|
|
19950
|
+
* Only a press the shell did NOT mark is held to this, because its text is all there is to go on. A marked
|
|
19951
|
+
* press is a file link by the shell's own word, so its value is taken as written and an extension-less path
|
|
19952
|
+
* (`Makefile`) is read as the file it is instead of being dismissed as a label.
|
|
19921
19953
|
*
|
|
19922
19954
|
* @param value - the candidate text.
|
|
19923
19955
|
* @returns whether it could name a file.
|
|
19924
19956
|
*/
|
|
19925
19957
|
function looksLikePath(value) {
|
|
19926
|
-
if (
|
|
19927
|
-
if (/^[A-Za-z][A-Za-z0-9+.-]*:/.test(value) && !/^[A-Za-z]:[\\/]/.test(value)) return false;
|
|
19958
|
+
if (isNonFileSpelling(value)) return false;
|
|
19928
19959
|
return /\.[A-Za-z0-9]{1,8}$/.test(value) || value.includes("/") || value.includes("\\");
|
|
19929
19960
|
}
|
|
19961
|
+
/** Whether the shell itself marked this press as one of its file links (see `MARKED_LINK_SELECTOR`). */
|
|
19962
|
+
function isMarkedFileLink(press) {
|
|
19963
|
+
return press.matches(MARKED_LINK_SELECTOR);
|
|
19964
|
+
}
|
|
19930
19965
|
/**
|
|
19931
19966
|
* The path a press names, or undefined when it names no file.
|
|
19932
19967
|
*
|
|
19968
|
+
* Every shape is read from the same three places — `title`, the text its `aria-describedby` points at, an
|
|
19969
|
+
* anchor's `href` — and what differs is the test the value must pass: a press the shell marked must only
|
|
19970
|
+
* not be spelled as a non-file, while one it did not mark must ALSO have the shape of a path.
|
|
19971
|
+
*
|
|
19933
19972
|
* @param press - the element the reader pressed.
|
|
19934
19973
|
* @returns the path, as the shell spelled it (the panel matches it against its own spelling).
|
|
19935
19974
|
*/
|
|
19936
19975
|
function pressPathOf(press) {
|
|
19976
|
+
const marked = isMarkedFileLink(press);
|
|
19977
|
+
const accept = (value) => {
|
|
19978
|
+
const path = pathOfReference(value);
|
|
19979
|
+
if (path === "") return void 0;
|
|
19980
|
+
if (marked) return isNonFileSpelling(path) ? void 0 : path;
|
|
19981
|
+
return looksLikePath(path) ? path : void 0;
|
|
19982
|
+
};
|
|
19937
19983
|
const title = press.getAttribute("title")?.trim();
|
|
19938
19984
|
if (title !== void 0) {
|
|
19939
|
-
const path =
|
|
19940
|
-
if (
|
|
19985
|
+
const path = accept(title);
|
|
19986
|
+
if (path !== void 0) return path;
|
|
19941
19987
|
}
|
|
19942
19988
|
const describedBy = press.getAttribute("aria-describedby");
|
|
19943
19989
|
if (describedBy !== null) {
|
|
19944
|
-
const path = press.ownerDocument.getElementById(describedBy)?.textContent?.trim() ?? "";
|
|
19945
|
-
if (
|
|
19990
|
+
const path = accept(press.ownerDocument.getElementById(describedBy)?.textContent?.trim() ?? "");
|
|
19991
|
+
if (path !== void 0) return path;
|
|
19946
19992
|
}
|
|
19947
19993
|
if (press instanceof HTMLAnchorElement) {
|
|
19948
|
-
const path = stripSuffix(press.getAttribute("href")?.trim() ?? "");
|
|
19949
|
-
if (
|
|
19994
|
+
const path = accept(stripSuffix(press.getAttribute("href")?.trim() ?? ""));
|
|
19995
|
+
if (path !== void 0) return path;
|
|
19950
19996
|
}
|
|
19951
19997
|
}
|
|
19952
19998
|
/** The press `replayFilePress` is replaying, if any: that press is the shell's, not the menu's. */
|
|
@@ -23084,14 +23130,7 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
|
|
|
23084
23130
|
const body = bodyRef.current;
|
|
23085
23131
|
if (body === null) return;
|
|
23086
23132
|
if (body.clientHeight <= 0) return;
|
|
23087
|
-
const
|
|
23088
|
-
const viewBottom = viewTop + body.clientHeight;
|
|
23089
|
-
const pairTop = off(pairIndex);
|
|
23090
|
-
const pairBottom = pairTop + ROW_HEIGHT_PX;
|
|
23091
|
-
let target;
|
|
23092
|
-
if (pairTop < viewTop) target = pairTop;
|
|
23093
|
-
else if (pairBottom > viewBottom) target = pairBottom - body.clientHeight;
|
|
23094
|
-
if (target === void 0) return;
|
|
23133
|
+
const target = off(pairIndex) + ROW_HEIGHT_PX / 2 - body.clientHeight / 2;
|
|
23095
23134
|
const clamped = Math.max(0, Math.min(target, body.scrollHeight - body.clientHeight));
|
|
23096
23135
|
if (body.scrollTop !== clamped) body.scrollTop = clamped;
|
|
23097
23136
|
setScrollTop(clamped);
|
|
@@ -24964,25 +25003,17 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
|
|
|
24964
25003
|
applyPreviewFrames();
|
|
24965
25004
|
};
|
|
24966
25005
|
/**
|
|
24967
|
-
*
|
|
24968
|
-
*
|
|
24969
|
-
*
|
|
24970
|
-
*
|
|
24971
|
-
* bottom edge. The settled offset is mirrored into the frames, as for a jump.
|
|
25006
|
+
* Put one preview search occurrence in the middle of the pane. Its only caller is the search JUMP — the
|
|
25007
|
+
* bar's next / previous, Enter, F3 — and the point of that jump is to read the occurrence it landed on,
|
|
25008
|
+
* which is the rule the code view and the side-by-side view now follow too. The settled offset is
|
|
25009
|
+
* mirrored into the frames, as for a jump.
|
|
24972
25010
|
* @param hit - the highlighted occurrence to reveal.
|
|
24973
25011
|
*/
|
|
24974
25012
|
const scrollPreviewHitIntoView = (hit) => {
|
|
24975
25013
|
const body = mdPreviewBodyRef.current;
|
|
24976
25014
|
if (body === null) return;
|
|
24977
25015
|
const bodyRect = body.getBoundingClientRect();
|
|
24978
|
-
const
|
|
24979
|
-
const hitBottom = hit.getBoundingClientRect().bottom - bodyRect.top + body.scrollTop;
|
|
24980
|
-
const viewTop = body.scrollTop;
|
|
24981
|
-
const viewBottom = viewTop + body.clientHeight;
|
|
24982
|
-
let target;
|
|
24983
|
-
if (hitTop - leadRows * ROW_HEIGHT_PX < viewTop) target = hitTop - leadRows * ROW_HEIGHT_PX;
|
|
24984
|
-
else if (hitBottom > viewBottom) target = hitBottom - body.clientHeight;
|
|
24985
|
-
if (target === void 0) return;
|
|
25016
|
+
const target = (hit.getBoundingClientRect().top - bodyRect.top + body.scrollTop + (hit.getBoundingClientRect().bottom - bodyRect.top + body.scrollTop)) / 2 - body.clientHeight / 2;
|
|
24986
25017
|
const maxTop = Math.max(0, body.scrollHeight - body.clientHeight);
|
|
24987
25018
|
const clamped = Math.max(0, Math.min(target, maxTop));
|
|
24988
25019
|
if (body.scrollTop !== clamped) body.scrollTop = clamped;
|
|
@@ -25078,9 +25109,13 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
|
|
|
25078
25109
|
}
|
|
25079
25110
|
if (searchMatches.length === 0) return;
|
|
25080
25111
|
if (cursorPosRef.current !== void 0) {
|
|
25112
|
+
centerSearchRef.current = true;
|
|
25113
|
+
setSearchJump((jump) => jump + 1);
|
|
25081
25114
|
setSearchIndex(startIndexFor(searchQuery));
|
|
25082
25115
|
return;
|
|
25083
25116
|
}
|
|
25117
|
+
centerSearchRef.current = true;
|
|
25118
|
+
setSearchJump((jump) => jump + 1);
|
|
25084
25119
|
setSearchIndex((current) => (current + direction + searchMatches.length) % searchMatches.length);
|
|
25085
25120
|
};
|
|
25086
25121
|
/**
|
|
@@ -25102,6 +25137,11 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
|
|
|
25102
25137
|
return at === -1 ? 0 : at;
|
|
25103
25138
|
};
|
|
25104
25139
|
const cursorPosRef = (0, react.useRef)(void 0);
|
|
25140
|
+
/** Set by a search JUMP: the next match is centred, where a typed query only brings it into view. */
|
|
25141
|
+
const centerSearchRef = (0, react.useRef)(false);
|
|
25142
|
+
/** Bumped by every JUMP, so the centring effect runs even when the match itself does not change (a single
|
|
25143
|
+
* hit, or a wrap back onto the same row). */
|
|
25144
|
+
const [searchJump, setSearchJump] = (0, react.useState)(0);
|
|
25105
25145
|
const lastRecordedCursorRef = (0, react.useRef)(void 0);
|
|
25106
25146
|
const openSearchWithSelection = () => {
|
|
25107
25147
|
if (searchOpen) {
|
|
@@ -25168,18 +25208,25 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
|
|
|
25168
25208
|
const body = bodyRef.current;
|
|
25169
25209
|
if (body === null) return;
|
|
25170
25210
|
if (body.clientHeight <= 0) return;
|
|
25211
|
+
const centring = centerSearchRef.current;
|
|
25212
|
+
centerSearchRef.current = false;
|
|
25171
25213
|
const viewTop = body.scrollTop;
|
|
25172
25214
|
const viewBottom = viewTop + body.clientHeight;
|
|
25173
25215
|
const rowTop = offsetOf(row);
|
|
25174
25216
|
const rowBottom = rowTop + extentOf(row, row);
|
|
25175
25217
|
let target;
|
|
25176
|
-
if (rowTop
|
|
25218
|
+
if (centring) target = (rowTop + rowBottom) / 2 - body.clientHeight / 2;
|
|
25219
|
+
else if (rowTop < viewTop) target = rowTop;
|
|
25177
25220
|
else if (rowBottom > viewBottom) target = rowBottom - body.clientHeight;
|
|
25178
25221
|
if (target === void 0) return;
|
|
25179
25222
|
const clamped = Math.max(0, Math.min(target, body.scrollHeight - body.clientHeight));
|
|
25180
25223
|
if (body.scrollTop !== clamped) body.scrollTop = clamped;
|
|
25181
25224
|
setScrollTop(clamped);
|
|
25182
|
-
}, [
|
|
25225
|
+
}, [
|
|
25226
|
+
currentSearchRow,
|
|
25227
|
+
searchMatches,
|
|
25228
|
+
searchJump
|
|
25229
|
+
]);
|
|
25183
25230
|
const blockIndexByRow = (0, react.useMemo)(() => {
|
|
25184
25231
|
const map = /* @__PURE__ */ new Map();
|
|
25185
25232
|
model.blocks.forEach((block, blockIndex) => {
|
|
@@ -8,16 +8,27 @@
|
|
|
8
8
|
* `[data-produced-files-row] button` 0.1.5's produced-file card `title` = the path
|
|
9
9
|
* `[data-presented-files-row] button` 0.1.7's presented-file card `title` = the path
|
|
10
10
|
* `[data-changed-files] button` 0.1.7's changed-files row `aria-describedby` = the path
|
|
11
|
-
* `button[title]` a message's file LINK (0.1.7) `title` = the path
|
|
12
11
|
* `button[data-ref-chip]` a `@file` chip in a message `title` = the token
|
|
13
|
-
* `
|
|
12
|
+
* `button._fileLink_…` / `._fileMention_…` a message's file LINK `title` = the path
|
|
14
13
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
14
|
+
* A PRESS IS ADMITTED BY THE SHELL'S OWN MARK, NEVER BY THE SHAPE OF ITS TEXT. The link row's classes are
|
|
15
|
+
* the shell's CSS-module names — measured on the live shell as `_fileMention_1ypvv_85 _fileLink_1ypvv_59`,
|
|
16
|
+
* carrying `title="src/client/produced-diff.ts"` — and that name survives the hash and line suffix the
|
|
17
|
+
* module adds per build, so the marker is the `_fileLink_` / `_fileMention_` substring rather than the
|
|
18
|
+
* whole class.
|
|
19
|
+
*
|
|
20
|
+
* This replaced two blanket rules, `button[title]` and every `a[href]`, which admitted a press by how its
|
|
21
|
+
* text was spelled and were measured wrong twice on 2026-10-09: a model picker's button carries a tooltip
|
|
22
|
+
* naming the model, and a version-like name (`…v3.2`) ends in the ".extension" the text filter accepted, so
|
|
23
|
+
* pressing 修改模型 raised this menu over the shell's own control; an in-app route link (`/settings`) holds a
|
|
24
|
+
* separator and was admitted the same way. A file press is one the shell marks as one.
|
|
25
|
+
*
|
|
26
|
+
* Everything else is left entirely alone: a link to a URL, an in-page anchor, a button whose tooltip names
|
|
27
|
+
* something that is not a file, a file LINK whose file this panel does not hold, a modified press
|
|
28
|
+
* (Ctrl/Cmd/Shift/Alt, or a middle click — gestures the harness may give its own meaning later), and any
|
|
29
|
+
* press inside this plugin's own surface, whose buttons carry paths too and belong to the panel that is
|
|
30
|
+
* already showing the file. A file-list ROW is the one press taken over whether or not the panel holds its
|
|
31
|
+
* file (see `ROW_SELECTOR`); "we do not hold it" then decides what the menu offers, not whether it appears.
|
|
21
32
|
*
|
|
22
33
|
* The name of the module (and of the event it raises) dates from when the produced-file card was the
|
|
23
34
|
* only press of this kind; `data-presented-files-row` is that same list under 0.1.7's name for it.
|
package/package.json
CHANGED