@davmash/bygone 0.8.12 → 0.8.14

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.
@@ -3158,7 +3158,31 @@ var require_menuUtils = __commonJS({
3158
3158
  canRemovePanel: isMultiDiff && (session2?.multi?.files?.length || 0) > 1
3159
3159
  };
3160
3160
  }
3161
- module2.exports = { getMenuCapabilities: getMenuCapabilities2 };
3161
+ async function collectComparisonSelection2(choosePaths, confirmSelection) {
3162
+ const paths = [];
3163
+ while (true) {
3164
+ const selectedPaths = await choosePaths(paths.length);
3165
+ if (!Array.isArray(selectedPaths) || selectedPaths.length === 0) {
3166
+ return [];
3167
+ }
3168
+ for (const selectedPath of selectedPaths) {
3169
+ if (typeof selectedPath === "string" && !paths.includes(selectedPath)) {
3170
+ paths.push(selectedPath);
3171
+ }
3172
+ }
3173
+ if (paths.length < 2) {
3174
+ continue;
3175
+ }
3176
+ const decision = await confirmSelection([...paths]);
3177
+ if (decision === "compare") {
3178
+ return paths;
3179
+ }
3180
+ if (decision !== "add") {
3181
+ return [];
3182
+ }
3183
+ }
3184
+ }
3185
+ module2.exports = { collectComparisonSelection: collectComparisonSelection2, getMenuCapabilities: getMenuCapabilities2 };
3162
3186
  }
3163
3187
  });
3164
3188
 
@@ -4077,16 +4101,41 @@ var require_changeTour = __commonJS({
4077
4101
  }
4078
4102
  validateMultiPanelFiles(value.files, panels.length, index);
4079
4103
  validateMultiPanelSteps(value.steps, panels.length, index, true);
4080
- if (Array.isArray(value.steps) && value.steps.length !== panels.length - 1) {
4081
- throw new Error(`scenes[${index}].steps must match the number of explanation stages.`);
4082
- }
4083
4104
  if (Array.isArray(value.steps)) {
4105
+ let previousPairIndex = -1;
4084
4106
  value.steps.forEach((step, stepIndex) => {
4085
- const panel = panels[stepIndex + 1];
4086
- if (!isRecord2(step) || !isRecord2(panel) || panel.stageId !== step.id) {
4087
- throw new Error(`scenes[${index}].panels must align with explanation stage ids.`);
4107
+ if (!isRecord2(step)) return;
4108
+ const pairIndex = Number(step.pairIndex);
4109
+ if (pairIndex < previousPairIndex) {
4110
+ throw new Error(`scenes[${index}].steps must remain grouped in explanation stage order.`);
4111
+ }
4112
+ previousPairIndex = pairIndex;
4113
+ if (step.stageId !== void 0) requireString(step.stageId, `scenes[${index}].steps[${stepIndex}].stageId`);
4114
+ if (step.stageIndex !== void 0) requireNonNegativeInteger(step.stageIndex, `scenes[${index}].steps[${stepIndex}].stageIndex`);
4115
+ if (step.focusIndex !== void 0) requireNonNegativeInteger(step.focusIndex, `scenes[${index}].steps[${stepIndex}].focusIndex`);
4116
+ if (step.focusCount !== void 0) requirePositiveInteger(step.focusCount, `scenes[${index}].steps[${stepIndex}].focusCount`);
4117
+ if (step.stageIndex !== void 0 && Number(step.stageIndex) !== pairIndex) {
4118
+ throw new Error(`scenes[${index}].steps[${stepIndex}].stageIndex must match pairIndex.`);
4119
+ }
4120
+ if (step.focusIndex !== void 0 && step.focusCount !== void 0 && Number(step.focusIndex) >= Number(step.focusCount)) {
4121
+ throw new Error(`scenes[${index}].steps[${stepIndex}].focusIndex must be less than focusCount.`);
4088
4122
  }
4089
4123
  });
4124
+ for (let stageIndex = 0; stageIndex < panels.length - 1; stageIndex += 1) {
4125
+ const panel = panels[stageIndex + 1];
4126
+ const stageSteps = value.steps.filter((step) => isRecord2(step) && Number(step.pairIndex) === stageIndex);
4127
+ if (!isRecord2(panel) || stageSteps.length === 0) {
4128
+ throw new Error(`scenes[${index}].steps must cover every explanation stage.`);
4129
+ }
4130
+ const firstStep = stageSteps[0];
4131
+ const fallbackStageId = isRecord2(firstStep) ? firstStep.id : void 0;
4132
+ if (stageSteps.some((step) => {
4133
+ if (!isRecord2(step)) return true;
4134
+ return (step.stageId ?? fallbackStageId) !== panel.stageId;
4135
+ })) {
4136
+ throw new Error(`scenes[${index}].panels must align with explanation stage ids.`);
4137
+ }
4138
+ }
4090
4139
  }
4091
4140
  }
4092
4141
  function validateMultiPanelFiles(value, panelCount, index) {
@@ -4115,7 +4164,6 @@ var require_changeTour = __commonJS({
4115
4164
  if (deconstructed) {
4116
4165
  requireStringArray(step.introducedHunks, `scenes[${index}].steps[${stepIndex}].introducedHunks`);
4117
4166
  if (step.introducedHunks.length === 0) throw new Error(`scenes[${index}].steps[${stepIndex}].introducedHunks must be non-empty.`);
4118
- if (Number(step.pairIndex) !== stepIndex) throw new Error(`scenes[${index}].steps[${stepIndex}].pairIndex must match its explanation stage.`);
4119
4167
  }
4120
4168
  if (step.side !== "left" && step.side !== "right") throw new Error(`scenes[${index}].steps[${stepIndex}].side must be left or right.`);
4121
4169
  if (step.startLine !== void 0) requirePositiveInteger(step.startLine, `scenes[${index}].steps[${stepIndex}].startLine`);
@@ -4755,85 +4803,1282 @@ var require_changeTour = __commonJS({
4755
4803
  for (const { stage, claims } of stageClaims) {
4756
4804
  const introducedHunks = [];
4757
4805
  const introducedFiles = [];
4806
+ const introducedChanges = [];
4758
4807
  for (const claim of claims) {
4759
4808
  const cumulative = cumulativeByFile.get(claim.file) || /* @__PURE__ */ new Set();
4760
4809
  claim.ids.forEach((id) => {
4761
4810
  cumulative.add(id);
4762
4811
  introducedHunks.push(id);
4763
4812
  });
4764
- cumulativeByFile.set(claim.file, cumulative);
4765
- if (!introducedFiles.includes(claim.file.path)) introducedFiles.push(claim.file.path);
4813
+ cumulativeByFile.set(claim.file, cumulative);
4814
+ if (!introducedFiles.includes(claim.file.path)) introducedFiles.push(claim.file.path);
4815
+ const introducedChange = introducedChanges.find((change) => change.file === claim.file.path);
4816
+ if (introducedChange) {
4817
+ introducedChange.hunks.push(...claim.ids);
4818
+ } else {
4819
+ introducedChanges.push({ file: claim.file.path, hunks: [...claim.ids] });
4820
+ }
4821
+ }
4822
+ stages.push({
4823
+ id: stage.id,
4824
+ title: stage.title,
4825
+ narration: stage.narration,
4826
+ introducedHunks,
4827
+ introducedFiles,
4828
+ introducedChanges,
4829
+ cumulativeHunks: [...cumulativeByFile.values()].flatMap((ids) => [...ids]),
4830
+ files: materializableFiles.map((file) => buildFileState(
4831
+ file,
4832
+ cumulativeByFile.get(file) || /* @__PURE__ */ new Set(),
4833
+ wholeFileExclusions.has(file)
4834
+ ))
4835
+ });
4836
+ }
4837
+ for (const file of materializableFiles) {
4838
+ const finalState = buildFileState(
4839
+ file,
4840
+ cumulativeByFile.get(file) || /* @__PURE__ */ new Set(),
4841
+ wholeFileExclusions.has(file)
4842
+ );
4843
+ const expectedExists = file.changeKind !== "deleted";
4844
+ if (finalState.exists !== expectedExists || finalState.content !== file.headContent) {
4845
+ throw new Error(`Final deconstructed state does not match the target for ${file.path}.`);
4846
+ }
4847
+ }
4848
+ return {
4849
+ id: scene.id,
4850
+ title: scene.title,
4851
+ baseOid: inventory.range.baseOid,
4852
+ targetOid: inventory.range.headOid,
4853
+ baselineFiles,
4854
+ excludedHunks: [...excludedByFile.values()].flatMap((ids) => [...ids]),
4855
+ excludedFiles,
4856
+ stages
4857
+ };
4858
+ }
4859
+ function requireInventoryFile(inventory, path9, owner) {
4860
+ const matches = inventory.files.filter((file) => file.path === path9 || file.previousPath === path9);
4861
+ if (matches.length === 0) throw new Error(`${owner} references a file outside the change inventory: ${path9}`);
4862
+ if (matches.length > 1) throw new Error(`${owner} references an ambiguous changed path: ${path9}`);
4863
+ return matches[0];
4864
+ }
4865
+ function claimHunks(file, ids, owner, ownership) {
4866
+ const known = new Set(file.units.map((unit) => unit.id));
4867
+ for (const id of ids) {
4868
+ if (!known.has(id)) throw new Error(`${owner} references an unknown hunk in ${file.path}: ${id}`);
4869
+ const key = unitKey(file, id);
4870
+ const previousOwner = ownership.get(key);
4871
+ if (previousOwner) throw new Error(`Change unit ${id} in ${file.path} is assigned to both ${previousOwner} and ${owner}.`);
4872
+ ownership.set(key, owner);
4873
+ }
4874
+ }
4875
+ function unitKey(file, id) {
4876
+ return `${file.previousPath || ""}\0${file.path}\0${id}`;
4877
+ }
4878
+ function buildFileState(file, selected, wholeFileExcluded) {
4879
+ if (file.baseContent === void 0) throw new Error(`Missing base content for ${file.path}.`);
4880
+ const content = materializeChangeUnits(file.baseContent, file.units, selected);
4881
+ let exists = true;
4882
+ if (file.changeKind === "added") exists = wholeFileExcluded || selected.size > 0;
4883
+ if (file.changeKind === "deleted") exists = !(wholeFileExcluded || selected.size === file.units.length);
4884
+ return {
4885
+ path: file.path,
4886
+ previousPath: file.previousPath,
4887
+ changeKind: file.changeKind,
4888
+ exists,
4889
+ content: exists ? content : "",
4890
+ appliedHunks: [...selected]
4891
+ };
4892
+ }
4893
+ function Diff2() {
4894
+ }
4895
+ Diff2.prototype = {
4896
+ diff: function diff2(oldString, newString) {
4897
+ var _options$timeout;
4898
+ var options = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : {};
4899
+ var callback = options.callback;
4900
+ if (typeof options === "function") {
4901
+ callback = options;
4902
+ options = {};
4903
+ }
4904
+ this.options = options;
4905
+ var self2 = this;
4906
+ function done(value) {
4907
+ if (callback) {
4908
+ setTimeout(function() {
4909
+ callback(void 0, value);
4910
+ }, 0);
4911
+ return true;
4912
+ } else {
4913
+ return value;
4914
+ }
4915
+ }
4916
+ oldString = this.castInput(oldString);
4917
+ newString = this.castInput(newString);
4918
+ oldString = this.removeEmpty(this.tokenize(oldString));
4919
+ newString = this.removeEmpty(this.tokenize(newString));
4920
+ var newLen = newString.length, oldLen = oldString.length;
4921
+ var editLength = 1;
4922
+ var maxEditLength = newLen + oldLen;
4923
+ if (options.maxEditLength) {
4924
+ maxEditLength = Math.min(maxEditLength, options.maxEditLength);
4925
+ }
4926
+ var maxExecutionTime = (_options$timeout = options.timeout) !== null && _options$timeout !== void 0 ? _options$timeout : Infinity;
4927
+ var abortAfterTimestamp = Date.now() + maxExecutionTime;
4928
+ var bestPath = [{
4929
+ oldPos: -1,
4930
+ lastComponent: void 0
4931
+ }];
4932
+ var newPos = this.extractCommon(bestPath[0], newString, oldString, 0);
4933
+ if (bestPath[0].oldPos + 1 >= oldLen && newPos + 1 >= newLen) {
4934
+ return done([{
4935
+ value: this.join(newString),
4936
+ count: newString.length
4937
+ }]);
4938
+ }
4939
+ var minDiagonalToConsider = -Infinity, maxDiagonalToConsider = Infinity;
4940
+ function execEditLength() {
4941
+ for (var diagonalPath = Math.max(minDiagonalToConsider, -editLength); diagonalPath <= Math.min(maxDiagonalToConsider, editLength); diagonalPath += 2) {
4942
+ var basePath = void 0;
4943
+ var removePath = bestPath[diagonalPath - 1], addPath = bestPath[diagonalPath + 1];
4944
+ if (removePath) {
4945
+ bestPath[diagonalPath - 1] = void 0;
4946
+ }
4947
+ var canAdd = false;
4948
+ if (addPath) {
4949
+ var addPathNewPos = addPath.oldPos - diagonalPath;
4950
+ canAdd = addPath && 0 <= addPathNewPos && addPathNewPos < newLen;
4951
+ }
4952
+ var canRemove = removePath && removePath.oldPos + 1 < oldLen;
4953
+ if (!canAdd && !canRemove) {
4954
+ bestPath[diagonalPath] = void 0;
4955
+ continue;
4956
+ }
4957
+ if (!canRemove || canAdd && removePath.oldPos + 1 < addPath.oldPos) {
4958
+ basePath = self2.addToPath(addPath, true, void 0, 0);
4959
+ } else {
4960
+ basePath = self2.addToPath(removePath, void 0, true, 1);
4961
+ }
4962
+ newPos = self2.extractCommon(basePath, newString, oldString, diagonalPath);
4963
+ if (basePath.oldPos + 1 >= oldLen && newPos + 1 >= newLen) {
4964
+ return done(buildValues2(self2, basePath.lastComponent, newString, oldString, self2.useLongestToken));
4965
+ } else {
4966
+ bestPath[diagonalPath] = basePath;
4967
+ if (basePath.oldPos + 1 >= oldLen) {
4968
+ maxDiagonalToConsider = Math.min(maxDiagonalToConsider, diagonalPath - 1);
4969
+ }
4970
+ if (newPos + 1 >= newLen) {
4971
+ minDiagonalToConsider = Math.max(minDiagonalToConsider, diagonalPath + 1);
4972
+ }
4973
+ }
4974
+ }
4975
+ editLength++;
4976
+ }
4977
+ if (callback) {
4978
+ (function exec() {
4979
+ setTimeout(function() {
4980
+ if (editLength > maxEditLength || Date.now() > abortAfterTimestamp) {
4981
+ return callback();
4982
+ }
4983
+ if (!execEditLength()) {
4984
+ exec();
4985
+ }
4986
+ }, 0);
4987
+ })();
4988
+ } else {
4989
+ while (editLength <= maxEditLength && Date.now() <= abortAfterTimestamp) {
4990
+ var ret = execEditLength();
4991
+ if (ret) {
4992
+ return ret;
4993
+ }
4994
+ }
4995
+ }
4996
+ },
4997
+ addToPath: function addToPath2(path9, added, removed, oldPosInc) {
4998
+ var last = path9.lastComponent;
4999
+ if (last && last.added === added && last.removed === removed) {
5000
+ return {
5001
+ oldPos: path9.oldPos + oldPosInc,
5002
+ lastComponent: {
5003
+ count: last.count + 1,
5004
+ added,
5005
+ removed,
5006
+ previousComponent: last.previousComponent
5007
+ }
5008
+ };
5009
+ } else {
5010
+ return {
5011
+ oldPos: path9.oldPos + oldPosInc,
5012
+ lastComponent: {
5013
+ count: 1,
5014
+ added,
5015
+ removed,
5016
+ previousComponent: last
5017
+ }
5018
+ };
5019
+ }
5020
+ },
5021
+ extractCommon: function extractCommon2(basePath, newString, oldString, diagonalPath) {
5022
+ var newLen = newString.length, oldLen = oldString.length, oldPos = basePath.oldPos, newPos = oldPos - diagonalPath, commonCount = 0;
5023
+ while (newPos + 1 < newLen && oldPos + 1 < oldLen && this.equals(newString[newPos + 1], oldString[oldPos + 1])) {
5024
+ newPos++;
5025
+ oldPos++;
5026
+ commonCount++;
5027
+ }
5028
+ if (commonCount) {
5029
+ basePath.lastComponent = {
5030
+ count: commonCount,
5031
+ previousComponent: basePath.lastComponent
5032
+ };
5033
+ }
5034
+ basePath.oldPos = oldPos;
5035
+ return newPos;
5036
+ },
5037
+ equals: function equals2(left, right) {
5038
+ if (this.options.comparator) {
5039
+ return this.options.comparator(left, right);
5040
+ } else {
5041
+ return left === right || this.options.ignoreCase && left.toLowerCase() === right.toLowerCase();
5042
+ }
5043
+ },
5044
+ removeEmpty: function removeEmpty2(array) {
5045
+ var ret = [];
5046
+ for (var i = 0; i < array.length; i++) {
5047
+ if (array[i]) {
5048
+ ret.push(array[i]);
5049
+ }
5050
+ }
5051
+ return ret;
5052
+ },
5053
+ castInput: function castInput2(value) {
5054
+ return value;
5055
+ },
5056
+ tokenize: function tokenize2(value) {
5057
+ return value.split("");
5058
+ },
5059
+ join: function join4(chars) {
5060
+ return chars.join("");
5061
+ }
5062
+ };
5063
+ function buildValues2(diff2, lastComponent, newString, oldString, useLongestToken) {
5064
+ var components = [];
5065
+ var nextComponent;
5066
+ while (lastComponent) {
5067
+ components.push(lastComponent);
5068
+ nextComponent = lastComponent.previousComponent;
5069
+ delete lastComponent.previousComponent;
5070
+ lastComponent = nextComponent;
5071
+ }
5072
+ components.reverse();
5073
+ var componentPos = 0, componentLen = components.length, newPos = 0, oldPos = 0;
5074
+ for (; componentPos < componentLen; componentPos++) {
5075
+ var component = components[componentPos];
5076
+ if (!component.removed) {
5077
+ if (!component.added && useLongestToken) {
5078
+ var value = newString.slice(newPos, newPos + component.count);
5079
+ value = value.map(function(value2, i) {
5080
+ var oldValue = oldString[oldPos + i];
5081
+ return oldValue.length > value2.length ? oldValue : value2;
5082
+ });
5083
+ component.value = diff2.join(value);
5084
+ } else {
5085
+ component.value = diff2.join(newString.slice(newPos, newPos + component.count));
5086
+ }
5087
+ newPos += component.count;
5088
+ if (!component.added) {
5089
+ oldPos += component.count;
5090
+ }
5091
+ } else {
5092
+ component.value = diff2.join(oldString.slice(oldPos, oldPos + component.count));
5093
+ oldPos += component.count;
5094
+ if (componentPos && components[componentPos - 1].added) {
5095
+ var tmp = components[componentPos - 1];
5096
+ components[componentPos - 1] = components[componentPos];
5097
+ components[componentPos] = tmp;
5098
+ }
5099
+ }
5100
+ }
5101
+ var finalComponent = components[componentLen - 1];
5102
+ if (componentLen > 1 && typeof finalComponent.value === "string" && (finalComponent.added || finalComponent.removed) && diff2.equals("", finalComponent.value)) {
5103
+ components[componentLen - 2].value += finalComponent.value;
5104
+ components.pop();
5105
+ }
5106
+ return components;
5107
+ }
5108
+ var characterDiff2 = new Diff2();
5109
+ function diffChars2(oldStr, newStr, options) {
5110
+ return characterDiff2.diff(oldStr, newStr, options);
5111
+ }
5112
+ var extendedWordChars2 = /^[A-Za-z\xC0-\u02C6\u02C8-\u02D7\u02DE-\u02FF\u1E00-\u1EFF]+$/;
5113
+ var reWhitespace2 = /\S/;
5114
+ var wordDiff2 = new Diff2();
5115
+ wordDiff2.equals = function(left, right) {
5116
+ if (this.options.ignoreCase) {
5117
+ left = left.toLowerCase();
5118
+ right = right.toLowerCase();
5119
+ }
5120
+ return left === right || this.options.ignoreWhitespace && !reWhitespace2.test(left) && !reWhitespace2.test(right);
5121
+ };
5122
+ wordDiff2.tokenize = function(value) {
5123
+ var tokens = value.split(/([^\S\r\n]+|[()[\]{}'"\r\n]|\b)/);
5124
+ for (var i = 0; i < tokens.length - 1; i++) {
5125
+ if (!tokens[i + 1] && tokens[i + 2] && extendedWordChars2.test(tokens[i]) && extendedWordChars2.test(tokens[i + 2])) {
5126
+ tokens[i] += tokens[i + 2];
5127
+ tokens.splice(i + 1, 2);
5128
+ i--;
5129
+ }
5130
+ }
5131
+ return tokens;
5132
+ };
5133
+ function diffWordsWithSpace2(oldStr, newStr, options) {
5134
+ return wordDiff2.diff(oldStr, newStr, options);
5135
+ }
5136
+ var lineDiff2 = new Diff2();
5137
+ lineDiff2.tokenize = function(value) {
5138
+ if (this.options.stripTrailingCr) {
5139
+ value = value.replace(/\r\n/g, "\n");
5140
+ }
5141
+ var retLines = [], linesAndNewlines = value.split(/(\n|\r\n)/);
5142
+ if (!linesAndNewlines[linesAndNewlines.length - 1]) {
5143
+ linesAndNewlines.pop();
5144
+ }
5145
+ for (var i = 0; i < linesAndNewlines.length; i++) {
5146
+ var line = linesAndNewlines[i];
5147
+ if (i % 2 && !this.options.newlineIsToken) {
5148
+ retLines[retLines.length - 1] += line;
5149
+ } else {
5150
+ if (this.options.ignoreWhitespace) {
5151
+ line = line.trim();
5152
+ }
5153
+ retLines.push(line);
5154
+ }
5155
+ }
5156
+ return retLines;
5157
+ };
5158
+ var sentenceDiff2 = new Diff2();
5159
+ sentenceDiff2.tokenize = function(value) {
5160
+ return value.split(/(\S.+?[.!?])(?=\s+|$)/);
5161
+ };
5162
+ var cssDiff2 = new Diff2();
5163
+ cssDiff2.tokenize = function(value) {
5164
+ return value.split(/([{}:;,]|\s+)/);
5165
+ };
5166
+ function _typeof2(obj) {
5167
+ "@babel/helpers - typeof";
5168
+ if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") {
5169
+ _typeof2 = function(obj2) {
5170
+ return typeof obj2;
5171
+ };
5172
+ } else {
5173
+ _typeof2 = function(obj2) {
5174
+ return obj2 && typeof Symbol === "function" && obj2.constructor === Symbol && obj2 !== Symbol.prototype ? "symbol" : typeof obj2;
5175
+ };
5176
+ }
5177
+ return _typeof2(obj);
5178
+ }
5179
+ var objectPrototypeToString2 = Object.prototype.toString;
5180
+ var jsonDiff2 = new Diff2();
5181
+ jsonDiff2.useLongestToken = true;
5182
+ jsonDiff2.tokenize = lineDiff2.tokenize;
5183
+ jsonDiff2.castInput = function(value) {
5184
+ var _this$options = this.options, undefinedReplacement = _this$options.undefinedReplacement, _this$options$stringi = _this$options.stringifyReplacer, stringifyReplacer = _this$options$stringi === void 0 ? function(k, v) {
5185
+ return typeof v === "undefined" ? undefinedReplacement : v;
5186
+ } : _this$options$stringi;
5187
+ return typeof value === "string" ? value : JSON.stringify(canonicalize2(value, null, null, stringifyReplacer), stringifyReplacer, " ");
5188
+ };
5189
+ jsonDiff2.equals = function(left, right) {
5190
+ return Diff2.prototype.equals.call(jsonDiff2, left.replace(/,([\r\n])/g, "$1"), right.replace(/,([\r\n])/g, "$1"));
5191
+ };
5192
+ function canonicalize2(obj, stack, replacementStack, replacer, key) {
5193
+ stack = stack || [];
5194
+ replacementStack = replacementStack || [];
5195
+ if (replacer) {
5196
+ obj = replacer(key, obj);
5197
+ }
5198
+ var i;
5199
+ for (i = 0; i < stack.length; i += 1) {
5200
+ if (stack[i] === obj) {
5201
+ return replacementStack[i];
5202
+ }
5203
+ }
5204
+ var canonicalizedObj;
5205
+ if ("[object Array]" === objectPrototypeToString2.call(obj)) {
5206
+ stack.push(obj);
5207
+ canonicalizedObj = new Array(obj.length);
5208
+ replacementStack.push(canonicalizedObj);
5209
+ for (i = 0; i < obj.length; i += 1) {
5210
+ canonicalizedObj[i] = canonicalize2(obj[i], stack, replacementStack, replacer, key);
5211
+ }
5212
+ stack.pop();
5213
+ replacementStack.pop();
5214
+ return canonicalizedObj;
5215
+ }
5216
+ if (obj && obj.toJSON) {
5217
+ obj = obj.toJSON();
5218
+ }
5219
+ if (_typeof2(obj) === "object" && obj !== null) {
5220
+ stack.push(obj);
5221
+ canonicalizedObj = {};
5222
+ replacementStack.push(canonicalizedObj);
5223
+ var sortedKeys = [], _key;
5224
+ for (_key in obj) {
5225
+ if (obj.hasOwnProperty(_key)) {
5226
+ sortedKeys.push(_key);
5227
+ }
5228
+ }
5229
+ sortedKeys.sort();
5230
+ for (i = 0; i < sortedKeys.length; i += 1) {
5231
+ _key = sortedKeys[i];
5232
+ canonicalizedObj[_key] = canonicalize2(obj[_key], stack, replacementStack, replacer, _key);
5233
+ }
5234
+ stack.pop();
5235
+ replacementStack.pop();
5236
+ } else {
5237
+ canonicalizedObj = obj;
5238
+ }
5239
+ return canonicalizedObj;
5240
+ }
5241
+ var arrayDiff2 = new Diff2();
5242
+ arrayDiff2.tokenize = function(value) {
5243
+ return value.slice();
5244
+ };
5245
+ arrayDiff2.join = arrayDiff2.removeEmpty = function(value) {
5246
+ return value;
5247
+ };
5248
+ function diffArrays2(oldArr, newArr, callback) {
5249
+ return arrayDiff2.diff(oldArr, newArr, callback);
5250
+ }
5251
+ var DEFAULT_MAX_INLINE_HIGHLIGHT_LINE_LENGTH2 = 500;
5252
+ var DEFAULT_DIFF_TIMEOUT_MS2 = 100;
5253
+ var MAX_INLINE_HIGHLIGHT_LINE_LENGTH2 = readPositiveIntegerEnv4(
5254
+ "BYGONE_MAX_INLINE_HIGHLIGHT_LINE_LENGTH",
5255
+ DEFAULT_MAX_INLINE_HIGHLIGHT_LINE_LENGTH2
5256
+ );
5257
+ var DIFF_TIMEOUT_MS2 = readPositiveIntegerEnv4("BYGONE_DIFF_TIMEOUT_MS", DEFAULT_DIFF_TIMEOUT_MS2);
5258
+ function buildTwoWayDiffModel3(leftContent, rightContent, options = {}) {
5259
+ const leftLines = normalizeLines2(leftContent);
5260
+ const rightLines = normalizeLines2(rightContent);
5261
+ const changes = diffArrays2(leftLines, rightLines, {
5262
+ timeout: options.timeoutMs ?? DIFF_TIMEOUT_MS2
5263
+ });
5264
+ if (!changes) {
5265
+ return buildConservativeTwoWayDiffModel2(leftLines, rightLines);
5266
+ }
5267
+ const rows = [];
5268
+ const renderedLeftLines = [];
5269
+ const renderedRightLines = [];
5270
+ const blocks = [];
5271
+ let leftLineNumber = 1;
5272
+ let rightLineNumber = 1;
5273
+ for (let index = 0; index < changes.length; index++) {
5274
+ const change = changes[index];
5275
+ const removedLines = change.removed ? change.value : [];
5276
+ const addedLines = change.added ? change.value : [];
5277
+ if (!change.added && !change.removed) {
5278
+ for (const line of change.value) {
5279
+ renderedLeftLines.push(makeDiffLine2("context", line, leftLineNumber));
5280
+ renderedRightLines.push(makeDiffLine2("context", line, rightLineNumber));
5281
+ rows.push(makeDiffRow2(
5282
+ makeDiffCell2("context", line, leftLineNumber++),
5283
+ makeDiffCell2("context", line, rightLineNumber++)
5284
+ ));
5285
+ }
5286
+ continue;
5287
+ }
5288
+ if (change.removed && index + 1 < changes.length && changes[index + 1].added) {
5289
+ const nextChange = changes[index + 1];
5290
+ const leftStart = renderedLeftLines.length;
5291
+ const rightStart = renderedRightLines.length;
5292
+ const alignedLines = alignReplacementLines2(removedLines, nextChange.value);
5293
+ for (const { left: removedLine, right: addedLine } of alignedLines) {
5294
+ let renderedLeftLine;
5295
+ let renderedRightLine;
5296
+ if (removedLine !== void 0) {
5297
+ renderedLeftLine = makeDiffLine2("removed", removedLine, leftLineNumber);
5298
+ renderedLeftLines.push(renderedLeftLine);
5299
+ }
5300
+ if (addedLine !== void 0) {
5301
+ renderedRightLine = makeDiffLine2("added", addedLine, rightLineNumber);
5302
+ renderedRightLines.push(renderedRightLine);
5303
+ }
5304
+ if (renderedLeftLine && renderedRightLine) {
5305
+ applyInlineHighlightPair2(renderedLeftLine, renderedRightLine);
5306
+ }
5307
+ rows.push(makeDiffRow2(
5308
+ removedLine === void 0 ? makePlaceholder2() : makeDiffCell2("removed", removedLine, leftLineNumber++),
5309
+ addedLine === void 0 ? makePlaceholder2() : makeDiffCell2("added", addedLine, rightLineNumber++)
5310
+ ));
5311
+ }
5312
+ blocks.push(makeDiffBlock2("replace", leftStart, renderedLeftLines.length, rightStart, renderedRightLines.length));
5313
+ index++;
5314
+ continue;
5315
+ }
5316
+ if (change.removed) {
5317
+ const leftStart = renderedLeftLines.length;
5318
+ const rightStart = renderedRightLines.length;
5319
+ for (const line of removedLines) {
5320
+ renderedLeftLines.push(makeDiffLine2("removed", line, leftLineNumber));
5321
+ rows.push(makeDiffRow2(
5322
+ makeDiffCell2("removed", line, leftLineNumber++),
5323
+ makePlaceholder2()
5324
+ ));
5325
+ }
5326
+ blocks.push(makeDiffBlock2("delete", leftStart, renderedLeftLines.length, rightStart, renderedRightLines.length));
5327
+ continue;
5328
+ }
5329
+ if (change.added) {
5330
+ const leftStart = renderedLeftLines.length;
5331
+ const rightStart = renderedRightLines.length;
5332
+ for (const line of addedLines) {
5333
+ renderedRightLines.push(makeDiffLine2("added", line, rightLineNumber));
5334
+ rows.push(makeDiffRow2(
5335
+ makePlaceholder2(),
5336
+ makeDiffCell2("added", line, rightLineNumber++)
5337
+ ));
5338
+ }
5339
+ blocks.push(makeDiffBlock2("insert", leftStart, renderedLeftLines.length, rightStart, renderedRightLines.length));
5340
+ }
5341
+ }
5342
+ const hasChanges = rows.some((row) => row.left.kind !== "context" || row.right.kind !== "context");
5343
+ return {
5344
+ rows,
5345
+ leftLines: renderedLeftLines,
5346
+ rightLines: renderedRightLines,
5347
+ blocks,
5348
+ hasChanges
5349
+ };
5350
+ }
5351
+ function buildConservativeTwoWayDiffModel2(leftLines, rightLines) {
5352
+ const renderedLeftLines = leftLines.map((line, index) => makeDiffLine2("removed", line, index + 1));
5353
+ const renderedRightLines = rightLines.map((line, index) => makeDiffLine2("added", line, index + 1));
5354
+ const rows = [
5355
+ ...leftLines.map((line, index) => makeDiffRow2(
5356
+ makeDiffCell2("removed", line, index + 1),
5357
+ makePlaceholder2()
5358
+ )),
5359
+ ...rightLines.map((line, index) => makeDiffRow2(
5360
+ makePlaceholder2(),
5361
+ makeDiffCell2("added", line, index + 1)
5362
+ ))
5363
+ ];
5364
+ let blockKind = "replace";
5365
+ if (leftLines.length === 0) {
5366
+ blockKind = "insert";
5367
+ } else if (rightLines.length === 0) {
5368
+ blockKind = "delete";
5369
+ }
5370
+ return {
5371
+ rows,
5372
+ leftLines: renderedLeftLines,
5373
+ rightLines: renderedRightLines,
5374
+ blocks: rows.length === 0 ? [] : [makeDiffBlock2(blockKind, 0, leftLines.length, 0, rightLines.length)],
5375
+ hasChanges: rows.length > 0
5376
+ };
5377
+ }
5378
+ function normalizeLines2(content) {
5379
+ if (content.length === 0) {
5380
+ return [];
5381
+ }
5382
+ const lines = content.replace(/\r\n/g, "\n").split("\n");
5383
+ if (lines[lines.length - 1] === "") {
5384
+ lines.pop();
5385
+ }
5386
+ return lines;
5387
+ }
5388
+ var MAX_ALIGNMENT_CELLS2 = 1e4;
5389
+ var MAX_SCORING_LINE_LENGTH2 = 2e3;
5390
+ var MINIMUM_MATCH_SCORE2 = 0.52;
5391
+ var MINIMUM_SINGLE_PAIR_SCORE2 = 0.35;
5392
+ var MINIMUM_CONTEXTUAL_MATCH_SCORE2 = 0.38;
5393
+ var MINIMUM_CONTEXTUAL_RUN_SCORE2 = 0.33;
5394
+ var HIGH_CONFIDENCE_MATCH_SCORE2 = 0.86;
5395
+ var AMBIGUITY_MARGIN2 = 0.08;
5396
+ var CONTEXTUAL_AMBIGUITY_MARGIN2 = 0.05;
5397
+ var GAP_PENALTY2 = -0.12;
5398
+ var MAX_BOUNDED_CANDIDATES2 = 5e3;
5399
+ var MAX_LARGE_HUNK_ANCHORS2 = 2e3;
5400
+ var MAX_RARE_TOKEN_OCCURRENCES2 = 4;
5401
+ var MAX_CONTEXTUAL_HUNK_LINES2 = 8;
5402
+ var MAX_CONTEXTUAL_LENGTH_DELTA2 = 4;
5403
+ function alignReplacementLines2(leftLines, rightLines) {
5404
+ if (leftLines.length === 0) {
5405
+ return rightLines.map((right) => ({ right }));
5406
+ }
5407
+ if (rightLines.length === 0) {
5408
+ return leftLines.map((left) => ({ left }));
5409
+ }
5410
+ if (leftLines.length === 1 && rightLines.length === 1) {
5411
+ const singletonScore = scoreReplacementLinePair2(leftLines[0], rightLines[0]);
5412
+ if ((singletonScore.eligible || singletonScore.score >= MINIMUM_SINGLE_PAIR_SCORE2) && haveCompatibleLineRoles2(leftLines[0], rightLines[0]) && isInformativeLine2(normalizeMatchingContent2(leftLines[0])) && isInformativeLine2(normalizeMatchingContent2(rightLines[0]))) {
5413
+ return [{ left: leftLines[0], right: rightLines[0] }];
5414
+ }
5415
+ return [{ left: leftLines[0] }, { right: rightLines[0] }];
5416
+ }
5417
+ if (leftLines.length * rightLines.length > MAX_ALIGNMENT_CELLS2) {
5418
+ return alignLargeReplacementLines2(leftLines, rightLines);
5419
+ }
5420
+ const pairScores = buildPairScores2(leftLines, rightLines);
5421
+ const scores = Array.from(
5422
+ { length: leftLines.length + 1 },
5423
+ () => new Array(rightLines.length + 1).fill(Number.NEGATIVE_INFINITY)
5424
+ );
5425
+ const moves = Array.from(
5426
+ { length: leftLines.length + 1 },
5427
+ () => new Array(rightLines.length + 1).fill(null)
5428
+ );
5429
+ scores[0][0] = 0;
5430
+ for (let leftIndex2 = 1; leftIndex2 <= leftLines.length; leftIndex2++) {
5431
+ scores[leftIndex2][0] = scores[leftIndex2 - 1][0] + GAP_PENALTY2;
5432
+ moves[leftIndex2][0] = "left";
5433
+ }
5434
+ for (let rightIndex2 = 1; rightIndex2 <= rightLines.length; rightIndex2++) {
5435
+ scores[0][rightIndex2] = scores[0][rightIndex2 - 1] + GAP_PENALTY2;
5436
+ moves[0][rightIndex2] = "right";
5437
+ }
5438
+ for (let leftIndex2 = 1; leftIndex2 <= leftLines.length; leftIndex2++) {
5439
+ for (let rightIndex2 = 1; rightIndex2 <= rightLines.length; rightIndex2++) {
5440
+ const pairScore = pairScores[leftIndex2 - 1][rightIndex2 - 1];
5441
+ const candidates = [
5442
+ { move: "left", score: scores[leftIndex2 - 1][rightIndex2] + GAP_PENALTY2 },
5443
+ { move: "right", score: scores[leftIndex2][rightIndex2 - 1] + GAP_PENALTY2 },
5444
+ {
5445
+ move: "match",
5446
+ score: pairScore === null ? Number.NEGATIVE_INFINITY : scores[leftIndex2 - 1][rightIndex2 - 1] + pairScore
5447
+ }
5448
+ ];
5449
+ const best = candidates.reduce((current, candidate) => candidate.score > current.score ? candidate : current);
5450
+ scores[leftIndex2][rightIndex2] = best.score;
5451
+ moves[leftIndex2][rightIndex2] = best.move;
5452
+ }
5453
+ }
5454
+ const aligned = [];
5455
+ let leftIndex = leftLines.length;
5456
+ let rightIndex = rightLines.length;
5457
+ while (leftIndex > 0 || rightIndex > 0) {
5458
+ const move = moves[leftIndex][rightIndex];
5459
+ if (move === "match") {
5460
+ aligned.push({ left: leftLines[--leftIndex], right: rightLines[--rightIndex] });
5461
+ } else if (move === "left") {
5462
+ aligned.push({ left: leftLines[--leftIndex] });
5463
+ } else {
5464
+ aligned.push({ right: rightLines[--rightIndex] });
5465
+ }
5466
+ }
5467
+ return aligned.reverse();
5468
+ }
5469
+ function scoreReplacementLinePair2(left, right) {
5470
+ const normalizedLeft = normalizeMatchingContent2(left);
5471
+ const normalizedRight = normalizeMatchingContent2(right);
5472
+ const leftInformative = isInformativeLine2(normalizedLeft);
5473
+ const rightInformative = isInformativeLine2(normalizedRight);
5474
+ if (!leftInformative || !rightInformative || normalizedLeft.length > MAX_SCORING_LINE_LENGTH2 || normalizedRight.length > MAX_SCORING_LINE_LENGTH2) {
5475
+ return {
5476
+ score: 0,
5477
+ eligible: false,
5478
+ characterSimilarity: 0,
5479
+ tokenSimilarity: 0,
5480
+ lengthSimilarity: 0
5481
+ };
5482
+ }
5483
+ if (normalizedLeft === normalizedRight) {
5484
+ return {
5485
+ score: 1,
5486
+ eligible: true,
5487
+ characterSimilarity: 1,
5488
+ tokenSimilarity: 1,
5489
+ lengthSimilarity: 1
5490
+ };
5491
+ }
5492
+ const leftTokens = tokenizeMatchingContent2(normalizedLeft);
5493
+ const rightTokens = tokenizeMatchingContent2(normalizedRight);
5494
+ const characterSimilarity = lineSimilarity2(normalizedLeft, normalizedRight);
5495
+ const tokenSimilarity = multisetDiceSimilarity2(leftTokens, rightTokens);
5496
+ const lengthSimilarity = Math.min(normalizedLeft.length, normalizedRight.length) / Math.max(normalizedLeft.length, normalizedRight.length);
5497
+ const boundarySimilarity = tokenBoundarySimilarity2(leftTokens, rightTokens);
5498
+ const score = characterSimilarity * 0.52 + tokenSimilarity * 0.32 + lengthSimilarity * 0.1 + boundarySimilarity * 0.06;
5499
+ const hasContentEvidence = characterSimilarity >= 0.52 || tokenSimilarity >= 0.45 || characterSimilarity >= 0.48 && tokenSimilarity >= 0.28;
5500
+ return {
5501
+ score,
5502
+ eligible: hasContentEvidence && score >= MINIMUM_MATCH_SCORE2,
5503
+ characterSimilarity,
5504
+ tokenSimilarity,
5505
+ lengthSimilarity
5506
+ };
5507
+ }
5508
+ function buildPairScores2(leftLines, rightLines) {
5509
+ const scored = leftLines.map((left) => rightLines.map((right) => scoreReplacementLinePair2(left, right)));
5510
+ const structuralPairs = collectUniqueStructuralPairs2(leftLines, rightLines);
5511
+ const leftRankings = scored.map((row) => rankEligibleScores2(row));
5512
+ const rightRankings = rightLines.map((_right, rightIndex) => rankEligibleScores2(scored.map((row) => row[rightIndex])));
5513
+ const accepted = scored.map((row, leftIndex) => row.map((candidate, rightIndex) => {
5514
+ if (structuralPairs.has(`${leftIndex}:${rightIndex}`)) {
5515
+ return Math.max(candidate.score, 1);
5516
+ }
5517
+ if (!candidate.eligible) {
5518
+ return null;
5519
+ }
5520
+ const leftRanking = leftRankings[leftIndex];
5521
+ const rightRanking = rightRankings[rightIndex];
5522
+ const isBestOnBothSides = candidate.score === leftRanking.best && candidate.score === rightRanking.best;
5523
+ const requiredMargin = candidate.score >= HIGH_CONFIDENCE_MATCH_SCORE2 ? AMBIGUITY_MARGIN2 / 2 : AMBIGUITY_MARGIN2;
5524
+ const hasLeftMargin = leftRanking.best - leftRanking.second >= requiredMargin;
5525
+ const hasRightMargin = rightRanking.best - rightRanking.second >= requiredMargin;
5526
+ return isBestOnBothSides && hasLeftMargin && hasRightMargin ? candidate.score : null;
5527
+ }));
5528
+ if (Math.max(leftLines.length, rightLines.length) > MAX_CONTEXTUAL_HUNK_LINES2 || Math.abs(leftLines.length - rightLines.length) > MAX_CONTEXTUAL_LENGTH_DELTA2) {
5529
+ return accepted;
5530
+ }
5531
+ const isContextualCandidate = (candidate, leftIndex, rightIndex) => candidate.score >= MINIMUM_CONTEXTUAL_MATCH_SCORE2 && haveCompatibleLineRoles2(leftLines[leftIndex], rightLines[rightIndex]) && Math.abs(leftIndex - rightIndex) <= MAX_CONTEXTUAL_LENGTH_DELTA2;
5532
+ const leftContextRankings = scored.map((row, leftIndex) => rankScores2(row, (candidate, rightIndex) => isContextualCandidate(candidate, leftIndex, rightIndex)));
5533
+ const rightContextRankings = rightLines.map((_right, rightIndex) => rankScores2(
5534
+ scored.map((row) => row[rightIndex]),
5535
+ (candidate, leftIndex) => isContextualCandidate(candidate, leftIndex, rightIndex)
5536
+ ));
5537
+ const isWeakRunCandidate = (candidate, leftIndex, rightIndex) => candidate.score >= MINIMUM_CONTEXTUAL_RUN_SCORE2 && haveCompatibleLineRoles2(leftLines[leftIndex], rightLines[rightIndex]) && Math.abs(leftIndex - rightIndex) <= MAX_CONTEXTUAL_LENGTH_DELTA2;
5538
+ const leftWeakRankings = scored.map((row, leftIndex) => rankScores2(row, (candidate, rightIndex) => isWeakRunCandidate(candidate, leftIndex, rightIndex)));
5539
+ const rightWeakRankings = rightLines.map((_right, rightIndex) => rankScores2(
5540
+ scored.map((row) => row[rightIndex]),
5541
+ (candidate, leftIndex) => isWeakRunCandidate(candidate, leftIndex, rightIndex)
5542
+ ));
5543
+ const isUnambiguousContextualCandidate = (leftIndex, rightIndex) => {
5544
+ const candidate = scored[leftIndex][rightIndex];
5545
+ if (!isContextualCandidate(candidate, leftIndex, rightIndex)) {
5546
+ return false;
5547
+ }
5548
+ const leftRanking = leftContextRankings[leftIndex];
5549
+ const rightRanking = rightContextRankings[rightIndex];
5550
+ return candidate.score === leftRanking.best && candidate.score === rightRanking.best && leftRanking.best - leftRanking.second >= CONTEXTUAL_AMBIGUITY_MARGIN2 && rightRanking.best - rightRanking.second >= CONTEXTUAL_AMBIGUITY_MARGIN2;
5551
+ };
5552
+ const isUnambiguousWeakRunAnchor = (leftIndex, rightIndex) => {
5553
+ const candidate = scored[leftIndex][rightIndex];
5554
+ if (!isWeakRunCandidate(candidate, leftIndex, rightIndex)) {
5555
+ return false;
5556
+ }
5557
+ const leftRanking = leftWeakRankings[leftIndex];
5558
+ const rightRanking = rightWeakRankings[rightIndex];
5559
+ return candidate.score === leftRanking.best && candidate.score === rightRanking.best && leftRanking.best - leftRanking.second >= CONTEXTUAL_AMBIGUITY_MARGIN2 && rightRanking.best - rightRanking.second >= CONTEXTUAL_AMBIGUITY_MARGIN2;
5560
+ };
5561
+ return accepted.map((row, leftIndex) => row.map((acceptedScore, rightIndex) => {
5562
+ if (acceptedScore !== null) {
5563
+ return acceptedScore;
5564
+ }
5565
+ const hasLocalSupport = isUnambiguousContextualCandidate(leftIndex, rightIndex) && (hasAcceptedDiagonalNeighbor2(accepted, leftIndex, rightIndex) || hasContextualDiagonalNeighbor2(
5566
+ scored,
5567
+ leftIndex,
5568
+ rightIndex,
5569
+ isUnambiguousContextualCandidate
5570
+ ));
5571
+ const hasRunSupport = hasAnchoredWeakDiagonalRun2(
5572
+ scored,
5573
+ leftIndex,
5574
+ rightIndex,
5575
+ (candidateLeftIndex, candidateRightIndex) => isWeakRunCandidate(
5576
+ scored[candidateLeftIndex][candidateRightIndex],
5577
+ candidateLeftIndex,
5578
+ candidateRightIndex
5579
+ ),
5580
+ isUnambiguousWeakRunAnchor
5581
+ );
5582
+ if (!hasLocalSupport && !hasRunSupport) {
5583
+ return null;
5584
+ }
5585
+ return scored[leftIndex][rightIndex].score;
5586
+ }));
5587
+ }
5588
+ function hasAcceptedDiagonalNeighbor2(accepted, leftIndex, rightIndex) {
5589
+ return leftIndex > 0 && rightIndex > 0 && accepted[leftIndex - 1][rightIndex - 1] !== null || leftIndex + 1 < accepted.length && rightIndex + 1 < accepted[leftIndex + 1].length && accepted[leftIndex + 1][rightIndex + 1] !== null;
5590
+ }
5591
+ function hasContextualDiagonalNeighbor2(scored, leftIndex, rightIndex, isCandidate) {
5592
+ return leftIndex > 0 && rightIndex > 0 && isCandidate(leftIndex - 1, rightIndex - 1) || leftIndex + 1 < scored.length && rightIndex + 1 < scored[leftIndex + 1].length && isCandidate(leftIndex + 1, rightIndex + 1);
5593
+ }
5594
+ function hasAnchoredWeakDiagonalRun2(scored, leftIndex, rightIndex, isCandidate, isAnchor) {
5595
+ let startLeft = leftIndex;
5596
+ let startRight = rightIndex;
5597
+ while (startLeft > 0 && startRight > 0 && isCandidate(startLeft - 1, startRight - 1)) {
5598
+ startLeft--;
5599
+ startRight--;
5600
+ }
5601
+ let endLeft = leftIndex;
5602
+ let endRight = rightIndex;
5603
+ let scoreTotal = scored[leftIndex][rightIndex].score;
5604
+ let length = 1;
5605
+ while (endLeft + 1 < scored.length && endRight + 1 < scored[endLeft + 1].length && isCandidate(endLeft + 1, endRight + 1)) {
5606
+ endLeft++;
5607
+ endRight++;
5608
+ scoreTotal += scored[endLeft][endRight].score;
5609
+ length++;
5610
+ }
5611
+ for (let scanLeft = leftIndex - 1, scanRight = rightIndex - 1; scanLeft >= startLeft && scanRight >= startRight; scanLeft--, scanRight--) {
5612
+ scoreTotal += scored[scanLeft][scanRight].score;
5613
+ length++;
5614
+ }
5615
+ return length >= 3 && scoreTotal / length >= MINIMUM_CONTEXTUAL_MATCH_SCORE2 && isAnchor(startLeft, startRight) && isAnchor(endLeft, endRight);
5616
+ }
5617
+ function rankScores2(scores, include) {
5618
+ let best = Number.NEGATIVE_INFINITY;
5619
+ let second = Number.NEGATIVE_INFINITY;
5620
+ scores.forEach((candidate, index) => {
5621
+ if (!include(candidate, index)) {
5622
+ return;
5623
+ }
5624
+ if (candidate.score > best) {
5625
+ second = best;
5626
+ best = candidate.score;
5627
+ } else if (candidate.score > second) {
5628
+ second = candidate.score;
5629
+ }
5630
+ });
5631
+ return { best, second };
5632
+ }
5633
+ function rankEligibleScores2(scores) {
5634
+ let best = Number.NEGATIVE_INFINITY;
5635
+ let second = Number.NEGATIVE_INFINITY;
5636
+ for (const candidate of scores) {
5637
+ if (!candidate.eligible) {
5638
+ continue;
5639
+ }
5640
+ if (candidate.score > best) {
5641
+ second = best;
5642
+ best = candidate.score;
5643
+ } else if (candidate.score > second) {
5644
+ second = candidate.score;
5645
+ }
5646
+ }
5647
+ return { best, second };
5648
+ }
5649
+ function alignLargeReplacementLines2(leftLines, rightLines) {
5650
+ const anchors = collectLargeHunkAnchors2(leftLines, rightLines);
5651
+ const aligned = [];
5652
+ let leftStart = 0;
5653
+ let rightStart = 0;
5654
+ for (const anchor of anchors) {
5655
+ appendBoundedSegment2(
5656
+ aligned,
5657
+ leftLines.slice(leftStart, anchor.leftIndex),
5658
+ rightLines.slice(rightStart, anchor.rightIndex)
5659
+ );
5660
+ aligned.push({ left: leftLines[anchor.leftIndex], right: rightLines[anchor.rightIndex] });
5661
+ leftStart = anchor.leftIndex + 1;
5662
+ rightStart = anchor.rightIndex + 1;
5663
+ }
5664
+ appendBoundedSegment2(aligned, leftLines.slice(leftStart), rightLines.slice(rightStart));
5665
+ return aligned;
5666
+ }
5667
+ function collectLargeHunkAnchors2(leftLines, rightLines) {
5668
+ const candidatePairs = collectRareTokenCandidatePairs2(leftLines, rightLines);
5669
+ if (candidatePairs === null) {
5670
+ return [];
5671
+ }
5672
+ const structuralPairs = collectUniqueStructuralPairs2(leftLines, rightLines);
5673
+ const scored = candidatePairs.map(({ leftIndex, rightIndex, exact }) => ({
5674
+ leftIndex,
5675
+ rightIndex,
5676
+ exact,
5677
+ structural: structuralPairs.has(`${leftIndex}:${rightIndex}`),
5678
+ match: scoreReplacementLinePair2(leftLines[leftIndex], rightLines[rightIndex])
5679
+ })).filter((candidate) => candidate.exact || candidate.structural || candidate.match.eligible);
5680
+ const leftRankings = rankAnchorCandidates2(scored, "leftIndex");
5681
+ const rightRankings = rankAnchorCandidates2(scored, "rightIndex");
5682
+ const confident = scored.filter((candidate) => {
5683
+ if (candidate.exact || candidate.structural) {
5684
+ return true;
5685
+ }
5686
+ const leftRanking = leftRankings.get(candidate.leftIndex);
5687
+ const rightRanking = rightRankings.get(candidate.rightIndex);
5688
+ if (!leftRanking || !rightRanking || candidate.match.score !== leftRanking.best || candidate.match.score !== rightRanking.best) {
5689
+ return false;
5690
+ }
5691
+ const requiredMargin = candidate.match.score >= HIGH_CONFIDENCE_MATCH_SCORE2 ? AMBIGUITY_MARGIN2 / 2 : AMBIGUITY_MARGIN2;
5692
+ return leftRanking.best - leftRanking.second >= requiredMargin && rightRanking.best - rightRanking.second >= requiredMargin;
5693
+ });
5694
+ const anchors = selectMaximumWeightMonotonicAnchors2(confident, rightLines.length);
5695
+ return anchors.length <= MAX_LARGE_HUNK_ANCHORS2 ? anchors : [];
5696
+ }
5697
+ function collectRareTokenCandidatePairs2(leftLines, rightLines) {
5698
+ const leftExact = buildLinePostings2(leftLines, normalizeMatchingContent2);
5699
+ const rightExact = buildLinePostings2(rightLines, normalizeMatchingContent2);
5700
+ const pairs = /* @__PURE__ */ new Map();
5701
+ for (const [line, leftIndices] of leftExact) {
5702
+ const rightIndices = rightExact.get(line);
5703
+ if (!isInformativeLine2(line) || leftIndices.length !== 1 || (rightIndices == null ? void 0 : rightIndices.length) !== 1) {
5704
+ continue;
5705
+ }
5706
+ const leftIndex = leftIndices[0];
5707
+ const rightIndex = rightIndices[0];
5708
+ pairs.set(`${leftIndex}:${rightIndex}`, { leftIndex, rightIndex, exact: true });
5709
+ if (pairs.size > MAX_BOUNDED_CANDIDATES2) {
5710
+ return null;
5711
+ }
5712
+ }
5713
+ const leftTokens = buildTokenPostings2(leftLines);
5714
+ const rightTokens = buildTokenPostings2(rightLines);
5715
+ for (const [token, leftIndices] of leftTokens) {
5716
+ const rightIndices = rightTokens.get(token);
5717
+ if (!rightIndices || leftIndices.length > MAX_RARE_TOKEN_OCCURRENCES2 || rightIndices.length > MAX_RARE_TOKEN_OCCURRENCES2) {
5718
+ continue;
5719
+ }
5720
+ for (const leftIndex of leftIndices) {
5721
+ for (const rightIndex of rightIndices) {
5722
+ const key = `${leftIndex}:${rightIndex}`;
5723
+ if (!pairs.has(key)) {
5724
+ pairs.set(key, { leftIndex, rightIndex, exact: false });
5725
+ if (pairs.size > MAX_BOUNDED_CANDIDATES2) {
5726
+ return null;
5727
+ }
5728
+ }
5729
+ }
5730
+ }
5731
+ }
5732
+ return [...pairs.values()];
5733
+ }
5734
+ function buildLinePostings2(lines, keyForLine) {
5735
+ const postings = /* @__PURE__ */ new Map();
5736
+ lines.forEach((line, index) => {
5737
+ const key = keyForLine(line);
5738
+ const indices = postings.get(key) ?? [];
5739
+ indices.push(index);
5740
+ postings.set(key, indices);
5741
+ });
5742
+ return postings;
5743
+ }
5744
+ function buildTokenPostings2(lines) {
5745
+ const postings = /* @__PURE__ */ new Map();
5746
+ lines.forEach((line, index) => {
5747
+ const tokens = new Set(tokenizeMatchingContent2(normalizeMatchingContent2(line)).filter(isMeaningfulAnchorToken2));
5748
+ for (const token of tokens) {
5749
+ const indices = postings.get(token) ?? [];
5750
+ indices.push(index);
5751
+ postings.set(token, indices);
5752
+ }
5753
+ });
5754
+ return postings;
5755
+ }
5756
+ function isMeaningfulAnchorToken2(token) {
5757
+ return token.length >= 2 && /[\p{L}\p{N}_$]/u.test(token);
5758
+ }
5759
+ function rankAnchorCandidates2(candidates, indexKey) {
5760
+ const grouped = /* @__PURE__ */ new Map();
5761
+ for (const candidate of candidates) {
5762
+ const scores = grouped.get(candidate[indexKey]) ?? [];
5763
+ scores.push(candidate.match);
5764
+ grouped.set(candidate[indexKey], scores);
5765
+ }
5766
+ return new Map([...grouped].map(([index, scores]) => [index, rankEligibleScores2(scores)]));
5767
+ }
5768
+ function selectMaximumWeightMonotonicAnchors2(candidates, rightLength) {
5769
+ const ordered = [...candidates].sort((left, right) => left.leftIndex - right.leftIndex || left.rightIndex - right.rightIndex);
5770
+ const treeScores = new Array(rightLength + 2).fill(0);
5771
+ const treeCandidates = new Array(rightLength + 2).fill(-1);
5772
+ const bestScores = new Array(ordered.length).fill(0);
5773
+ const previous = new Array(ordered.length).fill(-1);
5774
+ for (let start = 0; start < ordered.length; ) {
5775
+ let end = start + 1;
5776
+ while (end < ordered.length && ordered[end].leftIndex === ordered[start].leftIndex) {
5777
+ end++;
5778
+ }
5779
+ for (let index = start; index < end; index++) {
5780
+ const prior = queryAnchorTree2(treeScores, treeCandidates, ordered[index].rightIndex);
5781
+ const anchorBonus = ordered[index].exact || ordered[index].structural ? 0.5 : 0;
5782
+ bestScores[index] = prior.score + ordered[index].match.score + anchorBonus;
5783
+ previous[index] = prior.candidateIndex;
5784
+ }
5785
+ for (let index = start; index < end; index++) {
5786
+ updateAnchorTree2(
5787
+ treeScores,
5788
+ treeCandidates,
5789
+ ordered[index].rightIndex + 1,
5790
+ bestScores[index],
5791
+ index
5792
+ );
5793
+ }
5794
+ start = end;
5795
+ }
5796
+ const best = queryAnchorTree2(treeScores, treeCandidates, rightLength + 1);
5797
+ const anchors = [];
5798
+ for (let index = best.candidateIndex; index >= 0; index = previous[index]) {
5799
+ anchors.push({ leftIndex: ordered[index].leftIndex, rightIndex: ordered[index].rightIndex });
5800
+ }
5801
+ return anchors.reverse();
5802
+ }
5803
+ function queryAnchorTree2(scores, candidates, position) {
5804
+ let score = 0;
5805
+ let candidateIndex = -1;
5806
+ for (let index = position; index > 0; index -= index & -index) {
5807
+ if (scores[index] > score) {
5808
+ score = scores[index];
5809
+ candidateIndex = candidates[index];
5810
+ }
5811
+ }
5812
+ return { score, candidateIndex };
5813
+ }
5814
+ function updateAnchorTree2(scores, candidates, position, score, candidateIndex) {
5815
+ for (let index = position; index < scores.length; index += index & -index) {
5816
+ if (score > scores[index]) {
5817
+ scores[index] = score;
5818
+ candidates[index] = candidateIndex;
5819
+ }
5820
+ }
5821
+ }
5822
+ function appendBoundedSegment2(aligned, leftLines, rightLines) {
5823
+ if (leftLines.length * rightLines.length <= MAX_ALIGNMENT_CELLS2) {
5824
+ aligned.push(...alignReplacementLines2(leftLines, rightLines));
5825
+ return;
5826
+ }
5827
+ aligned.push(...leftLines.map((left) => ({ left })));
5828
+ aligned.push(...rightLines.map((right) => ({ right })));
5829
+ }
5830
+ function normalizeMatchingContent2(line) {
5831
+ return line.trim().replace(/\s+/g, " ");
5832
+ }
5833
+ function collectUniqueStructuralPairs2(leftLines, rightLines) {
5834
+ const leftPostings = buildStructuralPostings2(leftLines);
5835
+ const rightPostings = buildStructuralPostings2(rightLines);
5836
+ const pairs = /* @__PURE__ */ new Set();
5837
+ for (const [key, leftIndices] of leftPostings) {
5838
+ const rightIndices = rightPostings.get(key);
5839
+ if (leftIndices.length === 1 && (rightIndices == null ? void 0 : rightIndices.length) === 1) {
5840
+ pairs.add(`${leftIndices[0]}:${rightIndices[0]}`);
5841
+ }
5842
+ }
5843
+ return pairs;
5844
+ }
5845
+ function buildStructuralPostings2(lines) {
5846
+ const postings = /* @__PURE__ */ new Map();
5847
+ lines.forEach((line, index) => {
5848
+ const key = declarationAnchorKey2(line);
5849
+ if (!key) {
5850
+ return;
5851
+ }
5852
+ const indices = postings.get(key) ?? [];
5853
+ indices.push(index);
5854
+ postings.set(key, indices);
5855
+ });
5856
+ return postings;
5857
+ }
5858
+ function haveCompatibleLineRoles2(left, right) {
5859
+ const leftKey = declarationAnchorKey2(left);
5860
+ const rightKey = declarationAnchorKey2(right);
5861
+ if (!leftKey || !rightKey) {
5862
+ return leftKey === rightKey;
5863
+ }
5864
+ return leftKey.split(":", 1)[0] === rightKey.split(":", 1)[0];
5865
+ }
5866
+ function declarationAnchorKey2(line) {
5867
+ const normalized = normalizeMatchingContent2(line);
5868
+ const patterns = [
5869
+ ["function", /^(?:(?:export|default)\s+)*(?:async\s+)?function\s*\*?\s*([\p{L}_$][\p{L}\p{N}_$]*)\s*\(/u],
5870
+ ["function", /^(?:async\s+)?def\s+([\p{L}_][\p{L}\p{N}_]*)\s*\(/u],
5871
+ ["function", /^(?:pub(?:\([^)]*\))?\s+)?(?:async\s+)?fn\s+([\p{L}_][\p{L}\p{N}_]*)\s*(?:<[^>]*>)?\s*\(/u],
5872
+ ["function", /^func\s+(?:\([^)]*\)\s*)?([\p{L}_][\p{L}\p{N}_]*)\s*\(/u],
5873
+ ["class", /^(?:(?:export|default|abstract|public)\s+)*class\s+([\p{L}_$][\p{L}\p{N}_$]*)\b/u]
5874
+ ];
5875
+ for (const [kind, pattern] of patterns) {
5876
+ const match = normalized.match(pattern);
5877
+ if (match) {
5878
+ return `${kind}:${match[1]}`;
5879
+ }
5880
+ }
5881
+ return null;
5882
+ }
5883
+ function isInformativeLine2(line) {
5884
+ if (line.length < 3) {
5885
+ return false;
5886
+ }
5887
+ return /[\p{L}\p{N}_$]/u.test(line) && !/^[\s()[\]{};,.:+\-*/=>|&!?'"`]+$/.test(line);
5888
+ }
5889
+ function tokenizeMatchingContent2(line) {
5890
+ return line.toLowerCase().match(new RegExp("[\\p{L}_$][\\p{L}\\p{N}_$]*|\\p{N}+(?:\\.\\p{N}+)?|[^\\s\\p{L}\\p{N}_$]", "gu")) ?? [];
5891
+ }
5892
+ function multisetDiceSimilarity2(leftTokens, rightTokens) {
5893
+ if (leftTokens.length + rightTokens.length === 0) {
5894
+ return 1;
5895
+ }
5896
+ const remaining = /* @__PURE__ */ new Map();
5897
+ leftTokens.forEach((token) => remaining.set(token, (remaining.get(token) ?? 0) + 1));
5898
+ let common = 0;
5899
+ for (const token of rightTokens) {
5900
+ const count = remaining.get(token) ?? 0;
5901
+ if (count > 0) {
5902
+ common++;
5903
+ remaining.set(token, count - 1);
5904
+ }
5905
+ }
5906
+ return 2 * common / (leftTokens.length + rightTokens.length);
5907
+ }
5908
+ function tokenBoundarySimilarity2(leftTokens, rightTokens) {
5909
+ if (leftTokens.length === 0 || rightTokens.length === 0) {
5910
+ return 0;
5911
+ }
5912
+ const sameStart = leftTokens[0] === rightTokens[0] ? 0.5 : 0;
5913
+ const sameEnd = leftTokens[leftTokens.length - 1] === rightTokens[rightTokens.length - 1] ? 0.5 : 0;
5914
+ return sameStart + sameEnd;
5915
+ }
5916
+ function lineSimilarity2(left, right) {
5917
+ const maxLength = left.length + right.length;
5918
+ if (maxLength === 0) {
5919
+ return 1;
5920
+ }
5921
+ const commonLength = diffChars2(left, right).filter((change) => !change.added && !change.removed).reduce((total, change) => total + change.value.length, 0);
5922
+ return 2 * commonLength / maxLength;
5923
+ }
5924
+ function applyInlineHighlightPair2(leftLine, rightLine) {
5925
+ if (leftLine.content.length > MAX_INLINE_HIGHLIGHT_LINE_LENGTH2 || rightLine.content.length > MAX_INLINE_HIGHLIGHT_LINE_LENGTH2) {
5926
+ return;
5927
+ }
5928
+ const { leftSegments, rightSegments, hasInlineChanges } = buildInlineSegments2(leftLine.content, rightLine.content);
5929
+ if (hasInlineChanges) {
5930
+ leftLine.segments = leftSegments;
5931
+ rightLine.segments = rightSegments;
5932
+ }
5933
+ }
5934
+ function buildInlineSegments2(leftContent, rightContent) {
5935
+ const changes = diffWordsWithSpace2(leftContent, rightContent);
5936
+ const leftSegments = [];
5937
+ const rightSegments = [];
5938
+ let hasInlineChanges = false;
5939
+ for (const change of changes) {
5940
+ const value = change.value;
5941
+ if (!change.added && !change.removed) {
5942
+ const contextSegment = {
5943
+ kind: "context",
5944
+ text: value,
5945
+ emphasis: false
5946
+ };
5947
+ leftSegments.push(contextSegment);
5948
+ rightSegments.push(contextSegment);
5949
+ continue;
5950
+ }
5951
+ const emphasis = /[^\s]/.test(value);
5952
+ hasInlineChanges = hasInlineChanges || emphasis;
5953
+ if (change.removed) {
5954
+ leftSegments.push({
5955
+ kind: "removed",
5956
+ text: value,
5957
+ emphasis
5958
+ });
4766
5959
  }
4767
- stages.push({
4768
- id: stage.id,
4769
- title: stage.title,
4770
- narration: stage.narration,
4771
- introducedHunks,
4772
- introducedFiles,
4773
- cumulativeHunks: [...cumulativeByFile.values()].flatMap((ids) => [...ids]),
4774
- files: materializableFiles.map((file) => buildFileState(
4775
- file,
4776
- cumulativeByFile.get(file) || /* @__PURE__ */ new Set(),
4777
- wholeFileExclusions.has(file)
4778
- ))
4779
- });
4780
- }
4781
- for (const file of materializableFiles) {
4782
- const finalState = buildFileState(
4783
- file,
4784
- cumulativeByFile.get(file) || /* @__PURE__ */ new Set(),
4785
- wholeFileExclusions.has(file)
4786
- );
4787
- const expectedExists = file.changeKind !== "deleted";
4788
- if (finalState.exists !== expectedExists || finalState.content !== file.headContent) {
4789
- throw new Error(`Final deconstructed state does not match the target for ${file.path}.`);
5960
+ if (change.added) {
5961
+ rightSegments.push({
5962
+ kind: "added",
5963
+ text: value,
5964
+ emphasis
5965
+ });
4790
5966
  }
4791
5967
  }
4792
5968
  return {
4793
- id: scene.id,
4794
- title: scene.title,
4795
- baseOid: inventory.range.baseOid,
4796
- targetOid: inventory.range.headOid,
4797
- baselineFiles,
4798
- excludedHunks: [...excludedByFile.values()].flatMap((ids) => [...ids]),
4799
- excludedFiles,
4800
- stages
5969
+ leftSegments,
5970
+ rightSegments,
5971
+ hasInlineChanges
4801
5972
  };
4802
5973
  }
4803
- function requireInventoryFile(inventory, path9, owner) {
4804
- const matches = inventory.files.filter((file) => file.path === path9 || file.previousPath === path9);
4805
- if (matches.length === 0) throw new Error(`${owner} references a file outside the change inventory: ${path9}`);
4806
- if (matches.length > 1) throw new Error(`${owner} references an ambiguous changed path: ${path9}`);
4807
- return matches[0];
5974
+ function makePlaceholder2() {
5975
+ return {
5976
+ kind: "placeholder",
5977
+ content: "",
5978
+ lineNumber: null
5979
+ };
4808
5980
  }
4809
- function claimHunks(file, ids, owner, ownership) {
4810
- const known = new Set(file.units.map((unit) => unit.id));
4811
- for (const id of ids) {
4812
- if (!known.has(id)) throw new Error(`${owner} references an unknown hunk in ${file.path}: ${id}`);
4813
- const key = unitKey(file, id);
4814
- const previousOwner = ownership.get(key);
4815
- if (previousOwner) throw new Error(`Change unit ${id} in ${file.path} is assigned to both ${previousOwner} and ${owner}.`);
4816
- ownership.set(key, owner);
4817
- }
5981
+ function makeDiffCell2(kind, content, lineNumber) {
5982
+ return { kind, content, lineNumber };
4818
5983
  }
4819
- function unitKey(file, id) {
4820
- return `${file.previousPath || ""}\0${file.path}\0${id}`;
5984
+ function makeDiffLine2(kind, content, lineNumber) {
5985
+ return { kind, content, lineNumber };
4821
5986
  }
4822
- function buildFileState(file, selected, wholeFileExcluded) {
4823
- if (file.baseContent === void 0) throw new Error(`Missing base content for ${file.path}.`);
4824
- const content = materializeChangeUnits(file.baseContent, file.units, selected);
4825
- let exists = true;
4826
- if (file.changeKind === "added") exists = wholeFileExcluded || selected.size > 0;
4827
- if (file.changeKind === "deleted") exists = !(wholeFileExcluded || selected.size === file.units.length);
5987
+ function makeDiffRow2(left, right) {
5988
+ return { left, right };
5989
+ }
5990
+ function makeDiffBlock2(kind, leftStart, leftEnd, rightStart, rightEnd) {
5991
+ return { kind, leftStart, leftEnd, rightStart, rightEnd };
5992
+ }
5993
+ function readPositiveIntegerEnv4(name, fallback) {
5994
+ var _a;
5995
+ const value = typeof process !== "undefined" ? (_a = process.env) == null ? void 0 : _a[name] : void 0;
5996
+ const parsed = Number.parseInt(value ?? "", 10);
5997
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
5998
+ }
5999
+ var MAX_AUTOMATIC_TOUR_FOCUS_RANGES = 4;
6000
+ var NEARBY_TOUR_FOCUS_LINE_GAP = 8;
6001
+ var LARGE_TOUR_FOCUS_RANGE_LINES = 120;
6002
+ var TOUR_FOCUS_BOUNDARY_SEARCH_LINES = 12;
6003
+ function getChangeSourceRange(diffModel, blockIndex, side) {
6004
+ var _a, _b, _c;
6005
+ const block = diffModel == null ? void 0 : diffModel.blocks[blockIndex];
6006
+ const lines = side === "left" ? diffModel == null ? void 0 : diffModel.leftLines : diffModel == null ? void 0 : diffModel.rightLines;
6007
+ if (!block || !(lines == null ? void 0 : lines.length)) {
6008
+ return void 0;
6009
+ }
6010
+ const start = side === "left" ? block.leftStart : block.rightStart;
6011
+ const end = side === "left" ? block.leftEnd : block.rightEnd;
6012
+ const changedLines = lines.slice(start, end);
6013
+ const startLine = ((_a = changedLines[0]) == null ? void 0 : _a.lineNumber) ?? ((_b = lines[Math.min(start, lines.length - 1)]) == null ? void 0 : _b.lineNumber);
6014
+ if (!Number.isInteger(startLine)) {
6015
+ return void 0;
6016
+ }
4828
6017
  return {
4829
- path: file.path,
4830
- previousPath: file.previousPath,
4831
- changeKind: file.changeKind,
4832
- exists,
4833
- content: exists ? content : "",
4834
- appliedHunks: [...selected]
6018
+ startLine,
6019
+ endLine: ((_c = changedLines.at(-1)) == null ? void 0 : _c.lineNumber) ?? startLine
4835
6020
  };
4836
6021
  }
6022
+ function buildTourFocusRanges(diffModel, side) {
6023
+ const ranges = diffModel.blocks.map((_, blockIndex) => getChangeSourceRange(diffModel, blockIndex, side)).filter((range) => Boolean(range));
6024
+ if (ranges.length === 0) return [];
6025
+ const nearby = ranges.reduce((grouped, range) => {
6026
+ const previous = grouped.at(-1);
6027
+ if (previous && range.startLine - previous.endLine <= NEARBY_TOUR_FOCUS_LINE_GAP) {
6028
+ previous.endLine = Math.max(previous.endLine, range.endLine);
6029
+ } else {
6030
+ grouped.push({ ...range });
6031
+ }
6032
+ return grouped;
6033
+ }, []);
6034
+ const capped = capFocusRanges(nearby, MAX_AUTOMATIC_TOUR_FOCUS_RANGES);
6035
+ if (capped.length !== 1) return capped;
6036
+ const [only] = capped;
6037
+ const lineCount = only.endLine - only.startLine + 1;
6038
+ if (lineCount < LARGE_TOUR_FOCUS_RANGE_LINES) return capped;
6039
+ const lines = side === "left" ? diffModel.leftLines : diffModel.rightLines;
6040
+ const partCount = Math.min(
6041
+ MAX_AUTOMATIC_TOUR_FOCUS_RANGES,
6042
+ Math.ceil(lineCount / LARGE_TOUR_FOCUS_RANGE_LINES)
6043
+ );
6044
+ const boundaries = [];
6045
+ for (let partIndex = 1; partIndex < partCount; partIndex += 1) {
6046
+ const target = only.startLine + Math.round(lineCount * partIndex / partCount) - 1;
6047
+ boundaries.push(findBlankLineBoundary(lines, target, only.startLine, only.endLine));
6048
+ }
6049
+ const uniqueBoundaries = [...new Set(boundaries)].filter((line) => line >= only.startLine && line < only.endLine).sort((left, right) => left - right);
6050
+ const split = [];
6051
+ let startLine = only.startLine;
6052
+ for (const endLine of uniqueBoundaries) {
6053
+ split.push({ startLine, endLine });
6054
+ startLine = endLine + 1;
6055
+ }
6056
+ split.push({ startLine, endLine: only.endLine });
6057
+ return split;
6058
+ }
6059
+ function capFocusRanges(ranges, maximum) {
6060
+ if (ranges.length <= maximum) return ranges.map((range) => ({ ...range }));
6061
+ const capped = [];
6062
+ for (let index = 0; index < maximum; index += 1) {
6063
+ const startIndex = Math.floor(index * ranges.length / maximum);
6064
+ const endIndex = Math.floor((index + 1) * ranges.length / maximum) - 1;
6065
+ capped.push({
6066
+ startLine: ranges[startIndex].startLine,
6067
+ endLine: ranges[endIndex].endLine
6068
+ });
6069
+ }
6070
+ return capped;
6071
+ }
6072
+ function findBlankLineBoundary(lines, target, minimum, maximum) {
6073
+ for (let distance = 0; distance <= TOUR_FOCUS_BOUNDARY_SEARCH_LINES; distance += 1) {
6074
+ for (const candidate of distance === 0 ? [target] : [target + distance, target - distance]) {
6075
+ if (candidate < minimum || candidate >= maximum) continue;
6076
+ const line = lines.find((entry) => entry.lineNumber === candidate);
6077
+ if ((line == null ? void 0 : line.content.trim()) === "") return candidate;
6078
+ }
6079
+ }
6080
+ return Math.min(Math.max(target, minimum), maximum - 1);
6081
+ }
4837
6082
  var import_child_process32 = require("child_process");
4838
6083
  var CHANGE_TOUR_CONTEXT_VERSION = 1;
4839
6084
  var DEFAULT_MAX_PATCH_BYTES = 128 * 1024;
@@ -5162,7 +6407,8 @@ var require_changeTour = __commonJS({
5162
6407
  repoRoot,
5163
6408
  authoredScene,
5164
6409
  authoredScene.base || ((_a = source.range) == null ? void 0 : _a.base),
5165
- authoredScene.target || ((_b = source.range) == null ? void 0 : _b.head)
6410
+ authoredScene.target || ((_b = source.range) == null ? void 0 : _b.head),
6411
+ [...available.keys()]
5166
6412
  );
5167
6413
  scenes.push(deconstructedScene);
5168
6414
  sceneIds.push(deconstructedScene.id);
@@ -5208,7 +6454,7 @@ var require_changeTour = __commonJS({
5208
6454
  }
5209
6455
  return { scenes, chapters };
5210
6456
  }
5211
- function buildDeconstructedManifestScene(repoRoot, source, baseRef, targetRef) {
6457
+ function buildDeconstructedManifestScene(repoRoot, source, baseRef, targetRef, availableFilePaths = []) {
5212
6458
  const compiled = buildDeconstructedScene(repoRoot, source, { baseRef, headRef: targetRef });
5213
6459
  const panels = [{
5214
6460
  id: "explanation-baseline",
@@ -5220,9 +6466,15 @@ var require_changeTour = __commonJS({
5220
6466
  role: "stage",
5221
6467
  stageId: stage.id
5222
6468
  }))];
5223
- const includedPaths = [...new Set(compiled.stages.flatMap((stage) => stage.introducedFiles))];
6469
+ const materializablePaths = new Set(compiled.baselineFiles.map((file) => file.path));
6470
+ const includedPaths = availableFilePaths.length > 0 ? availableFilePaths.filter((filePath) => materializablePaths.has(filePath)).sort() : [...materializablePaths].sort();
6471
+ const missingIntroducedPath = compiled.stages.flatMap((stage) => stage.introducedFiles).find((filePath) => !includedPaths.includes(filePath));
6472
+ if (missingIntroducedPath) {
6473
+ throw new Error(`Deconstructed tour file is not renderable: ${missingIntroducedPath}`);
6474
+ }
6475
+ const introducedPaths = new Set(compiled.stages.flatMap((stage) => stage.introducedFiles));
5224
6476
  let totalContentBytes = 0;
5225
- const files = includedPaths.map((filePath) => {
6477
+ const files = includedPaths.flatMap((filePath) => {
5226
6478
  const states = [
5227
6479
  compiled.baselineFiles.find((file) => file.path === filePath),
5228
6480
  ...compiled.stages.map((stage) => stage.files.find((file) => file.path === filePath))
@@ -5230,18 +6482,23 @@ var require_changeTour = __commonJS({
5230
6482
  if (states.some((state) => !state)) {
5231
6483
  throw new Error(`Deconstructed scene ${source.id} is missing virtual state for ${filePath}.`);
5232
6484
  }
5233
- return {
6485
+ const fileContentBytes = states.reduce((total, state) => total + Buffer.byteLength((state == null ? void 0 : state.content) || "", "utf8"), 0);
6486
+ const exceedsFileLimit = states.some((state) => {
6487
+ const content = Buffer.from((state == null ? void 0 : state.content) || "", "utf8");
6488
+ return content.length > DEFAULT_MAX_TOUR_FILE_BYTES || hasLineLongerThan(content, DEFAULT_MAX_TOUR_LINE_BYTES);
6489
+ });
6490
+ const exceedsSceneLimit = totalContentBytes + fileContentBytes > DEFAULT_MAX_STACK_CONTENT_BYTES;
6491
+ if (exceedsFileLimit || exceedsSceneLimit) {
6492
+ if (introducedPaths.has(filePath)) {
6493
+ throw new Error(exceedsFileLimit ? `Deconstructed tour file is too large to present: ${filePath}` : `Deconstructed tour content exceeds ${DEFAULT_MAX_STACK_CONTENT_BYTES} bytes.`);
6494
+ }
6495
+ return [];
6496
+ }
6497
+ totalContentBytes += fileContentBytes;
6498
+ return [{
5234
6499
  path: filePath,
5235
6500
  panels: states.map((state, index) => {
5236
6501
  if (!state) throw new Error(`Missing virtual state for ${filePath}.`);
5237
- const content = Buffer.from(state.content, "utf8");
5238
- if (content.length > DEFAULT_MAX_TOUR_FILE_BYTES || hasLineLongerThan(content, DEFAULT_MAX_TOUR_LINE_BYTES)) {
5239
- throw new Error(`Deconstructed tour file is too large to present: ${filePath}`);
5240
- }
5241
- totalContentBytes += content.length;
5242
- if (totalContentBytes > DEFAULT_MAX_STACK_CONTENT_BYTES) {
5243
- throw new Error(`Deconstructed tour content exceeds ${DEFAULT_MAX_STACK_CONTENT_BYTES} bytes.`);
5244
- }
5245
6502
  return {
5246
6503
  id: panels[index].id,
5247
6504
  label: `${panels[index].label} / ${filePath}${state.exists ? "" : " (absent)"}`,
@@ -5250,9 +6507,10 @@ var require_changeTour = __commonJS({
5250
6507
  exists: state.exists
5251
6508
  };
5252
6509
  })
5253
- };
6510
+ }];
5254
6511
  });
5255
6512
  if (files.length === 0) throw new Error(`Deconstructed scene ${source.id} has no staged files.`);
6513
+ const steps = buildDeconstructedFocusSteps(source.id, compiled.stages, files);
5256
6514
  return {
5257
6515
  id: source.id,
5258
6516
  kind: "deconstructed-diff",
@@ -5270,17 +6528,63 @@ var require_changeTour = __commonJS({
5270
6528
  },
5271
6529
  panels,
5272
6530
  files,
5273
- steps: compiled.stages.map((stage, index) => ({
5274
- id: stage.id,
5275
- title: stage.title,
5276
- body: stage.narration,
5277
- file: stage.introducedFiles[0],
5278
- pairIndex: index,
5279
- side: "right",
5280
- introducedHunks: stage.introducedHunks
5281
- }))
6531
+ steps
5282
6532
  };
5283
6533
  }
6534
+ function buildDeconstructedFocusSteps(sceneId, stages, files) {
6535
+ const reservedIds = new Set(stages.map((stage) => stage.id));
6536
+ const usedIds = /* @__PURE__ */ new Set();
6537
+ return stages.flatMap((stage, stageIndex) => {
6538
+ const focuses = stage.introducedChanges.flatMap((change) => {
6539
+ const file = files.find((candidate) => candidate.path === change.file);
6540
+ const left = file == null ? void 0 : file.panels[stageIndex];
6541
+ const right = file == null ? void 0 : file.panels[stageIndex + 1];
6542
+ if (!file || !left || !right) {
6543
+ throw new Error(`Deconstructed scene ${sceneId} is missing comparison state for ${change.file}.`);
6544
+ }
6545
+ const diffModel = buildTwoWayDiffModel3(left.content, right.content);
6546
+ const side = left.exists && !right.exists ? "left" : "right";
6547
+ const ranges = buildTourFocusRanges(diffModel, side);
6548
+ if (ranges.length === 0) {
6549
+ throw new Error(`Deconstructed stage ${stage.id} has no visible change for ${change.file}.`);
6550
+ }
6551
+ return ranges.map((range) => ({
6552
+ file: change.file,
6553
+ hunks: change.hunks,
6554
+ side,
6555
+ ...range
6556
+ }));
6557
+ });
6558
+ return focuses.map((focus, focusIndex) => ({
6559
+ id: focusIndex === 0 ? claimDeconstructedStageId(stage.id, usedIds) : claimDeconstructedFocusId(`${stage.id}-focus-${focusIndex + 1}`, reservedIds, usedIds),
6560
+ title: focuses.length === 1 ? stage.title : `${stage.title} \xB7 ${focus.file} \xB7 Focus ${focusIndex + 1}/${focuses.length}`,
6561
+ body: stage.narration,
6562
+ file: focus.file,
6563
+ pairIndex: stageIndex,
6564
+ side: focus.side,
6565
+ startLine: focus.startLine,
6566
+ endLine: focus.endLine,
6567
+ introducedHunks: [...focus.hunks],
6568
+ stageId: stage.id,
6569
+ stageIndex,
6570
+ focusIndex,
6571
+ focusCount: focuses.length
6572
+ }));
6573
+ });
6574
+ }
6575
+ function claimDeconstructedStageId(stageId, usedIds) {
6576
+ usedIds.add(stageId);
6577
+ return stageId;
6578
+ }
6579
+ function claimDeconstructedFocusId(preferredId, reservedIds, usedIds) {
6580
+ let candidate = preferredId;
6581
+ let suffix = 2;
6582
+ while (usedIds.has(candidate) || reservedIds.has(candidate)) {
6583
+ candidate = `${preferredId}-${suffix++}`;
6584
+ }
6585
+ usedIds.add(candidate);
6586
+ return candidate;
6587
+ }
5284
6588
  function buildStackedScene(repoRoot, source, availableFilePaths) {
5285
6589
  const stack = source.stack.map((entry) => ({
5286
6590
  id: entry.id,
@@ -9297,7 +10601,7 @@ var require_package = __commonJS({
9297
10601
  displayName: "Bygone",
9298
10602
  desktopName: "com.davidmashburn.bygone.desktop",
9299
10603
  description: "See how change happened. Visual diff and file history for VS Code.",
9300
- version: "0.8.12",
10604
+ version: "0.8.14",
9301
10605
  author: "David Mashburn",
9302
10606
  license: "MIT",
9303
10607
  private: false,
@@ -22531,7 +23835,7 @@ var { searchFileHistory: searchFileHistory2 } = (init_gitHistorySearch(), __toCo
22531
23835
  var { classifyAuthoredTourPaths: classifyAuthoredTourPaths2, discoverAuthoredTourDocument: discoverAuthoredTourDocument2 } = (init_tourDocument(), __toCommonJS(tourDocument_exports));
22532
23836
  var { materializeBranchReviewTrees: materializeBranchReviewTrees2, materializeGitTree: materializeGitTree2, resolveBranchReviewRange: resolveBranchReviewRange2, resolveReviewPathPair: resolveReviewPathPair2 } = (init_gitComparison(), __toCommonJS(gitComparison_exports));
22533
23837
  var { buildDirectoryNavigationState } = require_navigationUtils();
22534
- var { getMenuCapabilities } = require_menuUtils();
23838
+ var { collectComparisonSelection, getMenuCapabilities } = require_menuUtils();
22535
23839
  var {
22536
23840
  cloneSessionSource,
22537
23841
  createBranchReviewSource,
@@ -22611,6 +23915,8 @@ var wordWrapEnabled = false;
22611
23915
  var wordWrapAvailable = false;
22612
23916
  var repositorySearch = null;
22613
23917
  var lastRepositoryReplacement = null;
23918
+ var pendingLaunchPrompt = null;
23919
+ var nextLaunchPromptId = 1;
22614
23920
  var appIsQuitting = false;
22615
23921
  var restoringWindowState = false;
22616
23922
  if (!singleInstanceLock) {
@@ -23041,40 +24347,52 @@ ${HELP_URL}`,
23041
24347
  {
23042
24348
  label: "File",
23043
24349
  submenu: [
24350
+ {
24351
+ label: "New Blank Comparison",
24352
+ accelerator: "CmdOrCtrl+N",
24353
+ click: () => runMenuAction("open a blank comparison", openBlankDiff)
24354
+ },
24355
+ { type: "separator" },
23044
24356
  {
23045
24357
  label: "Compare Files\u2026",
23046
24358
  accelerator: "CmdOrCtrl+O",
23047
- click: () => {
23048
- void openCompareFilesDialog();
23049
- }
24359
+ click: () => runMenuAction("compare files", openCompareFilesDialog)
23050
24360
  },
23051
24361
  {
23052
24362
  label: "Compare Directories\u2026",
23053
24363
  accelerator: "CmdOrCtrl+Shift+O",
23054
- click: () => {
23055
- void openCompareDirectoriesDialog();
23056
- }
24364
+ click: () => runMenuAction("compare directories", openCompareDirectoriesDialog)
23057
24365
  },
24366
+ { type: "separator" },
23058
24367
  {
23059
- label: "Add Panel to Left\u2026",
23060
- enabled: canAddPanel,
23061
- click: () => {
23062
- void addPanelFromMenu("left");
23063
- }
24368
+ label: "View File or Directory History\u2026",
24369
+ accelerator: "CmdOrCtrl+Shift+H",
24370
+ click: () => runMenuAction("open Git history", openHistoryDialog)
23064
24371
  },
23065
24372
  {
23066
- label: "Add Panel to Right\u2026",
23067
- enabled: canAddPanel,
23068
- click: () => {
23069
- void addPanelFromMenu("right");
23070
- }
24373
+ label: "Open Authored Tour\u2026",
24374
+ click: () => runMenuAction("open an authored tour", openAuthoredTourDialog)
23071
24375
  },
24376
+ { type: "separator" },
23072
24377
  {
23073
- label: "Remove Active Panel",
23074
- enabled: canRemovePanel,
23075
- click: () => {
23076
- void removeActivePanelFromMenu();
23077
- }
24378
+ label: "Comparison Panels",
24379
+ submenu: [
24380
+ {
24381
+ label: "Add Panel to Left\u2026",
24382
+ enabled: canAddPanel,
24383
+ click: () => runMenuAction("add a panel", () => addPanelFromMenu("left"))
24384
+ },
24385
+ {
24386
+ label: "Add Panel to Right\u2026",
24387
+ enabled: canAddPanel,
24388
+ click: () => runMenuAction("add a panel", () => addPanelFromMenu("right"))
24389
+ },
24390
+ {
24391
+ label: "Remove Active Panel",
24392
+ enabled: canRemovePanel,
24393
+ click: () => runMenuAction("remove the active panel", removeActivePanelFromMenu)
24394
+ }
24395
+ ]
23078
24396
  },
23079
24397
  { type: "separator" },
23080
24398
  ...fileActionItems,
@@ -23136,9 +24454,7 @@ ${HELP_URL}`,
23136
24454
  {
23137
24455
  label: "Search in Files\u2026",
23138
24456
  accelerator: "CmdOrCtrl+Alt+F",
23139
- click: () => {
23140
- void openRepositorySearchDialog();
23141
- }
24457
+ click: () => runMenuAction("search in files", openRepositorySearchDialog)
23142
24458
  },
23143
24459
  { type: "separator" },
23144
24460
  {
@@ -23182,35 +24498,13 @@ ${HELP_URL}`,
23182
24498
  label: "Git",
23183
24499
  submenu: [
23184
24500
  {
23185
- label: "View File or Directory History\u2026",
23186
- accelerator: "CmdOrCtrl+Shift+H",
23187
- click: () => {
23188
- void openHistoryDialog();
23189
- }
24501
+ label: "Compare Revisions\u2026",
24502
+ click: () => runMenuAction("compare Git revisions", openGitRefsDialog)
23190
24503
  },
23191
24504
  {
23192
- label: "Explore Current Branch Change",
24505
+ label: "Review Branch Change\u2026",
23193
24506
  accelerator: "CmdOrCtrl+Shift+R",
23194
- click: () => {
23195
- void openBranchReviewDialog();
23196
- }
23197
- },
23198
- { type: "separator" },
23199
- {
23200
- label: "Older Commit",
23201
- accelerator: "Alt+Left",
23202
- enabled: isHistory,
23203
- click: () => {
23204
- void navigateHistory("back");
23205
- }
23206
- },
23207
- {
23208
- label: "Newer Commit",
23209
- accelerator: "Alt+Right",
23210
- enabled: isHistory,
23211
- click: () => {
23212
- void navigateHistory("forward");
23213
- }
24507
+ click: () => runMenuAction("review a branch change", openBranchReviewDialog)
23214
24508
  }
23215
24509
  ]
23216
24510
  },
@@ -23219,15 +24513,11 @@ ${HELP_URL}`,
23219
24513
  submenu: [
23220
24514
  {
23221
24515
  label: "Present Current Branch",
23222
- click: () => {
23223
- void presentCurrentBranch();
23224
- }
24516
+ click: () => runMenuAction("present the current branch", presentCurrentBranch)
23225
24517
  },
23226
24518
  {
23227
- label: "Open Authored Tour\u2026",
23228
- click: () => {
23229
- void openAuthoredTourDialog().catch((error) => showError(`Could not open Bygone presentation: ${getErrorMessage(error)}`));
23230
- }
24519
+ label: "Present Branch or Ref\u2026",
24520
+ click: () => runMenuAction("present a branch or ref", openPresentBranchDialog)
23231
24521
  },
23232
24522
  { type: "separator" },
23233
24523
  {
@@ -23260,6 +24550,23 @@ ${HELP_URL}`,
23260
24550
  click: () => {
23261
24551
  void returnToDirectoryView();
23262
24552
  }
24553
+ },
24554
+ { type: "separator" },
24555
+ {
24556
+ label: "Older Commit",
24557
+ accelerator: "Alt+Left",
24558
+ enabled: isHistory,
24559
+ click: () => {
24560
+ void navigateHistory("back");
24561
+ }
24562
+ },
24563
+ {
24564
+ label: "Newer Commit",
24565
+ accelerator: "Alt+Right",
24566
+ enabled: isHistory,
24567
+ click: () => {
24568
+ void navigateHistory("forward");
24569
+ }
23263
24570
  }
23264
24571
  ]
23265
24572
  },
@@ -23362,9 +24669,38 @@ ${HELP_URL}`,
23362
24669
  ];
23363
24670
  Menu.setApplicationMenu(Menu.buildFromTemplate(template));
23364
24671
  }
24672
+ function runMenuAction(description, action) {
24673
+ void Promise.resolve().then(action).catch((error) => showError(`Could not ${description}: ${getErrorMessage(error)}`));
24674
+ }
23365
24675
  async function presentCurrentBranch() {
23366
- const cwd = getSourceRepoRoot(session.source) || process.cwd();
23367
- await openTourPresentation([], cwd);
24676
+ const repoRoot = await resolveOrChooseGitRepository("Select a Git repository to present");
24677
+ if (repoRoot) {
24678
+ await openTourPresentation([], repoRoot);
24679
+ }
24680
+ }
24681
+ async function openPresentBranchDialog() {
24682
+ const repoRoot = await resolveOrChooseGitRepository("Select a Git repository to present");
24683
+ if (!repoRoot) {
24684
+ return;
24685
+ }
24686
+ openLaunchPrompt({
24687
+ kind: "present-branch",
24688
+ title: "Present Branch or Ref",
24689
+ description: `Repository: ${repoRoot}`,
24690
+ submitLabel: "Present",
24691
+ fields: [
24692
+ { name: "head", label: "Head branch or ref", value: "HEAD", required: true },
24693
+ { name: "base", label: "Base branch or ref (optional)", value: "" }
24694
+ ]
24695
+ }, async (values) => {
24696
+ const head = values.head.trim() || "HEAD";
24697
+ const base = values.base.trim();
24698
+ const args = [head];
24699
+ if (base) {
24700
+ args.push(tokensFor("base")[0], base);
24701
+ }
24702
+ await openTourPresentation(args, repoRoot);
24703
+ });
23368
24704
  }
23369
24705
  async function showRepositorySearchStatus() {
23370
24706
  const capability = detectRipgrepCapability2();
@@ -23973,6 +25309,24 @@ async function handleRendererMessage(message) {
23973
25309
  await refreshSession();
23974
25310
  return;
23975
25311
  }
25312
+ if (message.type === "submitLaunchPrompt" && Number.isInteger(message.requestId) && message.requestId === pendingLaunchPrompt?.requestId && message.values && typeof message.values === "object" && !Array.isArray(message.values)) {
25313
+ const prompt = pendingLaunchPrompt;
25314
+ pendingLaunchPrompt = null;
25315
+ const values = Object.fromEntries(prompt.fieldNames.map((fieldName) => [
25316
+ fieldName,
25317
+ typeof message.values[fieldName] === "string" ? message.values[fieldName] : ""
25318
+ ]));
25319
+ try {
25320
+ await prompt.submit(values);
25321
+ } catch (error) {
25322
+ await showError(`Could not ${prompt.description}: ${getErrorMessage(error)}`);
25323
+ }
25324
+ return;
25325
+ }
25326
+ if (message.type === "cancelLaunchPrompt" && Number.isInteger(message.requestId) && message.requestId === pendingLaunchPrompt?.requestId) {
25327
+ pendingLaunchPrompt = null;
25328
+ return;
25329
+ }
23976
25330
  if (message.type === "searchChangeSet" && Number.isInteger(message.requestId)) {
23977
25331
  searchCurrentChangeSet(message);
23978
25332
  return;
@@ -24126,24 +25480,115 @@ async function handleRendererMessage(message) {
24126
25480
  }
24127
25481
  }
24128
25482
  async function openCompareFilesDialog() {
24129
- if (!mainWindow) {
25483
+ const filePaths = await collectComparisonPaths("file");
25484
+ if (filePaths.length < 2) {
24130
25485
  return;
24131
25486
  }
24132
- const result = await dialog.showOpenDialog(mainWindow, {
24133
- title: "Select two files to compare",
24134
- properties: ["openFile", "multiSelections"]
25487
+ if (filePaths.length === 2) {
25488
+ await openDiff(filePaths[0], filePaths[1]);
25489
+ } else {
25490
+ await openMultiDiff(filePaths);
25491
+ }
25492
+ }
25493
+ async function collectComparisonPaths(kind) {
25494
+ ensureExploreWindow();
25495
+ const isDirectory = kind === "directory";
25496
+ const noun = isDirectory ? "directory" : "file";
25497
+ return collectComparisonSelection(async (selectedCount) => {
25498
+ const result = await dialog.showOpenDialog(mainWindow, {
25499
+ title: selectedCount === 0 ? `Select ${noun}s to compare` : `Add ${noun}${selectedCount === 1 ? "" : "s"} to the comparison`,
25500
+ properties: [isDirectory ? "openDirectory" : "openFile", "multiSelections"]
25501
+ });
25502
+ if (result.canceled) {
25503
+ return null;
25504
+ }
25505
+ return result.filePaths.map((selectedPath) => path8.resolve(selectedPath));
25506
+ }, async (paths) => {
25507
+ const choice = await dialog.showMessageBox(mainWindow, {
25508
+ type: "question",
25509
+ buttons: [`Compare ${paths.length}`, "Add More\u2026", "Cancel"],
25510
+ defaultId: 0,
25511
+ cancelId: 2,
25512
+ message: `Compare ${paths.length} selected ${noun}${paths.length === 1 ? "" : "s"}?`,
25513
+ detail: paths.join("\n")
25514
+ });
25515
+ if (choice.response === 0) {
25516
+ return "compare";
25517
+ }
25518
+ return choice.response === 1 ? "add" : "cancel";
25519
+ });
25520
+ }
25521
+ function ensureExploreWindow() {
25522
+ ensureMainWindow();
25523
+ if (!mainWindow.isVisible()) {
25524
+ mainWindow.show();
25525
+ }
25526
+ mainWindow.focus();
25527
+ }
25528
+ function openLaunchPrompt(config, submit) {
25529
+ ensureExploreWindow();
25530
+ const requestId = nextLaunchPromptId++;
25531
+ pendingLaunchPrompt = {
25532
+ requestId,
25533
+ fieldNames: config.fields.map((field) => field.name),
25534
+ description: config.failureDescription || config.title.toLowerCase(),
25535
+ submit
25536
+ };
25537
+ postToRenderer({
25538
+ type: "openLaunchPrompt",
25539
+ requestId,
25540
+ ...config
24135
25541
  });
24136
- if (result.canceled || result.filePaths.length < 2) {
24137
- if (!result.canceled) {
24138
- await showInfo("Select at least two files to compare.");
25542
+ }
25543
+ async function resolveOrChooseGitRepository(title) {
25544
+ const sourcePath = session.source?.repoRoot || session.source?.path || session.source?.paths?.[0];
25545
+ const sourceCandidate = sourcePath && getPathKind(sourcePath) === "file" ? path8.dirname(sourcePath) : sourcePath;
25546
+ const candidates = [sourceCandidate, launchArguments.cwd, process.cwd()].filter((candidate, index, all) => candidate && all.indexOf(candidate) === index);
25547
+ for (const candidate of candidates) {
25548
+ try {
25549
+ return fs7.realpathSync(runGit(["rev-parse", "--show-toplevel"], candidate));
25550
+ } catch {
24139
25551
  }
24140
- return;
24141
25552
  }
24142
- if (result.filePaths.length === 2) {
24143
- await openDiff(result.filePaths[0], result.filePaths[1]);
24144
- } else {
24145
- await openMultiDiff(result.filePaths);
25553
+ ensureExploreWindow();
25554
+ const result = await dialog.showOpenDialog(mainWindow, {
25555
+ title,
25556
+ properties: ["openDirectory"]
25557
+ });
25558
+ if (result.canceled || result.filePaths.length === 0) {
25559
+ return null;
25560
+ }
25561
+ try {
25562
+ return fs7.realpathSync(runGit(["rev-parse", "--show-toplevel"], result.filePaths[0]));
25563
+ } catch (error) {
25564
+ await showError(`The selected directory is not inside a Git repository: ${getErrorMessage(error)}`);
25565
+ return null;
25566
+ }
25567
+ }
25568
+ async function openGitRefsDialog() {
25569
+ const repoRoot = await resolveOrChooseGitRepository("Select a Git repository to compare");
25570
+ if (!repoRoot) {
25571
+ return;
24146
25572
  }
25573
+ openLaunchPrompt({
25574
+ kind: "git-refs",
25575
+ title: "Compare Git Revisions",
25576
+ description: `Repository: ${repoRoot}`,
25577
+ submitLabel: "Compare",
25578
+ fields: [{
25579
+ name: "refs",
25580
+ label: "Revisions (two or more, separated by spaces)",
25581
+ value: "HEAD~1 HEAD",
25582
+ required: true
25583
+ }]
25584
+ }, async (values) => {
25585
+ const refs = values.refs.trim().split(/\s+/).filter(Boolean);
25586
+ if (refs.length < 2) {
25587
+ await showInfo("Enter at least two Git revisions to compare.");
25588
+ return;
25589
+ }
25590
+ await openGitRefs(repoRoot, refs);
25591
+ });
24147
25592
  }
24148
25593
  async function openDroppedFiles(paths) {
24149
25594
  const normalizedPaths = paths.filter((candidate) => typeof candidate === "string" && candidate.length > 0).map((candidate) => path8.resolve(candidate)).filter((candidate, index, all) => all.indexOf(candidate) === index);
@@ -24208,37 +25653,29 @@ async function openHistoryDialog() {
24208
25653
  await openHistory(targetPath, historyIncludeStagedPreference);
24209
25654
  }
24210
25655
  async function openBranchReviewDialog() {
24211
- let reviewRoot = launchArguments.cwd || process.cwd();
24212
- try {
24213
- runGit(["rev-parse", "--show-toplevel"], reviewRoot);
24214
- } catch {
24215
- if (!mainWindow) {
24216
- return;
24217
- }
24218
- const result = await dialog.showOpenDialog(mainWindow, {
24219
- title: "Select a Git repository to review",
24220
- properties: ["openDirectory"]
24221
- });
24222
- if (result.canceled || result.filePaths.length === 0) {
24223
- return;
24224
- }
24225
- reviewRoot = result.filePaths[0];
24226
- }
24227
- await openGitBranchReview(reviewRoot, "HEAD");
24228
- }
24229
- async function openCompareDirectoriesDialog() {
24230
- if (!mainWindow) {
25656
+ const repoRoot = await resolveOrChooseGitRepository("Select a Git repository to review");
25657
+ if (!repoRoot) {
24231
25658
  return;
24232
25659
  }
24233
- const result = await dialog.showOpenDialog(mainWindow, {
24234
- title: "Select directories to compare",
24235
- properties: ["openDirectory", "multiSelections"]
25660
+ openLaunchPrompt({
25661
+ kind: "review-branch",
25662
+ title: "Review Branch Change",
25663
+ description: `Repository: ${repoRoot}`,
25664
+ submitLabel: "Review",
25665
+ fields: [
25666
+ { name: "head", label: "Head branch or ref", value: "HEAD", required: true },
25667
+ { name: "base", label: "Base branch or ref (optional)", value: "" }
25668
+ ]
25669
+ }, async (values) => {
25670
+ await openGitBranchReview(repoRoot, values.head.trim() || "HEAD", values.base.trim() || void 0);
24236
25671
  });
24237
- if (result.canceled || result.filePaths.length < 2) {
24238
- if (!result.canceled) await showInfo("Select at least two directories to compare.");
25672
+ }
25673
+ async function openCompareDirectoriesDialog() {
25674
+ const directoryPaths = await collectComparisonPaths("directory");
25675
+ if (directoryPaths.length < 2) {
24239
25676
  return;
24240
25677
  }
24241
- await openDirectories(result.filePaths);
25678
+ await openDirectories(directoryPaths);
24242
25679
  }
24243
25680
  async function openDirectories(dirs, options = {}) {
24244
25681
  const resolvedDirs = dirs.map((dir) => path8.resolve(dir)).filter((dir, index, all) => all.indexOf(dir) === index);