dsh-diff-approval 0.2.0 → 0.3.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
@@ -11523,7 +11523,7 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
11523
11523
  }
11524
11524
  //#endregion
11525
11525
  //#region \0dsh-css:C:\PROJECTS\dsh-diff-approval\src\client\PendingPanel.module.css.mjs
11526
- const css = ".Ay7oXG_layer{flex:none;align-items:center;width:100%;height:49px;margin:8px 0 0;display:flex;position:relative}.Ay7oXG_footerButtons{align-items:center;width:100%;display:flex}.Ay7oXG_badge{width:100%;height:49px;color:var(--dsw-alias-label-primary);cursor:pointer;background:0 0;border:none;border-radius:12px;align-items:center;gap:8px;padding:0 8px 0 6px;font-family:inherit;font-size:14px;display:inline-flex;overflow:hidden}.Ay7oXG_badge:hover{background:var(--dsw-alias-interactive-bg-hover-solid)}.Ay7oXG_badge[data-active]{background:var(--dsw-alias-interactive-bg-hover)}.Ay7oXG_badgeLabel{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.Ay7oXG_badgeCount{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;flex:none;margin-left:auto;font-size:12px;line-height:16px}.Ay7oXG_layer.Ay7oXG_rail{width:36px;height:36px;margin:0}.Ay7oXG_rail .Ay7oXG_badge{border-radius:50%;justify-content:center;gap:0;width:36px;height:36px;padding:0}.Ay7oXG_rail .Ay7oXG_badgeLabel{display:none}.Ay7oXG_rail .Ay7oXG_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}.Ay7oXG_panel{z-index:30;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-base);width:auto;max-width:none;max-height:86vh;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;bottom:128px;left:8px;right:8px;overflow:hidden}.Ay7oXG_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}.Ay7oXG_body{flex:1;min-height:0;padding:4px 12px 12px;overflow-y:auto}.Ay7oXG_title{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:500;line-height:20px}.Ay7oXG_note,.Ay7oXG_readError,.Ay7oXG_hint{color:var(--dsw-alias-label-tertiary);margin:4px 0;font-size:12px;line-height:18px}.Ay7oXG_readError{color:var(--dsw-alias-state-error-primary)}.Ay7oXG_group{color:var(--dsw-alias-label-tertiary);margin:8px 0 4px;font-size:12px;font-weight:500;line-height:16px}.Ay7oXG_rows{margin:0;padding:0;list-style:none}.Ay7oXG_row{border:1px solid #0000;border-radius:10px;margin:2px 0}.Ay7oXG_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}.Ay7oXG_rowHead:hover{background:var(--dsw-alias-interactive-bg-hover-solid)}.Ay7oXG_rowHead[data-selected]{background:var(--dsw-alias-interactive-bg-hover)}.Ay7oXG_rowPath{text-overflow:ellipsis;white-space:nowrap;min-width:0;font:var(--dsw-font-markdown-code-block);text-align:left;direction:rtl;overflow:hidden}.Ay7oXG_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}.Ay7oXG_kindHint{color:var(--dsw-alias-label-tertiary);flex:none;font-size:12px;line-height:16px}.Ay7oXG_divergedHint,.Ay7oXG_missingHint{border-bottom:1px solid var(--dsw-alias-border-l2);margin:0;padding:6px 8px;font-size:12px;line-height:18px}.Ay7oXG_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}.Ay7oXG_missingHint{color:var(--dsw-alias-state-warn-label,var(--dsw-alias-state-warn-primary))}.Ay7oXG_rowMeta{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;flex:none;margin-left:auto;font-size:12px;line-height:16px}.Ay7oXG_diff{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-markdown-code-block);border-radius:12px;margin:4px 0 8px;position:relative;overflow:hidden}.Ay7oXG_diffActions{border-bottom:1px solid var(--dsw-alias-border-l2);align-items:center;gap:8px;padding:6px 8px;display:flex}.Ay7oXG_diffStats{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;font-size:12px;line-height:16px}.Ay7oXG_flexSpacer{flex:1}.Ay7oXG_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}.Ay7oXG_action:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.Ay7oXG_action:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}.Ay7oXG_diffBody{max-height:360px;font:var(--dsw-font-markdown-code-block);padding:8px 10px;overflow:auto}.Ay7oXG_lines{border-spacing:0;width:max-content;display:table}.Ay7oXG_line{user-select:none;display:table-row}.Ay7oXG_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}.Ay7oXG_code{white-space:pre;display:table-cell}.Ay7oXG_context{color:var(--dsw-alias-label-primary)}.Ay7oXG_del{background-color:color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, transparent)}.Ay7oXG_add{background-color:color-mix(in srgb, var(--dsw-alias-state-success-primary) 10%, transparent)}.Ay7oXG_toolbar{z-index:2;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-base);box-shadow:var(--dsw-shadow-lv2);border-radius:8px;gap:4px;padding:3px;display:flex;position:absolute;right:10px}.Ay7oXG_toolbarButton{color:var(--dsw-alias-label-primary);white-space:nowrap;cursor:pointer;background:0 0;border:none;border-radius:6px;padding:3px 10px;font-family:inherit;font-size:12px;line-height:18px}.Ay7oXG_toolbarButton:hover{background:var(--dsw-alias-interactive-bg-hover)}";
11526
+ const css = ".Ay7oXG_layer{flex:none;align-items:center;width:100%;height:49px;margin:8px 0 0;display:flex;position:relative}.Ay7oXG_footerButtons{align-items:center;width:100%;display:flex}.Ay7oXG_badge{width:100%;height:49px;color:var(--dsw-alias-label-primary);cursor:pointer;background:0 0;border:none;border-radius:12px;align-items:center;gap:8px;padding:0 8px 0 6px;font-family:inherit;font-size:14px;display:inline-flex;overflow:hidden}.Ay7oXG_badge:hover{background:var(--dsw-alias-interactive-bg-hover-solid)}.Ay7oXG_badge[data-active]{background:var(--dsw-alias-interactive-bg-hover)}.Ay7oXG_badgeLabel{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.Ay7oXG_badgeCount{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;flex:none;margin-left:auto;font-size:12px;line-height:16px}.Ay7oXG_layer.Ay7oXG_rail{width:36px;height:36px;margin:0}.Ay7oXG_rail .Ay7oXG_badge{border-radius:50%;justify-content:center;gap:0;width:36px;height:36px;padding:0}.Ay7oXG_rail .Ay7oXG_badgeLabel{display:none}.Ay7oXG_rail .Ay7oXG_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}.Ay7oXG_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}.Ay7oXG_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}.Ay7oXG_states{flex:1;min-height:0;padding:4px 12px 12px;overflow-y:auto}.Ay7oXG_split{flex:1;align-items:stretch;min-height:0;display:flex}.Ay7oXG_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}.Ay7oXG_resizeHandle{cursor:col-resize;background:0 0;flex:none;width:5px;margin:0 -2px}.Ay7oXG_resizeHandle:hover{background:var(--dsw-alias-border-l2)}.Ay7oXG_detail{flex-direction:column;flex:1;min-width:0;min-height:0;display:flex}.Ay7oXG_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}.Ay7oXG_title{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:500;line-height:20px}.Ay7oXG_note,.Ay7oXG_readError,.Ay7oXG_hint{color:var(--dsw-alias-label-tertiary);margin:4px 0;font-size:12px;line-height:18px}.Ay7oXG_readError{color:var(--dsw-alias-state-error-primary)}.Ay7oXG_group{color:var(--dsw-alias-label-tertiary);margin:8px 0 4px;font-size:12px;font-weight:500;line-height:16px}.Ay7oXG_rows{margin:0;padding:0;list-style:none}.Ay7oXG_row{border:1px solid #0000;border-radius:10px;margin:2px 0}.Ay7oXG_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}.Ay7oXG_rowHead:hover{background:var(--dsw-alias-interactive-bg-hover-solid)}.Ay7oXG_rowHead[data-selected]{background:var(--dsw-alias-interactive-bg-hover)}.Ay7oXG_rowPath{text-overflow:ellipsis;white-space:nowrap;min-width:0;font:var(--dsw-font-markdown-code-block);text-align:left;direction:rtl;overflow:hidden}.Ay7oXG_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}.Ay7oXG_kindHint{color:var(--dsw-alias-label-tertiary);flex:none;font-size:12px;line-height:16px}.Ay7oXG_divergedHint,.Ay7oXG_missingHint{border-bottom:1px solid var(--dsw-alias-border-l2);margin:0;padding:6px 8px;font-size:12px;line-height:18px}.Ay7oXG_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}.Ay7oXG_missingHint{color:var(--dsw-alias-state-warn-label,var(--dsw-alias-state-warn-primary))}.Ay7oXG_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}.Ay7oXG_addCount{color:color-mix(in srgb, var(--dsw-alias-state-success-primary) 60%, var(--dsw-alias-label-primary))}.Ay7oXG_delCount{color:color-mix(in srgb, var(--dsw-alias-state-error-primary) 60%, var(--dsw-alias-label-primary))}.Ay7oXG_diff{background:var(--dsw-alias-markdown-code-block);flex-direction:column;flex:1;min-height:0;display:flex;position:relative;overflow:hidden}.Ay7oXG_diffActions{border-bottom:1px solid var(--dsw-alias-border-l2);align-items:center;gap:8px;padding:6px 8px;display:flex}.Ay7oXG_diffStats{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;font-size:12px;line-height:16px}.Ay7oXG_flexSpacer{flex:1}.Ay7oXG_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}.Ay7oXG_action:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.Ay7oXG_action:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}.Ay7oXG_iconAction{justify-content:center;align-items:center;padding:4px 6px;display:inline-flex}.Ay7oXG_diffBody{min-height:0;font:var(--dsw-font-markdown-code-block);flex:1;padding:8px 10px;overflow:auto}.Ay7oXG_lines{border-spacing:0;width:max-content;min-width:100%;display:table}.Ay7oXG_line{user-select:none;display:table-row}.Ay7oXG_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}.Ay7oXG_code{white-space:pre;display:table-cell}.Ay7oXG_context{color:var(--dsw-alias-label-primary)}.Ay7oXG_del{background-color:color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, transparent)}.Ay7oXG_add{background-color:color-mix(in srgb, var(--dsw-alias-state-success-primary) 10%, transparent)}.Ay7oXG_toolbar{z-index:2;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-base);box-shadow:var(--dsw-shadow-lv2);border-radius:8px;gap:4px;padding:3px;display:flex;position:absolute;right:10px}.Ay7oXG_toolbarButton{color:var(--dsw-alias-label-primary);white-space:nowrap;cursor:pointer;background:0 0;border:none;border-radius:6px;padding:3px 10px;font-family:inherit;font-size:12px;line-height:18px}.Ay7oXG_toolbarButton:hover{background:var(--dsw-alias-interactive-bg-hover)}";
11527
11527
  const tagId = "dsh-diff-approval/PendingPanel.module.css";
11528
11528
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
11529
11529
  const tag = document.createElement("style");
@@ -11533,49 +11533,57 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
11533
11533
  document.head.appendChild(tag);
11534
11534
  }
11535
11535
  var PendingPanel_module_css_default = {
11536
+ "panel": "Ay7oXG_panel",
11537
+ "note": "Ay7oXG_note",
11538
+ "code": "Ay7oXG_code",
11539
+ "rowHead": "Ay7oXG_rowHead",
11540
+ "line": "Ay7oXG_line",
11536
11541
  "diffActions": "Ay7oXG_diffActions",
11537
- "rows": "Ay7oXG_rows",
11538
- "divergedHint": "Ay7oXG_divergedHint",
11542
+ "diff": "Ay7oXG_diff",
11543
+ "rowPath": "Ay7oXG_rowPath",
11544
+ "missing": "Ay7oXG_missing",
11545
+ "add": "Ay7oXG_add",
11546
+ "badge": "Ay7oXG_badge",
11539
11547
  "badgeCount": "Ay7oXG_badgeCount",
11540
- "rail": "Ay7oXG_rail",
11541
- "kindHint": "Ay7oXG_kindHint",
11542
11548
  "row": "Ay7oXG_row",
11543
- "add": "Ay7oXG_add",
11544
- "flexSpacer": "Ay7oXG_flexSpacer",
11545
- "group": "Ay7oXG_group",
11546
- "title": "Ay7oXG_title",
11547
- "missingHint": "Ay7oXG_missingHint",
11549
+ "kindHint": "Ay7oXG_kindHint",
11550
+ "badgeLabel": "Ay7oXG_badgeLabel",
11551
+ "resizeHandle": "Ay7oXG_resizeHandle",
11552
+ "iconAction": "Ay7oXG_iconAction",
11553
+ "gutter": "Ay7oXG_gutter",
11548
11554
  "diffStats": "Ay7oXG_diffStats",
11549
- "toolbarButton": "Ay7oXG_toolbarButton",
11550
- "missing": "Ay7oXG_missing",
11555
+ "missingHint": "Ay7oXG_missingHint",
11556
+ "states": "Ay7oXG_states",
11557
+ "layer": "Ay7oXG_layer",
11558
+ "detail": "Ay7oXG_detail",
11559
+ "readError": "Ay7oXG_readError",
11560
+ "del": "Ay7oXG_del",
11561
+ "detailEmpty": "Ay7oXG_detailEmpty",
11562
+ "title": "Ay7oXG_title",
11551
11563
  "hint": "Ay7oXG_hint",
11552
- "panel": "Ay7oXG_panel",
11553
- "rowHead": "Ay7oXG_rowHead",
11554
- "badgeLabel": "Ay7oXG_badgeLabel",
11555
- "badge": "Ay7oXG_badge",
11556
- "header": "Ay7oXG_header",
11557
- "rowPath": "Ay7oXG_rowPath",
11558
11564
  "rowMeta": "Ay7oXG_rowMeta",
11559
- "readError": "Ay7oXG_readError",
11560
- "diff": "Ay7oXG_diff",
11561
- "footerButtons": "Ay7oXG_footerButtons",
11565
+ "rows": "Ay7oXG_rows",
11566
+ "divergedHint": "Ay7oXG_divergedHint",
11567
+ "fileList": "Ay7oXG_fileList",
11568
+ "addCount": "Ay7oXG_addCount",
11569
+ "split": "Ay7oXG_split",
11570
+ "rail": "Ay7oXG_rail",
11571
+ "delCount": "Ay7oXG_delCount",
11572
+ "kindTag": "Ay7oXG_kindTag",
11562
11573
  "action": "Ay7oXG_action",
11563
- "note": "Ay7oXG_note",
11564
11574
  "diffBody": "Ay7oXG_diffBody",
11565
- "gutter": "Ay7oXG_gutter",
11566
- "kindTag": "Ay7oXG_kindTag",
11567
- "line": "Ay7oXG_line",
11568
- "context": "Ay7oXG_context",
11569
- "lines": "Ay7oXG_lines",
11570
11575
  "toolbar": "Ay7oXG_toolbar",
11571
- "del": "Ay7oXG_del",
11572
- "body": "Ay7oXG_body",
11573
- "layer": "Ay7oXG_layer",
11574
- "code": "Ay7oXG_code"
11576
+ "toolbarButton": "Ay7oXG_toolbarButton",
11577
+ "header": "Ay7oXG_header",
11578
+ "footerButtons": "Ay7oXG_footerButtons",
11579
+ "context": "Ay7oXG_context",
11580
+ "group": "Ay7oXG_group",
11581
+ "flexSpacer": "Ay7oXG_flexSpacer",
11582
+ "lines": "Ay7oXG_lines"
11575
11583
  };
11576
11584
  //#endregion
11577
11585
  //#region lib/types/client/PendingPanel.js
11578
- /** Sidebar-foot pending-edit review action and the whole-file diff list it opens. */
11586
+ /** Sidebar-foot pending-edit review action and the split review panel it opens. */
11579
11587
  /**
11580
11588
  * How often the panel re-reads the pending list. An external plugin cannot
11581
11589
  * register on the host's forwarded-event allowlist, so polling is its change
@@ -11583,6 +11591,15 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
11583
11591
  * even with the panel closed. The read is one small RPC per second.
11584
11592
  */
11585
11593
  const POLL_INTERVAL_MS = 1e3;
11594
+ /** Panel bottom offset when the composer cannot be measured, in px. */
11595
+ const FALLBACK_BOTTOM_PX = 128;
11596
+ /** Gap kept between the composer's top edge and the panel bottom, in px. */
11597
+ const COMPOSER_GAP_PX = 12;
11598
+ /** The harness composer's stable hook (ui-conversation InputBar). */
11599
+ const COMPOSER_SELECTOR = "[data-composer-card]";
11600
+ /** File-list pane width bounds for the manual split drag, in px. */
11601
+ const MIN_LIST_WIDTH_PX = 160;
11602
+ const MAX_LIST_WIDTH_PX = 560;
11586
11603
  /** The diff body's row class per line kind. */
11587
11604
  const ROW_CLASS = {
11588
11605
  context: PendingPanel_module_css_default.context,
@@ -11610,10 +11627,51 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
11610
11627
  });
11611
11628
  return blocks;
11612
11629
  }
11613
- function PendingRow({ file, files, selected, busy, t, onSelect, onKeep, onRevert }) {
11630
+ /** One row of the file list: the clickable head in the left pane. */
11631
+ function PendingFileRow({ file, files, selected, t, onSelect }) {
11632
+ const stats = (0, react.useMemo)(() => computeWholeFileDiff(file.oldText, file.newText), [file.oldText, file.newText]);
11633
+ return (0, react_jsx_runtime.jsx)("li", {
11634
+ className: PendingPanel_module_css_default.row,
11635
+ children: (0, react_jsx_runtime.jsxs)("button", {
11636
+ type: "button",
11637
+ className: PendingPanel_module_css_default.rowHead,
11638
+ "data-selected": selected || void 0,
11639
+ onClick: () => {
11640
+ onSelect(selected ? "" : file.id);
11641
+ },
11642
+ children: [
11643
+ (0, react_jsx_runtime.jsx)("span", {
11644
+ className: PendingPanel_module_css_default.rowPath,
11645
+ title: file.path,
11646
+ children: copyDisplayPath(file.path, files)
11647
+ }),
11648
+ file.kind === "create" && (0, react_jsx_runtime.jsx)("span", {
11649
+ className: PendingPanel_module_css_default.kindTag,
11650
+ children: t("row.create")
11651
+ }),
11652
+ file.missing && (0, react_jsx_runtime.jsx)("span", {
11653
+ className: PendingPanel_module_css_default.missing,
11654
+ title: t("panel.missingHint"),
11655
+ children: t("panel.missing")
11656
+ }),
11657
+ (0, react_jsx_runtime.jsxs)("span", {
11658
+ className: PendingPanel_module_css_default.rowMeta,
11659
+ children: [(0, react_jsx_runtime.jsx)("span", {
11660
+ className: PendingPanel_module_css_default.addCount,
11661
+ children: t("row.added", { added: stats.added })
11662
+ }), (0, react_jsx_runtime.jsx)("span", {
11663
+ className: PendingPanel_module_css_default.delCount,
11664
+ children: t("row.removed", { removed: stats.removed })
11665
+ })]
11666
+ })
11667
+ ]
11668
+ })
11669
+ });
11670
+ }
11671
+ /** The selected file's diff, actions, jump controls, and copy toolbar. */
11672
+ function PendingDiff({ file, files, busy, t, onKeep, onRevert, onOpen }) {
11614
11673
  const lang = (0, react.useMemo)(() => langFromPath(file.path), [file.path]);
11615
11674
  const model = (0, react.useMemo)(() => {
11616
- if (!selected) return void 0;
11617
11675
  const diff = computeWholeFileDiff(file.oldText, file.newText);
11618
11676
  return {
11619
11677
  diff,
@@ -11622,7 +11680,6 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
11622
11680
  newRuns: highlightLines(file.newText, lang)
11623
11681
  };
11624
11682
  }, [
11625
- selected,
11626
11683
  file.oldText,
11627
11684
  file.newText,
11628
11685
  lang
@@ -11638,16 +11695,16 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
11638
11695
  setDrag(void 0);
11639
11696
  setRange(void 0);
11640
11697
  setCopied(false);
11641
- }, [file.id, selected]);
11698
+ }, [file.id]);
11642
11699
  (0, react.useEffect)(() => {
11643
- if (model === void 0 || model.blocks.length === 0) return;
11700
+ if (model.blocks.length === 0) return;
11644
11701
  const block = model.blocks[focus];
11645
11702
  if (block === void 0) return;
11646
11703
  rowRefs.current.get(block.start)?.scrollIntoView({ block: "center" });
11647
11704
  }, [model, focus]);
11648
11705
  const jump = (direction) => {
11649
11706
  setFocus((current) => {
11650
- if (model === void 0 || model.blocks.length === 0) return current;
11707
+ if (model.blocks.length === 0) return current;
11651
11708
  if (direction === -1) return (current - 1 + model.blocks.length) % model.blocks.length;
11652
11709
  const bodyRect = bodyRef.current?.getBoundingClientRect();
11653
11710
  for (let index = current + 1; index < model.blocks.length; index++) {
@@ -11698,7 +11755,7 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
11698
11755
  });
11699
11756
  });
11700
11757
  };
11701
- const selection = drag === void 0 && range !== void 0 && model !== void 0 ? range : void 0;
11758
+ const selection = drag === void 0 && range !== void 0 ? range : void 0;
11702
11759
  const selectionTop = (() => {
11703
11760
  if (selection === void 0) return 0;
11704
11761
  const row = rowRefs.current.get(selection.start);
@@ -11710,7 +11767,7 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
11710
11767
  return Math.min(clamped, Math.max(bodyTop, bodyTop + body.clientHeight - 36));
11711
11768
  })();
11712
11769
  const copySelection = async () => {
11713
- if (selection === void 0 || model === void 0) return;
11770
+ if (selection === void 0) return;
11714
11771
  const lineNumbers = model.diff.rows.slice(selection.start, selection.end + 1).map((row) => row.newLine ?? row.oldLine).filter((number) => number !== void 0);
11715
11772
  if (lineNumbers.length === 0) return;
11716
11773
  if (!await (0, _deepseek_ai_dsh_client_ui_primitives.writeClipboard)(referenceOf(file.path, files, Math.min(...lineNumbers), Math.max(...lineNumbers)))) return;
@@ -11722,161 +11779,161 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
11722
11779
  /** One line's text: token spans when highlighted, plain text otherwise. */
11723
11780
  const renderLine = (row) => {
11724
11781
  const lineNumber = row.kind === "del" ? row.oldLine : row.newLine;
11725
- const runs = model === void 0 || lineNumber === void 0 ? void 0 : (row.kind === "del" ? model.oldRuns : model.newRuns)?.[lineNumber - 1];
11782
+ const runs = lineNumber === void 0 ? void 0 : (row.kind === "del" ? model.oldRuns : model.newRuns)?.[lineNumber - 1];
11726
11783
  if (runs === void 0 || runs.length === 0) return row.text === "" ? "\xA0" : row.text;
11727
11784
  return runs.map((span, index) => (0, react_jsx_runtime.jsx)("span", {
11728
11785
  style: span.style,
11729
11786
  children: span.text
11730
11787
  }, index));
11731
11788
  };
11732
- const focusedBlock = model !== void 0 && model.blocks.length > 0 ? model.blocks[focus] : void 0;
11789
+ const focusedBlock = model.blocks.length > 0 ? model.blocks[focus] : void 0;
11733
11790
  const inFocusedBlock = (index) => focusedBlock !== void 0 && index >= focusedBlock.start && index <= focusedBlock.end;
11734
- return (0, react_jsx_runtime.jsxs)("li", {
11735
- className: PendingPanel_module_css_default.row,
11736
- children: [(0, react_jsx_runtime.jsxs)("button", {
11737
- type: "button",
11738
- className: PendingPanel_module_css_default.rowHead,
11739
- "data-selected": selected || void 0,
11740
- "aria-expanded": selected,
11741
- onClick: () => {
11742
- onSelect(selected ? "" : file.id);
11743
- },
11744
- children: [
11745
- (0, react_jsx_runtime.jsx)("span", {
11746
- className: PendingPanel_module_css_default.rowPath,
11747
- title: file.path,
11748
- children: copyDisplayPath(file.path, files)
11749
- }),
11750
- file.kind === "create" && (0, react_jsx_runtime.jsx)("span", {
11751
- className: PendingPanel_module_css_default.kindTag,
11752
- children: t("row.create")
11753
- }),
11754
- file.missing && (0, react_jsx_runtime.jsx)("span", {
11755
- className: PendingPanel_module_css_default.missing,
11756
- title: t("panel.missingHint"),
11757
- children: t("panel.missing")
11758
- })
11759
- ]
11760
- }), selected && model !== void 0 && (0, react_jsx_runtime.jsxs)("div", {
11761
- className: PendingPanel_module_css_default.diff,
11762
- "data-diff-approval-diff": true,
11763
- onMouseUp: endDrag,
11764
- children: [
11765
- (0, react_jsx_runtime.jsxs)("div", {
11766
- className: PendingPanel_module_css_default.diffActions,
11767
- children: [
11768
- (0, react_jsx_runtime.jsx)("span", {
11769
- className: PendingPanel_module_css_default.diffStats,
11770
- children: t("panel.stats", {
11771
- added: model.diff.added,
11772
- removed: model.diff.removed
11773
- })
11774
- }),
11775
- file.kind === "create" && (0, react_jsx_runtime.jsx)("span", {
11776
- className: PendingPanel_module_css_default.kindHint,
11777
- children: t("panel.createHint")
11778
- }),
11779
- model.blocks.length > 0 && (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsx)("button", {
11780
- type: "button",
11781
- className: PendingPanel_module_css_default.action,
11782
- "data-diff-prev": true,
11783
- disabled: busy,
11784
- onClick: () => {
11785
- jump(-1);
11786
- },
11787
- children: t("action.prevDiff")
11788
- }), (0, react_jsx_runtime.jsx)("button", {
11789
- type: "button",
11790
- className: PendingPanel_module_css_default.action,
11791
- "data-diff-next": true,
11792
- disabled: busy,
11793
- onClick: () => {
11794
- jump(1);
11795
- },
11796
- children: t("action.nextDiff")
11797
- })] }),
11798
- (0, react_jsx_runtime.jsx)("span", { className: PendingPanel_module_css_default.flexSpacer }),
11799
- (0, react_jsx_runtime.jsx)("button", {
11800
- type: "button",
11801
- className: PendingPanel_module_css_default.action,
11802
- "data-diff-keep": true,
11803
- disabled: busy,
11804
- onClick: () => {
11805
- onKeep(file.sessionId, file.id);
11806
- },
11807
- children: busy ? t("action.busy") : t("action.keep")
11808
- }),
11809
- (0, react_jsx_runtime.jsx)("button", {
11810
- type: "button",
11811
- className: PendingPanel_module_css_default.action,
11812
- "data-diff-revert": true,
11813
- disabled: busy,
11814
- onClick: () => {
11815
- onRevert(file.sessionId, file.id);
11816
- },
11817
- children: busy ? t("action.busy") : t("action.revert")
11791
+ return (0, react_jsx_runtime.jsxs)("div", {
11792
+ className: PendingPanel_module_css_default.diff,
11793
+ "data-diff-approval-diff": true,
11794
+ onMouseUp: endDrag,
11795
+ children: [
11796
+ (0, react_jsx_runtime.jsxs)("div", {
11797
+ className: PendingPanel_module_css_default.diffActions,
11798
+ children: [
11799
+ (0, react_jsx_runtime.jsx)("span", {
11800
+ className: PendingPanel_module_css_default.diffStats,
11801
+ children: t("panel.stats", {
11802
+ added: model.diff.added,
11803
+ removed: model.diff.removed
11818
11804
  })
11819
- ]
11820
- }),
11821
- file.missing && (0, react_jsx_runtime.jsx)("p", {
11822
- className: PendingPanel_module_css_default.missingHint,
11823
- children: t("panel.missingHint")
11824
- }),
11825
- (0, react_jsx_runtime.jsx)("div", {
11826
- className: PendingPanel_module_css_default.diffBody,
11827
- ref: bodyRef,
11828
- children: (0, react_jsx_runtime.jsx)("div", {
11829
- className: PendingPanel_module_css_default.lines,
11830
- children: model.diff.rows.map((row, index) => (0, react_jsx_runtime.jsxs)("div", {
11831
- ref: registerRow(index),
11832
- className: `${PendingPanel_module_css_default.line} ${ROW_CLASS[row.kind]}`,
11833
- "data-diff-line": row.kind,
11834
- "data-diff-focused": inFocusedBlock(index) ? "" : void 0,
11835
- onMouseDown: rowMouseDown(index),
11836
- onMouseEnter: rowMouseEnter(index),
11837
- children: [
11838
- (0, react_jsx_runtime.jsx)("span", {
11839
- className: PendingPanel_module_css_default.gutter,
11840
- children: row.oldLine ?? ""
11841
- }),
11842
- (0, react_jsx_runtime.jsx)("span", {
11843
- className: PendingPanel_module_css_default.gutter,
11844
- children: row.newLine ?? ""
11845
- }),
11846
- (0, react_jsx_runtime.jsx)("span", {
11847
- className: PendingPanel_module_css_default.code,
11848
- children: renderLine(row)
11849
- })
11850
- ]
11851
- }, index))
11852
- })
11853
- }),
11854
- selection !== void 0 && (0, react_jsx_runtime.jsx)("div", {
11855
- className: PendingPanel_module_css_default.toolbar,
11856
- style: { top: selectionTop },
11857
- "data-diff-selection-toolbar": true,
11858
- children: (0, react_jsx_runtime.jsx)("button", {
11805
+ }),
11806
+ file.kind === "create" && (0, react_jsx_runtime.jsx)("span", {
11807
+ className: PendingPanel_module_css_default.kindHint,
11808
+ children: t("panel.createHint")
11809
+ }),
11810
+ model.blocks.length > 0 && (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsx)("button", {
11811
+ type: "button",
11812
+ className: `${PendingPanel_module_css_default.action} ${PendingPanel_module_css_default.iconAction}`,
11813
+ "data-diff-prev": true,
11814
+ "aria-label": t("action.prevDiff"),
11815
+ title: t("action.prevDiff"),
11816
+ disabled: busy,
11817
+ onClick: () => {
11818
+ jump(-1);
11819
+ },
11820
+ children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronUpOutline14, { size: 14 })
11821
+ }), (0, react_jsx_runtime.jsx)("button", {
11822
+ type: "button",
11823
+ className: `${PendingPanel_module_css_default.action} ${PendingPanel_module_css_default.iconAction}`,
11824
+ "data-diff-next": true,
11825
+ "aria-label": t("action.nextDiff"),
11826
+ title: t("action.nextDiff"),
11827
+ disabled: busy,
11828
+ onClick: () => {
11829
+ jump(1);
11830
+ },
11831
+ children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, { size: 14 })
11832
+ })] }),
11833
+ (0, react_jsx_runtime.jsx)("button", {
11834
+ type: "button",
11835
+ className: PendingPanel_module_css_default.action,
11836
+ "data-diff-open": true,
11837
+ onClick: () => {
11838
+ onOpen(file.sessionId, file.id, "open");
11839
+ },
11840
+ children: t("action.openFile")
11841
+ }),
11842
+ (0, react_jsx_runtime.jsx)("button", {
11843
+ type: "button",
11844
+ className: PendingPanel_module_css_default.action,
11845
+ "data-diff-reveal": true,
11846
+ onClick: () => {
11847
+ onOpen(file.sessionId, file.id, "reveal");
11848
+ },
11849
+ children: t("action.revealFile")
11850
+ }),
11851
+ (0, react_jsx_runtime.jsx)("span", { className: PendingPanel_module_css_default.flexSpacer }),
11852
+ (0, react_jsx_runtime.jsx)("button", {
11853
+ type: "button",
11854
+ className: PendingPanel_module_css_default.action,
11855
+ "data-diff-keep": true,
11856
+ disabled: busy,
11857
+ onClick: () => {
11858
+ onKeep(file.sessionId, file.id);
11859
+ },
11860
+ children: busy ? t("action.busy") : t("action.keep")
11861
+ }),
11862
+ (0, react_jsx_runtime.jsx)("button", {
11859
11863
  type: "button",
11860
- className: PendingPanel_module_css_default.toolbarButton,
11861
- "data-diff-copy": true,
11864
+ className: PendingPanel_module_css_default.action,
11865
+ "data-diff-revert": true,
11866
+ disabled: busy,
11862
11867
  onClick: () => {
11863
- copySelection();
11868
+ onRevert(file.sessionId, file.id);
11864
11869
  },
11865
- children: copied ? t("action.copied") : t("action.copyRange")
11870
+ children: busy ? t("action.busy") : t("action.revert")
11866
11871
  })
11872
+ ]
11873
+ }),
11874
+ file.missing && (0, react_jsx_runtime.jsx)("p", {
11875
+ className: PendingPanel_module_css_default.missingHint,
11876
+ children: t("panel.missingHint")
11877
+ }),
11878
+ (0, react_jsx_runtime.jsx)("div", {
11879
+ className: PendingPanel_module_css_default.diffBody,
11880
+ ref: bodyRef,
11881
+ children: (0, react_jsx_runtime.jsx)("div", {
11882
+ className: PendingPanel_module_css_default.lines,
11883
+ children: model.diff.rows.map((row, index) => (0, react_jsx_runtime.jsxs)("div", {
11884
+ ref: registerRow(index),
11885
+ className: `${PendingPanel_module_css_default.line} ${ROW_CLASS[row.kind]}`,
11886
+ "data-diff-line": row.kind,
11887
+ "data-diff-focused": inFocusedBlock(index) ? "" : void 0,
11888
+ onMouseDown: rowMouseDown(index),
11889
+ onMouseEnter: rowMouseEnter(index),
11890
+ children: [
11891
+ (0, react_jsx_runtime.jsx)("span", {
11892
+ className: PendingPanel_module_css_default.gutter,
11893
+ children: row.oldLine ?? ""
11894
+ }),
11895
+ (0, react_jsx_runtime.jsx)("span", {
11896
+ className: PendingPanel_module_css_default.gutter,
11897
+ children: row.newLine ?? ""
11898
+ }),
11899
+ (0, react_jsx_runtime.jsx)("span", {
11900
+ className: PendingPanel_module_css_default.code,
11901
+ children: renderLine(row)
11902
+ })
11903
+ ]
11904
+ }, index))
11867
11905
  })
11868
- ]
11869
- })]
11906
+ }),
11907
+ selection !== void 0 && (0, react_jsx_runtime.jsx)("div", {
11908
+ className: PendingPanel_module_css_default.toolbar,
11909
+ style: { top: selectionTop },
11910
+ "data-diff-selection-toolbar": true,
11911
+ children: (0, react_jsx_runtime.jsx)("button", {
11912
+ type: "button",
11913
+ className: PendingPanel_module_css_default.toolbarButton,
11914
+ "data-diff-copy": true,
11915
+ onClick: () => {
11916
+ copySelection();
11917
+ },
11918
+ children: copied ? t("action.copied") : t("action.copyRange")
11919
+ })
11920
+ })
11921
+ ]
11870
11922
  });
11871
11923
  }
11872
11924
  /** Render the pending-edit review panel and its unified footer action. */
11873
- function PendingPanel({ wide, useSessions, usePending, onRefresh, onKeep, onRevert, t }) {
11925
+ function PendingPanel({ wide, useSessions, usePending, onRefresh, onKeep, onRevert, onOpen, t }) {
11874
11926
  const current = useSessions((state) => state.current);
11875
11927
  const snapshot = usePending((snapshot) => snapshot);
11876
11928
  const [open, setOpen] = (0, react.useState)(false);
11877
11929
  const [selected, setSelected] = (0, react.useState)("");
11878
11930
  /** Pending count captured at open time; auto-close needs a list that emptied. */
11879
11931
  const [openedCount, setOpenedCount] = (0, react.useState)(0);
11932
+ /** Bottom offset tracking the chat composer's top edge so the input stays visible. */
11933
+ const [bottomPx, setBottomPx] = (0, react.useState)(FALLBACK_BOTTOM_PX);
11934
+ /** File-list pane width, adjustable by dragging the divider. */
11935
+ const [listWidth, setListWidth] = (0, react.useState)(240);
11936
+ const resizeDrag = (0, react.useRef)(null);
11880
11937
  (0, react.useEffect)(() => {
11881
11938
  onRefresh(current);
11882
11939
  const timer = setInterval(() => {
@@ -11886,6 +11943,30 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
11886
11943
  clearInterval(timer);
11887
11944
  };
11888
11945
  }, [current, onRefresh]);
11946
+ (0, react.useEffect)(() => {
11947
+ if (!open) return;
11948
+ const measure = () => {
11949
+ const composer = document.querySelector(COMPOSER_SELECTOR);
11950
+ if (composer === null) {
11951
+ setBottomPx(FALLBACK_BOTTOM_PX);
11952
+ return;
11953
+ }
11954
+ const top = composer.getBoundingClientRect().top;
11955
+ if (top <= 0) return;
11956
+ setBottomPx(Math.round(window.innerHeight - top) + COMPOSER_GAP_PX);
11957
+ };
11958
+ measure();
11959
+ if (typeof ResizeObserver === "undefined") return;
11960
+ const composer = document.querySelector(COMPOSER_SELECTOR);
11961
+ if (composer === null) return;
11962
+ const observer = new ResizeObserver(measure);
11963
+ observer.observe(composer);
11964
+ window.addEventListener("resize", measure);
11965
+ return () => {
11966
+ observer.disconnect();
11967
+ window.removeEventListener("resize", measure);
11968
+ };
11969
+ }, [open]);
11889
11970
  const files = snapshot.files;
11890
11971
  const mine = files.filter((file) => file.sessionId === current);
11891
11972
  const theirs = files.filter((file) => file.sessionId !== current);
@@ -11905,20 +11986,45 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
11905
11986
  if (!open) setOpenedCount(files.length);
11906
11987
  setOpen((value) => !value);
11907
11988
  };
11908
- const renderEntry = (entry) => (0, react_jsx_runtime.jsx)(PendingRow, {
11989
+ const renderEntry = (entry) => (0, react_jsx_runtime.jsx)(PendingFileRow, {
11909
11990
  file: entry,
11910
11991
  files,
11911
11992
  selected: selected === entry.id,
11912
- busy: snapshot.busy.has(entry.id),
11913
11993
  t,
11914
- onSelect: setSelected,
11915
- onKeep,
11916
- onRevert
11994
+ onSelect: setSelected
11917
11995
  }, entry.id);
11996
+ const selectedFile = files.find((file) => file.id === selected);
11997
+ /** Drag the list/detail divider; width follows the pointer within its bounds. */
11998
+ const startResize = (event) => {
11999
+ if (event.button !== 0) return;
12000
+ event.preventDefault();
12001
+ resizeDrag.current = {
12002
+ startX: event.clientX,
12003
+ startWidth: listWidth
12004
+ };
12005
+ document.body.style.userSelect = "none";
12006
+ document.body.style.cursor = "col-resize";
12007
+ const onMove = (move) => {
12008
+ const start = resizeDrag.current;
12009
+ if (start === null) return;
12010
+ const next = start.startWidth + (move.clientX - start.startX);
12011
+ setListWidth(Math.min(Math.max(next, MIN_LIST_WIDTH_PX), MAX_LIST_WIDTH_PX));
12012
+ };
12013
+ const onUp = () => {
12014
+ resizeDrag.current = null;
12015
+ document.body.style.userSelect = "";
12016
+ document.body.style.cursor = "";
12017
+ window.removeEventListener("mousemove", onMove);
12018
+ window.removeEventListener("mouseup", onUp);
12019
+ };
12020
+ window.addEventListener("mousemove", onMove);
12021
+ window.addEventListener("mouseup", onUp);
12022
+ };
11918
12023
  return (0, react_jsx_runtime.jsxs)("div", {
11919
12024
  className: wide ? PendingPanel_module_css_default.layer : `${PendingPanel_module_css_default.layer} ${PendingPanel_module_css_default.rail}`,
11920
12025
  children: [open && (0, react_jsx_runtime.jsxs)("section", {
11921
12026
  className: PendingPanel_module_css_default.panel,
12027
+ style: { bottom: bottomPx },
11922
12028
  "data-diff-approval-panel": true,
11923
12029
  "aria-label": t("panel.title"),
11924
12030
  children: [(0, react_jsx_runtime.jsx)("header", {
@@ -11927,8 +12033,8 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
11927
12033
  className: PendingPanel_module_css_default.title,
11928
12034
  children: t("panel.title")
11929
12035
  })
11930
- }), (0, react_jsx_runtime.jsxs)("div", {
11931
- className: PendingPanel_module_css_default.body,
12036
+ }), snapshot.error !== void 0 || !snapshot.read || files.length === 0 ? (0, react_jsx_runtime.jsxs)("div", {
12037
+ className: PendingPanel_module_css_default.states,
11932
12038
  children: [
11933
12039
  snapshot.error !== void 0 && (0, react_jsx_runtime.jsx)("p", {
11934
12040
  className: PendingPanel_module_css_default.readError,
@@ -11942,25 +12048,49 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
11942
12048
  snapshot.read && snapshot.error === void 0 && files.length === 0 && (0, react_jsx_runtime.jsx)("p", {
11943
12049
  className: PendingPanel_module_css_default.note,
11944
12050
  children: t("panel.empty")
12051
+ })
12052
+ ]
12053
+ }) : (0, react_jsx_runtime.jsxs)("div", {
12054
+ className: PendingPanel_module_css_default.split,
12055
+ children: [
12056
+ (0, react_jsx_runtime.jsxs)("nav", {
12057
+ className: PendingPanel_module_css_default.fileList,
12058
+ style: { width: listWidth },
12059
+ "data-diff-approval-file-list": true,
12060
+ children: [mine.length > 0 && (0, react_jsx_runtime.jsxs)("section", { children: [(0, react_jsx_runtime.jsx)("h3", {
12061
+ className: PendingPanel_module_css_default.group,
12062
+ children: t("panel.group.current")
12063
+ }), (0, react_jsx_runtime.jsx)("ul", {
12064
+ className: PendingPanel_module_css_default.rows,
12065
+ children: mine.map(renderEntry)
12066
+ })] }), theirs.length > 0 && (0, react_jsx_runtime.jsxs)("section", { children: [(0, react_jsx_runtime.jsx)("h3", {
12067
+ className: PendingPanel_module_css_default.group,
12068
+ children: t("panel.group.others")
12069
+ }), (0, react_jsx_runtime.jsx)("ul", {
12070
+ className: PendingPanel_module_css_default.rows,
12071
+ children: theirs.map(renderEntry)
12072
+ })] })]
11945
12073
  }),
11946
- snapshot.read && files.length > 0 && (0, react_jsx_runtime.jsx)("p", {
11947
- className: PendingPanel_module_css_default.hint,
11948
- children: t("panel.selectHint")
12074
+ (0, react_jsx_runtime.jsx)("div", {
12075
+ className: PendingPanel_module_css_default.resizeHandle,
12076
+ "data-diff-resize": true,
12077
+ onMouseDown: startResize
11949
12078
  }),
11950
- mine.length > 0 && (0, react_jsx_runtime.jsxs)("section", { children: [(0, react_jsx_runtime.jsx)("h3", {
11951
- className: PendingPanel_module_css_default.group,
11952
- children: t("panel.group.current")
11953
- }), (0, react_jsx_runtime.jsx)("ul", {
11954
- className: PendingPanel_module_css_default.rows,
11955
- children: mine.map(renderEntry)
11956
- })] }),
11957
- theirs.length > 0 && (0, react_jsx_runtime.jsxs)("section", { children: [(0, react_jsx_runtime.jsx)("h3", {
11958
- className: PendingPanel_module_css_default.group,
11959
- children: t("panel.group.others")
11960
- }), (0, react_jsx_runtime.jsx)("ul", {
11961
- className: PendingPanel_module_css_default.rows,
11962
- children: theirs.map(renderEntry)
11963
- })] })
12079
+ (0, react_jsx_runtime.jsx)("div", {
12080
+ className: PendingPanel_module_css_default.detail,
12081
+ children: selectedFile === void 0 ? (0, react_jsx_runtime.jsx)("p", {
12082
+ className: PendingPanel_module_css_default.detailEmpty,
12083
+ children: t("panel.selectHint")
12084
+ }) : (0, react_jsx_runtime.jsx)(PendingDiff, {
12085
+ file: selectedFile,
12086
+ files,
12087
+ busy: snapshot.busy.has(selectedFile.id),
12088
+ t,
12089
+ onKeep,
12090
+ onRevert,
12091
+ onOpen
12092
+ })
12093
+ })
11964
12094
  ]
11965
12095
  })]
11966
12096
  }), (0, react_jsx_runtime.jsx)("div", {
@@ -12018,6 +12148,13 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
12018
12148
  sessionId,
12019
12149
  id
12020
12150
  }));
12151
+ },
12152
+ async open(sessionId, id, action) {
12153
+ return openOf(await rpc.call(DIFF_APPROVAL_CHANNEL, "open", {
12154
+ sessionId,
12155
+ id,
12156
+ action
12157
+ }));
12021
12158
  }
12022
12159
  };
12023
12160
  }
@@ -12066,6 +12203,15 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
12066
12203
  if (outcome !== "kept" && outcome !== "reverted" && outcome !== "missing") throw new Error("the action returned a malformed outcome");
12067
12204
  return { outcome };
12068
12205
  }
12206
+ /** Narrow the open endpoint's value; a malformed wire value is an open failure. */
12207
+ function openOf(result) {
12208
+ if (!result.ok) throw new Error(`${result.error.code}: ${result.error.message}`);
12209
+ const value = result.value;
12210
+ if (typeof value !== "object" || value === null) throw new Error("the action returned a malformed value");
12211
+ const outcome = value.outcome;
12212
+ if (outcome !== "opened" && outcome !== "missing") throw new Error("the action returned a malformed outcome");
12213
+ return { outcome };
12214
+ }
12069
12215
  //#endregion
12070
12216
  //#region lib/types/client/store.js
12071
12217
  /**
@@ -12158,6 +12304,16 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
12158
12304
  await port.revert(sessionId, id);
12159
12305
  });
12160
12306
  },
12307
+ async open(sessionId, id, action) {
12308
+ try {
12309
+ await port.open(sessionId, id, action);
12310
+ } catch (error) {
12311
+ publish({
12312
+ ...snapshot,
12313
+ error: error instanceof Error ? error.message : String(error)
12314
+ });
12315
+ }
12316
+ },
12161
12317
  reset() {
12162
12318
  publish({
12163
12319
  read: false,
@@ -12186,8 +12342,12 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
12186
12342
  "panel.missingHint": "该文件已不存在,回退会恢复该文件。",
12187
12343
  "panel.createHint": "回退会删除该新建的文件。",
12188
12344
  "row.create": "新增文件",
12345
+ "row.added": "+{added}",
12346
+ "row.removed": "-{removed}",
12189
12347
  "action.keep": "保留",
12190
12348
  "action.revert": "回退",
12349
+ "action.openFile": "打开文件",
12350
+ "action.revealFile": "打开所在目录",
12191
12351
  "action.busy": "处理中…",
12192
12352
  "action.prevDiff": "上一处差异",
12193
12353
  "action.nextDiff": "下一处差异",
@@ -12212,8 +12372,12 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
12212
12372
  "panel.missingHint": "This file no longer exists; reverting restores it.",
12213
12373
  "panel.createHint": "Reverting removes this created file.",
12214
12374
  "row.create": "New file",
12375
+ "row.added": "+{added}",
12376
+ "row.removed": "-{removed}",
12215
12377
  "action.keep": "Keep",
12216
12378
  "action.revert": "Revert",
12379
+ "action.openFile": "Open file",
12380
+ "action.revealFile": "Reveal in folder",
12217
12381
  "action.busy": "Working…",
12218
12382
  "action.prevDiff": "Previous diff",
12219
12383
  "action.nextDiff": "Next diff",
@@ -12256,7 +12420,8 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
12256
12420
  store.refresh(sessionId);
12257
12421
  },
12258
12422
  onKeep: (sessionId, path) => store.keep(sessionId, path),
12259
- onRevert: (sessionId, path) => store.revert(sessionId, path)
12423
+ onRevert: (sessionId, path) => store.revert(sessionId, path),
12424
+ onOpen: (sessionId, id, action) => store.open(sessionId, id, action)
12260
12425
  })
12261
12426
  }, PendingPanel));
12262
12427
  }
package/lib/index.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
3
- import { join, resolve } from "node:path";
3
+ import { dirname, join, resolve } from "node:path";
4
4
  import { dshHomePath, expandHomePath } from "@deepseek-ai/dsh-home-paths";
5
5
  import { SessionId } from "@deepseek-ai/dsh-session";
6
+ import { spawn } from "node:child_process";
6
7
  //#region lib/types/pending.js
7
8
  /**
8
9
  * In-memory pending-diff store: one entry per (session, path), holding the
@@ -269,17 +270,70 @@ var PendingPersistence = class {
269
270
  }
270
271
  };
271
272
  //#endregion
273
+ //#region lib/types/open.js
274
+ /**
275
+ * OS-level "open file" and "reveal in folder" launches for the host half's
276
+ * `open` endpoint. The plugin's own tests inject a double instead of running
277
+ * these; real profiles get the platform commands below.
278
+ * @module dsh-diff-approval/open
279
+ */
280
+ /** Run one detached command; resolves once the process spawns, never waits for exit. */
281
+ function launch(command, args) {
282
+ return new Promise((resolvePromise, reject) => {
283
+ const child = spawn(command, [...args], {
284
+ detached: true,
285
+ stdio: "ignore",
286
+ windowsHide: true
287
+ });
288
+ child.once("error", reject);
289
+ child.once("spawn", () => {
290
+ child.unref();
291
+ resolvePromise();
292
+ });
293
+ });
294
+ }
295
+ /**
296
+ * Launch a path through the current platform's default handler: `open` runs
297
+ * the file with its default application, `reveal` selects it in the file
298
+ * manager.
299
+ * @param path - backend execution-world path to act on.
300
+ * @param action - what to do with the path.
301
+ * @returns resolution once the launcher process has spawned.
302
+ */
303
+ function defaultOpenPath(path, action) {
304
+ switch (process.platform) {
305
+ case "win32": return action === "open" ? launch("cmd", [
306
+ "/c",
307
+ "start",
308
+ "",
309
+ path
310
+ ]) : launch("cmd", [
311
+ "/c",
312
+ "start",
313
+ "",
314
+ "explorer.exe",
315
+ `/select,${path}`
316
+ ]);
317
+ case "darwin": return action === "open" ? launch("open", [path]) : launch("open", ["-R", path]);
318
+ default: return action === "open" ? launch("xdg-open", [path]) : launch("xdg-open", [dirname(path)]);
319
+ }
320
+ }
321
+ //#endregion
272
322
  //#region lib/types/index.js
273
323
  /**
274
324
  * Pending-edit review, host half. Captures every successful `edit` and `write`
275
325
  * tool result (an unscoped `tools/result` listener receives per-session tool
276
326
  * executions because scoped emissions route through the shared root hook
277
- * table), folds each operation into its file's entry in the
327
+ * table) and every `str_replace_editor` mutation (whose result carries only a
328
+ * success message, so its pre-write basis is snapshotted at the
329
+ * `fs/edit-intent` / `fs/write-intent` seams and paired with the settle),
330
+ * folds each operation into its file's entry in the
278
331
  * {@link PendingDiffStore} (one entry per path), serves the `/diff-approval`
279
- * connection RPC channel (list/keep/revert), and applies a revert by writing
280
- * the entry's `oldText` back through `ctx.fs` (a created file's revert
281
- * removes it, and a tracked file that has since disappeared is restored by
282
- * its revert).
332
+ * connection RPC channel (list/keep/revert/open), and applies a revert by
333
+ * writing the entry's `oldText` back through `ctx.fs` (a created file's
334
+ * revert removes it, and a tracked file that has since disappeared is
335
+ * restored by its revert). `open` launches the file with its default
336
+ * application or reveals it in the file manager.
283
337
  *
284
338
  * Mount this row in any profile's `cordis.patch.yml`:
285
339
  *
@@ -363,6 +417,22 @@ function writeOutcomeOf(value) {
363
417
  function errorMessage(error) {
364
418
  return error instanceof Error ? error.message : String(error);
365
419
  }
420
+ /** Narrow a tool-execution-shaped value to its name, call id, and agent. */
421
+ function actorOf(value) {
422
+ if (typeof value !== "object" || value === null) return void 0;
423
+ const { name, callId, agent } = value;
424
+ return {
425
+ name,
426
+ callId,
427
+ agent
428
+ };
429
+ }
430
+ /** Narrow an agent-shaped value to its session id. */
431
+ function sessionOfAgent(agent) {
432
+ if (typeof agent !== "object" || agent === null) return void 0;
433
+ const id = agent.id;
434
+ return typeof id === "string" && id.length > 0 ? SessionId(id) : void 0;
435
+ }
366
436
  /**
367
437
  * Build one channel error in the closed RPC error vocabulary. `internal` is
368
438
  * the catch-all: business misses ride the success branch as `outcome: 'missing'`.
@@ -389,8 +459,78 @@ function apply(ctx, config) {
389
459
  if (storageDir !== void 0 && (typeof storageDir !== "string" || storageDir.trim().length === 0)) throw new Error("diff-approval: storageDir must be a non-empty string");
390
460
  const store = new PendingDiffStore();
391
461
  const persistence = new PendingPersistence(resolve(expandHomePath(storageDir ?? defaultStorageDir())));
462
+ const launchPath = config?.openPath ?? defaultOpenPath;
392
463
  const loaded = /* @__PURE__ */ new Set();
393
464
  const loading = /* @__PURE__ */ new Map();
465
+ /** Pre-write bases captured at the intent seams, keyed by the tool call id. */
466
+ const editorIntents = /* @__PURE__ */ new Map();
467
+ /**
468
+ * Snapshot one str_replace_editor mutation's basis at its intent seam. A
469
+ * `create` has an empty basis; an edit reads the pre-write content. Any
470
+ * failure tracks nothing — the settle-side pairing then sees no basis.
471
+ * @param target - the resolved target about to be written.
472
+ * @param actor - the tool execution running the mutation.
473
+ * @param kind - whether the mutation creates or edits the file.
474
+ */
475
+ async function stashEditorIntent(target, actor, kind) {
476
+ const shaped = actorOf(actor);
477
+ if (shaped === void 0 || shaped.name !== "str_replace_editor") return;
478
+ if (typeof shaped.callId !== "string") return;
479
+ const sessionId = sessionOfAgent(shaped.agent);
480
+ if (sessionId === void 0) return;
481
+ if (kind === "create") {
482
+ editorIntents.set(shaped.callId, {
483
+ target,
484
+ kind,
485
+ before: "",
486
+ sessionId
487
+ });
488
+ return;
489
+ }
490
+ try {
491
+ const before = await ctx.fs.readText(target, void 0) ?? "";
492
+ editorIntents.set(shaped.callId, {
493
+ target,
494
+ kind,
495
+ before,
496
+ sessionId
497
+ });
498
+ } catch {}
499
+ }
500
+ /**
501
+ * Fold one str_replace_editor mutation into its file's entry. The tool's
502
+ * result carries only a success message, so the settle reads the post-write
503
+ * content and pairs it with the basis snapshotted at the intent seam.
504
+ * @param exec - the settled str_replace_editor execution.
505
+ * @param result - its outcome.
506
+ */
507
+ async function captureEditorMutation(exec, result) {
508
+ const basis = editorIntents.get(exec.callId);
509
+ editorIntents.delete(exec.callId);
510
+ if (basis === void 0 || basis.sessionId === void 0 || result.isError) return;
511
+ const argumentsValue = exec.arguments;
512
+ const command = typeof argumentsValue === "object" && argumentsValue !== null ? argumentsValue.command : void 0;
513
+ if (!(basis.kind === "create" ? command === "create" : command === "str_replace" || command === "insert")) return;
514
+ let after;
515
+ try {
516
+ after = await ctx.fs.readText(basis.target, void 0) ?? "";
517
+ } catch {
518
+ return;
519
+ }
520
+ if (basis.kind === "edit" && after === basis.before) return;
521
+ const sessionId = basis.sessionId;
522
+ const entry = {
523
+ id: randomUUID(),
524
+ sessionId,
525
+ path: basis.target.displayPath,
526
+ kind: basis.kind,
527
+ oldText: basis.before,
528
+ newText: after,
529
+ updatedAt: Date.now()
530
+ };
531
+ await ensureLoaded(sessionId);
532
+ if (store.fold(entry)) await persistSession(sessionId);
533
+ }
394
534
  /**
395
535
  * Read one path's live state: present content, an unresolvable (missing)
396
536
  * path, or a resolved-but-unreadable file.
@@ -504,6 +644,10 @@ function apply(ctx, config) {
504
644
  }
505
645
  }
506
646
  ctx.on("tools/result", (exec, result) => {
647
+ if (exec.name === "str_replace_editor") {
648
+ captureEditorMutation(exec, result);
649
+ return;
650
+ }
507
651
  if (result.isError || exec.agent === void 0) return;
508
652
  const outcome = exec.name === "edit" ? editOutcomeOf(result.value) : exec.name === "write" ? writeOutcomeOf(result.value) : void 0;
509
653
  if (outcome === void 0 || outcome.oldText === outcome.newText) return;
@@ -564,10 +708,38 @@ function apply(ctx, config) {
564
708
  value: { outcome: "reverted" }
565
709
  };
566
710
  }
711
+ case "open": {
712
+ const target = openTargetOf(payload);
713
+ if (target === void 0) return rpcError("sessionId, id, and action must be valid");
714
+ await ensureLoaded(target.sessionId);
715
+ const entry = store.get(target.sessionId, target.id);
716
+ if (entry === void 0) return {
717
+ ok: true,
718
+ value: { outcome: "missing" }
719
+ };
720
+ try {
721
+ const resolved = await ctx.fs.resolve(entry.path, { signal });
722
+ await launchPath(ctx.fs.processPath(resolved), target.action);
723
+ } catch (error) {
724
+ return rpcError(`${target.action} failed: ${errorMessage(error)}`);
725
+ }
726
+ return {
727
+ ok: true,
728
+ value: { outcome: "opened" }
729
+ };
730
+ }
567
731
  default: return rpcError(`unknown endpoint ${JSON.stringify(endpoint)}`);
568
732
  }
569
733
  };
570
734
  ctx.effect(() => ctx.connection.rpc.handle(DIFF_APPROVAL_CHANNEL, handle, { authority: "trusted-host" }), "diff-approval: review channel");
735
+ ctx.effect(() => ctx.on("fs/edit-intent", async (target, actor, next) => {
736
+ await stashEditorIntent(target, actor, "edit");
737
+ return next();
738
+ }, { prepend: true }), "diff-approval: str_replace_editor edit basis");
739
+ ctx.effect(() => ctx.on("fs/write-intent", async (target, actor, next) => {
740
+ await stashEditorIntent(target, actor, "create");
741
+ return next();
742
+ }, { prepend: true }), "diff-approval: str_replace_editor create basis");
571
743
  }
572
744
  /** Narrow a wire payload's `sessionId` field to a branded session id. */
573
745
  function sessionOf(payload) {
@@ -587,5 +759,16 @@ function targetOf(payload) {
587
759
  id
588
760
  };
589
761
  }
762
+ /** Narrow a wire payload to one open target: the keep/revert pair plus the action. */
763
+ function openTargetOf(payload) {
764
+ const target = targetOf(payload);
765
+ if (target === void 0) return void 0;
766
+ const action = payload.action;
767
+ if (action !== "open" && action !== "reveal") return void 0;
768
+ return {
769
+ ...target,
770
+ action
771
+ };
772
+ }
590
773
  //#endregion
591
- export { DIFF_APPROVAL_CHANNEL, PendingDiffStore, PendingPersistence, apply, defaultStorageDir, inject, name };
774
+ export { DIFF_APPROVAL_CHANNEL, PendingDiffStore, PendingPersistence, apply, defaultOpenPath, defaultStorageDir, inject, name };
@@ -1,7 +1,7 @@
1
- /** Sidebar-foot pending-edit review action and the whole-file diff list it opens. */
1
+ /** Sidebar-foot pending-edit review action and the split review panel it opens. */
2
2
  import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
3
3
  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, t, }: PendingPanelProps): import("react").JSX.Element;
7
+ export declare function PendingPanel({ wide, useSessions, usePending, onRefresh, onKeep, onRevert, onOpen, t, }: PendingPanelProps): import("react").JSX.Element;
@@ -15,8 +15,12 @@ export declare const zh: {
15
15
  'panel.missingHint': string;
16
16
  'panel.createHint': string;
17
17
  'row.create': string;
18
+ 'row.added': string;
19
+ 'row.removed': string;
18
20
  'action.keep': string;
19
21
  'action.revert': string;
22
+ 'action.openFile': string;
23
+ 'action.revealFile': string;
20
24
  'action.busy': string;
21
25
  'action.prevDiff': string;
22
26
  'action.nextDiff': string;
@@ -49,8 +53,12 @@ export declare const en: {
49
53
  'panel.missingHint': string;
50
54
  'panel.createHint': string;
51
55
  'row.create': string;
56
+ 'row.added': string;
57
+ 'row.removed': string;
52
58
  'action.keep': string;
53
59
  'action.revert': string;
60
+ 'action.openFile': string;
61
+ 'action.revealFile': string;
54
62
  'action.busy': string;
55
63
  'action.prevDiff': string;
56
64
  'action.nextDiff': string;
@@ -5,7 +5,7 @@
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, PendingFileDiff } from '../types.ts';
8
+ import type { DiffApprovalActionValue, DiffApprovalOpenAction, DiffApprovalOpenValue, PendingFileDiff } 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. */
@@ -16,6 +16,8 @@ export interface DiffApprovalPort {
16
16
  keep(sessionId: SessionId, id: string): Promise<DiffApprovalActionValue>;
17
17
  /** Revert one operation. */
18
18
  revert(sessionId: SessionId, id: string): Promise<DiffApprovalActionValue>;
19
+ /** Open one file with its default application or reveal it in the folder. */
20
+ open(sessionId: SessionId, id: string, action: DiffApprovalOpenAction): Promise<DiffApprovalOpenValue>;
19
21
  }
20
22
  /** Build the port over one generic RPC caller.
21
23
  * @param rpc - the connection's channel caller.
@@ -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 { PendingFileDiff } from '../types.ts';
4
+ import type { 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. */
@@ -25,4 +25,6 @@ export interface PendingPanelFace {
25
25
  onKeep: (sessionId: SessionId, id: string) => Promise<void>;
26
26
  /** Revert one operation (restore its prior content, or remove a created file). */
27
27
  onRevert: (sessionId: SessionId, id: string) => Promise<void>;
28
+ /** Open one file with its default application or reveal it in the folder. */
29
+ onOpen: (sessionId: SessionId, id: string, action: DiffApprovalOpenAction) => Promise<void>;
28
30
  }
@@ -7,6 +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
11
  import type { PendingDiffSnapshot } from './slots.ts';
11
12
  import type { DiffApprovalPort } from './port.ts';
12
13
  /** The observable the panel reads and the plugin body drives. */
@@ -17,6 +18,8 @@ export interface PendingDiffStore extends HostObservable<PendingDiffSnapshot> {
17
18
  keep: (sessionId: SessionId, id: string) => Promise<void>;
18
19
  /** Revert one operation. */
19
20
  revert: (sessionId: SessionId, id: string) => Promise<void>;
21
+ /** Open one file with its default application or reveal it in the folder. */
22
+ open: (sessionId: SessionId, id: string, action: DiffApprovalOpenAction) => Promise<void>;
20
23
  /** Drop every local fact (used on connection reset). */
21
24
  reset: () => void;
22
25
  }
@@ -2,12 +2,16 @@
2
2
  * Pending-edit review, host half. Captures every successful `edit` and `write`
3
3
  * tool result (an unscoped `tools/result` listener receives per-session tool
4
4
  * executions because scoped emissions route through the shared root hook
5
- * table), folds each operation into its file's entry in the
5
+ * table) and every `str_replace_editor` mutation (whose result carries only a
6
+ * success message, so its pre-write basis is snapshotted at the
7
+ * `fs/edit-intent` / `fs/write-intent` seams and paired with the settle),
8
+ * folds each operation into its file's entry in the
6
9
  * {@link PendingDiffStore} (one entry per path), serves the `/diff-approval`
7
- * connection RPC channel (list/keep/revert), and applies a revert by writing
8
- * the entry's `oldText` back through `ctx.fs` (a created file's revert
9
- * removes it, and a tracked file that has since disappeared is restored by
10
- * its revert).
10
+ * connection RPC channel (list/keep/revert/open), and applies a revert by
11
+ * writing the entry's `oldText` back through `ctx.fs` (a created file's
12
+ * revert removes it, and a tracked file that has since disappeared is
13
+ * restored by its revert). `open` launches the file with its default
14
+ * application or reveals it in the file manager.
11
15
  *
12
16
  * Mount this row in any profile's `cordis.patch.yml`:
13
17
  *
@@ -29,9 +33,11 @@
29
33
  * @module dsh-diff-approval
30
34
  */
31
35
  import { Context } from '@deepseek-ai/cordis';
32
- export type { DiffApprovalActionOutcome, DiffApprovalActionValue, DiffApprovalListValue, PendingEntry, PendingEntryKind, PendingFileDiff, } from './types.ts';
36
+ import type { DiffApprovalOpenAction } from './types.ts';
37
+ export type { DiffApprovalActionOutcome, DiffApprovalActionValue, DiffApprovalListValue, DiffApprovalOpenAction, DiffApprovalOpenValue, PendingEntry, PendingEntryKind, PendingFileDiff, } from './types.ts';
33
38
  export { PendingDiffStore } from './pending.ts';
34
39
  export { PendingPersistence, defaultStorageDir } from './persist.ts';
40
+ export { defaultOpenPath } from './open.ts';
35
41
  /** Stable Cordis plugin name. */
36
42
  export declare const name = "diff-approval";
37
43
  /** Services required before the review surface activates. */
@@ -48,6 +54,11 @@ export interface DiffApprovalConfig {
48
54
  * `~` prefixes expand to the OS home.
49
55
  */
50
56
  storageDir?: string;
57
+ /**
58
+ * Launcher for the `open` endpoint, defaulting to the platform commands.
59
+ * Injectable for tests; receives the backend execution-world path.
60
+ */
61
+ openPath?: (path: string, action: DiffApprovalOpenAction) => Promise<void>;
51
62
  }
52
63
  /**
53
64
  * Mount the pending-edit review surface.
@@ -0,0 +1,17 @@
1
+ /**
2
+ * OS-level "open file" and "reveal in folder" launches for the host half's
3
+ * `open` endpoint. The plugin's own tests inject a double instead of running
4
+ * these; real profiles get the platform commands below.
5
+ * @module dsh-diff-approval/open
6
+ */
7
+ /** What the open endpoint asks the OS to do with a path. */
8
+ export type OpenAction = 'open' | 'reveal';
9
+ /**
10
+ * Launch a path through the current platform's default handler: `open` runs
11
+ * the file with its default application, `reveal` selects it in the file
12
+ * manager.
13
+ * @param path - backend execution-world path to act on.
14
+ * @param action - what to do with the path.
15
+ * @returns resolution once the launcher process has spawned.
16
+ */
17
+ export declare function defaultOpenPath(path: string, action: OpenAction): Promise<void>;
@@ -52,6 +52,13 @@ export interface DiffApprovalListValue {
52
52
  /** Pending entries for the requested session, oldest capture first. */
53
53
  files: PendingFileDiff[];
54
54
  }
55
+ /** What the open endpoint asks the OS to do with a file. */
56
+ export type DiffApprovalOpenAction = 'open' | 'reveal';
57
+ /** Value returned by the channel's open endpoint. */
58
+ export interface DiffApprovalOpenValue {
59
+ /** What the request did; `missing` means no pending entry existed. */
60
+ outcome: 'opened' | 'missing';
61
+ }
55
62
  /** Outcome of one keep/revert request. */
56
63
  export type DiffApprovalActionOutcome = 'kept' | 'reverted' | 'missing';
57
64
  /** Value returned by the channel's keep and revert endpoints. */
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "dsh-diff-approval",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "DeepSeek Harness plugin: pending-edit review with whole-file diff and Keep/Revert",
5
+ "packageManager": "pnpm@11.21.0",
5
6
  "author": "Wu Zhiwei",
6
7
  "repository": {
7
8
  "type": "git",