dsh-diff-approval 0.32.0 → 0.34.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/index.js CHANGED
@@ -3347,9 +3347,12 @@ function refusalText(refusal, path) {
3347
3347
  /**
3348
3348
  * The sentence a file that could not be listed is refused with.
3349
3349
  *
3350
- * All three are the workspace's rules rather than the annotation's: the panel can only show what the
3351
- * session's workspace contains and the file service can read, and naming which one failed is what lets
3352
- * the agent pick another file, or stop annotating that one.
3350
+ * Both are the workspace's rules rather than the annotation's: there is either no workspace to hold a
3351
+ * list, or the file service could not read the file as text. Naming which one failed is what lets the
3352
+ * agent pick another file, or stop annotating that one.
3353
+ *
3354
+ * A path OUTSIDE the workspace is not in this list: the panel can hold such a file (an edit to one is
3355
+ * captured like any other), so it is listed and annotated rather than refused (see `resolveForListing`).
3353
3356
  *
3354
3357
  * @param kind - why listing failed.
3355
3358
  * @param path - the path the caller named.
@@ -3357,7 +3360,6 @@ function refusalText(refusal, path) {
3357
3360
  */
3358
3361
  function unlistedText(kind, path) {
3359
3362
  switch (kind) {
3360
- case "outside": return `refused: "${path}" is not inside this session's workspace, so the review panel cannot list it. Annotate a file of the workspace the user is working in.`;
3361
3363
  case "no-workspace": return `refused: this session has no workspace, so there is no file list to add "${path}" to.`;
3362
3364
  default: return `refused: "${path}" could not be read as a text file, so there is nothing to annotate. Check the path (a workspace-relative path is fine) and try again.`;
3363
3365
  }
@@ -4392,6 +4394,30 @@ function resolveInsideWorkspace(root, input) {
4392
4394
  const absolute = isAbsolute(input) ? resolve(input) : resolve(root, input);
4393
4395
  return workspaceRelativeOf(root, absolute) === void 0 ? void 0 : absolute;
4394
4396
  }
4397
+ /**
4398
+ * Resolve one caller-supplied path for LISTING, which an absolute path need not keep inside the workspace.
4399
+ *
4400
+ * A relative path is workspace-relative, exactly as before. An absolute one is taken as it is, wherever it
4401
+ * points: the fence `resolveInsideWorkspace` draws is the right one for a SUBTREE question — a scan of the
4402
+ * workspace's own VCS — and the wrong one for "show me this file".
4403
+ *
4404
+ * The panel can already hold a file outside the workspace: the tracked capture behind an `edit`/`write`/`
4405
+ * `str_replace_editor` has never had a workspace fence (`tools/result` folds `outcome.path` as the tool
4406
+ * spelled it), so a file the agent edited elsewhere is listed, kept and reverted like any other. Refusing
4407
+ * to READ one into the list therefore only made the plugin disagree with itself: a card could not hang on a
4408
+ * file the panel was already able to show. Both ways in — the agent's annotate call and the reader's own
4409
+ * "add this path" — go through this, so they agree.
4410
+ *
4411
+ * What is read is still the harness's file service, whose reads are not workspace-confined (only its
4412
+ * mutations are fenced, by the sandbox policy), so this adds no read the deployment does not already allow.
4413
+ *
4414
+ * @param root - the workspace root, for a relative path.
4415
+ * @param input - the caller's path (absolute anywhere, or workspace-relative).
4416
+ * @returns the absolute path to list.
4417
+ */
4418
+ function resolveForListing(root, input) {
4419
+ return isAbsolute(input) ? resolve(input) : resolve(root, input);
4420
+ }
4395
4421
  /** The workspace-relative parent of a relative directory path (`''` at the root). */
4396
4422
  /** Fold one path for comparison: absolute, `/`-separated, case-folded on Windows.
4397
4423
  * Entries are keyed by the path spelling their capture carried (a tool's display
@@ -4610,8 +4636,8 @@ function apply(ctx, config) {
4610
4636
  * already lands with — so the file opens at its own content and the card sits on the lines it names.
4611
4637
  *
4612
4638
  * It is deliberately that ROUTE and not a raw insert: a path already listed is answered from the list
4613
- * (so annotating twice never re-baselines a review in progress), a path outside the workspace is
4614
- * refused here rather than confined later, and what lands goes through the same fold + persist + undo
4639
+ * (so annotating twice never re-baselines a review in progress), a path outside the workspace is listed
4640
+ * like any other (see `resolveForListing`), and what lands goes through the same fold + persist + undo
4615
4641
  * bookkeeping as every other admission.
4616
4642
  *
4617
4643
  * @param sessionId - the session whose list gains the file.
@@ -4623,8 +4649,7 @@ function apply(ctx, config) {
4623
4649
  await ensureLoaded();
4624
4650
  const workspace = workspaceOf(sessionId);
4625
4651
  if (workspace === void 0) return { kind: "no-workspace" };
4626
- const absolute = resolveInsideWorkspace(workspace.path, asked);
4627
- if (absolute === void 0) return { kind: "outside" };
4652
+ const absolute = resolveForListing(workspace.path, asked);
4628
4653
  const listed = store.list(sessionId).find((entry) => pathIdentity(entry.path) === pathIdentity(absolute));
4629
4654
  if (listed !== void 0) return {
4630
4655
  kind: "listed",
@@ -6888,8 +6913,8 @@ function apply(ctx, config) {
6888
6913
  if (target === void 0) return rpcError("sessionId and path must be non-empty strings");
6889
6914
  const workspace = workspaceOf(target.sessionId);
6890
6915
  if (workspace === void 0) return rpcError("add unavailable: the session has no workspace");
6891
- const absolute = resolveInsideWorkspace(workspace.path, target.path);
6892
- if (absolute === void 0) return {
6916
+ const absolute = resolveForListing(workspace.path, target.path);
6917
+ if (!target.exact && workspaceRelativeOf(workspace.path, absolute) === void 0) return {
6893
6918
  ok: true,
6894
6919
  value: {
6895
6920
  outcome: "outside",
@@ -7316,7 +7341,7 @@ function commentAddOf(payload) {
7316
7341
  if (typeof startLine !== "number" || !Number.isFinite(startLine)) return void 0;
7317
7342
  if (typeof endLine !== "number" || !Number.isFinite(endLine)) return void 0;
7318
7343
  if (typeof quote !== "string") return void 0;
7319
- if (typeof text !== "string" || text.trim().length === 0) return void 0;
7344
+ if (typeof text !== "string") return void 0;
7320
7345
  const id = body.id;
7321
7346
  const quoteContext = body.quoteContext;
7322
7347
  const quoteLines = [];
@@ -168,8 +168,6 @@ export type ListFileOutcome = {
168
168
  kind: 'listed';
169
169
  entry: PendingEntry;
170
170
  added: boolean;
171
- } | {
172
- kind: 'outside';
173
171
  } | {
174
172
  kind: 'missing';
175
173
  } | {
@@ -205,15 +203,18 @@ export interface AnnotateRunDeps {
205
203
  /**
206
204
  * The sentence a file that could not be listed is refused with.
207
205
  *
208
- * All three are the workspace's rules rather than the annotation's: the panel can only show what the
209
- * session's workspace contains and the file service can read, and naming which one failed is what lets
210
- * the agent pick another file, or stop annotating that one.
206
+ * Both are the workspace's rules rather than the annotation's: there is either no workspace to hold a
207
+ * list, or the file service could not read the file as text. Naming which one failed is what lets the
208
+ * agent pick another file, or stop annotating that one.
209
+ *
210
+ * A path OUTSIDE the workspace is not in this list: the panel can hold such a file (an edit to one is
211
+ * captured like any other), so it is listed and annotated rather than refused (see `resolveForListing`).
211
212
  *
212
213
  * @param kind - why listing failed.
213
214
  * @param path - the path the caller named.
214
215
  * @returns the sentence the caller reads.
215
216
  */
216
- export declare function unlistedText(kind: 'outside' | 'missing' | 'no-workspace', path: string): string;
217
+ export declare function unlistedText(kind: 'missing' | 'no-workspace', path: string): string;
217
218
  /**
218
219
  * Build the body one tool call runs: resolve the file, apply the rule, store the record.
219
220
  *
@@ -15,6 +15,13 @@
15
15
  * @module dsh-diff-approval/client/panel-memory
16
16
  */
17
17
  import type { Discussion, DiscussionQuoteLine } from './discussion.ts';
18
+ /** One place a deliberate jump left behind: the file, and how far down it was. */
19
+ export interface PanelJumpPlace {
20
+ /** The pending entry the reader was in. */
21
+ fileId: string;
22
+ /** The code view's `scrollTop` there — what `rememberPanelView` keeps current as the reader scrolls. */
23
+ scrollTop: number;
24
+ }
18
25
  /**
19
26
  * What the panel keeps about one thread that is NOT the host's to keep: what the reader is
20
27
  * typing, whether the block is folded, and the body height the panel last measured for it.
@@ -182,6 +189,29 @@ export declare function lastPanelFile(sessionId: string | undefined): string | u
182
189
  * @returns the remembered scrollTop, or undefined when there is none.
183
190
  */
184
191
  export declare function panelFileOffset(sessionId: string | undefined, fileId: string): number | undefined;
192
+ /**
193
+ * Remember the place a deliberate jump is leaving, so the reader can come back to it.
194
+ *
195
+ * ONLY JUMPS PASS THROUGH HERE — never a scroll of the reader's own, which is the whole point. Their place
196
+ * is already kept per file (`rememberPanelView`), kept current by every scroll they make, so a back step
197
+ * has nothing to measure: it hands that same memory back. Reading it from there rather than from the DOM is
198
+ * also what makes this work in both views, one column and side by side, whose scrollers are different
199
+ * elements and only one of which is ever mounted.
200
+ *
201
+ * A jump that would push the place already on top is dropped: two entries for one place would cost the
202
+ * reader two presses of the back button to get anywhere.
203
+ *
204
+ * @param sessionId - the session the panel is reviewing; nothing is recorded without one.
205
+ * @param fileId - the entry being LEFT, not the one being jumped to.
206
+ */
207
+ export declare function rememberJumpAway(sessionId: string | undefined, fileId: string | undefined): void;
208
+ /**
209
+ * Take the last place a deliberate jump left, for a back step. Spent here, so a second press goes one step
210
+ * further back rather than landing where the reader already is.
211
+ * @param sessionId - the session, if any.
212
+ * @returns that place, or undefined when there is nothing to go back to.
213
+ */
214
+ export declare function popJumpAway(sessionId: string | undefined): PanelJumpPlace | undefined;
185
215
  /**
186
216
  * Forget every session's record. A page reload does this by itself; a test calls
187
217
  * it to start from a clean page rather than inheriting the previous case's place.
@@ -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
- * `a[href]` any other link naming a file `href` = the path
12
+ * `button._fileLink_…` / `._fileMention_…` a message's file LINK `title` = the path
14
13
  *
15
- * Everything else is left entirely alone: a link to a URL, an in-page anchor, a chip naming something
16
- * that is not a file, a file LINK whose file this panel does not hold, a modified press (Ctrl/Cmd/Shift/
17
- * Alt, or a middle click — gestures the harness may give its own meaning later), and any press inside this
18
- * plugin's own surface, whose buttons carry paths too and belong to the panel that is already showing
19
- * the file. A file-list ROW is the one press taken over whether or not the panel holds its file (see
20
- * `ROW_SELECTOR`); "we do not hold it" then decides what the menu offers, not whether it appears.
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.
@@ -38,6 +38,11 @@ export interface SplitDiff {
38
38
  * alignment, each deletion pairs with its most-similar addition (order
39
39
  * preserved, threshold-bounded) so a mixed insert/delete block does not force a
40
40
  * wrong line together. A stray deletion or addition stays a one-sided pair.
41
+ *
42
+ * THE ORDER OF THE RESULT IS PART OF THE CONTRACT: going down `pairs`, neither
43
+ * column's line numbers ever go backwards. Alignment may therefore move a line
44
+ * onto the row opposite it, but never past a line of its own side — an unpaired
45
+ * line between two pairs is emitted where it belongs, not after them.
41
46
  * @param rows - the whole-file diff rows.
42
47
  * @param alignBySimilarity - align a change block by similarity instead of order.
43
48
  * @returns the split pairs plus the row→pair index map.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-diff-approval",
3
- "version": "0.32.0",
3
+ "version": "0.34.0",
4
4
  "description": "DeepSeek Harness plugin: pending-edit review with whole-file diff and Keep/Revert",
5
5
  "packageManager": "pnpm@11.21.0",
6
6
  "author": "Wu Zhiwei",