@galda/cli 0.10.111 → 0.10.112
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/index.html +107 -24
- package/engine/lib.mjs +11 -0
- package/engine/server.mjs +6 -2
- package/package.json +1 -1
package/app/index.html
CHANGED
|
@@ -6431,6 +6431,38 @@ function prefersJapaneseText(...values){
|
|
|
6431
6431
|
return values.some((v) => /[\u3040-\u30ff\u3400-\u9fff]/.test(String(v || '')));
|
|
6432
6432
|
}
|
|
6433
6433
|
function summarizeAttention(goal, tasks){
|
|
6434
|
+
// A pending question / approval request is the one case where we already HAVE the
|
|
6435
|
+
// worker's own words (engine writes goal.question = {kind,text,options} and
|
|
6436
|
+
// goal.approvalRequest). Show them verbatim. The board's question card already does
|
|
6437
|
+
// this (.qq at the question-card render), and the two paths must not disagree about
|
|
6438
|
+
// the same field — the template built below by pattern-matching the log is a last
|
|
6439
|
+
// resort for goals that stopped without asking anything (PRD §0.1.1-5).
|
|
6440
|
+
if (goal.status === 'needsInput' && goal.question?.text) {
|
|
6441
|
+
const opts = (goal.question.options || []).map((o) => String(o).trim()).filter(Boolean);
|
|
6442
|
+
const jq = prefersJapaneseText(goal.question.text, goal.text);
|
|
6443
|
+
return {
|
|
6444
|
+
label: jq ? '質問' : 'Question',
|
|
6445
|
+
happened: String(goal.question.text).trim(),
|
|
6446
|
+
decision: opts.length
|
|
6447
|
+
? (jq ? `次のどれかで答えてください: ${opts.join(' / ')}` : `Answer with one of: ${opts.join(' / ')}`)
|
|
6448
|
+
: (jq ? '下の入力欄に答えを書いて送ってください。' : 'Type your answer in the box below and send it.'),
|
|
6449
|
+
};
|
|
6450
|
+
}
|
|
6451
|
+
if (goal.status === 'needsApproval') {
|
|
6452
|
+
const a = goal.approvalRequest || {};
|
|
6453
|
+
const ask = String(a.question || a.action || '').trim();
|
|
6454
|
+
if (ask) {
|
|
6455
|
+
const scope = (a.scope || []).map((s) => String(s).trim()).filter(Boolean);
|
|
6456
|
+
const jq = prefersJapaneseText(ask, goal.text);
|
|
6457
|
+
return {
|
|
6458
|
+
label: jq ? '許可の依頼' : 'Approval requested',
|
|
6459
|
+
happened: scope.length ? `${ask}\n\n${jq ? '対象' : 'Scope'}: ${scope.join(' · ')}` : ask,
|
|
6460
|
+
decision: jq
|
|
6461
|
+
? 'Proceed で許可、Cancel で断ります。範囲を変えたい場合は Edit scope。'
|
|
6462
|
+
: 'Proceed to allow it, Cancel to refuse. Use Edit scope to narrow what it may touch.',
|
|
6463
|
+
};
|
|
6464
|
+
}
|
|
6465
|
+
}
|
|
6434
6466
|
const fm = goal.failureMemory?.[goal.failureMemory.length - 1];
|
|
6435
6467
|
if (fm?.whatHappened || fm?.yourCall) {
|
|
6436
6468
|
return {
|
|
@@ -6483,14 +6515,14 @@ function summarizeAttention(goal, tasks){
|
|
|
6483
6515
|
}
|
|
6484
6516
|
return { happened: happened.replace(/\.+$/, '.'), decision };
|
|
6485
6517
|
}
|
|
6486
|
-
function renderGoalBrief({ goal, tasks, isReview, isAttention }){
|
|
6518
|
+
function renderGoalBrief({ goal, tasks, isReview, isAttention, isAsking }){
|
|
6487
6519
|
const brief = $('gdBrief');
|
|
6488
|
-
if (!isReview && !isAttention) { brief.classList.remove('show'); brief.innerHTML = ''; return; }
|
|
6520
|
+
if (!isReview && !isAttention && !isAsking) { brief.classList.remove('show'); brief.innerHTML = ''; return; }
|
|
6489
6521
|
let rows;
|
|
6490
|
-
if (isAttention) {
|
|
6522
|
+
if (isAttention || isAsking) {
|
|
6491
6523
|
const s = summarizeAttention(goal, tasks);
|
|
6492
6524
|
rows = [
|
|
6493
|
-
['What happened', s.happened],
|
|
6525
|
+
[s.label || 'What happened', s.happened],
|
|
6494
6526
|
['Your call', s.decision],
|
|
6495
6527
|
];
|
|
6496
6528
|
} else {
|
|
@@ -6513,9 +6545,17 @@ function renderGoalDetail(){
|
|
|
6513
6545
|
const isReview = goal.status === 'review';
|
|
6514
6546
|
const isAttention = ['blocked', 'partial', 'failed', 'interrupted'].includes(goal.status)
|
|
6515
6547
|
|| tasks.some((t) => ['failed', 'interrupted'].includes(t.status));
|
|
6516
|
-
|
|
6548
|
+
// A goal waiting on an answer/approval is not "stuck" — it is asking. It used to
|
|
6549
|
+
// fall through both flags, so the brief was hidden entirely and the panel showed the
|
|
6550
|
+
// original request with no sign of what the worker actually asked (Masa 2026-07-30).
|
|
6551
|
+
// Kept separate from isAttention so the Discard button's condition does not change.
|
|
6552
|
+
const isAsking = ['needsInput', 'needsApproval'].includes(goal.status);
|
|
6553
|
+
$('gdTitle').textContent = isReview ? 'Review'
|
|
6554
|
+
: goal.status === 'needsInput' ? 'Question'
|
|
6555
|
+
: goal.status === 'needsApproval' ? 'Approval'
|
|
6556
|
+
: 'Attention';
|
|
6517
6557
|
$('gdRequirement').textContent = goal.text ?? '';
|
|
6518
|
-
renderGoalBrief({ goal, tasks, isReview, isAttention });
|
|
6558
|
+
renderGoalBrief({ goal, tasks, isReview, isAttention, isAsking });
|
|
6519
6559
|
// review-only items carry no worker tasks — show the author's note (or a
|
|
6520
6560
|
// plain hint) instead of "No worker reports yet.", and any attached images
|
|
6521
6561
|
// as the thing to look at.
|
|
@@ -6544,10 +6584,22 @@ function renderGoalDetail(){
|
|
|
6544
6584
|
$('gdLog').innerHTML = logLines.length
|
|
6545
6585
|
? `<div class="gdlog">${logLines.map((s) => `<div class="gdlogline">${esc(s)}</div>`).join('')}</div>`
|
|
6546
6586
|
: '<div class="gdempty">No log captured yet.</div>';
|
|
6587
|
+
// A goal that is only ASKING has not produced anything yet, so "No worker reports
|
|
6588
|
+
// yet." / "No proof available yet." / "No log captured yet." are three empty boxes
|
|
6589
|
+
// that push the actual question down and out (Masa 2026-07-30: "高さが少なくてみづらい").
|
|
6590
|
+
// §0.1.1-5: do not show the container for something that does not exist. Scoped to the
|
|
6591
|
+
// asking states so Review — where "no proof" is itself a finding — is unchanged.
|
|
6592
|
+
const hideIfEmpty = isAsking && !tasks.length;
|
|
6593
|
+
for (const [id, empty] of [['gdWhatWasDone', !tasks.length && !goal.note], ['gdProof', !proofTasks.length && !reviewImgs.length], ['gdLog', !logLines.length]]) {
|
|
6594
|
+
const sec = $(id).closest('.gdsec');
|
|
6595
|
+
if (sec) sec.style.display = hideIfEmpty && empty ? 'none' : '';
|
|
6596
|
+
}
|
|
6547
6597
|
$('gdApprove').style.display = isReview ? '' : 'none';
|
|
6548
6598
|
$('gdDismiss').style.display = isReview ? '' : 'none';
|
|
6549
6599
|
$('gdDiscard').style.display = isAttention ? '' : 'none';
|
|
6550
|
-
$('gdRevisionInput').placeholder = isReview ? 'Write revision instructions…'
|
|
6600
|
+
$('gdRevisionInput').placeholder = isReview ? 'Write revision instructions…'
|
|
6601
|
+
: goal.status === 'needsInput' ? 'Answer the question…'
|
|
6602
|
+
: 'Tell the worker what to try next…';
|
|
6551
6603
|
$('gdSend').title = isReview ? 'Send back' : 'Send';
|
|
6552
6604
|
}
|
|
6553
6605
|
$('gdClose').onclick = closeGoalDetail;
|
|
@@ -9559,7 +9611,12 @@ function closeSummaryPera(){ $('summaryOverlay').classList.remove('show'); }
|
|
|
9559
9611
|
// 4 archived / 5 reverted — session-local; the server round-trip is what
|
|
9560
9612
|
// settles it (see saSync's "returned to review" reset).
|
|
9561
9613
|
// ============================================================================
|
|
9562
|
-
|
|
9614
|
+
// `draft` holds the half-typed reply per goal. It exists because the text used to live
|
|
9615
|
+
// ONLY in the DOM, so the sole way to keep it was for saSync() to refuse to run at all
|
|
9616
|
+
// while the box had text — which froze the To Do / Doing / Review status updates behind
|
|
9617
|
+
// the dialog too. With the text in SA it survives a rebuild on its own, and saSync no
|
|
9618
|
+
// longer has to stop (2026-07-30).
|
|
9619
|
+
const SA = { open: false, items: [], diffs: {}, actLog: {}, msgs: {}, msgCounts: {}, convOpen: new Set(), shotBox: {}, detOpen: new Set(), diffOpen: new Set(), attachments: {}, draft: {}, focusGid: null, focusScrolled: false, focusTimer: null, cur: null, solo: null, lastMarkup: null };
|
|
9563
9620
|
// Same convention as the existing ?dev=1 devbar flag — off unless someone opts in by URL.
|
|
9564
9621
|
const SA_DEV = new URLSearchParams(location.search).get('dev') === '1';
|
|
9565
9622
|
// task-971: 白黒(モノクロ)版のレールを ?railmono=1 で見比べられるようにする。
|
|
@@ -9837,7 +9894,10 @@ function saLedgerRows(){
|
|
|
9837
9894
|
// (summarizeAttention), mapped into the review card instead of a bespoke "What happened /
|
|
9838
9895
|
// Your call" layout (Masa 2026-07-24). The Request section already shows the ask, so drop
|
|
9839
9896
|
// the summary's "必要だったこと:/Needed:" lead to avoid repeating it.
|
|
9840
|
-
|
|
9897
|
+
// needsInput/needsApproval are included so this path shows the worker's verbatim
|
|
9898
|
+
// question too — summarizeAttention returns it as-is for those. Both renders of the
|
|
9899
|
+
// same goal.question must say the same thing (PRD §0.1.1-5).
|
|
9900
|
+
if (['blocked', 'partial', 'failed', 'interrupted', 'needsInput', 'needsApproval'].includes(g.status)) {
|
|
9841
9901
|
const s = summarizeAttention(g, ts.filter((t) => t.goalId === g.id));
|
|
9842
9902
|
const why = String(s.happened || '').replace(/^\s*(必要だったこと:|Needed:)[^。.]*[。.]\s*/, '').trim();
|
|
9843
9903
|
const merged = [why, s.decision].filter(Boolean).join('\n\n');
|
|
@@ -10547,7 +10607,7 @@ function saItemHtml(it, i){
|
|
|
10547
10607
|
${conversationSec}
|
|
10548
10608
|
${activitySec}
|
|
10549
10609
|
${S('Workflow', saWorkflowHtml(it))}
|
|
10550
|
-
<div class="sa-act">${attn ? '' : `<div class="sa-approve-wrap"><button class="sa-app" data-saact="app:${i}">${SA_SVG.ok}Approve</button>${delivery}</div><button class="sa-dis" data-saact="dis:${i}">Dismiss</button>`}<div class="sa-replybox"><div class="sa-replyattach${SA.attachments[it.goalId]?.length ? ' has' : ''}" data-saattach="${it.goalId}">${saReplyAttachmentsHtml(it.goalId)}</div><div class="sa-chatwrap"><input class="sa-chat" data-i="${i}" data-goal="${it.goalId}" placeholder="Reply, ask, or paste an image…"><button class="sa-send" data-saact="send:${i}" title="Send"><svg viewBox="0 0 24 24"><path d="M12 19V5M5 12l7-7 7 7"/></svg></button></div></div><button class="sa-ic" title="Archive" data-saact="arch:${i}">${SA_SVG.arch}</button><button class="sa-ic" title="Revert" data-saact="rev:${i}">${SA_SVG.trash}</button></div>
|
|
10610
|
+
<div class="sa-act">${attn ? '' : `<div class="sa-approve-wrap"><button class="sa-app" data-saact="app:${i}">${SA_SVG.ok}Approve</button>${delivery}</div><button class="sa-dis" data-saact="dis:${i}">Dismiss</button>`}<div class="sa-replybox"><div class="sa-replyattach${SA.attachments[it.goalId]?.length ? ' has' : ''}" data-saattach="${it.goalId}">${saReplyAttachmentsHtml(it.goalId)}</div><div class="sa-chatwrap"><input class="sa-chat" data-i="${i}" data-goal="${it.goalId}" placeholder="Reply, ask, or paste an image…" value="${esc(SA.draft[it.goalId] ?? '')}"><button class="sa-send" data-saact="send:${i}" title="Send"><svg viewBox="0 0 24 24"><path d="M12 19V5M5 12l7-7 7 7"/></svg></button></div></div><button class="sa-ic" title="Archive" data-saact="arch:${i}">${SA_SVG.arch}</button><button class="sa-ic" title="Revert" data-saact="rev:${i}">${SA_SVG.trash}</button></div>
|
|
10551
10611
|
</div>`;
|
|
10552
10612
|
}
|
|
10553
10613
|
// Scroll the See-all body so a card's top edge (.rhead) sits at the top of the visible
|
|
@@ -10695,9 +10755,12 @@ function saRender(){
|
|
|
10695
10755
|
}) || cards.find((el) => el.getBoundingClientRect().bottom > bodyRect.top);
|
|
10696
10756
|
if (anchor) { anchorGid = anchor.dataset.gid; anchorOffset = anchor.getBoundingClientRect().top - bodyRect.top; }
|
|
10697
10757
|
}
|
|
10698
|
-
|
|
10699
|
-
|
|
10700
|
-
|
|
10758
|
+
// Remember WHERE the caret was too, not just which box had focus: the text now comes
|
|
10759
|
+
// back from SA.draft, and putting it back without the caret would jump the cursor to
|
|
10760
|
+
// the end mid-word on every SSE tick.
|
|
10761
|
+
const activeChat = document.activeElement?.matches?.('#seeall .sa-chat') ? document.activeElement : null;
|
|
10762
|
+
const focusedReplyGoal = activeChat?.dataset.goal ?? null;
|
|
10763
|
+
const focusedCaret = activeChat ? { start: activeChat.selectionStart, end: activeChat.selectionEnd } : null;
|
|
10701
10764
|
const pending = SA.items.filter((x) => x.st === 0);
|
|
10702
10765
|
const green = pending.filter((x) => !x.testResult || x.testResult.ok !== false).length;
|
|
10703
10766
|
// Solo mode = a single "Needs you" goal opened as this card. It is not a review batch, so the
|
|
@@ -10828,10 +10891,24 @@ function saRender(){
|
|
|
10828
10891
|
if (!SA.focusTimer) SA.focusTimer = setTimeout(() => { SA.focusGid = null; SA.focusTimer = null; }, 1400);
|
|
10829
10892
|
});
|
|
10830
10893
|
}
|
|
10831
|
-
//
|
|
10832
|
-
//
|
|
10833
|
-
//
|
|
10834
|
-
|
|
10894
|
+
// Every reply box carries its draft back from SA.draft via the value attribute, so a
|
|
10895
|
+
// rebuild no longer loses what was typed. Keep SA.draft current as the person types —
|
|
10896
|
+
// this is what lets saSync() run during typing instead of freezing.
|
|
10897
|
+
for (const inp of S.querySelectorAll('.sa-chat[data-goal]')) {
|
|
10898
|
+
inp.addEventListener('input', () => {
|
|
10899
|
+
const gid = inp.dataset.goal;
|
|
10900
|
+
if (inp.value) SA.draft[gid] = inp.value; else delete SA.draft[gid];
|
|
10901
|
+
});
|
|
10902
|
+
}
|
|
10903
|
+
// A relevant status change may legitimately rebuild the card. Put focus AND the caret
|
|
10904
|
+
// back where they were.
|
|
10905
|
+
if (focusedReplyGoal != null) {
|
|
10906
|
+
const inp = S.querySelector(`.sa-chat[data-goal="${focusedReplyGoal}"]`);
|
|
10907
|
+
if (inp) {
|
|
10908
|
+
inp.focus({ preventScroll: true });
|
|
10909
|
+
if (focusedCaret) { try { inp.setSelectionRange(focusedCaret.start, focusedCaret.end); } catch { /* not selectable */ } }
|
|
10910
|
+
}
|
|
10911
|
+
}
|
|
10835
10912
|
}
|
|
10836
10913
|
// keep the open Ledger in sync with server state (SSE/refresh → render() → here).
|
|
10837
10914
|
// An item whose goal has RETURNED to 'review' (retest ×3 green, rework done,
|
|
@@ -10839,10 +10916,11 @@ function saRender(){
|
|
|
10839
10916
|
// blocks the rebuild (same "vanishes mid-type" guard as renderFsLane).
|
|
10840
10917
|
function saSync(){
|
|
10841
10918
|
if (!SA.open) return;
|
|
10842
|
-
|
|
10843
|
-
//
|
|
10844
|
-
//
|
|
10845
|
-
|
|
10919
|
+
// No typing guard here any more. It used to `return` whenever the focused input had
|
|
10920
|
+
// text, which protected the draft by freezing EVERYTHING — status changes behind the
|
|
10921
|
+
// dialog included, so a card could sit on "Working on it" for as long as someone was
|
|
10922
|
+
// composing a reply. The draft now lives in SA.draft and is restored with its caret by
|
|
10923
|
+
// saRender, so the sync is free to run (2026-07-30).
|
|
10846
10924
|
// Working cards are intentionally absent from the Review ledger rows after the
|
|
10847
10925
|
// server moves them to To Do / Doing. Keep their held dialog snapshot wired to
|
|
10848
10926
|
// live goal/task state so the status changes in place instead of disappearing.
|
|
@@ -10878,11 +10956,15 @@ async function saFetchDiffs(){
|
|
|
10878
10956
|
if (SA.open) saSync();
|
|
10879
10957
|
}
|
|
10880
10958
|
function saOpen(focusGoalId, solo = false){
|
|
10959
|
+
const wasOpen = SA.open;
|
|
10881
10960
|
SA.open = true;
|
|
10882
10961
|
SA.lastMarkup = null; // a newly opened/focused dialog always gets a clean first paint
|
|
10883
10962
|
SA.solo = solo ? focusGoalId : null; // solo = a single "Needs you" goal opened as one card
|
|
10884
10963
|
SA.items = saLedgerRows().map((r) => ({ ...r, st: 0 }));
|
|
10885
|
-
|
|
10964
|
+
// Only a FRESH open starts with everything folded. saOpen also runs when the person taps
|
|
10965
|
+
// a different card while the dialog is already up, and clearing the set there closed the
|
|
10966
|
+
// Activity / Conversation folds they had just opened on the other cards.
|
|
10967
|
+
if (!wasOpen) SA.detOpen = new Set();
|
|
10886
10968
|
// H14: focus a specific goal's card (single-review tap). Map the tapped goal
|
|
10887
10969
|
// to the card that CONTAINS it (goals can group in the digest), then let
|
|
10888
10970
|
// saRender center it + flash a blue ring. Real data keys by goal id.
|
|
@@ -11015,8 +11097,9 @@ async function saSendReply(it, i, text){
|
|
|
11015
11097
|
text = displayText;
|
|
11016
11098
|
await saEnsureThread(it); // fold in any existing history BEFORE this reply (so it isn't hidden)
|
|
11017
11099
|
const seq = saNextSeq(); // reserve send order NOW, so a late-resolving route keeps its place
|
|
11018
|
-
// Clear the box in place — keep it FOCUSED, not blurred
|
|
11019
|
-
//
|
|
11100
|
+
// Clear the box in place — keep it FOCUSED, not blurred. The draft must be dropped from
|
|
11101
|
+
// SA.draft as well, or the next rebuild would re-populate the box from the sent text.
|
|
11102
|
+
delete SA.draft[goalId];
|
|
11020
11103
|
document.querySelectorAll(`#seeall .sa-item[data-gid="${goalId}"] .sa-chat`).forEach((inp) => { inp.value = ''; });
|
|
11021
11104
|
delete SA.attachments[goalId]; saPaintAttachments(goalId);
|
|
11022
11105
|
// Append the line to the card's own thread immediately — this is what the person sees, and
|
package/engine/lib.mjs
CHANGED
|
@@ -1396,6 +1396,17 @@ export function shouldAutoScroll(scrollTop, scrollHeight, clientHeight, threshol
|
|
|
1396
1396
|
return scrollHeight - clientHeight - scrollTop <= threshold;
|
|
1397
1397
|
}
|
|
1398
1398
|
|
|
1399
|
+
// Should an Enter keydown submit an input (send chat / save / answer)? Only when
|
|
1400
|
+
// the user is NOT mid-IME-composition. Pressing Enter to CONFIRM a Japanese
|
|
1401
|
+
// conversion candidate must commit the text, not send the message. Browsers flag
|
|
1402
|
+
// this via `isComposing` (and legacy `keyCode === 229`); every submit-on-Enter
|
|
1403
|
+
// handler in app/index.html shares this guard so IME confirm never mis-sends.
|
|
1404
|
+
export function shouldSubmitOnEnter(event = {}) {
|
|
1405
|
+
if (event.key !== 'Enter') return false;
|
|
1406
|
+
if (event.isComposing || event.keyCode === 229) return false;
|
|
1407
|
+
return true;
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1399
1410
|
// Decide the scrollTop to apply *after* a re-render replaces #stream's HTML
|
|
1400
1411
|
// (task 93). `pre` is the element's geometry captured BEFORE the innerHTML
|
|
1401
1412
|
// swap; `newScrollHeight` is its height AFTER. If the user was near the bottom
|
package/engine/server.mjs
CHANGED
|
@@ -3032,7 +3032,6 @@ async function runTask(task) {
|
|
|
3032
3032
|
task.changedFiles = afterR.filter((l) => !before.includes(l)).map((l) => l.slice(3));
|
|
3033
3033
|
task.unmappedChangedFiles = unmappedChangedFiles(task.changedFiles, task.requirementEvidence);
|
|
3034
3034
|
notePreviewScope(task);
|
|
3035
|
-
task.requirementVerification = { proof: task.proof ?? null, run: task.run ?? null, changedFiles: task.changedFiles };
|
|
3036
3035
|
sendProcessAct(task, `Detected ${task.changedFiles.length} changed file${task.changedFiles.length === 1 ? '' : 's'} from this follow-up.`);
|
|
3037
3036
|
// An explicit worker receipt takes precedence over Galda's legacy prose
|
|
3038
3037
|
// guard. The agent owns this decision; Manager only transports it.
|
|
@@ -3258,7 +3257,6 @@ async function runTask(task) {
|
|
|
3258
3257
|
task.changedFiles = after.filter((l) => !before.includes(l)).map((l) => l.slice(3));
|
|
3259
3258
|
task.unmappedChangedFiles = unmappedChangedFiles(task.changedFiles, task.requirementEvidence);
|
|
3260
3259
|
notePreviewScope(task);
|
|
3261
|
-
task.requirementVerification = { proof: task.proof ?? null, run: task.run ?? null, changedFiles: task.changedFiles };
|
|
3262
3260
|
sendProcessAct(task, `Detected ${task.changedFiles.length} changed file${task.changedFiles.length === 1 ? '' : 's'} from this task.`);
|
|
3263
3261
|
// An explicit worker receipt takes precedence over Galda's legacy prose
|
|
3264
3262
|
// guard. The agent owns this decision; Manager only transports it.
|
|
@@ -3679,6 +3677,12 @@ function goalChangedFileUnion(goalId) {
|
|
|
3679
3677
|
}
|
|
3680
3678
|
function computeWorktreeChangeLocations(dir) {
|
|
3681
3679
|
try {
|
|
3680
|
+
// `git diff` alone never reports untracked files, so goals that only add
|
|
3681
|
+
// new files would get changeLocations=[] and could never match by
|
|
3682
|
+
// position for auto-fold. `add -N` (intent-to-add) registers the paths
|
|
3683
|
+
// without staging their content, which is enough for them to show up as
|
|
3684
|
+
// normal diff hunks.
|
|
3685
|
+
gitOut(dir, ['add', '-N', '-A']);
|
|
3682
3686
|
return parseGitUnifiedDiffLocations(gitOut(dir, ['diff', '--unified=0']) ?? '');
|
|
3683
3687
|
} catch {
|
|
3684
3688
|
return [];
|
package/package.json
CHANGED