dsh-diff-approval 0.5.0 → 0.6.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 CHANGED
@@ -11582,30 +11582,21 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
11582
11582
  //#endregion
11583
11583
  //#region lib/types/client/reference.js
11584
11584
  /**
11585
- * Reference labels for the selection toolbar: a file name plus a 1-based line
11586
- * range, using the short name when unambiguous and the full path otherwise.
11587
- * Pure derivation so the display rule is unit-testable without the panel.
11588
- * @module dsh-diff-approval/client/reference
11589
- */
11590
- /**
11591
- * Last path segment of a file path, any separator style.
11592
- * @param path - the path to shorten.
11593
- * @returns the segment after the final separator.
11594
- */
11595
- function basenameOf$1(path) {
11596
- const normalized = path.replaceAll("\\", "/");
11597
- return normalized.slice(normalized.lastIndexOf("/") + 1);
11598
- }
11599
- /**
11600
- * The path shown in a copied reference: the short name when no other listed
11601
- * file shares it, the full path otherwise.
11585
+ * The path embedded in a copied reference: workspace-relative (forward
11586
+ * slashes) when the file lives inside the current workspace, the absolute
11587
+ * path otherwise. A bare file name is never enough a reference must resolve
11588
+ * to exactly one file.
11602
11589
  * @param path - the selected file's path.
11603
- * @param files - every currently listed pending file.
11604
- * @returns the display path for a copied reference.
11590
+ * @param workspacePath - the current workspace root, or `undefined`.
11591
+ * @returns the reference path.
11605
11592
  */
11606
- function copyDisplayPath(path, files) {
11607
- const base = basenameOf$1(path);
11608
- return files.some((file) => file.path !== path && basenameOf$1(file.path) === base) ? path : base;
11593
+ function referencePathOf(path, workspacePath) {
11594
+ if (workspacePath === void 0 || workspacePath.length === 0) return path;
11595
+ const normalized = path.replaceAll("\\", "/");
11596
+ const root = workspacePath.replaceAll("\\", "/");
11597
+ const prefix = root.endsWith("/") ? root : `${root}/`;
11598
+ if (normalized.length > prefix.length && normalized.slice(0, prefix.length).toLowerCase() === prefix.toLowerCase()) return normalized.slice(prefix.length);
11599
+ return path;
11609
11600
  }
11610
11601
  /**
11611
11602
  * Format one line range as a reference suffix: a single line number, or the
@@ -11620,17 +11611,17 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
11620
11611
  /**
11621
11612
  * Build the clipboard text for a selected line range.
11622
11613
  * @param path - the selected file's path.
11623
- * @param files - every currently listed pending file.
11614
+ * @param workspacePath - the current workspace root, or `undefined`.
11624
11615
  * @param start - first selected line number.
11625
11616
  * @param end - last selected line number.
11626
11617
  * @returns the `path:range` reference text.
11627
11618
  */
11628
- function referenceOf(path, files, start, end) {
11629
- return `${copyDisplayPath(path, files)}:${lineRangeLabel(start, end)}`;
11619
+ function referenceOf(path, workspacePath, start, end) {
11620
+ return `${referencePathOf(path, workspacePath)}:${lineRangeLabel(start, end)}`;
11630
11621
  }
11631
11622
  //#endregion
11632
11623
  //#region \0dsh-css:/home/runner/work/dsh-diff-approval/dsh-diff-approval/src/client/PendingPanel.module.css.mjs
11633
- const css = ".F1KBNa_layer{box-sizing:border-box;flex:none;align-items:center;width:calc(100% + 8px);height:42px;margin:4px -4px;display:flex;position:relative}.F1KBNa_footerButtons{align-items:center;width:100%;display:flex}.F1KBNa_badge{box-sizing:border-box;width:100%;height:42px;color:var(--dsw-alias-label-primary);cursor:pointer;background:0 0;border:none;border-radius:12px;align-items:center;gap:8px;padding:0 10px 0 8px;font-family:inherit;font-size:14px;line-height:22px;display:flex;overflow:hidden}.F1KBNa_badge:hover{background:var(--dsw-alias-interactive-bg-hover)}.F1KBNa_badge[data-active]{background:var(--dsw-alias-interactive-bg-hover-solid)}.F1KBNa_badgeLabel{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.F1KBNa_badgeCount{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;flex:none;margin-left:auto;font-size:12px;line-height:16px}.F1KBNa_layer.F1KBNa_rail{width:36px;height:36px;margin:8px 0 10px}.F1KBNa_rail .F1KBNa_badge{border-radius:50%;justify-content:center;gap:0;width:36px;height:36px;padding:0}.F1KBNa_rail .F1KBNa_badgeLabel{display:none}.F1KBNa_rail .F1KBNa_badgeCount{box-sizing:border-box;background:var(--dsw-alias-state-business-primary);min-width:18px;height:18px;color:var(--dsw-alias-label-primary-foreground);font-variant-numeric:tabular-nums;border-radius:9px;justify-content:center;align-items:center;padding:0 4px;font-size:11px;line-height:18px;display:flex;position:absolute;top:-3px;right:-7px}.F1KBNa_panel{z-index:30;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-base);width:auto;max-width:none;box-shadow:var(--dsw-shadow-lv2);--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);border-radius:12px;flex-direction:column;display:flex;position:fixed;inset:8px 8px 128px;overflow:hidden}.F1KBNa_header{box-sizing:border-box;border-bottom:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base);flex:none;justify-content:space-between;align-items:center;min-height:44px;padding:10px 12px;display:flex}.F1KBNa_headerActions{align-items:center;gap:2px;display:flex}.F1KBNa_states{flex:1;min-height:0;padding:4px 12px 12px;overflow-y:auto}.F1KBNa_split{flex:1;align-items:stretch;min-height:0;display:flex}.F1KBNa_fileList{box-sizing:border-box;border-right:1px solid var(--dsw-alias-border-l2);flex:none;width:240px;min-height:0;padding:4px 8px 12px;overflow-y:auto}.F1KBNa_resizeHandle{cursor:col-resize;background:0 0;flex:none;width:5px;margin:0 -2px}.F1KBNa_resizeHandle:hover{background:var(--dsw-alias-border-l2)}.F1KBNa_detail{flex-direction:column;flex:1;min-width:0;min-height:0;display:flex}.F1KBNa_detailEmpty{color:var(--dsw-alias-label-tertiary);text-align:center;flex:1;justify-content:center;align-items:center;margin:0;padding:24px;font-size:12px;line-height:18px;display:flex}.F1KBNa_title{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:500;line-height:20px}.F1KBNa_note,.F1KBNa_readError,.F1KBNa_hint{color:var(--dsw-alias-label-tertiary);margin:4px 0;font-size:12px;line-height:18px}.F1KBNa_readError{color:var(--dsw-alias-state-error-primary)}.F1KBNa_group{color:var(--dsw-alias-label-tertiary);margin:8px 0 4px;font-size:12px;font-weight:500;line-height:16px}.F1KBNa_rows{margin:0;padding:0;list-style:none}.F1KBNa_row{border:1px solid #0000;border-radius:10px;margin:2px 0}.F1KBNa_rowHead{width:100%;color:var(--dsw-alias-label-primary);font:inherit;text-align:left;cursor:pointer;background:0 0;border:none;border-radius:10px;align-items:baseline;gap:8px;padding:6px 8px;display:flex}.F1KBNa_rowHead:hover{background:var(--dsw-alias-interactive-bg-hover-solid)}.F1KBNa_rowHead[data-selected]{background:var(--dsw-alias-interactive-bg-hover)}.F1KBNa_rowPath{text-overflow:ellipsis;white-space:nowrap;min-width:0;font:var(--dsw-font-markdown-code-block);overflow:hidden}.F1KBNa_kindTag{border:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-tertiary);white-space:nowrap;border-radius:6px;flex:none;padding:1px 6px;font-size:11px;line-height:14px}.F1KBNa_kindHint{color:var(--dsw-alias-label-tertiary);flex:none;font-size:12px;line-height:16px}.F1KBNa_divergedHint,.F1KBNa_missingHint{border-bottom:1px solid var(--dsw-alias-border-l2);margin:0;padding:6px 8px;font-size:12px;line-height:18px}.F1KBNa_missing{border:1px solid var(--dsw-alias-state-warn-primary);color:var(--dsw-alias-state-warn-label,var(--dsw-alias-state-warn-primary));white-space:nowrap;border-radius:6px;flex:none;padding:1px 6px;font-size:11px;line-height:14px}.F1KBNa_missingHint{color:var(--dsw-alias-state-warn-label,var(--dsw-alias-state-warn-primary))}.F1KBNa_rowMeta{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;flex:none;gap:6px;margin-left:auto;font-size:12px;line-height:16px;display:inline-flex}.F1KBNa_addCount{color:color-mix(in srgb, var(--dsw-alias-state-success-primary) 60%, var(--dsw-alias-label-primary))}.F1KBNa_delCount{color:color-mix(in srgb, var(--dsw-alias-state-error-primary) 60%, var(--dsw-alias-label-primary))}.F1KBNa_diff{background:var(--dsw-alias-markdown-code-block);flex-direction:column;flex:1;min-height:0;display:flex;position:relative;overflow:hidden}.F1KBNa_diffHeader{border-bottom:1px solid var(--dsw-alias-border-l2);align-items:center;gap:8px;padding:6px 8px;display:flex}.F1KBNa_diffPath{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-label-primary);font:var(--dsw-font-markdown-code-block);flex:1;font-size:12px;line-height:18px;overflow:hidden}.F1KBNa_diffActions{border-bottom:1px solid var(--dsw-alias-border-l2);align-items:center;gap:8px;padding:6px 8px;display:flex}.F1KBNa_diffStats{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;font-size:12px;line-height:16px}.F1KBNa_flexSpacer{flex:1}.F1KBNa_action{border:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-primary);cursor:pointer;background:0 0;border-radius:8px;padding:3px 12px;font-family:inherit;font-size:12px;line-height:18px}.F1KBNa_action:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.F1KBNa_action:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}.F1KBNa_iconAction{justify-content:center;align-items:center;padding:4px 6px;display:inline-flex}.F1KBNa_close{cursor:pointer;width:28px;height:28px;color:var(--dsw-alias-label-tertiary);background:0 0;border:none;border-radius:28px;justify-content:center;align-items:center;padding:0;display:inline-flex}.F1KBNa_close:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.F1KBNa_expand{cursor:pointer;width:28px;height:28px;color:var(--dsw-alias-label-tertiary);background:0 0;border:none;border-radius:28px;justify-content:center;align-items:center;padding:0;display:inline-flex}.F1KBNa_expand:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.F1KBNa_expandExpanded svg{transform:rotate(180deg)}.F1KBNa_diffBodyWrap{flex:1;min-height:0;display:flex;position:relative}.F1KBNa_diffBody{min-width:0;font:var(--dsw-font-markdown-code-block);flex:1;padding:8px 10px;overflow:auto}.F1KBNa_overviewRuler{pointer-events:none;opacity:.5;width:8px;position:absolute;top:0;bottom:0;right:0}.F1KBNa_overviewMarker{border-radius:2px;width:100%;min-height:2px;position:absolute;right:0}.F1KBNa_markerDel{background-color:var(--dsw-alias-state-error-primary)}.F1KBNa_markerAdd{background-color:var(--dsw-alias-state-success-primary)}.F1KBNa_lines{border-spacing:0;width:max-content;min-width:100%;display:table}.F1KBNa_line{height:22px;line-height:22px;display:table-row}.F1KBNa_vSpacer{display:table-row}.F1KBNa_gutter{box-sizing:border-box;text-align:right;width:44px;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;user-select:none;padding-right:10px;display:table-cell}.F1KBNa_code{white-space:pre;display:table-cell}.F1KBNa_context{color:var(--dsw-alias-label-primary)}.F1KBNa_del{background-color:color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, transparent)}.F1KBNa_add{background-color:color-mix(in srgb, var(--dsw-alias-state-success-primary) 10%, transparent)}.F1KBNa_statusBar{box-sizing:border-box;border-top:1px solid var(--dsw-alias-border-l2);height:34px;color:var(--dsw-alias-label-tertiary);font-family:var(--dsw-font-markdown-code-block);font-variant-numeric:tabular-nums;flex:none;align-items:center;padding:0 8px;font-size:12px;line-height:18px;display:flex}.F1KBNa_statusAction{border:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-primary);white-space:nowrap;cursor:pointer;background:0 0;border-radius:8px;padding:3px 12px;font-family:inherit;font-size:12px;line-height:18px}.F1KBNa_statusAction:hover{background:var(--dsw-alias-interactive-bg-hover)}.F1KBNa_langSelect{border:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-primary);cursor:pointer;background:0 0;border-radius:8px;flex:none;align-items:center;gap:4px;padding:3px 12px;font-family:inherit;font-size:12px;line-height:18px;display:inline-flex}.F1KBNa_langSelect:hover{background:var(--dsw-alias-interactive-bg-hover)}.F1KBNa_langLabel{text-overflow:ellipsis;white-space:nowrap;max-width:140px;overflow:hidden}";
11624
+ const css = ".F1KBNa_layer{box-sizing:border-box;flex:none;align-items:center;width:calc(100% + 8px);height:42px;margin:4px -4px;display:flex;position:relative}.F1KBNa_footerButtons{align-items:center;width:100%;display:flex}.F1KBNa_badge{box-sizing:border-box;width:100%;height:42px;color:var(--dsw-alias-label-primary);cursor:pointer;background:0 0;border:none;border-radius:12px;align-items:center;gap:8px;padding:0 10px 0 8px;font-family:inherit;font-size:14px;line-height:22px;display:flex;overflow:hidden}.F1KBNa_badge:hover{background:var(--dsw-alias-interactive-bg-hover)}.F1KBNa_badge[data-active]{background:var(--dsw-alias-interactive-bg-hover-solid)}.F1KBNa_badgeLabel{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.F1KBNa_badgeCount{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;flex:none;margin-left:auto;font-size:12px;line-height:16px}.F1KBNa_layer.F1KBNa_rail{width:36px;height:36px;margin:8px 0 10px}.F1KBNa_rail .F1KBNa_badge{border-radius:50%;justify-content:center;gap:0;width:36px;height:36px;padding:0}.F1KBNa_rail .F1KBNa_badgeLabel{display:none}.F1KBNa_rail .F1KBNa_badgeCount{box-sizing:border-box;background:var(--dsw-alias-state-business-primary);min-width:18px;height:18px;color:var(--dsw-alias-label-primary-foreground);font-variant-numeric:tabular-nums;border-radius:9px;justify-content:center;align-items:center;padding:0 4px;font-size:11px;line-height:18px;display:flex;position:absolute;top:-3px;right:-7px}.F1KBNa_panel{z-index:30;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-base);width:auto;max-width:none;box-shadow:var(--dsw-shadow-lv2);--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);border-radius:12px;flex-direction:column;display:flex;position:fixed;inset:8px 8px 128px;overflow:hidden}.F1KBNa_header{box-sizing:border-box;border-bottom:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base);flex:none;justify-content:space-between;align-items:center;min-height:44px;padding:10px 12px;display:flex}.F1KBNa_headerActions{align-items:center;gap:2px;display:flex}.F1KBNa_states{flex:1;min-height:0;padding:4px 12px 12px;overflow-y:auto}.F1KBNa_split{flex:1;align-items:stretch;min-height:0;display:flex}.F1KBNa_fileList{box-sizing:border-box;border-right:1px solid var(--dsw-alias-border-l2);flex:none;width:240px;min-height:0;padding:4px 8px 12px;overflow-y:auto}.F1KBNa_resizeHandle{cursor:col-resize;background:0 0;flex:none;width:5px;margin:0 -2px}.F1KBNa_resizeHandle:hover{background:var(--dsw-alias-border-l2)}.F1KBNa_detail{flex-direction:column;flex:1;min-width:0;min-height:0;display:flex}.F1KBNa_detailEmpty{color:var(--dsw-alias-label-tertiary);text-align:center;flex:1;justify-content:center;align-items:center;margin:0;padding:24px;font-size:12px;line-height:18px;display:flex}.F1KBNa_title{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:500;line-height:20px}.F1KBNa_note,.F1KBNa_readError,.F1KBNa_hint{color:var(--dsw-alias-label-tertiary);margin:4px 0;font-size:12px;line-height:18px}.F1KBNa_readError{color:var(--dsw-alias-state-error-primary)}.F1KBNa_group{color:var(--dsw-alias-label-tertiary);margin:8px 0 4px;font-size:12px;font-weight:500;line-height:16px}.F1KBNa_rows{margin:0;padding:0;list-style:none}.F1KBNa_row{border:1px solid #0000;border-radius:10px;margin:2px 0}.F1KBNa_rowHead{width:100%;color:var(--dsw-alias-label-primary);font:inherit;text-align:left;cursor:pointer;background:0 0;border:none;border-radius:10px;align-items:baseline;gap:8px;padding:6px 8px;display:flex}.F1KBNa_rowHead:hover{background:var(--dsw-alias-interactive-bg-hover-solid)}.F1KBNa_rowHead[data-selected]{background:var(--dsw-alias-interactive-bg-hover)}.F1KBNa_rowPath{text-overflow:ellipsis;white-space:nowrap;min-width:0;font:var(--dsw-font-markdown-code-block);overflow:hidden}.F1KBNa_kindTag{border:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-tertiary);white-space:nowrap;border-radius:6px;flex:none;padding:1px 6px;font-size:11px;line-height:14px}.F1KBNa_kindHint{color:var(--dsw-alias-label-tertiary);flex:none;font-size:12px;line-height:16px}.F1KBNa_divergedHint,.F1KBNa_missingHint{border-bottom:1px solid var(--dsw-alias-border-l2);margin:0;padding:6px 8px;font-size:12px;line-height:18px}.F1KBNa_missing{border:1px solid var(--dsw-alias-state-warn-primary);color:var(--dsw-alias-state-warn-label,var(--dsw-alias-state-warn-primary));white-space:nowrap;border-radius:6px;flex:none;padding:1px 6px;font-size:11px;line-height:14px}.F1KBNa_missingHint{color:var(--dsw-alias-state-warn-label,var(--dsw-alias-state-warn-primary))}.F1KBNa_rowMeta{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;flex:none;gap:6px;margin-left:auto;font-size:12px;line-height:16px;display:inline-flex}.F1KBNa_addCount{color:color-mix(in srgb, var(--dsw-alias-state-success-primary) 60%, var(--dsw-alias-label-primary))}.F1KBNa_delCount{color:color-mix(in srgb, var(--dsw-alias-state-error-primary) 60%, var(--dsw-alias-label-primary))}.F1KBNa_diff{background:var(--dsw-alias-markdown-code-block);flex-direction:column;flex:1;min-height:0;display:flex;position:relative;overflow:hidden}.F1KBNa_diffHeader{border-bottom:1px solid var(--dsw-alias-border-l2);align-items:center;gap:8px;padding:6px 8px;display:flex}.F1KBNa_diffPath{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-label-primary);font:var(--dsw-font-markdown-code-block);flex:1;font-size:12px;line-height:18px;overflow:hidden}.F1KBNa_diffActions{border-bottom:1px solid var(--dsw-alias-border-l2);align-items:center;gap:8px;padding:6px 8px;display:flex}.F1KBNa_diffStats{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;font-size:12px;line-height:16px}.F1KBNa_flexSpacer{flex:1}.F1KBNa_action{border:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-primary);cursor:pointer;background:0 0;border-radius:8px;padding:3px 12px;font-family:inherit;font-size:12px;line-height:18px}.F1KBNa_action:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.F1KBNa_action:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}.F1KBNa_actionPrimary{background:var(--dsw-alias-state-business-primary);border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-static-neutral-bluish-50)}.F1KBNa_actionPrimary:hover:not(:disabled){background:color-mix(in srgb, var(--dsw-alias-state-business-primary) 85%, var(--dsw-static-neutral-bluish-1000))}.F1KBNa_actionPrimary:disabled{background:var(--dsw-alias-state-business-primary);color:var(--dsw-static-neutral-bluish-50);opacity:.55}.F1KBNa_iconAction{justify-content:center;align-items:center;min-height:25px;padding:4px 6px;display:inline-flex}.F1KBNa_close{cursor:pointer;width:28px;height:28px;color:var(--dsw-alias-label-tertiary);background:0 0;border:none;border-radius:28px;justify-content:center;align-items:center;padding:0;display:inline-flex}.F1KBNa_close:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.F1KBNa_expand{cursor:pointer;width:28px;height:28px;color:var(--dsw-alias-label-tertiary);background:0 0;border:none;border-radius:28px;justify-content:center;align-items:center;padding:0;display:inline-flex}.F1KBNa_expand:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.F1KBNa_expandExpanded svg{transform:rotate(180deg)}.F1KBNa_diffBodyWrap{flex:1;min-height:0;display:flex;position:relative}.F1KBNa_diffBody{min-width:0;font:var(--dsw-font-markdown-code-block);cursor:text;flex:1;padding:0;position:relative;overflow:auto}.F1KBNa_diffBody::-webkit-scrollbar,.F1KBNa_diffBody::-webkit-scrollbar-track,.F1KBNa_diffBody::-webkit-scrollbar-thumb{cursor:default}.F1KBNa_blockActions{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-markdown-code-block);box-shadow:var(--dsw-shadow-lv1);z-index:2;border-radius:10px;align-items:center;gap:4px;padding:4px;display:flex;position:absolute;right:6px}.F1KBNa_overviewRuler{pointer-events:none;opacity:.5;width:4px;position:absolute;top:0;bottom:0;right:0}.F1KBNa_overviewMarker{border-radius:2px;width:100%;min-height:2px;position:absolute;right:0}.F1KBNa_markerDel{background-color:var(--dsw-alias-state-error-primary)}.F1KBNa_markerAdd{background-color:var(--dsw-alias-state-success-primary)}.F1KBNa_lines{border-spacing:0;width:max-content;min-width:100%;display:table}.F1KBNa_line{height:22px;line-height:22px;display:table-row}.F1KBNa_vSpacer{display:table-row}.F1KBNa_gutter{box-sizing:border-box;text-align:right;width:44px;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;user-select:none;cursor:default;padding-right:10px;display:table-cell}.F1KBNa_code{white-space:pre;display:table-cell}.F1KBNa_context{color:var(--dsw-alias-label-primary)}.F1KBNa_del{background-color:color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, transparent)}.F1KBNa_add{background-color:color-mix(in srgb, var(--dsw-alias-state-success-primary) 10%, transparent)}.F1KBNa_statusBar{box-sizing:border-box;border-top:1px solid var(--dsw-alias-border-l2);height:34px;color:var(--dsw-alias-label-tertiary);font-family:var(--dsw-font-markdown-code-block);font-variant-numeric:tabular-nums;flex:none;align-items:center;padding:0 8px;font-size:12px;line-height:18px;display:flex}.F1KBNa_statusAction{border:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-primary);white-space:nowrap;cursor:pointer;background:0 0;border-radius:8px;padding:3px 12px;font-family:inherit;font-size:12px;line-height:18px}.F1KBNa_statusAction:hover{background:var(--dsw-alias-interactive-bg-hover)}.F1KBNa_langSelect{border:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-primary);cursor:pointer;background:0 0;border-radius:8px;flex:none;align-items:center;gap:4px;padding:3px 12px;font-family:inherit;font-size:12px;line-height:18px;display:inline-flex}.F1KBNa_langSelect:hover{background:var(--dsw-alias-interactive-bg-hover)}.F1KBNa_langLabel{text-overflow:ellipsis;white-space:nowrap;max-width:140px;overflow:hidden}";
11634
11625
  const tagId = "dsh-diff-approval/PendingPanel.module.css";
11635
11626
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
11636
11627
  const tag = document.createElement("style");
@@ -11640,67 +11631,69 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
11640
11631
  document.head.appendChild(tag);
11641
11632
  }
11642
11633
  var PendingPanel_module_css_default = {
11643
- "code": "F1KBNa_code",
11644
- "diffBodyWrap": "F1KBNa_diffBodyWrap",
11645
- "badge": "F1KBNa_badge",
11646
- "readError": "F1KBNa_readError",
11647
- "diff": "F1KBNa_diff",
11648
- "markerDel": "F1KBNa_markerDel",
11649
- "group": "F1KBNa_group",
11650
- "divergedHint": "F1KBNa_divergedHint",
11651
- "detailEmpty": "F1KBNa_detailEmpty",
11652
- "iconAction": "F1KBNa_iconAction",
11634
+ "diffHeader": "F1KBNa_diffHeader",
11635
+ "missing": "F1KBNa_missing",
11636
+ "resizeHandle": "F1KBNa_resizeHandle",
11653
11637
  "diffBody": "F1KBNa_diffBody",
11654
11638
  "rowPath": "F1KBNa_rowPath",
11655
- "addCount": "F1KBNa_addCount",
11656
- "vSpacer": "F1KBNa_vSpacer",
11657
- "add": "F1KBNa_add",
11658
- "overviewMarker": "F1KBNa_overviewMarker",
11659
- "panel": "F1KBNa_panel",
11660
- "del": "F1KBNa_del",
11661
- "row": "F1KBNa_row",
11662
- "kindHint": "F1KBNa_kindHint",
11663
- "layer": "F1KBNa_layer",
11664
- "rowMeta": "F1KBNa_rowMeta",
11665
- "resizeHandle": "F1KBNa_resizeHandle",
11666
- "footerButtons": "F1KBNa_footerButtons",
11667
- "rowHead": "F1KBNa_rowHead",
11639
+ "expandExpanded": "F1KBNa_expandExpanded",
11640
+ "line": "F1KBNa_line",
11641
+ "headerActions": "F1KBNa_headerActions",
11642
+ "states": "F1KBNa_states",
11643
+ "detailEmpty": "F1KBNa_detailEmpty",
11644
+ "markerDel": "F1KBNa_markerDel",
11668
11645
  "close": "F1KBNa_close",
11646
+ "markerAdd": "F1KBNa_markerAdd",
11647
+ "statusBar": "F1KBNa_statusBar",
11648
+ "diffStats": "F1KBNa_diffStats",
11649
+ "footerButtons": "F1KBNa_footerButtons",
11669
11650
  "title": "F1KBNa_title",
11670
- "overviewRuler": "F1KBNa_overviewRuler",
11671
- "lines": "F1KBNa_lines",
11672
- "missingHint": "F1KBNa_missingHint",
11673
- "diffPath": "F1KBNa_diffPath",
11674
- "detail": "F1KBNa_detail",
11675
- "states": "F1KBNa_states",
11676
- "headerActions": "F1KBNa_headerActions",
11677
- "fileList": "F1KBNa_fileList",
11678
- "langSelect": "F1KBNa_langSelect",
11651
+ "row": "F1KBNa_row",
11652
+ "delCount": "F1KBNa_delCount",
11679
11653
  "expand": "F1KBNa_expand",
11680
- "langLabel": "F1KBNa_langLabel",
11681
- "statusAction": "F1KBNa_statusAction",
11682
- "badgeLabel": "F1KBNa_badgeLabel",
11654
+ "layer": "F1KBNa_layer",
11683
11655
  "diffActions": "F1KBNa_diffActions",
11684
- "flexSpacer": "F1KBNa_flexSpacer",
11685
- "kindTag": "F1KBNa_kindTag",
11686
- "gutter": "F1KBNa_gutter",
11687
- "markerAdd": "F1KBNa_markerAdd",
11688
- "rows": "F1KBNa_rows",
11689
- "delCount": "F1KBNa_delCount",
11690
11656
  "context": "F1KBNa_context",
11691
- "action": "F1KBNa_action",
11692
- "statusBar": "F1KBNa_statusBar",
11657
+ "langSelect": "F1KBNa_langSelect",
11658
+ "kindTag": "F1KBNa_kindTag",
11659
+ "langLabel": "F1KBNa_langLabel",
11660
+ "flexSpacer": "F1KBNa_flexSpacer",
11661
+ "add": "F1KBNa_add",
11662
+ "badgeLabel": "F1KBNa_badgeLabel",
11663
+ "diffPath": "F1KBNa_diffPath",
11693
11664
  "badgeCount": "F1KBNa_badgeCount",
11694
- "diffHeader": "F1KBNa_diffHeader",
11695
- "diffStats": "F1KBNa_diffStats",
11696
- "split": "F1KBNa_split",
11697
- "line": "F1KBNa_line",
11698
- "expandExpanded": "F1KBNa_expandExpanded",
11665
+ "rows": "F1KBNa_rows",
11699
11666
  "rail": "F1KBNa_rail",
11700
- "note": "F1KBNa_note",
11701
- "missing": "F1KBNa_missing",
11667
+ "lines": "F1KBNa_lines",
11668
+ "hint": "F1KBNa_hint",
11669
+ "code": "F1KBNa_code",
11670
+ "detail": "F1KBNa_detail",
11671
+ "readError": "F1KBNa_readError",
11672
+ "action": "F1KBNa_action",
11702
11673
  "header": "F1KBNa_header",
11703
- "hint": "F1KBNa_hint"
11674
+ "statusAction": "F1KBNa_statusAction",
11675
+ "panel": "F1KBNa_panel",
11676
+ "overviewMarker": "F1KBNa_overviewMarker",
11677
+ "blockActions": "F1KBNa_blockActions",
11678
+ "iconAction": "F1KBNa_iconAction",
11679
+ "rowHead": "F1KBNa_rowHead",
11680
+ "vSpacer": "F1KBNa_vSpacer",
11681
+ "missingHint": "F1KBNa_missingHint",
11682
+ "note": "F1KBNa_note",
11683
+ "gutter": "F1KBNa_gutter",
11684
+ "split": "F1KBNa_split",
11685
+ "divergedHint": "F1KBNa_divergedHint",
11686
+ "overviewRuler": "F1KBNa_overviewRuler",
11687
+ "rowMeta": "F1KBNa_rowMeta",
11688
+ "addCount": "F1KBNa_addCount",
11689
+ "diff": "F1KBNa_diff",
11690
+ "fileList": "F1KBNa_fileList",
11691
+ "actionPrimary": "F1KBNa_actionPrimary",
11692
+ "kindHint": "F1KBNa_kindHint",
11693
+ "diffBodyWrap": "F1KBNa_diffBodyWrap",
11694
+ "del": "F1KBNa_del",
11695
+ "badge": "F1KBNa_badge",
11696
+ "group": "F1KBNa_group"
11704
11697
  };
11705
11698
  //#endregion
11706
11699
  //#region lib/types/client/PendingPanel.js
@@ -11752,7 +11745,7 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
11752
11745
  * does not re-render rows whose content, highlight, and focus are unchanged.
11753
11746
  */
11754
11747
  const DiffRow = (0, react.memo)(function DiffRow(props) {
11755
- const { index, row, runs, focused } = props;
11748
+ const { index, row, runs, focused, onRowHover } = props;
11756
11749
  const lineNumber = row.kind === "del" ? row.oldLine : row.newLine;
11757
11750
  const sideRuns = row.kind === "del" ? runs?.oldRuns : runs?.newRuns;
11758
11751
  const lineRuns = lineNumber === void 0 ? void 0 : sideRuns?.[lineNumber - 1];
@@ -11765,6 +11758,9 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
11765
11758
  "data-diff-line": row.kind,
11766
11759
  "data-diff-row": index,
11767
11760
  "data-diff-focused": focused ? "" : void 0,
11761
+ onMouseEnter: () => {
11762
+ onRowHover(index);
11763
+ },
11768
11764
  children: [
11769
11765
  (0, react_jsx_runtime.jsx)("span", {
11770
11766
  className: PendingPanel_module_css_default.gutter,
@@ -11782,6 +11778,48 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
11782
11778
  ]
11783
11779
  });
11784
11780
  });
11781
+ /**
11782
+ * One diff block's old/new line ranges, 1-based inclusive, for block-level
11783
+ * keep/revert. A side with no lines (a pure addition or deletion) is empty;
11784
+ * its start is that side's insertion point — the line after the surrounding
11785
+ * context — so the host can insert there.
11786
+ * @param rows - the whole-file diff rows.
11787
+ * @param block - the block's row range.
11788
+ * @returns the old and new line ranges.
11789
+ */
11790
+ function blockRangesOf(rows, block) {
11791
+ let oldStart = Infinity;
11792
+ let oldEnd = -Infinity;
11793
+ let newStart = Infinity;
11794
+ let newEnd = -Infinity;
11795
+ for (let index = block.start; index <= block.end; index++) {
11796
+ const row = rows[index];
11797
+ if (row === void 0) continue;
11798
+ if (row.oldLine !== void 0) {
11799
+ oldStart = Math.min(oldStart, row.oldLine);
11800
+ oldEnd = Math.max(oldEnd, row.oldLine);
11801
+ }
11802
+ if (row.newLine !== void 0) {
11803
+ newStart = Math.min(newStart, row.newLine);
11804
+ newEnd = Math.max(newEnd, row.newLine);
11805
+ }
11806
+ }
11807
+ const before = rows[block.start - 1];
11808
+ if (oldStart === Infinity) {
11809
+ oldStart = (before?.oldLine ?? 0) + 1;
11810
+ oldEnd = oldStart - 1;
11811
+ }
11812
+ if (newStart === Infinity) {
11813
+ newStart = (before?.newLine ?? 0) + 1;
11814
+ newEnd = newStart - 1;
11815
+ }
11816
+ return {
11817
+ oldStart,
11818
+ oldEnd,
11819
+ newStart,
11820
+ newEnd
11821
+ };
11822
+ }
11785
11823
  /** Split a row list into maximal runs of non-context rows. */
11786
11824
  function changeBlocksOf(diff) {
11787
11825
  const blocks = [];
@@ -11866,48 +11904,48 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
11866
11904
  const stats = (0, react.useMemo)(() => computeWholeFileDiff(file.oldText, file.newText), [file.oldText, file.newText]);
11867
11905
  return (0, react_jsx_runtime.jsx)("li", {
11868
11906
  className: PendingPanel_module_css_default.row,
11869
- children: (0, react_jsx_runtime.jsxs)("button", {
11870
- type: "button",
11871
- className: PendingPanel_module_css_default.rowHead,
11872
- "data-selected": selected || void 0,
11873
- onClick: () => {
11874
- onSelect(file.id);
11875
- },
11876
- children: [
11877
- (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
11878
- label: file.path,
11879
- delayMs: 500,
11880
- maxWidth: 560,
11881
- children: (0, react_jsx_runtime.jsx)("span", {
11907
+ children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
11908
+ label: file.path,
11909
+ delayMs: 500,
11910
+ maxWidth: 560,
11911
+ children: (0, react_jsx_runtime.jsxs)("button", {
11912
+ type: "button",
11913
+ className: PendingPanel_module_css_default.rowHead,
11914
+ "data-selected": selected || void 0,
11915
+ onClick: () => {
11916
+ onSelect(file.id);
11917
+ },
11918
+ children: [
11919
+ (0, react_jsx_runtime.jsx)("span", {
11882
11920
  className: PendingPanel_module_css_default.rowPath,
11883
11921
  children: basenameOf(file.path)
11922
+ }),
11923
+ file.kind === "create" && (0, react_jsx_runtime.jsx)("span", {
11924
+ className: PendingPanel_module_css_default.kindTag,
11925
+ children: t("row.create")
11926
+ }),
11927
+ file.missing && (0, react_jsx_runtime.jsx)("span", {
11928
+ className: PendingPanel_module_css_default.missing,
11929
+ title: t("panel.missingHint"),
11930
+ children: t("panel.missing")
11931
+ }),
11932
+ (0, react_jsx_runtime.jsxs)("span", {
11933
+ className: PendingPanel_module_css_default.rowMeta,
11934
+ children: [(0, react_jsx_runtime.jsx)("span", {
11935
+ className: PendingPanel_module_css_default.addCount,
11936
+ children: t("row.added", { added: stats.added })
11937
+ }), (0, react_jsx_runtime.jsx)("span", {
11938
+ className: PendingPanel_module_css_default.delCount,
11939
+ children: t("row.removed", { removed: stats.removed })
11940
+ })]
11884
11941
  })
11885
- }),
11886
- file.kind === "create" && (0, react_jsx_runtime.jsx)("span", {
11887
- className: PendingPanel_module_css_default.kindTag,
11888
- children: t("row.create")
11889
- }),
11890
- file.missing && (0, react_jsx_runtime.jsx)("span", {
11891
- className: PendingPanel_module_css_default.missing,
11892
- title: t("panel.missingHint"),
11893
- children: t("panel.missing")
11894
- }),
11895
- (0, react_jsx_runtime.jsxs)("span", {
11896
- className: PendingPanel_module_css_default.rowMeta,
11897
- children: [(0, react_jsx_runtime.jsx)("span", {
11898
- className: PendingPanel_module_css_default.addCount,
11899
- children: t("row.added", { added: stats.added })
11900
- }), (0, react_jsx_runtime.jsx)("span", {
11901
- className: PendingPanel_module_css_default.delCount,
11902
- children: t("row.removed", { removed: stats.removed })
11903
- })]
11904
- })
11905
- ]
11942
+ ]
11943
+ })
11906
11944
  })
11907
11945
  });
11908
11946
  }
11909
11947
  /** The selected file's diff, actions, jump controls, and copy toolbar. */
11910
- function PendingDiff({ file, files, busy, t, onKeep, onRevert, onOpen }) {
11948
+ function PendingDiff({ file, busy, workspacePath, t, onKeep, onRevert, onBlockKeep, onBlockRevert, onOpen }) {
11911
11949
  const [langOverride, setLangOverride] = (0, react.useState)(void 0);
11912
11950
  const [langMenuOpen, setLangMenuOpen] = (0, react.useState)(false);
11913
11951
  const langMenuItems = (0, react.useMemo)(() => [{
@@ -11985,17 +12023,32 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
11985
12023
  const [scrollTick, setScrollTick] = (0, react.useState)(0);
11986
12024
  const [scrollTop, setScrollTop] = (0, react.useState)(0);
11987
12025
  const [viewportHeight, setViewportHeight] = (0, react.useState)(0);
12026
+ const [hoveredBlock, setHoveredBlock] = (0, react.useState)(void 0);
11988
12027
  const [selection, setSelection] = (0, react.useState)(void 0);
11989
12028
  const [copied, setCopied] = (0, react.useState)(false);
11990
12029
  (0, react.useEffect)(() => {
11991
12030
  setFocus(0);
11992
12031
  setScrollTop(0);
11993
12032
  if (bodyRef.current !== null) bodyRef.current.scrollTop = 0;
12033
+ setHoveredBlock(void 0);
11994
12034
  setSelection(void 0);
11995
12035
  setLangOverride(void 0);
11996
12036
  setLangMenuOpen(false);
11997
12037
  setCopied(false);
11998
12038
  }, [file.id]);
12039
+ const blockRanges = (0, react.useMemo)(() => {
12040
+ return model.blocks.map((block) => blockRangesOf(model.diff.rows, block));
12041
+ }, [model]);
12042
+ const blockIndexByRow = (0, react.useMemo)(() => {
12043
+ const map = /* @__PURE__ */ new Map();
12044
+ model.blocks.forEach((block, blockIndex) => {
12045
+ for (let index = block.start; index <= block.end; index++) map.set(index, blockIndex);
12046
+ });
12047
+ return map;
12048
+ }, [model]);
12049
+ const onRowHover = (0, react.useCallback)((index) => {
12050
+ setHoveredBlock(blockIndexByRow.get(index));
12051
+ }, [blockIndexByRow]);
11999
12052
  const rows = model.diff.rows;
12000
12053
  const rowCount = rows.length;
12001
12054
  const viewport = viewportHeight > 0 ? viewportHeight : rowCount * ROW_HEIGHT_PX;
@@ -12066,9 +12119,9 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
12066
12119
  }, [file.id]);
12067
12120
  const selectionReference = (() => {
12068
12121
  if (selection === void 0) return void 0;
12069
- const lineNumbers = model.diff.rows.slice(selection.start, selection.end + 1).map((row) => row.newLine ?? row.oldLine).filter((number) => number !== void 0);
12122
+ const lineNumbers = model.diff.rows.slice(selection.start, selection.end + 1).map((row) => row.newLine).filter((number) => number !== void 0);
12070
12123
  if (lineNumbers.length === 0) return void 0;
12071
- return referenceOf(file.path, files, Math.min(...lineNumbers), Math.max(...lineNumbers));
12124
+ return referenceOf(file.path, workspacePath, Math.min(...lineNumbers), Math.max(...lineNumbers));
12072
12125
  })();
12073
12126
  const copySelection = (0, react.useCallback)(async () => {
12074
12127
  if (selectionReference === void 0) return;
@@ -12104,27 +12157,35 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
12104
12157
  className: PendingPanel_module_css_default.diffPath,
12105
12158
  children: file.path
12106
12159
  }),
12107
- (0, react_jsx_runtime.jsx)("button", {
12108
- type: "button",
12109
- className: `${PendingPanel_module_css_default.action} ${PendingPanel_module_css_default.iconAction}`,
12110
- "data-diff-open": true,
12111
- "aria-label": t("action.openFile"),
12112
- title: t("action.openFile"),
12113
- onClick: () => {
12114
- onOpen(file.sessionId, file.id, "open");
12115
- },
12116
- children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconBrowseOutline16, { size: 14 })
12160
+ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
12161
+ label: t("action.openFile"),
12162
+ side: "bottom",
12163
+ delayMs: 500,
12164
+ children: (0, react_jsx_runtime.jsx)("button", {
12165
+ type: "button",
12166
+ className: `${PendingPanel_module_css_default.action} ${PendingPanel_module_css_default.iconAction}`,
12167
+ "data-diff-open": true,
12168
+ "aria-label": t("action.openFile"),
12169
+ onClick: () => {
12170
+ onOpen(file.sessionId, file.id, "open");
12171
+ },
12172
+ children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconBrowseOutline16, { size: 14 })
12173
+ })
12117
12174
  }),
12118
- (0, react_jsx_runtime.jsx)("button", {
12119
- type: "button",
12120
- className: `${PendingPanel_module_css_default.action} ${PendingPanel_module_css_default.iconAction}`,
12121
- "data-diff-reveal": true,
12122
- "aria-label": t("action.revealFile"),
12123
- title: t("action.revealFile"),
12124
- onClick: () => {
12125
- onOpen(file.sessionId, file.id, "reveal");
12126
- },
12127
- children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFolderOpenOutline16, { size: 14 })
12175
+ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
12176
+ label: t("action.revealFile"),
12177
+ side: "bottom",
12178
+ delayMs: 500,
12179
+ children: (0, react_jsx_runtime.jsx)("button", {
12180
+ type: "button",
12181
+ className: `${PendingPanel_module_css_default.action} ${PendingPanel_module_css_default.iconAction}`,
12182
+ "data-diff-reveal": true,
12183
+ "aria-label": t("action.revealFile"),
12184
+ onClick: () => {
12185
+ onOpen(file.sessionId, file.id, "reveal");
12186
+ },
12187
+ children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFolderOpenOutline16, { size: 14 })
12188
+ })
12128
12189
  })
12129
12190
  ]
12130
12191
  }),
@@ -12142,33 +12203,41 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
12142
12203
  className: PendingPanel_module_css_default.kindHint,
12143
12204
  children: t("panel.createHint")
12144
12205
  }),
12145
- model.blocks.length > 0 && (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsx)("button", {
12146
- type: "button",
12147
- className: `${PendingPanel_module_css_default.action} ${PendingPanel_module_css_default.iconAction}`,
12148
- "data-diff-prev": true,
12149
- "aria-label": t("action.prevDiff"),
12150
- title: t("action.prevDiff"),
12151
- disabled: busy,
12152
- onClick: () => {
12153
- jump(-1);
12154
- },
12155
- children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronUpOutline14, { size: 14 })
12156
- }), (0, react_jsx_runtime.jsx)("button", {
12157
- type: "button",
12158
- className: `${PendingPanel_module_css_default.action} ${PendingPanel_module_css_default.iconAction}`,
12159
- "data-diff-next": true,
12160
- "aria-label": t("action.nextDiff"),
12161
- title: t("action.nextDiff"),
12162
- disabled: busy,
12163
- onClick: () => {
12164
- jump(1);
12165
- },
12166
- children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, { size: 14 })
12206
+ model.blocks.length > 0 && (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
12207
+ label: t("action.prevDiff"),
12208
+ side: "bottom",
12209
+ delayMs: 500,
12210
+ children: (0, react_jsx_runtime.jsx)("button", {
12211
+ type: "button",
12212
+ className: `${PendingPanel_module_css_default.action} ${PendingPanel_module_css_default.iconAction}`,
12213
+ "data-diff-prev": true,
12214
+ "aria-label": t("action.prevDiff"),
12215
+ disabled: busy,
12216
+ onClick: () => {
12217
+ jump(-1);
12218
+ },
12219
+ children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronUpOutline14, { size: 14 })
12220
+ })
12221
+ }), (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
12222
+ label: t("action.nextDiff"),
12223
+ side: "bottom",
12224
+ delayMs: 500,
12225
+ children: (0, react_jsx_runtime.jsx)("button", {
12226
+ type: "button",
12227
+ className: `${PendingPanel_module_css_default.action} ${PendingPanel_module_css_default.iconAction}`,
12228
+ "data-diff-next": true,
12229
+ "aria-label": t("action.nextDiff"),
12230
+ disabled: busy,
12231
+ onClick: () => {
12232
+ jump(1);
12233
+ },
12234
+ children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, { size: 14 })
12235
+ })
12167
12236
  })] }),
12168
12237
  (0, react_jsx_runtime.jsx)("span", { className: PendingPanel_module_css_default.flexSpacer }),
12169
12238
  (0, react_jsx_runtime.jsx)("button", {
12170
12239
  type: "button",
12171
- className: PendingPanel_module_css_default.action,
12240
+ className: `${PendingPanel_module_css_default.action} ${PendingPanel_module_css_default.actionPrimary}`,
12172
12241
  "data-diff-keep": true,
12173
12242
  disabled: busy,
12174
12243
  onClick: () => {
@@ -12194,12 +12263,15 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
12194
12263
  }),
12195
12264
  (0, react_jsx_runtime.jsxs)("div", {
12196
12265
  className: PendingPanel_module_css_default.diffBodyWrap,
12197
- children: [(0, react_jsx_runtime.jsx)("div", {
12266
+ children: [(0, react_jsx_runtime.jsxs)("div", {
12198
12267
  className: PendingPanel_module_css_default.diffBody,
12199
12268
  ref: bodyRef,
12200
12269
  onScroll,
12270
+ onMouseLeave: () => {
12271
+ setHoveredBlock(void 0);
12272
+ },
12201
12273
  "data-diff-body": true,
12202
- children: (0, react_jsx_runtime.jsxs)("div", {
12274
+ children: [(0, react_jsx_runtime.jsxs)("div", {
12203
12275
  className: PendingPanel_module_css_default.lines,
12204
12276
  style: { minWidth: `max(100%, ${widestLine}ch)` },
12205
12277
  children: [
@@ -12214,7 +12286,8 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
12214
12286
  index,
12215
12287
  row,
12216
12288
  runs,
12217
- focused: inFocusedBlock(index)
12289
+ focused: inFocusedBlock(index),
12290
+ onRowHover
12218
12291
  }, index);
12219
12292
  }),
12220
12293
  end < rowCount && (0, react_jsx_runtime.jsx)("div", {
@@ -12223,7 +12296,30 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
12223
12296
  "aria-hidden": "true"
12224
12297
  })
12225
12298
  ]
12226
- })
12299
+ }), hoveredBlock !== void 0 && model.blocks[hoveredBlock] !== void 0 && (0, react_jsx_runtime.jsxs)("div", {
12300
+ className: PendingPanel_module_css_default.blockActions,
12301
+ "data-diff-block-actions": true,
12302
+ style: { top: model.blocks[hoveredBlock].start * ROW_HEIGHT_PX },
12303
+ children: [(0, react_jsx_runtime.jsx)("button", {
12304
+ type: "button",
12305
+ className: `${PendingPanel_module_css_default.action} ${PendingPanel_module_css_default.actionPrimary}`,
12306
+ "data-diff-block-keep": true,
12307
+ disabled: busy,
12308
+ onClick: () => {
12309
+ onBlockKeep(file.sessionId, file.id, blockRanges[hoveredBlock]);
12310
+ },
12311
+ children: t("action.keep")
12312
+ }), (0, react_jsx_runtime.jsx)("button", {
12313
+ type: "button",
12314
+ className: PendingPanel_module_css_default.action,
12315
+ "data-diff-block-revert": true,
12316
+ disabled: busy,
12317
+ onClick: () => {
12318
+ onBlockRevert(file.sessionId, file.id, blockRanges[hoveredBlock]);
12319
+ },
12320
+ children: t("action.revert")
12321
+ })]
12322
+ })]
12227
12323
  }), rulerMarkers.length > 0 && (0, react_jsx_runtime.jsx)("div", {
12228
12324
  className: PendingPanel_module_css_default.overviewRuler,
12229
12325
  "data-diff-approval-ruler": true,
@@ -12274,19 +12370,23 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
12274
12370
  onClose: () => {
12275
12371
  setLangMenuOpen(false);
12276
12372
  },
12277
- anchor: (0, react_jsx_runtime.jsxs)("button", {
12278
- type: "button",
12279
- className: PendingPanel_module_css_default.langSelect,
12280
- "data-diff-lang": true,
12281
- "aria-label": t("action.langSelect"),
12282
- title: t("action.langSelect"),
12283
- onClick: () => {
12284
- setLangMenuOpen((value) => !value);
12285
- },
12286
- children: [(0, react_jsx_runtime.jsx)("span", {
12287
- className: PendingPanel_module_css_default.langLabel,
12288
- children: langLabel
12289
- }), (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, { size: 12 })]
12373
+ anchor: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
12374
+ label: t("action.langSelect"),
12375
+ side: "top",
12376
+ delayMs: 500,
12377
+ children: (0, react_jsx_runtime.jsxs)("button", {
12378
+ type: "button",
12379
+ className: PendingPanel_module_css_default.langSelect,
12380
+ "data-diff-lang": true,
12381
+ "aria-label": t("action.langSelect"),
12382
+ onClick: () => {
12383
+ setLangMenuOpen((value) => !value);
12384
+ },
12385
+ children: [(0, react_jsx_runtime.jsx)("span", {
12386
+ className: PendingPanel_module_css_default.langLabel,
12387
+ children: langLabel
12388
+ }), (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, { size: 12 })]
12389
+ })
12290
12390
  })
12291
12391
  })
12292
12392
  ]
@@ -12295,7 +12395,7 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
12295
12395
  });
12296
12396
  }
12297
12397
  /** Render the pending-edit review panel and its unified footer action. */
12298
- function PendingPanel({ wide, useSessions, usePending, onRefresh, onKeep, onRevert, onOpen, t }) {
12398
+ function PendingPanel({ wide, useSessions, usePending, onRefresh, onKeep, onRevert, onBlockKeep, onBlockRevert, onOpen, t }) {
12299
12399
  const current = useSessions((state) => state.current);
12300
12400
  const snapshot = usePending((snapshot) => snapshot);
12301
12401
  const [open, setOpen] = (0, react.useState)(false);
@@ -12438,26 +12538,34 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
12438
12538
  children: t("panel.title")
12439
12539
  }), (0, react_jsx_runtime.jsxs)("div", {
12440
12540
  className: PendingPanel_module_css_default.headerActions,
12441
- children: [(0, react_jsx_runtime.jsx)("button", {
12442
- type: "button",
12443
- className: expanded ? `${PendingPanel_module_css_default.expand} ${PendingPanel_module_css_default.expandExpanded}` : PendingPanel_module_css_default.expand,
12444
- "data-diff-approval-expand": true,
12445
- "aria-label": t(expanded ? "action.exitFullscreen" : "action.expand"),
12446
- title: t(expanded ? "action.exitFullscreen" : "action.expand"),
12447
- onClick: () => {
12448
- setExpanded((value) => !value);
12449
- },
12450
- children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFullscreenOutline16, { size: 14 })
12451
- }), (0, react_jsx_runtime.jsx)("button", {
12452
- type: "button",
12453
- className: PendingPanel_module_css_default.close,
12454
- "data-diff-approval-close": true,
12455
- "aria-label": t("action.close"),
12456
- title: t("action.close"),
12457
- onClick: () => {
12458
- setOpen(false);
12459
- },
12460
- children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCloseOutline16, { size: 14 })
12541
+ children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
12542
+ label: t(expanded ? "action.exitFullscreen" : "action.expand"),
12543
+ side: "bottom",
12544
+ delayMs: 500,
12545
+ children: (0, react_jsx_runtime.jsx)("button", {
12546
+ type: "button",
12547
+ className: expanded ? `${PendingPanel_module_css_default.expand} ${PendingPanel_module_css_default.expandExpanded}` : PendingPanel_module_css_default.expand,
12548
+ "data-diff-approval-expand": true,
12549
+ "aria-label": t(expanded ? "action.exitFullscreen" : "action.expand"),
12550
+ onClick: () => {
12551
+ setExpanded((value) => !value);
12552
+ },
12553
+ children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFullscreenOutline16, { size: 14 })
12554
+ })
12555
+ }), (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
12556
+ label: t("action.close"),
12557
+ side: "bottom",
12558
+ delayMs: 500,
12559
+ children: (0, react_jsx_runtime.jsx)("button", {
12560
+ type: "button",
12561
+ className: PendingPanel_module_css_default.close,
12562
+ "data-diff-approval-close": true,
12563
+ "aria-label": t("action.close"),
12564
+ onClick: () => {
12565
+ setOpen(false);
12566
+ },
12567
+ children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCloseOutline16, { size: 14 })
12568
+ })
12461
12569
  })]
12462
12570
  })]
12463
12571
  }), snapshot.error !== void 0 || !snapshot.read || files.length === 0 ? (0, react_jsx_runtime.jsxs)("div", {
@@ -12510,11 +12618,13 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
12510
12618
  children: t("panel.selectHint")
12511
12619
  }) : (0, react_jsx_runtime.jsx)(PendingDiff, {
12512
12620
  file: selectedFile,
12513
- files,
12514
12621
  busy: snapshot.busy.has(selectedFile.id),
12622
+ workspacePath: snapshot.workspacePath,
12515
12623
  t,
12516
12624
  onKeep,
12517
12625
  onRevert,
12626
+ onBlockKeep,
12627
+ onBlockRevert,
12518
12628
  onOpen
12519
12629
  })
12520
12630
  })
@@ -12562,7 +12672,7 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
12562
12672
  function createDiffApprovalPort(rpc) {
12563
12673
  return {
12564
12674
  async list(sessionId) {
12565
- return filesOf(await rpc.call(DIFF_APPROVAL_CHANNEL, "list", { sessionId }));
12675
+ return listValueOf(await rpc.call(DIFF_APPROVAL_CHANNEL, "list", { sessionId }));
12566
12676
  },
12567
12677
  async keep(sessionId, id) {
12568
12678
  return actionOf(await rpc.call(DIFF_APPROVAL_CHANNEL, "keep", {
@@ -12576,6 +12686,20 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
12576
12686
  id
12577
12687
  }));
12578
12688
  },
12689
+ async blockKeep(sessionId, id, block) {
12690
+ return actionOf(await rpc.call(DIFF_APPROVAL_CHANNEL, "block-keep", {
12691
+ sessionId,
12692
+ id,
12693
+ block
12694
+ }));
12695
+ },
12696
+ async blockRevert(sessionId, id, block) {
12697
+ return actionOf(await rpc.call(DIFF_APPROVAL_CHANNEL, "block-revert", {
12698
+ sessionId,
12699
+ id,
12700
+ block
12701
+ }));
12702
+ },
12579
12703
  async open(sessionId, id, action) {
12580
12704
  return openOf(await rpc.call(DIFF_APPROVAL_CHANNEL, "open", {
12581
12705
  sessionId,
@@ -12608,18 +12732,23 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
12608
12732
  };
12609
12733
  }
12610
12734
  /** Narrow the list endpoint's value; a malformed wire value is a read failure. */
12611
- function filesOf(result) {
12735
+ function listValueOf(result) {
12612
12736
  if (!result.ok) throw new Error(`${result.error.code}: ${result.error.message}`);
12613
12737
  const value = result.value;
12614
12738
  if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("list returned a malformed value");
12615
12739
  const rows = value.files;
12616
12740
  if (!Array.isArray(rows)) throw new Error("list returned a malformed value");
12741
+ const workspace = value.workspacePath;
12742
+ const workspacePath = typeof workspace === "string" && workspace.length > 0 ? workspace : void 0;
12617
12743
  const files = [];
12618
12744
  for (const row of rows) {
12619
12745
  const file = pendingFileOf(row);
12620
12746
  if (file !== void 0) files.push(file);
12621
12747
  }
12622
- return files;
12748
+ return {
12749
+ files,
12750
+ workspacePath
12751
+ };
12623
12752
  }
12624
12753
  /** Narrow one action endpoint's value; a malformed wire value is an action failure. */
12625
12754
  function actionOf(result) {
@@ -12706,16 +12835,18 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
12706
12835
  return;
12707
12836
  }
12708
12837
  try {
12709
- const files = await port.list(sessionId);
12838
+ const { files, workspacePath } = await port.list(sessionId);
12710
12839
  publish({
12711
12840
  read: true,
12712
12841
  files,
12842
+ workspacePath,
12713
12843
  busy: EMPTY_BUSY
12714
12844
  });
12715
12845
  } catch (error) {
12716
12846
  publish({
12717
12847
  read: true,
12718
12848
  files: snapshot.files,
12849
+ workspacePath: snapshot.workspacePath,
12719
12850
  error: error instanceof Error ? error.message : String(error),
12720
12851
  busy: snapshot.busy
12721
12852
  });
@@ -12731,6 +12862,42 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
12731
12862
  await port.revert(sessionId, id);
12732
12863
  });
12733
12864
  },
12865
+ async blockKeep(sessionId, id, block) {
12866
+ const { error: _cleared, ...base } = snapshot;
12867
+ publish({
12868
+ ...base,
12869
+ busy: /* @__PURE__ */ new Set([...snapshot.busy, id])
12870
+ });
12871
+ try {
12872
+ await port.blockKeep(sessionId, id, block);
12873
+ } catch (error) {
12874
+ publish({
12875
+ ...snapshot,
12876
+ error: error instanceof Error ? error.message : String(error),
12877
+ busy: new Set([...snapshot.busy].filter((busy) => busy !== id))
12878
+ });
12879
+ return;
12880
+ }
12881
+ await this.refresh(sessionId);
12882
+ },
12883
+ async blockRevert(sessionId, id, block) {
12884
+ const { error: _cleared, ...base } = snapshot;
12885
+ publish({
12886
+ ...base,
12887
+ busy: /* @__PURE__ */ new Set([...snapshot.busy, id])
12888
+ });
12889
+ try {
12890
+ await port.blockRevert(sessionId, id, block);
12891
+ } catch (error) {
12892
+ publish({
12893
+ ...snapshot,
12894
+ error: error instanceof Error ? error.message : String(error),
12895
+ busy: new Set([...snapshot.busy].filter((busy) => busy !== id))
12896
+ });
12897
+ return;
12898
+ }
12899
+ await this.refresh(sessionId);
12900
+ },
12734
12901
  async open(sessionId, id, action) {
12735
12902
  try {
12736
12903
  await port.open(sessionId, id, action);
@@ -12860,6 +13027,8 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
12860
13027
  },
12861
13028
  onKeep: (sessionId, path) => store.keep(sessionId, path),
12862
13029
  onRevert: (sessionId, path) => store.revert(sessionId, path),
13030
+ onBlockKeep: (sessionId, id, block) => store.blockKeep(sessionId, id, block),
13031
+ onBlockRevert: (sessionId, id, block) => store.blockRevert(sessionId, id, block),
12863
13032
  onOpen: (sessionId, id, action) => store.open(sessionId, id, action)
12864
13033
  })
12865
13034
  }, PendingPanel));
package/lib/index.js CHANGED
@@ -108,6 +108,28 @@ var PendingDiffStore = class {
108
108
  for (const [key, indexedId] of this.pathIndex) if (indexedId === id) this.pathIndex.delete(key);
109
109
  return true;
110
110
  }
111
+ /**
112
+ * Advance one entry's tracked content after a block-level keep/revert. The
113
+ * entry keeps its id and path; only the given side's text and capture time
114
+ * move. When the caller decides the sides now match, it removes the entry
115
+ * instead of updating it.
116
+ * @param sessionId - the owning session.
117
+ * @param id - the entry id.
118
+ * @param patch - the side to advance (`oldText` for keep, `newText` for revert).
119
+ * @returns whether the entry changed.
120
+ */
121
+ update(sessionId, id, patch) {
122
+ const key = entryKey(sessionId, id);
123
+ const entry = this.entries.get(key);
124
+ if (entry === void 0) return false;
125
+ const next = {
126
+ ...entry,
127
+ ...patch,
128
+ updatedAt: Date.now()
129
+ };
130
+ this.entries.set(key, next);
131
+ return true;
132
+ }
111
133
  /** Total entry count across all sessions. */
112
134
  get size() {
113
135
  return this.entries.size;
@@ -760,7 +782,10 @@ function apply(ctx, config) {
760
782
  if (sessionId === void 0) return rpcError("sessionId must be a non-empty string");
761
783
  return {
762
784
  ok: true,
763
- value: { files: await listWithState(await workspaceEntries(sessionId)) }
785
+ value: {
786
+ files: await listWithState(await workspaceEntries(sessionId)),
787
+ workspacePath: workspaceOf(sessionId)?.path
788
+ }
764
789
  };
765
790
  }
766
791
  case "keep": {
@@ -797,6 +822,51 @@ function apply(ctx, config) {
797
822
  value: { outcome: "reverted" }
798
823
  };
799
824
  }
825
+ case "block-keep": {
826
+ const blockTarget = blockTargetOf(payload);
827
+ if (blockTarget === void 0) return rpcError("sessionId, id, and block must be valid");
828
+ await ensureLoaded(blockTarget.sessionId);
829
+ const entry = store.get(blockTarget.sessionId, blockTarget.id);
830
+ if (entry === void 0) return {
831
+ ok: true,
832
+ value: { outcome: "missing" }
833
+ };
834
+ const accepted = contentRangeOf(entry.newText, blockTarget.block.newStart, blockTarget.block.newEnd);
835
+ const updatedOld = replaceContentLines(entry.oldText, blockTarget.block.oldStart, blockTarget.block.oldEnd, accepted);
836
+ if (updatedOld === entry.newText) store.remove(blockTarget.sessionId, blockTarget.id);
837
+ else store.update(blockTarget.sessionId, blockTarget.id, { oldText: updatedOld });
838
+ await persistSession(blockTarget.sessionId);
839
+ return {
840
+ ok: true,
841
+ value: { outcome: "kept" }
842
+ };
843
+ }
844
+ case "block-revert": {
845
+ const blockTarget = blockTargetOf(payload);
846
+ if (blockTarget === void 0) return rpcError("sessionId, id, and block must be valid");
847
+ await ensureLoaded(blockTarget.sessionId);
848
+ const entry = store.get(blockTarget.sessionId, blockTarget.id);
849
+ if (entry === void 0) return {
850
+ ok: true,
851
+ value: { outcome: "missing" }
852
+ };
853
+ const restored = contentRangeOf(entry.oldText, blockTarget.block.oldStart, blockTarget.block.oldEnd);
854
+ const updatedNew = replaceContentLines(entry.newText, blockTarget.block.newStart, blockTarget.block.newEnd, restored);
855
+ try {
856
+ const resolved = await ctx.fs.resolve(entry.path, { signal });
857
+ if (entry.kind === "create" && updatedNew === "") await rm(ctx.fs.processPath(resolved), { force: true });
858
+ else await ctx.fs.writeText(resolved, updatedNew, void 0, signal);
859
+ } catch (error) {
860
+ return rpcError(`block revert failed: ${errorMessage(error)}`);
861
+ }
862
+ if (updatedNew === entry.oldText) store.remove(blockTarget.sessionId, blockTarget.id);
863
+ else store.update(blockTarget.sessionId, blockTarget.id, { newText: updatedNew });
864
+ await persistSession(blockTarget.sessionId);
865
+ return {
866
+ ok: true,
867
+ value: { outcome: "reverted" }
868
+ };
869
+ }
800
870
  case "open": {
801
871
  const target = openTargetOf(payload);
802
872
  if (target === void 0) return rpcError("sessionId, id, and action must be valid");
@@ -836,6 +906,73 @@ function sessionOf(payload) {
836
906
  const value = payload.sessionId;
837
907
  return typeof value === "string" && value.length > 0 ? SessionId(value) : void 0;
838
908
  }
909
+ /** The content lines of `text`, matching the diff's line numbering (a single
910
+ trailing newline is a terminator, not an extra empty line). */
911
+ function contentLinesOf(text) {
912
+ if (text === "") return [];
913
+ const lines = text.split("\n");
914
+ if (lines[lines.length - 1] === "") lines.pop();
915
+ return lines;
916
+ }
917
+ /** Rebuild text from content lines, keeping `original`'s trailing-newline convention. */
918
+ function fromContentLines(original, lines) {
919
+ if (lines.length === 0) return "";
920
+ return lines.join("\n") + (original.endsWith("\n") ? "\n" : "");
921
+ }
922
+ /** The content lines [start..end] (1-based inclusive) of `text`; empty when start > end. */
923
+ function contentRangeOf(text, start, end) {
924
+ if (start > end) return [];
925
+ return contentLinesOf(text).slice(start - 1, end);
926
+ }
927
+ /**
928
+ * Replace the content lines [start..end] (1-based) of `text` with `replacement`
929
+ * lines. An empty range (`start > end`) inserts before line `start`. Out-of-range
930
+ * bounds clamp; the trailing-newline convention of `text` is preserved.
931
+ */
932
+ function replaceContentLines(text, start, end, replacement) {
933
+ const lines = contentLinesOf(text);
934
+ const count = lines.length;
935
+ if (start > end) {
936
+ const at = Math.min(Math.max(start, 1), count + 1);
937
+ return fromContentLines(text, [
938
+ ...lines.slice(0, at - 1),
939
+ ...replacement,
940
+ ...lines.slice(at - 1)
941
+ ]);
942
+ }
943
+ const s = Math.min(Math.max(start, 1), count + 1);
944
+ const e = Math.min(Math.max(end, 1), count);
945
+ if (s > e) return text;
946
+ return fromContentLines(text, [
947
+ ...lines.slice(0, s - 1),
948
+ ...replacement,
949
+ ...lines.slice(e)
950
+ ]);
951
+ }
952
+ /** Narrow a wire payload to one block keep/revert target. */
953
+ function blockTargetOf(payload) {
954
+ const target = targetOf(payload);
955
+ if (target === void 0) return void 0;
956
+ if (typeof payload !== "object" || payload === null || Array.isArray(payload)) return void 0;
957
+ const block = payload.block;
958
+ if (typeof block !== "object" || block === null || Array.isArray(block)) return void 0;
959
+ const { oldStart, oldEnd, newStart, newEnd } = block;
960
+ if (![
961
+ oldStart,
962
+ oldEnd,
963
+ newStart,
964
+ newEnd
965
+ ].every((value) => typeof value === "number" && Number.isFinite(value))) return void 0;
966
+ return {
967
+ ...target,
968
+ block: {
969
+ oldStart,
970
+ oldEnd,
971
+ newStart,
972
+ newEnd
973
+ }
974
+ };
975
+ }
839
976
  /** Narrow a wire payload to one keep/revert target. */
840
977
  function targetOf(payload) {
841
978
  const sessionId = sessionOf(payload);
@@ -4,4 +4,4 @@ import type { PendingPanelFace } from './slots.ts';
4
4
  /** Full panel props composed by the sidebar footer-action slot. */
5
5
  export type PendingPanelProps = PropsRuntime<'sidebar.footer.action'> & InjectFace<PendingPanelFace> & PropsLocale<'diff-approval'>;
6
6
  /** Render the pending-edit review panel and its unified footer action. */
7
- export declare function PendingPanel({ wide, useSessions, usePending, onRefresh, onKeep, onRevert, onOpen, t, }: PendingPanelProps): import("react").JSX.Element;
7
+ export declare function PendingPanel({ wide, useSessions, usePending, onRefresh, onKeep, onRevert, onBlockKeep, onBlockRevert, onOpen, t, }: PendingPanelProps): import("react").JSX.Element;
@@ -5,17 +5,21 @@
5
5
  * @module dsh-diff-approval/client/port
6
6
  */
7
7
  import type { ClientConnectionRpc, SessionId } from '@deepseek-ai/dsh-client-connection/client';
8
- import type { DiffApprovalActionValue, DiffApprovalOpenAction, DiffApprovalOpenValue, PendingFileDiff } from '../types.ts';
8
+ import type { DiffApprovalActionValue, DiffApprovalBlockRange, DiffApprovalListValue, DiffApprovalOpenAction, DiffApprovalOpenValue } from '../types.ts';
9
9
  /** The channel the host half registers and this port calls. */
10
10
  export declare const DIFF_APPROVAL_CHANNEL = "/diff-approval";
11
11
  /** This package's business verbs over the review channel. */
12
12
  export interface DiffApprovalPort {
13
- /** Read one session's pending entries, oldest capture first. */
14
- list(sessionId: SessionId): Promise<PendingFileDiff[]>;
13
+ /** Read one session's pending entries (plus its workspace root), oldest capture first. */
14
+ list(sessionId: SessionId): Promise<DiffApprovalListValue>;
15
15
  /** Keep one operation. */
16
16
  keep(sessionId: SessionId, id: string): Promise<DiffApprovalActionValue>;
17
17
  /** Revert one operation. */
18
18
  revert(sessionId: SessionId, id: string): Promise<DiffApprovalActionValue>;
19
+ /** Keep one diff block (accept its change into the tracked baseline). */
20
+ blockKeep(sessionId: SessionId, id: string, block: DiffApprovalBlockRange): Promise<DiffApprovalActionValue>;
21
+ /** Revert one diff block (restore its old lines in the file). */
22
+ blockRevert(sessionId: SessionId, id: string, block: DiffApprovalBlockRange): Promise<DiffApprovalActionValue>;
19
23
  /** Open one file with its default application or reveal it in the folder. */
20
24
  open(sessionId: SessionId, id: string, action: DiffApprovalOpenAction): Promise<DiffApprovalOpenValue>;
21
25
  }
@@ -1,10 +1,10 @@
1
1
  /**
2
- * Reference labels for the selection toolbar: a file name plus a 1-based line
3
- * range, using the short name when unambiguous and the full path otherwise.
4
- * Pure derivation so the display rule is unit-testable without the panel.
2
+ * Reference labels for the selection toolbar: a workspace-relative path (or
3
+ * the absolute path when the file is outside the workspace) plus a 1-based
4
+ * line range. Pure derivation so the display rule is unit-testable without
5
+ * the panel.
5
6
  * @module dsh-diff-approval/client/reference
6
7
  */
7
- import type { PendingFileDiff } from '../types.ts';
8
8
  /**
9
9
  * Last path segment of a file path, any separator style.
10
10
  * @param path - the path to shorten.
@@ -12,13 +12,15 @@ import type { PendingFileDiff } from '../types.ts';
12
12
  */
13
13
  export declare function basenameOf(path: string): string;
14
14
  /**
15
- * The path shown in a copied reference: the short name when no other listed
16
- * file shares it, the full path otherwise.
15
+ * The path embedded in a copied reference: workspace-relative (forward
16
+ * slashes) when the file lives inside the current workspace, the absolute
17
+ * path otherwise. A bare file name is never enough — a reference must resolve
18
+ * to exactly one file.
17
19
  * @param path - the selected file's path.
18
- * @param files - every currently listed pending file.
19
- * @returns the display path for a copied reference.
20
+ * @param workspacePath - the current workspace root, or `undefined`.
21
+ * @returns the reference path.
20
22
  */
21
- export declare function copyDisplayPath(path: string, files: readonly PendingFileDiff[]): string;
23
+ export declare function referencePathOf(path: string, workspacePath: string | undefined): string;
22
24
  /**
23
25
  * Format one line range as a reference suffix: a single line number, or the
24
26
  * inclusive range when the selection spans more than one line.
@@ -30,9 +32,9 @@ export declare function lineRangeLabel(start: number, end: number): string;
30
32
  /**
31
33
  * Build the clipboard text for a selected line range.
32
34
  * @param path - the selected file's path.
33
- * @param files - every currently listed pending file.
35
+ * @param workspacePath - the current workspace root, or `undefined`.
34
36
  * @param start - first selected line number.
35
37
  * @param end - last selected line number.
36
38
  * @returns the `path:range` reference text.
37
39
  */
38
- export declare function referenceOf(path: string, files: readonly PendingFileDiff[], start: number, end: number): string;
40
+ export declare function referenceOf(path: string, workspacePath: string | undefined, start: number, end: number): string;
@@ -1,7 +1,7 @@
1
1
  /** The panel's injected business face and its observable snapshot. */
2
2
  import type { HostObservable } from '@deepseek-ai/dsh-client-ui-slots';
3
3
  import type { SessionId } from '@deepseek-ai/dsh-client-connection/client';
4
- import type { DiffApprovalOpenAction, PendingFileDiff } from '../types.ts';
4
+ import type { DiffApprovalBlockRange, DiffApprovalOpenAction, PendingFileDiff } from '../types.ts';
5
5
  /** What the panel reads and drives: the pending list plus in-flight entries. */
6
6
  export interface PendingDiffSnapshot {
7
7
  /** Whether a list read has completed at least once. */
@@ -12,6 +12,8 @@ export interface PendingDiffSnapshot {
12
12
  error?: string;
13
13
  /** Entry ids whose keep/revert is in flight; their controls are disabled. */
14
14
  busy: ReadonlySet<string>;
15
+ /** The viewing session's workspace root (when it has one); enables workspace-relative references. */
16
+ workspacePath?: string | undefined;
15
17
  }
16
18
  /** The injected face the panel component receives from the plugin body. */
17
19
  export interface PendingPanelFace {
@@ -25,6 +27,10 @@ export interface PendingPanelFace {
25
27
  onKeep: (sessionId: SessionId, id: string) => Promise<void>;
26
28
  /** Revert one operation (restore its prior content, or remove a created file). */
27
29
  onRevert: (sessionId: SessionId, id: string) => Promise<void>;
30
+ /** Keep one diff block (accept its change into the tracked baseline). */
31
+ onBlockKeep: (sessionId: SessionId, id: string, block: DiffApprovalBlockRange) => Promise<void>;
32
+ /** Revert one diff block (restore its old lines in the file). */
33
+ onBlockRevert: (sessionId: SessionId, id: string, block: DiffApprovalBlockRange) => Promise<void>;
28
34
  /** Open one file with its default application or reveal it in the folder. */
29
35
  onOpen: (sessionId: SessionId, id: string, action: DiffApprovalOpenAction) => Promise<void>;
30
36
  }
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import type { SessionId } from '@deepseek-ai/dsh-client-connection/client';
9
9
  import type { HostObservable } from '@deepseek-ai/dsh-client-ui-slots';
10
- import type { DiffApprovalOpenAction } from '../types.ts';
10
+ import type { DiffApprovalBlockRange, DiffApprovalOpenAction } from '../types.ts';
11
11
  import type { PendingDiffSnapshot } from './slots.ts';
12
12
  import type { DiffApprovalPort } from './port.ts';
13
13
  /** The observable the panel reads and the plugin body drives. */
@@ -18,6 +18,10 @@ export interface PendingDiffStore extends HostObservable<PendingDiffSnapshot> {
18
18
  keep: (sessionId: SessionId, id: string) => Promise<void>;
19
19
  /** Revert one operation. */
20
20
  revert: (sessionId: SessionId, id: string) => Promise<void>;
21
+ /** Keep one diff block, then refresh so the entry's diff reflects the accept. */
22
+ blockKeep: (sessionId: SessionId, id: string, block: DiffApprovalBlockRange) => Promise<void>;
23
+ /** Revert one diff block, then refresh so the entry's diff reflects the undo. */
24
+ blockRevert: (sessionId: SessionId, id: string, block: DiffApprovalBlockRange) => Promise<void>;
21
25
  /** Open one file with its default application or reveal it in the folder. */
22
26
  open: (sessionId: SessionId, id: string, action: DiffApprovalOpenAction) => Promise<void>;
23
27
  /** Drop every local fact (used on connection reset). */
@@ -35,7 +35,7 @@
35
35
  */
36
36
  import { Context } from '@deepseek-ai/cordis';
37
37
  import type { DiffApprovalOpenAction } from './types.ts';
38
- export type { DiffApprovalActionOutcome, DiffApprovalActionValue, DiffApprovalListValue, DiffApprovalOpenAction, DiffApprovalOpenValue, PendingEntry, PendingEntryKind, PendingFileDiff, } from './types.ts';
38
+ export type { DiffApprovalActionOutcome, DiffApprovalActionValue, DiffApprovalBlockRange, DiffApprovalBlockTarget, DiffApprovalListValue, DiffApprovalOpenAction, DiffApprovalOpenValue, PendingEntry, PendingEntryKind, PendingFileDiff, } from './types.ts';
39
39
  export { PendingDiffStore } from './pending.ts';
40
40
  export { PendingPersistence, defaultStorageDir } from './persist.ts';
41
41
  export { defaultOpenPath } from './open.ts';
@@ -55,6 +55,20 @@ export declare class PendingDiffStore {
55
55
  * @returns whether an entry was removed.
56
56
  */
57
57
  remove(sessionId: SessionId, id: string): boolean;
58
+ /**
59
+ * Advance one entry's tracked content after a block-level keep/revert. The
60
+ * entry keeps its id and path; only the given side's text and capture time
61
+ * move. When the caller decides the sides now match, it removes the entry
62
+ * instead of updating it.
63
+ * @param sessionId - the owning session.
64
+ * @param id - the entry id.
65
+ * @param patch - the side to advance (`oldText` for keep, `newText` for revert).
66
+ * @returns whether the entry changed.
67
+ */
68
+ update(sessionId: SessionId, id: string, patch: {
69
+ oldText?: string;
70
+ newText?: string;
71
+ }): boolean;
58
72
  /** Total entry count across all sessions. */
59
73
  get size(): number;
60
74
  }
@@ -51,6 +51,8 @@ export interface PendingFileDiff extends PendingEntry {
51
51
  export interface DiffApprovalListValue {
52
52
  /** Pending entries for the requested session, oldest capture first. */
53
53
  files: PendingFileDiff[];
54
+ /** The viewing session's workspace root (when it has one), for workspace-relative references. */
55
+ workspacePath?: string | undefined;
54
56
  }
55
57
  /** What the open endpoint asks the OS to do with a file. */
56
58
  export type DiffApprovalOpenAction = 'open' | 'reveal';
@@ -59,6 +61,27 @@ export interface DiffApprovalOpenValue {
59
61
  /** What the request did; `missing` means no pending entry existed. */
60
62
  outcome: 'opened' | 'missing';
61
63
  }
64
+ /**
65
+ * One diff block's line ranges on the old and new sides, 1-based inclusive.
66
+ * A side is empty when its start exceeds its end; an empty side's start is
67
+ * the insertion point (the line before which content inserts) on that side.
68
+ */
69
+ export interface DiffApprovalBlockRange {
70
+ /** First old-file line this block spans; the insertion point for pure additions. */
71
+ oldStart: number;
72
+ /** Last old-file line; `oldStart - 1` when the block has no old side. */
73
+ oldEnd: number;
74
+ /** First new-file line this block spans; the insertion point for pure deletions. */
75
+ newStart: number;
76
+ /** Last new-file line; `newStart - 1` when the block has no new side. */
77
+ newEnd: number;
78
+ }
79
+ /** Target of one block-level keep/revert: the entry plus the block range. */
80
+ export interface DiffApprovalBlockTarget {
81
+ sessionId: SessionId;
82
+ id: string;
83
+ block: DiffApprovalBlockRange;
84
+ }
62
85
  /** Outcome of one keep/revert request. */
63
86
  export type DiffApprovalActionOutcome = 'kept' | 'reverted' | 'missing';
64
87
  /** Value returned by the channel's keep and revert endpoints. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-diff-approval",
3
- "version": "0.5.0",
3
+ "version": "0.6.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",