diff --git a/scripts/lib/control-pane/proximity.js b/scripts/lib/control-pane/proximity.js index e287ebee3..63de333ed 100644 --- a/scripts/lib/control-pane/proximity.js +++ b/scripts/lib/control-pane/proximity.js @@ -28,9 +28,13 @@ function parseDiffRanges(diff) { const byFile = new Map(); let current = null; for (const line of String(diff || '').split('\n')) { + if (line.startsWith('diff --git ')) { + current = null; + continue; + } const fileMatch = line.match(/^\+\+\+ b\/(.+)$/); if (fileMatch) { - const name = fileMatch[1].trim(); + const name = fileMatch[1].replace(/\t$/, ''); current = name === '/dev/null' ? null : name; if (current && !byFile.has(current)) byFile.set(current, []); continue; @@ -80,8 +84,12 @@ function defaultWorkingSetFor(session) { try { const ranges = parseDiffRanges(runGitDiff(wt.path, base, ['--unified=0'])); // Deletions, binary changes and quoted paths have no usable new-side hunks. - for (const file of runGitDiff(wt.path, base, ['--name-only', '-z']).split('\0').filter(Boolean)) { - if (!ranges.has(file)) ranges.set(file, []); + try { + for (const file of runGitDiff(wt.path, base, ['--name-only', '-z']).split('\0').filter(Boolean)) { + if (!ranges.has(file)) ranges.set(file, []); + } + } catch { + // Preserve ranges already read if the supplementary filename probe fails. } for (const file of untrackedFiles(wt.path)) ranges.set(file, []); return [...ranges.entries()].map(([path, lines]) => (lines.length > 0 ? { path, lines } : { path })); diff --git a/tests/lib/control-pane-proximity.test.js b/tests/lib/control-pane-proximity.test.js index 042576eaf..74978efaf 100644 --- a/tests/lib/control-pane-proximity.test.js +++ b/tests/lib/control-pane-proximity.test.js @@ -153,6 +153,7 @@ test('quoted staged paths and untracked paths share identity, and untracked fail const wt = path.join(dir, name); git(repo, 'worktree', 'add', '-q', '-b', name, wt, 'main'); fs.writeFileSync(path.join(wt, file), `export const value = '${name}';\n`); + if (name === 'a') fs.writeFileSync(path.join(wt, 'README'), 'changed\n'); return { id: name, worktree: { path: wt, base: 'main' } }; }); git(worktrees[0].worktree.path, 'add', file); @@ -169,7 +170,14 @@ test('quoted staged paths and untracked paths share identity, and untracked fail const fresh = require(modulePath); const [tracked] = fresh.sessionsToAgents([worktrees[0]]); assert.ok(tracked, `${code}: tracked changes must survive`); - assert.deepStrictEqual(tracked.files.map(entry => entry.path), [file]); + assert.deepStrictEqual(tracked.files.map(entry => entry.path).sort(), ['README', file].sort()); + childProcess.execFileSync = (command, args, options) => { + if (args.includes('--name-only')) throw Object.assign(new Error('tracked names unavailable'), { code }); + return originalExec(command, args, options); + }; + delete require.cache[modulePath]; + const partial = require(modulePath).sessionsToAgents([worktrees[0]]); + assert.deepStrictEqual(partial[0].files.map(entry => entry.path), ['README']); } } finally { childProcess.execFileSync = originalExec; @@ -221,6 +229,18 @@ test('parseDiffRanges: extracts new-side line ranges per file', () => { ]); }); +test('diff headers isolate quoted/deleted hunks and preserve trailing-space paths', () => { + const ranges = parseDiffRanges([ + 'diff --git a/first.js b/first.js', '+++ b/first.js', '@@ -1 +1 @@', + 'diff --git "a/café.js" "b/café.js"', '+++ "b/café.js"', '@@ -20 +20 @@', + 'diff --git a/deleted.js b/deleted.js', '+++ /dev/null', '@@ -30 +0,0 @@', + 'diff --git a/trailing b/trailing', '+++ b/trailing ', '@@ -40 +40 @@', + ].join('\n')); + assert.deepStrictEqual(ranges.get('first.js'), [[1, 1]]); + assert.deepStrictEqual(ranges.get('trailing '), [[40, 40]]); + assert.strictEqual(ranges.has('trailing'), false); +}); + test('line-range channel: same file but disjoint ranges ⇒ no resolution', () => { // Two agents in the same file, far-apart functions. workingSetFor provides ranges. const workingSetFor = s =>