git-coco 0.61.0 → 0.62.1
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/dist/index.esm.mjs +235 -32
- package/dist/index.js +235 -32
- package/package.json +1 -1
package/dist/index.esm.mjs
CHANGED
|
@@ -61,7 +61,7 @@ import { pathToFileURL } from 'url';
|
|
|
61
61
|
/**
|
|
62
62
|
* Current build version from package.json
|
|
63
63
|
*/
|
|
64
|
-
const BUILD_VERSION = "0.
|
|
64
|
+
const BUILD_VERSION = "0.62.1";
|
|
65
65
|
|
|
66
66
|
const isInteractive = (config) => {
|
|
67
67
|
return config?.mode === 'interactive' || !!config?.interactive;
|
|
@@ -22433,6 +22433,18 @@ function getLogInkWorkflowActions() {
|
|
|
22433
22433
|
kind: 'destructive',
|
|
22434
22434
|
requiresConfirmation: true,
|
|
22435
22435
|
},
|
|
22436
|
+
{
|
|
22437
|
+
// No key binding — this is raised by the runtime as a second
|
|
22438
|
+
// confirmation when a safe `delete-branch` (`git branch -d`) is
|
|
22439
|
+
// rejected for an unmerged branch. Reachable from the `:` palette
|
|
22440
|
+
// too, as an explicit force-delete that still gates on y-confirm.
|
|
22441
|
+
id: 'force-delete-branch',
|
|
22442
|
+
key: '',
|
|
22443
|
+
label: 'Force-delete branch',
|
|
22444
|
+
description: 'Force-delete the selected branch even if it is not fully merged (git branch -D).',
|
|
22445
|
+
kind: 'destructive',
|
|
22446
|
+
requiresConfirmation: true,
|
|
22447
|
+
},
|
|
22436
22448
|
{
|
|
22437
22449
|
id: 'delete-tag',
|
|
22438
22450
|
key: 'T',
|
|
@@ -25149,6 +25161,15 @@ function formatSortIndicator(mode, options = {}) {
|
|
|
25149
25161
|
return `${options.ascii ? 'v' : '▼'} ${mode}`;
|
|
25150
25162
|
}
|
|
25151
25163
|
|
|
25164
|
+
/**
|
|
25165
|
+
* True when `pending` (a `state.pendingDeletion`) targets this exact row.
|
|
25166
|
+
* Shared by every deletable surface + the sidebar so the spinner-swap
|
|
25167
|
+
* test is identical everywhere. Takes the field value (not the whole
|
|
25168
|
+
* state) so it can live next to the type without a forward reference.
|
|
25169
|
+
*/
|
|
25170
|
+
function isPendingDeletion(pending, kind, id) {
|
|
25171
|
+
return pending?.kind === kind && pending.id === id;
|
|
25172
|
+
}
|
|
25152
25173
|
const DEFAULT_CHANGELOG_VIEW_STATE = {
|
|
25153
25174
|
status: 'idle',
|
|
25154
25175
|
scrollOffset: 0,
|
|
@@ -26354,6 +26375,10 @@ function applyLogInkAction(state, action) {
|
|
|
26354
26375
|
workflowActionId: action.value ? undefined : state.workflowActionId,
|
|
26355
26376
|
pendingKey: undefined,
|
|
26356
26377
|
};
|
|
26378
|
+
case 'setPendingDeletion':
|
|
26379
|
+
// Pure marker for the in-flight delete; touches nothing else so the
|
|
26380
|
+
// list keeps rendering normally underneath the one spinner'd row.
|
|
26381
|
+
return { ...state, pendingDeletion: action.value };
|
|
26357
26382
|
case 'toggleFilterMode':
|
|
26358
26383
|
return {
|
|
26359
26384
|
...state,
|
|
@@ -29978,6 +30003,24 @@ const SPINNER_TICK_MS = 80;
|
|
|
29978
30003
|
function pickSpinnerFrame(tick) {
|
|
29979
30004
|
return SPINNER_FRAMES[Math.max(0, tick) % SPINNER_FRAMES.length];
|
|
29980
30005
|
}
|
|
30006
|
+
/**
|
|
30007
|
+
* ASCII-safe spinner frames for `NO_COLOR` / ASCII terminals where the
|
|
30008
|
+
* braille dots either don't render or look like noise. The four-frame
|
|
30009
|
+
* `|/-\` cycle is the classic terminal spinner and reads as motion in
|
|
30010
|
+
* any encoding.
|
|
30011
|
+
*/
|
|
30012
|
+
const ASCII_SPINNER_FRAMES = ['|', '/', '-', '\\'];
|
|
30013
|
+
/**
|
|
30014
|
+
* Inline per-item pending glyph — used in place of (or appended to) a
|
|
30015
|
+
* list row's status icon while that row's mutation (a delete) is in
|
|
30016
|
+
* flight. Braille spinner normally; the ASCII cycle under `ascii`
|
|
30017
|
+
* themes so the indicator survives `NO_COLOR` / dumb terminals.
|
|
30018
|
+
*/
|
|
30019
|
+
function inlineSpinnerGlyph(tick, ascii) {
|
|
30020
|
+
return ascii
|
|
30021
|
+
? ASCII_SPINNER_FRAMES[Math.max(0, tick) % ASCII_SPINNER_FRAMES.length]
|
|
30022
|
+
: pickSpinnerFrame(tick);
|
|
30023
|
+
}
|
|
29981
30024
|
|
|
29982
30025
|
/**
|
|
29983
30026
|
* Build the initial `LogInkContextStatus` for a freshly-created frame
|
|
@@ -30932,7 +30975,7 @@ function createBranch(git, branchName, startPoint) {
|
|
|
30932
30975
|
function renameBranch(git, oldName, newName) {
|
|
30933
30976
|
return runAction$5(() => git.raw(['branch', '-m', oldName, newName]), `Renamed ${oldName} to ${newName}`);
|
|
30934
30977
|
}
|
|
30935
|
-
function deleteBranch(git, branch) {
|
|
30978
|
+
function deleteBranch(git, branch, force = false) {
|
|
30936
30979
|
if (branch.type !== 'local') {
|
|
30937
30980
|
return Promise.resolve({
|
|
30938
30981
|
ok: false,
|
|
@@ -30945,7 +30988,18 @@ function deleteBranch(git, branch) {
|
|
|
30945
30988
|
message: 'Cannot delete the current branch.',
|
|
30946
30989
|
});
|
|
30947
30990
|
}
|
|
30948
|
-
|
|
30991
|
+
// `-d` is the safe delete (refuses unmerged branches); `-D` forces it.
|
|
30992
|
+
// The TUI starts with `-d` and only escalates to `-D` after the user
|
|
30993
|
+
// confirms a second time on the "not fully merged" error.
|
|
30994
|
+
return runAction$5(() => git.raw(['branch', force ? '-D' : '-d', branch.shortName]), force ? `Force-deleted branch ${branch.shortName}` : `Deleted branch ${branch.shortName}`);
|
|
30995
|
+
}
|
|
30996
|
+
/**
|
|
30997
|
+
* True when a failed `git branch -d` was rejected specifically because the
|
|
30998
|
+
* branch isn't fully merged (the one case worth offering a force-delete
|
|
30999
|
+
* for). Matches git's wording across versions ("not fully merged").
|
|
31000
|
+
*/
|
|
31001
|
+
function isBranchNotFullyMergedError(message) {
|
|
31002
|
+
return /not fully merged/i.test(message || '');
|
|
30949
31003
|
}
|
|
30950
31004
|
function fetchRemotes(git) {
|
|
30951
31005
|
return runAction$5(() => git.raw(['fetch', '--all', '--prune']), 'Fetched all remotes');
|
|
@@ -34353,7 +34407,13 @@ function renderActiveStatusTabContent(h, Text, context, contextStatus, width, th
|
|
|
34353
34407
|
* rows so they read as the same severity scale used in the main status
|
|
34354
34408
|
* surface; every other tab falls through to selectable rows.
|
|
34355
34409
|
*/
|
|
34356
|
-
function renderActiveSidebarContent(h, Text, tab, state, context, contextStatus, width, bodyRows, theme) {
|
|
34410
|
+
function renderActiveSidebarContent(h, Text, tab, state, context, contextStatus, width, bodyRows, theme, spinnerFrame) {
|
|
34411
|
+
// Inline pending-delete glyph: while a row's delete is in flight it
|
|
34412
|
+
// shows this spinner in place of its leading marker (branches /
|
|
34413
|
+
// worktrees) or appended to the row (tags / stashes, which have no
|
|
34414
|
+
// leading status icon). `pending` is the single in-flight target.
|
|
34415
|
+
const pending = state.pendingDeletion;
|
|
34416
|
+
const spin = inlineSpinnerGlyph(spinnerFrame, theme.ascii);
|
|
34357
34417
|
// Available rows for the active tab's list. The sidebar chrome
|
|
34358
34418
|
// takes ~10 rows (panel title + spacer + 5 tab headers + 4 inter-tab
|
|
34359
34419
|
// spacers); the branches tab eats 3 more for its summary header
|
|
@@ -34390,7 +34450,12 @@ function renderActiveSidebarContent(h, Text, tab, state, context, contextStatus,
|
|
|
34390
34450
|
];
|
|
34391
34451
|
return [
|
|
34392
34452
|
...headerRows,
|
|
34393
|
-
...renderSelectableSidebarRows(h, Text, sortedBranches, state.selectedBranchIndex, focused, width, theme, (branch) =>
|
|
34453
|
+
...renderSelectableSidebarRows(h, Text, sortedBranches, state.selectedBranchIndex, focused, width, theme, (branch) => {
|
|
34454
|
+
const glyph = isPendingDeletion(pending, 'branch', branch.shortName)
|
|
34455
|
+
? spin
|
|
34456
|
+
: branchRowMarker(branch, { ascii: theme.ascii }).glyph;
|
|
34457
|
+
return `${glyph} ${branch.shortName}`;
|
|
34458
|
+
}, 'tab-branches', visibleListCount),
|
|
34394
34459
|
];
|
|
34395
34460
|
}
|
|
34396
34461
|
if (tab === 'tags') {
|
|
@@ -34401,7 +34466,12 @@ function renderActiveSidebarContent(h, Text, tab, state, context, contextStatus,
|
|
|
34401
34466
|
if (tags.length === 0) {
|
|
34402
34467
|
return [h(Text, { key: 'tab-tags-empty', dimColor: true }, ' No tags found')];
|
|
34403
34468
|
}
|
|
34404
|
-
return renderSelectableSidebarRows(h, Text, tags, state.selectedTagIndex, focused, width, theme, (tag) =>
|
|
34469
|
+
return renderSelectableSidebarRows(h, Text, tags, state.selectedTagIndex, focused, width, theme, (tag) => {
|
|
34470
|
+
const base = `${truncateCells(tag.name, 16)} ${tag.subject}`;
|
|
34471
|
+
// Tags have no leading status icon, so the pending spinner is
|
|
34472
|
+
// appended to the row instead of replacing a glyph.
|
|
34473
|
+
return isPendingDeletion(pending, 'tag', tag.name) ? `${base} ${spin}` : base;
|
|
34474
|
+
}, 'tab-tags', visibleListCount);
|
|
34405
34475
|
}
|
|
34406
34476
|
if (tab === 'stashes') {
|
|
34407
34477
|
if (isLogInkContextKeyLoading(contextStatus, 'stashes')) {
|
|
@@ -34411,7 +34481,12 @@ function renderActiveSidebarContent(h, Text, tab, state, context, contextStatus,
|
|
|
34411
34481
|
if (stashes.length === 0) {
|
|
34412
34482
|
return [h(Text, { key: 'tab-stashes-empty', dimColor: true }, ' No stashes found')];
|
|
34413
34483
|
}
|
|
34414
|
-
return renderSelectableSidebarRows(h, Text, stashes, state.selectedStashIndex, focused, width, theme, (stash, index) =>
|
|
34484
|
+
return renderSelectableSidebarRows(h, Text, stashes, state.selectedStashIndex, focused, width, theme, (stash, index) => {
|
|
34485
|
+
const base = `@{${index}} ${stash.message || '(no message)'}`;
|
|
34486
|
+
// `@{N}` is the stash ref, not a status icon, so append the
|
|
34487
|
+
// spinner rather than replacing it.
|
|
34488
|
+
return isPendingDeletion(pending, 'stash', stash.ref) ? `${base} ${spin}` : base;
|
|
34489
|
+
}, 'tab-stashes', visibleListCount);
|
|
34415
34490
|
}
|
|
34416
34491
|
// worktrees
|
|
34417
34492
|
if (isLogInkContextKeyLoading(contextStatus, 'worktreeList')) {
|
|
@@ -34422,12 +34497,14 @@ function renderActiveSidebarContent(h, Text, tab, state, context, contextStatus,
|
|
|
34422
34497
|
return [h(Text, { key: 'tab-worktrees-empty', dimColor: true }, ' No linked worktrees')];
|
|
34423
34498
|
}
|
|
34424
34499
|
return renderSelectableSidebarRows(h, Text, worktrees, state.selectedWorktreeListIndex, focused, width, theme, (worktree) => {
|
|
34425
|
-
const marker =
|
|
34500
|
+
const marker = isPendingDeletion(pending, 'worktree', worktree.path)
|
|
34501
|
+
? spin
|
|
34502
|
+
: worktree.current ? '*' : ' ';
|
|
34426
34503
|
const wstate = worktree.dirty ? 'dirty' : 'clean';
|
|
34427
34504
|
return `${marker} ${worktree.branch || worktree.path} ${wstate}`;
|
|
34428
34505
|
}, 'tab-worktrees', visibleListCount);
|
|
34429
34506
|
}
|
|
34430
|
-
function renderSidebar$1(h, components, state, context, contextStatus, width, bodyRows, theme) {
|
|
34507
|
+
function renderSidebar$1(h, components, state, context, contextStatus, width, bodyRows, theme, spinnerFrame = 0) {
|
|
34431
34508
|
const { Box, Text } = components;
|
|
34432
34509
|
const focused = state.focus === 'sidebar';
|
|
34433
34510
|
const tabs = getLogInkSidebarTabs();
|
|
@@ -34463,7 +34540,7 @@ function renderSidebar$1(h, components, state, context, contextStatus, width, bo
|
|
|
34463
34540
|
inverse: headerSelected,
|
|
34464
34541
|
}, headerText));
|
|
34465
34542
|
if (isActive) {
|
|
34466
|
-
blocks.push(...renderActiveSidebarContent(h, Text, tab, state, context, contextStatus, width, bodyRows, theme));
|
|
34543
|
+
blocks.push(...renderActiveSidebarContent(h, Text, tab, state, context, contextStatus, width, bodyRows, theme, spinnerFrame));
|
|
34467
34544
|
}
|
|
34468
34545
|
return blocks;
|
|
34469
34546
|
});
|
|
@@ -34814,7 +34891,7 @@ function formatLogInkGitHubNoRemote({ resource, }) {
|
|
|
34814
34891
|
* Extracted from `src/commands/log/inkRuntime.ts` as part of phase 5a.2
|
|
34815
34892
|
* of #890. No behavior change.
|
|
34816
34893
|
*/
|
|
34817
|
-
function renderBranchesSurface(ctx) {
|
|
34894
|
+
function renderBranchesSurface(ctx, spinnerFrame = 0) {
|
|
34818
34895
|
const { h, components, state, context, contextStatus, bodyRows, width, theme } = ctx;
|
|
34819
34896
|
const { Box, Text } = components;
|
|
34820
34897
|
const focused = state.focus === 'commits';
|
|
@@ -34853,7 +34930,14 @@ function renderBranchesSurface(ctx) {
|
|
|
34853
34930
|
const isSelected = index === selected;
|
|
34854
34931
|
const cursor = isSelected ? '>' : ' ';
|
|
34855
34932
|
const marker = branchRowMarker(branch, { ascii: theme.ascii });
|
|
34856
|
-
|
|
34933
|
+
// While this branch's delete is in flight, its sync-state marker
|
|
34934
|
+
// is replaced by an inline spinner (accent-coloured) so the row
|
|
34935
|
+
// reads as "deleting" until it vanishes on refresh.
|
|
34936
|
+
const deleting = isPendingDeletion(state.pendingDeletion, 'branch', branch.shortName);
|
|
34937
|
+
const glyph = deleting ? inlineSpinnerGlyph(spinnerFrame, theme.ascii) : marker.glyph;
|
|
34938
|
+
const glyphColor = deleting
|
|
34939
|
+
? (theme.noColor ? undefined : theme.colors.accent)
|
|
34940
|
+
: getBranchRowMarkerColor(marker.kind, theme);
|
|
34857
34941
|
const divergence = formatBranchDivergence(branch, { ascii: theme.ascii });
|
|
34858
34942
|
const lastTouched = formatBranchLastTouched(branch.date, getRenderNow());
|
|
34859
34943
|
// Split the row into spans so the timestamp stays dim even on the
|
|
@@ -34868,7 +34952,7 @@ function renderBranchesSurface(ctx) {
|
|
|
34868
34952
|
// Truncate the assembled line to the actual panel width so a
|
|
34869
34953
|
// narrow inspector / sidebar focus doesn't push branch rows
|
|
34870
34954
|
// onto a second visual line (#830).
|
|
34871
|
-
const fullText = `${cursorAndPad}${
|
|
34955
|
+
const fullText = `${cursorAndPad}${glyph}${trailingName}${timestampPadded}${trailingDivergence}`;
|
|
34872
34956
|
const truncated = truncateCells(fullText, Math.max(20, width - 4));
|
|
34873
34957
|
// If truncation chopped into the timestamp/divergence portion,
|
|
34874
34958
|
// fall back to a single Text to keep the visible width honest.
|
|
@@ -34891,7 +34975,7 @@ function renderBranchesSurface(ctx) {
|
|
|
34891
34975
|
// no-upstream kinds return undefined from
|
|
34892
34976
|
// `getBranchRowMarkerColor`, so those markers inherit the
|
|
34893
34977
|
// row's dim and read as quiet chrome.
|
|
34894
|
-
h(Text, { color:
|
|
34978
|
+
h(Text, { color: glyphColor, dimColor: glyphColor ? false : undefined }, glyph), trailingName, h(Text, { dimColor: true }, timestampPadded), trailingDivergence);
|
|
34895
34979
|
});
|
|
34896
34980
|
// Scroll indicators — same "N more above/below" pattern as the
|
|
34897
34981
|
// sidebar and help overlay so the user knows the list continues.
|
|
@@ -37513,9 +37597,13 @@ function renderConfirmationPanel(h, components, state, width, theme, focused) {
|
|
|
37513
37597
|
? 'You have an unsaved commit draft. Press y to discard it and quit.'
|
|
37514
37598
|
: state.pendingMutationConfirmation
|
|
37515
37599
|
? 'This discards local changes and cannot be undone by Coco.'
|
|
37516
|
-
|
|
37517
|
-
|
|
37518
|
-
|
|
37600
|
+
// Second-stage confirm raised when a safe delete hit an unmerged
|
|
37601
|
+
// branch — name the reason so the force isn't a blind "y again".
|
|
37602
|
+
: state.pendingConfirmationId === 'force-delete-branch'
|
|
37603
|
+
? 'Not fully merged. Force-delete (git branch -D) is irreversible.'
|
|
37604
|
+
: action?.kind === 'ai'
|
|
37605
|
+
? `AI action requires confirmation. Estimated ${action.estimatedTokens || '<unknown>'} tokens.`
|
|
37606
|
+
: 'Destructive Git action requires confirmation.';
|
|
37519
37607
|
return h(Box, {
|
|
37520
37608
|
borderColor: focusBorderColor(theme, focused),
|
|
37521
37609
|
borderStyle: theme.borderStyle,
|
|
@@ -38686,7 +38774,7 @@ function renderReflogSurface(ctx) {
|
|
|
38686
38774
|
* Extracted from `src/commands/log/inkRuntime.ts` as part of phase 5a.1
|
|
38687
38775
|
* of #890. No behavior change.
|
|
38688
38776
|
*/
|
|
38689
|
-
function renderStashSurface(ctx) {
|
|
38777
|
+
function renderStashSurface(ctx, spinnerFrame = 0) {
|
|
38690
38778
|
const { h, components, state, context, contextStatus, bodyRows, width, theme } = ctx;
|
|
38691
38779
|
const { Box, Text } = components;
|
|
38692
38780
|
const focused = state.focus === 'commits';
|
|
@@ -38732,11 +38820,18 @@ function renderStashSurface(ctx) {
|
|
|
38732
38820
|
const rowText = meta
|
|
38733
38821
|
? `${cursor} ${stash.ref.padEnd(11)} ${meta} ${stash.message}`
|
|
38734
38822
|
: `${cursor} ${stash.ref.padEnd(11)} ${stash.message}`;
|
|
38823
|
+
// The `stash@{N}` ref is an identifier, not a status icon, so a
|
|
38824
|
+
// delete-in-flight appends an accent spinner at the row's end
|
|
38825
|
+
// (2 cells reserved from the width budget).
|
|
38826
|
+
const deleting = isPendingDeletion(state.pendingDeletion, 'stash', stash.ref);
|
|
38827
|
+
const spinnerSpan = deleting
|
|
38828
|
+
? h(Text, { color: theme.noColor ? undefined : theme.colors.accent, dimColor: false }, ` ${inlineSpinnerGlyph(spinnerFrame, theme.ascii)}`)
|
|
38829
|
+
: null;
|
|
38735
38830
|
return h(Text, {
|
|
38736
38831
|
key: `stash-${index}`,
|
|
38737
38832
|
bold: isSelected,
|
|
38738
38833
|
dimColor: !isSelected,
|
|
38739
|
-
}, truncateCells(rowText, rowWidth));
|
|
38834
|
+
}, truncateCells(rowText, rowWidth - (deleting ? 2 : 0)), spinnerSpan);
|
|
38740
38835
|
});
|
|
38741
38836
|
const stashHasMoreAbove = startIndex > 0 && stashes.length > 0;
|
|
38742
38837
|
const stashHasMoreBelow = startIndex + listRows < stashes.length;
|
|
@@ -39084,7 +39179,7 @@ function formatHyperlink(text, url, env = process.env) {
|
|
|
39084
39179
|
* Extracted from `src/commands/log/inkRuntime.ts` as part of phase 5a.2
|
|
39085
39180
|
* of #890. No behavior change.
|
|
39086
39181
|
*/
|
|
39087
|
-
function renderTagsSurface(ctx) {
|
|
39182
|
+
function renderTagsSurface(ctx, spinnerFrame = 0) {
|
|
39088
39183
|
const { h, components, state, context, contextStatus, bodyRows, width, theme } = ctx;
|
|
39089
39184
|
const { Box, Text } = components;
|
|
39090
39185
|
const focused = state.focus === 'commits';
|
|
@@ -39125,13 +39220,20 @@ function renderTagsSurface(ctx) {
|
|
|
39125
39220
|
// intact.
|
|
39126
39221
|
const url = buildRefUrl(context.provider?.repository, tag.name);
|
|
39127
39222
|
const namePadded = truncateCells(tag.name, tagNameColWidth).padEnd(tagNameColWidth);
|
|
39128
|
-
|
|
39223
|
+
// Tags have no leading status icon, so a delete-in-flight appends
|
|
39224
|
+
// an accent spinner at the row's end. Reserve its 2 cells from the
|
|
39225
|
+
// truncation budget so it never pushes the row past the panel.
|
|
39226
|
+
const deleting = isPendingDeletion(state.pendingDeletion, 'tag', tag.name);
|
|
39227
|
+
const spinnerSpan = deleting
|
|
39228
|
+
? h(Text, { color: theme.noColor ? undefined : theme.colors.accent, dimColor: false }, ` ${inlineSpinnerGlyph(spinnerFrame, theme.ascii)}`)
|
|
39229
|
+
: null;
|
|
39230
|
+
const lineText = truncateCells(`${cursor} ${namePadded} ${tag.subject}`, Math.max(20, width - 4 - (deleting ? 2 : 0)));
|
|
39129
39231
|
if (!url || lineText.indexOf(namePadded) < 0) {
|
|
39130
39232
|
return h(Text, {
|
|
39131
39233
|
key: `tag-${index}`,
|
|
39132
39234
|
bold: isSelected,
|
|
39133
39235
|
dimColor: !isSelected,
|
|
39134
|
-
}, lineText);
|
|
39236
|
+
}, lineText, spinnerSpan);
|
|
39135
39237
|
}
|
|
39136
39238
|
const linkStart = lineText.indexOf(namePadded);
|
|
39137
39239
|
const before = lineText.slice(0, linkStart);
|
|
@@ -39140,7 +39242,7 @@ function renderTagsSurface(ctx) {
|
|
|
39140
39242
|
key: `tag-${index}`,
|
|
39141
39243
|
bold: isSelected,
|
|
39142
39244
|
dimColor: !isSelected,
|
|
39143
|
-
}, before, formatHyperlink(namePadded, url), after);
|
|
39245
|
+
}, before, formatHyperlink(namePadded, url), after, spinnerSpan);
|
|
39144
39246
|
});
|
|
39145
39247
|
const tagsHasMoreAbove = startIndex > 0 && tags.length > 0;
|
|
39146
39248
|
const tagsHasMoreBelow = startIndex + listRows < tags.length;
|
|
@@ -39167,7 +39269,7 @@ function renderTagsSurface(ctx) {
|
|
|
39167
39269
|
* Extracted from `src/commands/log/inkRuntime.ts` as part of phase 5a.1
|
|
39168
39270
|
* of #890. No behavior change.
|
|
39169
39271
|
*/
|
|
39170
|
-
function renderWorktreesSurface(ctx) {
|
|
39272
|
+
function renderWorktreesSurface(ctx, spinnerFrame = 0) {
|
|
39171
39273
|
const { h, components, state, context, contextStatus, bodyRows, width, theme } = ctx;
|
|
39172
39274
|
const { Box, Text } = components;
|
|
39173
39275
|
const focused = state.focus === 'commits';
|
|
@@ -39203,7 +39305,9 @@ function renderWorktreesSurface(ctx) {
|
|
|
39203
39305
|
const index = startIndex + offset;
|
|
39204
39306
|
const isSelected = index === selected;
|
|
39205
39307
|
const cursor = isSelected ? '>' : ' ';
|
|
39206
|
-
const marker =
|
|
39308
|
+
const marker = isPendingDeletion(state.pendingDeletion, 'worktree', entry.path)
|
|
39309
|
+
? inlineSpinnerGlyph(spinnerFrame, theme.ascii)
|
|
39310
|
+
: entry.current ? '*' : ' ';
|
|
39207
39311
|
const branchLabel = entry.branch ? entry.branch : entry.head || '<detached>';
|
|
39208
39312
|
const stateLabel = entry.dirty ? 'dirty' : 'clean';
|
|
39209
39313
|
const branchPadded = truncateCells(branchLabel, branchColWidth).padEnd(branchColWidth);
|
|
@@ -39273,10 +39377,10 @@ function renderMainPanel(surface, worktreeDiff, worktreeDiffLoading, worktreeHun
|
|
|
39273
39377
|
return renderComposeSurface(surface, spinnerFrame);
|
|
39274
39378
|
}
|
|
39275
39379
|
if (state.activeView === 'branches') {
|
|
39276
|
-
return renderBranchesSurface(surface);
|
|
39380
|
+
return renderBranchesSurface(surface, spinnerFrame);
|
|
39277
39381
|
}
|
|
39278
39382
|
if (state.activeView === 'tags') {
|
|
39279
|
-
return renderTagsSurface(surface);
|
|
39383
|
+
return renderTagsSurface(surface, spinnerFrame);
|
|
39280
39384
|
}
|
|
39281
39385
|
if (state.activeView === 'reflog') {
|
|
39282
39386
|
return renderReflogSurface(surface);
|
|
@@ -39285,10 +39389,10 @@ function renderMainPanel(surface, worktreeDiff, worktreeDiffLoading, worktreeHun
|
|
|
39285
39389
|
return renderBisectSurface(surface, bisectCandidateDetail, bisectCandidateLoading);
|
|
39286
39390
|
}
|
|
39287
39391
|
if (state.activeView === 'stash') {
|
|
39288
|
-
return renderStashSurface(surface);
|
|
39392
|
+
return renderStashSurface(surface, spinnerFrame);
|
|
39289
39393
|
}
|
|
39290
39394
|
if (state.activeView === 'worktrees') {
|
|
39291
|
-
return renderWorktreesSurface(surface);
|
|
39395
|
+
return renderWorktreesSurface(surface, spinnerFrame);
|
|
39292
39396
|
}
|
|
39293
39397
|
if (state.activeView === 'submodules') {
|
|
39294
39398
|
return renderSubmodulesSurface(surface);
|
|
@@ -40661,6 +40765,53 @@ const REMOTE_OP_LOADERS = {
|
|
|
40661
40765
|
'pull-selected-branch': { kind: 'pull', label: 'Pulling branch from remote…' },
|
|
40662
40766
|
'push-selected-branch': { kind: 'push', label: 'Pushing branch to remote…' },
|
|
40663
40767
|
};
|
|
40768
|
+
/**
|
|
40769
|
+
* Resolve which list row a delete workflow is about to act on, so the
|
|
40770
|
+
* runner can mark it pending (inline spinner) for the duration of the
|
|
40771
|
+
* git call. Mirrors the cursored-target resolution inside each delete
|
|
40772
|
+
* handler exactly — same sort, same promoted-filter, same selection
|
|
40773
|
+
* index — so the spinner lands on the row that actually gets deleted.
|
|
40774
|
+
* Returns `undefined` for non-delete workflows (and when nothing is
|
|
40775
|
+
* selected), which the runner treats as "no pending marker".
|
|
40776
|
+
*/
|
|
40777
|
+
function resolvePendingDeletion(id, state, context) {
|
|
40778
|
+
const { filter } = state;
|
|
40779
|
+
if (id === 'delete-branch' || id === 'force-delete-branch') {
|
|
40780
|
+
const all = sortBranches(context.branches?.localBranches || [], state.branchSort);
|
|
40781
|
+
const visible = filter
|
|
40782
|
+
? all.filter((b) => matchesPromotedFilter([b.shortName, b.upstream || ''], filter))
|
|
40783
|
+
: all;
|
|
40784
|
+
const branch = visible[Math.min(state.selectedBranchIndex, visible.length - 1)];
|
|
40785
|
+
return branch ? { kind: 'branch', id: branch.shortName } : undefined;
|
|
40786
|
+
}
|
|
40787
|
+
if (id === 'delete-tag') {
|
|
40788
|
+
const all = sortTags(context.tags?.tags || [], state.tagSort);
|
|
40789
|
+
const visible = filter
|
|
40790
|
+
? all.filter((t) => matchesPromotedFilter([t.name, t.subject], filter))
|
|
40791
|
+
: all;
|
|
40792
|
+
const tag = visible[Math.min(state.selectedTagIndex, visible.length - 1)];
|
|
40793
|
+
return tag ? { kind: 'tag', id: tag.name } : undefined;
|
|
40794
|
+
}
|
|
40795
|
+
if (id === 'drop-stash') {
|
|
40796
|
+
const all = context.stashes?.stashes || [];
|
|
40797
|
+
const visible = filter
|
|
40798
|
+
? all.filter((s) => matchesPromotedFilter([s.ref, s.message], filter))
|
|
40799
|
+
: all;
|
|
40800
|
+
const stash = visible[Math.min(state.selectedStashIndex, visible.length - 1)];
|
|
40801
|
+
return stash ? { kind: 'stash', id: stash.ref } : undefined;
|
|
40802
|
+
}
|
|
40803
|
+
if (id === 'remove-worktree') {
|
|
40804
|
+
const all = context.worktreeList?.worktrees || [];
|
|
40805
|
+
const visible = filter
|
|
40806
|
+
? all.filter((w) => matchesPromotedFilter([w.path, w.branch || ''], filter))
|
|
40807
|
+
: all;
|
|
40808
|
+
const wt = visible.length
|
|
40809
|
+
? visible[Math.min(state.selectedWorktreeListIndex, visible.length - 1)]
|
|
40810
|
+
: all[Math.min(state.selectedWorktreeListIndex, Math.max(0, all.length - 1))];
|
|
40811
|
+
return wt ? { kind: 'worktree', id: wt.path } : undefined;
|
|
40812
|
+
}
|
|
40813
|
+
return undefined;
|
|
40814
|
+
}
|
|
40664
40815
|
function predictNextFilter(action, currentFilter) {
|
|
40665
40816
|
switch (action.type) {
|
|
40666
40817
|
case 'appendFilter':
|
|
@@ -40977,7 +41128,10 @@ function LogInkApp(deps) {
|
|
|
40977
41128
|
state.changelogView.status === 'loading' ||
|
|
40978
41129
|
state.commitCompose.loading ||
|
|
40979
41130
|
Boolean(state.remoteOp) ||
|
|
40980
|
-
Boolean(state.statusLoading)
|
|
41131
|
+
Boolean(state.statusLoading) ||
|
|
41132
|
+
// Keep the shared spinner ticking while a list-item delete is in
|
|
41133
|
+
// flight so its inline pending glyph animates instead of freezing.
|
|
41134
|
+
Boolean(state.pendingDeletion);
|
|
40981
41135
|
React.useEffect(() => {
|
|
40982
41136
|
if (!anyLoading) {
|
|
40983
41137
|
// Reset to 0 so the next loading state starts from a known
|
|
@@ -43134,6 +43288,16 @@ function LogInkApp(deps) {
|
|
|
43134
43288
|
return { ok: false, message: 'No branch selected' };
|
|
43135
43289
|
return deleteBranch(git, branch);
|
|
43136
43290
|
},
|
|
43291
|
+
'force-delete-branch': async () => {
|
|
43292
|
+
const all = sortBranches(context.branches?.localBranches || [], state.branchSort);
|
|
43293
|
+
const visible = state.filter
|
|
43294
|
+
? all.filter((b) => matchesPromotedFilter([b.shortName, b.upstream || ''], state.filter))
|
|
43295
|
+
: all;
|
|
43296
|
+
const branch = visible[Math.min(state.selectedBranchIndex, visible.length - 1)];
|
|
43297
|
+
if (!branch)
|
|
43298
|
+
return { ok: false, message: 'No branch selected' };
|
|
43299
|
+
return deleteBranch(git, branch, true);
|
|
43300
|
+
},
|
|
43137
43301
|
'delete-tag': async () => {
|
|
43138
43302
|
const all = sortTags(context.tags?.tags || [], state.tagSort);
|
|
43139
43303
|
const visible = state.filter
|
|
@@ -43907,9 +44071,26 @@ function LogInkApp(deps) {
|
|
|
43907
44071
|
if (remoteOp) {
|
|
43908
44072
|
dispatch({ type: 'setRemoteOp', value: remoteOp });
|
|
43909
44073
|
}
|
|
44074
|
+
// Mark the cursored row as deleting so it shows an inline pending
|
|
44075
|
+
// spinner while the git call runs. Cleared in `finally` after the
|
|
44076
|
+
// refresh, so a successful delete hands straight off to the row
|
|
44077
|
+
// vanishing, and a failed one (e.g. an unmerged branch) restores
|
|
44078
|
+
// the row's normal icon alongside the error status.
|
|
44079
|
+
const pendingDeletion = resolvePendingDeletion(id, state, context);
|
|
44080
|
+
if (pendingDeletion) {
|
|
44081
|
+
dispatch({ type: 'setPendingDeletion', value: pendingDeletion });
|
|
44082
|
+
}
|
|
43910
44083
|
try {
|
|
43911
44084
|
const result = await handler();
|
|
43912
44085
|
dispatch({ type: 'setStatus', value: result?.message || 'Workflow action complete' });
|
|
44086
|
+
// A safe `delete-branch` (`git branch -d`) refuses branches that
|
|
44087
|
+
// aren't fully merged. Rather than dead-end on git's raw error, raise
|
|
44088
|
+
// a second y-confirm offering the force-delete (`git branch -D`). The
|
|
44089
|
+
// cursor hasn't moved (the delete failed), so the force handler
|
|
44090
|
+
// re-resolves the same branch.
|
|
44091
|
+
if (id === 'delete-branch' && !result?.ok && isBranchNotFullyMergedError(result?.message)) {
|
|
44092
|
+
dispatch({ type: 'setPendingConfirmation', value: 'force-delete-branch' });
|
|
44093
|
+
}
|
|
43913
44094
|
// Refresh history rows AS WELL when the workflow could have
|
|
43914
44095
|
// changed the commits the user sees (#945 follow-up). The
|
|
43915
44096
|
// workflow IDs below all either create/rewrite local commits or
|
|
@@ -44008,6 +44189,12 @@ function LogInkApp(deps) {
|
|
|
44008
44189
|
if (remoteOp) {
|
|
44009
44190
|
dispatch({ type: 'setRemoteOp', value: undefined });
|
|
44010
44191
|
}
|
|
44192
|
+
// Same guarantee for the per-row delete spinner: clear it whether
|
|
44193
|
+
// the delete succeeded, failed, or the refresh threw, so no row is
|
|
44194
|
+
// left spinning forever.
|
|
44195
|
+
if (pendingDeletion) {
|
|
44196
|
+
dispatch({ type: 'setPendingDeletion', value: undefined });
|
|
44197
|
+
}
|
|
44011
44198
|
}
|
|
44012
44199
|
}, [context, dispatch, git, refreshContext, refreshHistoryRows, refreshWorktreeContext,
|
|
44013
44200
|
state.branchSort, state.filter, state.selectedBranchIndex,
|
|
@@ -44867,7 +45054,7 @@ function LogInkApp(deps) {
|
|
|
44867
45054
|
// Panel renderers are thunks so single-pane mode can build only the
|
|
44868
45055
|
// visible pane — the main-panel render in particular is expensive, so
|
|
44869
45056
|
// we don't want to invoke the two hidden ones just to drop them.
|
|
44870
|
-
const sidebarPanel = () => renderSidebar$1(h, { Box, Text }, state, context, contextStatus, layout.sidebarWidth, layout.bodyRows, theme);
|
|
45057
|
+
const sidebarPanel = () => renderSidebar$1(h, { Box, Text }, state, context, contextStatus, layout.sidebarWidth, layout.bodyRows, theme, spinnerFrame);
|
|
44871
45058
|
const mainSurface = {
|
|
44872
45059
|
h,
|
|
44873
45060
|
components: { Box, Text },
|
|
@@ -45178,12 +45365,23 @@ function createLogArgvFromUiArgv(argv) {
|
|
|
45178
45365
|
// starting state. `coco ui --no-all` opts back to
|
|
45179
45366
|
// current-branch-only.
|
|
45180
45367
|
//
|
|
45368
|
+
// `?? true` re-asserts that default for the synthetic-argv path:
|
|
45369
|
+
// bare `coco` routes through defaultRouteHandler → buildSyntheticArgv,
|
|
45370
|
+
// which bypasses yargs and so never applies the `default: true` from
|
|
45371
|
+
// ui/config.ts — leaving `argv.all` undefined (#1169). Without the
|
|
45372
|
+
// fallback the workstation booted in compact (`--first-parent
|
|
45373
|
+
// --no-merges`) mode: fewer commits, branches "ahead" of HEAD hidden,
|
|
45374
|
+
// and cursoring/checking-out a branch whose tip wasn't in the compact
|
|
45375
|
+
// window triggered an anchored-context append that looped the graph
|
|
45376
|
+
// back to the initial commit. Explicit `--no-all` (argv.all === false)
|
|
45377
|
+
// is preserved because `false ?? true === false`.
|
|
45378
|
+
//
|
|
45181
45379
|
// Note: passing `--branch foo` does NOT automatically scope away
|
|
45182
45380
|
// from --all. If the user wants strictly that branch, they pass
|
|
45183
45381
|
// `coco ui --branch foo --no-all`. We considered the implicit
|
|
45184
45382
|
// scope-narrowing but it surprises users who pass `--branch` as
|
|
45185
45383
|
// a "highlight this branch in the all-refs view" hint.
|
|
45186
|
-
all: argv.all,
|
|
45384
|
+
all: argv.all ?? true,
|
|
45187
45385
|
branch: argv.branch,
|
|
45188
45386
|
format: 'table',
|
|
45189
45387
|
interactive: true,
|
|
@@ -50197,6 +50395,11 @@ async function probeIsGitRepo() {
|
|
|
50197
50395
|
* `InitArgv`, `UiArgv`, `WorkspaceArgv`) so we can't just spread the
|
|
50198
50396
|
* raw default argv — we have to project the shared fields and let
|
|
50199
50397
|
* the handler fill in command-specific defaults.
|
|
50398
|
+
*
|
|
50399
|
+
* Exported for testing: this is the path bare `coco` takes to the
|
|
50400
|
+
* workstation, and it intentionally does NOT set `all` — yargs's
|
|
50401
|
+
* `default: true` for `coco ui` never runs here, so the ui→log argv
|
|
50402
|
+
* mapping has to re-assert that default (#1169).
|
|
50200
50403
|
*/
|
|
50201
50404
|
function buildSyntheticArgv(argv) {
|
|
50202
50405
|
return {
|
package/dist/index.js
CHANGED
|
@@ -78,7 +78,7 @@ var readline__namespace = /*#__PURE__*/_interopNamespaceDefault(readline);
|
|
|
78
78
|
/**
|
|
79
79
|
* Current build version from package.json
|
|
80
80
|
*/
|
|
81
|
-
const BUILD_VERSION = "0.
|
|
81
|
+
const BUILD_VERSION = "0.62.1";
|
|
82
82
|
|
|
83
83
|
const isInteractive = (config) => {
|
|
84
84
|
return config?.mode === 'interactive' || !!config?.interactive;
|
|
@@ -22450,6 +22450,18 @@ function getLogInkWorkflowActions() {
|
|
|
22450
22450
|
kind: 'destructive',
|
|
22451
22451
|
requiresConfirmation: true,
|
|
22452
22452
|
},
|
|
22453
|
+
{
|
|
22454
|
+
// No key binding — this is raised by the runtime as a second
|
|
22455
|
+
// confirmation when a safe `delete-branch` (`git branch -d`) is
|
|
22456
|
+
// rejected for an unmerged branch. Reachable from the `:` palette
|
|
22457
|
+
// too, as an explicit force-delete that still gates on y-confirm.
|
|
22458
|
+
id: 'force-delete-branch',
|
|
22459
|
+
key: '',
|
|
22460
|
+
label: 'Force-delete branch',
|
|
22461
|
+
description: 'Force-delete the selected branch even if it is not fully merged (git branch -D).',
|
|
22462
|
+
kind: 'destructive',
|
|
22463
|
+
requiresConfirmation: true,
|
|
22464
|
+
},
|
|
22453
22465
|
{
|
|
22454
22466
|
id: 'delete-tag',
|
|
22455
22467
|
key: 'T',
|
|
@@ -25166,6 +25178,15 @@ function formatSortIndicator(mode, options = {}) {
|
|
|
25166
25178
|
return `${options.ascii ? 'v' : '▼'} ${mode}`;
|
|
25167
25179
|
}
|
|
25168
25180
|
|
|
25181
|
+
/**
|
|
25182
|
+
* True when `pending` (a `state.pendingDeletion`) targets this exact row.
|
|
25183
|
+
* Shared by every deletable surface + the sidebar so the spinner-swap
|
|
25184
|
+
* test is identical everywhere. Takes the field value (not the whole
|
|
25185
|
+
* state) so it can live next to the type without a forward reference.
|
|
25186
|
+
*/
|
|
25187
|
+
function isPendingDeletion(pending, kind, id) {
|
|
25188
|
+
return pending?.kind === kind && pending.id === id;
|
|
25189
|
+
}
|
|
25169
25190
|
const DEFAULT_CHANGELOG_VIEW_STATE = {
|
|
25170
25191
|
status: 'idle',
|
|
25171
25192
|
scrollOffset: 0,
|
|
@@ -26371,6 +26392,10 @@ function applyLogInkAction(state, action) {
|
|
|
26371
26392
|
workflowActionId: action.value ? undefined : state.workflowActionId,
|
|
26372
26393
|
pendingKey: undefined,
|
|
26373
26394
|
};
|
|
26395
|
+
case 'setPendingDeletion':
|
|
26396
|
+
// Pure marker for the in-flight delete; touches nothing else so the
|
|
26397
|
+
// list keeps rendering normally underneath the one spinner'd row.
|
|
26398
|
+
return { ...state, pendingDeletion: action.value };
|
|
26374
26399
|
case 'toggleFilterMode':
|
|
26375
26400
|
return {
|
|
26376
26401
|
...state,
|
|
@@ -29995,6 +30020,24 @@ const SPINNER_TICK_MS = 80;
|
|
|
29995
30020
|
function pickSpinnerFrame(tick) {
|
|
29996
30021
|
return SPINNER_FRAMES[Math.max(0, tick) % SPINNER_FRAMES.length];
|
|
29997
30022
|
}
|
|
30023
|
+
/**
|
|
30024
|
+
* ASCII-safe spinner frames for `NO_COLOR` / ASCII terminals where the
|
|
30025
|
+
* braille dots either don't render or look like noise. The four-frame
|
|
30026
|
+
* `|/-\` cycle is the classic terminal spinner and reads as motion in
|
|
30027
|
+
* any encoding.
|
|
30028
|
+
*/
|
|
30029
|
+
const ASCII_SPINNER_FRAMES = ['|', '/', '-', '\\'];
|
|
30030
|
+
/**
|
|
30031
|
+
* Inline per-item pending glyph — used in place of (or appended to) a
|
|
30032
|
+
* list row's status icon while that row's mutation (a delete) is in
|
|
30033
|
+
* flight. Braille spinner normally; the ASCII cycle under `ascii`
|
|
30034
|
+
* themes so the indicator survives `NO_COLOR` / dumb terminals.
|
|
30035
|
+
*/
|
|
30036
|
+
function inlineSpinnerGlyph(tick, ascii) {
|
|
30037
|
+
return ascii
|
|
30038
|
+
? ASCII_SPINNER_FRAMES[Math.max(0, tick) % ASCII_SPINNER_FRAMES.length]
|
|
30039
|
+
: pickSpinnerFrame(tick);
|
|
30040
|
+
}
|
|
29998
30041
|
|
|
29999
30042
|
/**
|
|
30000
30043
|
* Build the initial `LogInkContextStatus` for a freshly-created frame
|
|
@@ -30949,7 +30992,7 @@ function createBranch(git, branchName, startPoint) {
|
|
|
30949
30992
|
function renameBranch(git, oldName, newName) {
|
|
30950
30993
|
return runAction$5(() => git.raw(['branch', '-m', oldName, newName]), `Renamed ${oldName} to ${newName}`);
|
|
30951
30994
|
}
|
|
30952
|
-
function deleteBranch(git, branch) {
|
|
30995
|
+
function deleteBranch(git, branch, force = false) {
|
|
30953
30996
|
if (branch.type !== 'local') {
|
|
30954
30997
|
return Promise.resolve({
|
|
30955
30998
|
ok: false,
|
|
@@ -30962,7 +31005,18 @@ function deleteBranch(git, branch) {
|
|
|
30962
31005
|
message: 'Cannot delete the current branch.',
|
|
30963
31006
|
});
|
|
30964
31007
|
}
|
|
30965
|
-
|
|
31008
|
+
// `-d` is the safe delete (refuses unmerged branches); `-D` forces it.
|
|
31009
|
+
// The TUI starts with `-d` and only escalates to `-D` after the user
|
|
31010
|
+
// confirms a second time on the "not fully merged" error.
|
|
31011
|
+
return runAction$5(() => git.raw(['branch', force ? '-D' : '-d', branch.shortName]), force ? `Force-deleted branch ${branch.shortName}` : `Deleted branch ${branch.shortName}`);
|
|
31012
|
+
}
|
|
31013
|
+
/**
|
|
31014
|
+
* True when a failed `git branch -d` was rejected specifically because the
|
|
31015
|
+
* branch isn't fully merged (the one case worth offering a force-delete
|
|
31016
|
+
* for). Matches git's wording across versions ("not fully merged").
|
|
31017
|
+
*/
|
|
31018
|
+
function isBranchNotFullyMergedError(message) {
|
|
31019
|
+
return /not fully merged/i.test(message || '');
|
|
30966
31020
|
}
|
|
30967
31021
|
function fetchRemotes(git) {
|
|
30968
31022
|
return runAction$5(() => git.raw(['fetch', '--all', '--prune']), 'Fetched all remotes');
|
|
@@ -34370,7 +34424,13 @@ function renderActiveStatusTabContent(h, Text, context, contextStatus, width, th
|
|
|
34370
34424
|
* rows so they read as the same severity scale used in the main status
|
|
34371
34425
|
* surface; every other tab falls through to selectable rows.
|
|
34372
34426
|
*/
|
|
34373
|
-
function renderActiveSidebarContent(h, Text, tab, state, context, contextStatus, width, bodyRows, theme) {
|
|
34427
|
+
function renderActiveSidebarContent(h, Text, tab, state, context, contextStatus, width, bodyRows, theme, spinnerFrame) {
|
|
34428
|
+
// Inline pending-delete glyph: while a row's delete is in flight it
|
|
34429
|
+
// shows this spinner in place of its leading marker (branches /
|
|
34430
|
+
// worktrees) or appended to the row (tags / stashes, which have no
|
|
34431
|
+
// leading status icon). `pending` is the single in-flight target.
|
|
34432
|
+
const pending = state.pendingDeletion;
|
|
34433
|
+
const spin = inlineSpinnerGlyph(spinnerFrame, theme.ascii);
|
|
34374
34434
|
// Available rows for the active tab's list. The sidebar chrome
|
|
34375
34435
|
// takes ~10 rows (panel title + spacer + 5 tab headers + 4 inter-tab
|
|
34376
34436
|
// spacers); the branches tab eats 3 more for its summary header
|
|
@@ -34407,7 +34467,12 @@ function renderActiveSidebarContent(h, Text, tab, state, context, contextStatus,
|
|
|
34407
34467
|
];
|
|
34408
34468
|
return [
|
|
34409
34469
|
...headerRows,
|
|
34410
|
-
...renderSelectableSidebarRows(h, Text, sortedBranches, state.selectedBranchIndex, focused, width, theme, (branch) =>
|
|
34470
|
+
...renderSelectableSidebarRows(h, Text, sortedBranches, state.selectedBranchIndex, focused, width, theme, (branch) => {
|
|
34471
|
+
const glyph = isPendingDeletion(pending, 'branch', branch.shortName)
|
|
34472
|
+
? spin
|
|
34473
|
+
: branchRowMarker(branch, { ascii: theme.ascii }).glyph;
|
|
34474
|
+
return `${glyph} ${branch.shortName}`;
|
|
34475
|
+
}, 'tab-branches', visibleListCount),
|
|
34411
34476
|
];
|
|
34412
34477
|
}
|
|
34413
34478
|
if (tab === 'tags') {
|
|
@@ -34418,7 +34483,12 @@ function renderActiveSidebarContent(h, Text, tab, state, context, contextStatus,
|
|
|
34418
34483
|
if (tags.length === 0) {
|
|
34419
34484
|
return [h(Text, { key: 'tab-tags-empty', dimColor: true }, ' No tags found')];
|
|
34420
34485
|
}
|
|
34421
|
-
return renderSelectableSidebarRows(h, Text, tags, state.selectedTagIndex, focused, width, theme, (tag) =>
|
|
34486
|
+
return renderSelectableSidebarRows(h, Text, tags, state.selectedTagIndex, focused, width, theme, (tag) => {
|
|
34487
|
+
const base = `${truncateCells(tag.name, 16)} ${tag.subject}`;
|
|
34488
|
+
// Tags have no leading status icon, so the pending spinner is
|
|
34489
|
+
// appended to the row instead of replacing a glyph.
|
|
34490
|
+
return isPendingDeletion(pending, 'tag', tag.name) ? `${base} ${spin}` : base;
|
|
34491
|
+
}, 'tab-tags', visibleListCount);
|
|
34422
34492
|
}
|
|
34423
34493
|
if (tab === 'stashes') {
|
|
34424
34494
|
if (isLogInkContextKeyLoading(contextStatus, 'stashes')) {
|
|
@@ -34428,7 +34498,12 @@ function renderActiveSidebarContent(h, Text, tab, state, context, contextStatus,
|
|
|
34428
34498
|
if (stashes.length === 0) {
|
|
34429
34499
|
return [h(Text, { key: 'tab-stashes-empty', dimColor: true }, ' No stashes found')];
|
|
34430
34500
|
}
|
|
34431
|
-
return renderSelectableSidebarRows(h, Text, stashes, state.selectedStashIndex, focused, width, theme, (stash, index) =>
|
|
34501
|
+
return renderSelectableSidebarRows(h, Text, stashes, state.selectedStashIndex, focused, width, theme, (stash, index) => {
|
|
34502
|
+
const base = `@{${index}} ${stash.message || '(no message)'}`;
|
|
34503
|
+
// `@{N}` is the stash ref, not a status icon, so append the
|
|
34504
|
+
// spinner rather than replacing it.
|
|
34505
|
+
return isPendingDeletion(pending, 'stash', stash.ref) ? `${base} ${spin}` : base;
|
|
34506
|
+
}, 'tab-stashes', visibleListCount);
|
|
34432
34507
|
}
|
|
34433
34508
|
// worktrees
|
|
34434
34509
|
if (isLogInkContextKeyLoading(contextStatus, 'worktreeList')) {
|
|
@@ -34439,12 +34514,14 @@ function renderActiveSidebarContent(h, Text, tab, state, context, contextStatus,
|
|
|
34439
34514
|
return [h(Text, { key: 'tab-worktrees-empty', dimColor: true }, ' No linked worktrees')];
|
|
34440
34515
|
}
|
|
34441
34516
|
return renderSelectableSidebarRows(h, Text, worktrees, state.selectedWorktreeListIndex, focused, width, theme, (worktree) => {
|
|
34442
|
-
const marker =
|
|
34517
|
+
const marker = isPendingDeletion(pending, 'worktree', worktree.path)
|
|
34518
|
+
? spin
|
|
34519
|
+
: worktree.current ? '*' : ' ';
|
|
34443
34520
|
const wstate = worktree.dirty ? 'dirty' : 'clean';
|
|
34444
34521
|
return `${marker} ${worktree.branch || worktree.path} ${wstate}`;
|
|
34445
34522
|
}, 'tab-worktrees', visibleListCount);
|
|
34446
34523
|
}
|
|
34447
|
-
function renderSidebar$1(h, components, state, context, contextStatus, width, bodyRows, theme) {
|
|
34524
|
+
function renderSidebar$1(h, components, state, context, contextStatus, width, bodyRows, theme, spinnerFrame = 0) {
|
|
34448
34525
|
const { Box, Text } = components;
|
|
34449
34526
|
const focused = state.focus === 'sidebar';
|
|
34450
34527
|
const tabs = getLogInkSidebarTabs();
|
|
@@ -34480,7 +34557,7 @@ function renderSidebar$1(h, components, state, context, contextStatus, width, bo
|
|
|
34480
34557
|
inverse: headerSelected,
|
|
34481
34558
|
}, headerText));
|
|
34482
34559
|
if (isActive) {
|
|
34483
|
-
blocks.push(...renderActiveSidebarContent(h, Text, tab, state, context, contextStatus, width, bodyRows, theme));
|
|
34560
|
+
blocks.push(...renderActiveSidebarContent(h, Text, tab, state, context, contextStatus, width, bodyRows, theme, spinnerFrame));
|
|
34484
34561
|
}
|
|
34485
34562
|
return blocks;
|
|
34486
34563
|
});
|
|
@@ -34831,7 +34908,7 @@ function formatLogInkGitHubNoRemote({ resource, }) {
|
|
|
34831
34908
|
* Extracted from `src/commands/log/inkRuntime.ts` as part of phase 5a.2
|
|
34832
34909
|
* of #890. No behavior change.
|
|
34833
34910
|
*/
|
|
34834
|
-
function renderBranchesSurface(ctx) {
|
|
34911
|
+
function renderBranchesSurface(ctx, spinnerFrame = 0) {
|
|
34835
34912
|
const { h, components, state, context, contextStatus, bodyRows, width, theme } = ctx;
|
|
34836
34913
|
const { Box, Text } = components;
|
|
34837
34914
|
const focused = state.focus === 'commits';
|
|
@@ -34870,7 +34947,14 @@ function renderBranchesSurface(ctx) {
|
|
|
34870
34947
|
const isSelected = index === selected;
|
|
34871
34948
|
const cursor = isSelected ? '>' : ' ';
|
|
34872
34949
|
const marker = branchRowMarker(branch, { ascii: theme.ascii });
|
|
34873
|
-
|
|
34950
|
+
// While this branch's delete is in flight, its sync-state marker
|
|
34951
|
+
// is replaced by an inline spinner (accent-coloured) so the row
|
|
34952
|
+
// reads as "deleting" until it vanishes on refresh.
|
|
34953
|
+
const deleting = isPendingDeletion(state.pendingDeletion, 'branch', branch.shortName);
|
|
34954
|
+
const glyph = deleting ? inlineSpinnerGlyph(spinnerFrame, theme.ascii) : marker.glyph;
|
|
34955
|
+
const glyphColor = deleting
|
|
34956
|
+
? (theme.noColor ? undefined : theme.colors.accent)
|
|
34957
|
+
: getBranchRowMarkerColor(marker.kind, theme);
|
|
34874
34958
|
const divergence = formatBranchDivergence(branch, { ascii: theme.ascii });
|
|
34875
34959
|
const lastTouched = formatBranchLastTouched(branch.date, getRenderNow());
|
|
34876
34960
|
// Split the row into spans so the timestamp stays dim even on the
|
|
@@ -34885,7 +34969,7 @@ function renderBranchesSurface(ctx) {
|
|
|
34885
34969
|
// Truncate the assembled line to the actual panel width so a
|
|
34886
34970
|
// narrow inspector / sidebar focus doesn't push branch rows
|
|
34887
34971
|
// onto a second visual line (#830).
|
|
34888
|
-
const fullText = `${cursorAndPad}${
|
|
34972
|
+
const fullText = `${cursorAndPad}${glyph}${trailingName}${timestampPadded}${trailingDivergence}`;
|
|
34889
34973
|
const truncated = truncateCells(fullText, Math.max(20, width - 4));
|
|
34890
34974
|
// If truncation chopped into the timestamp/divergence portion,
|
|
34891
34975
|
// fall back to a single Text to keep the visible width honest.
|
|
@@ -34908,7 +34992,7 @@ function renderBranchesSurface(ctx) {
|
|
|
34908
34992
|
// no-upstream kinds return undefined from
|
|
34909
34993
|
// `getBranchRowMarkerColor`, so those markers inherit the
|
|
34910
34994
|
// row's dim and read as quiet chrome.
|
|
34911
|
-
h(Text, { color:
|
|
34995
|
+
h(Text, { color: glyphColor, dimColor: glyphColor ? false : undefined }, glyph), trailingName, h(Text, { dimColor: true }, timestampPadded), trailingDivergence);
|
|
34912
34996
|
});
|
|
34913
34997
|
// Scroll indicators — same "N more above/below" pattern as the
|
|
34914
34998
|
// sidebar and help overlay so the user knows the list continues.
|
|
@@ -37530,9 +37614,13 @@ function renderConfirmationPanel(h, components, state, width, theme, focused) {
|
|
|
37530
37614
|
? 'You have an unsaved commit draft. Press y to discard it and quit.'
|
|
37531
37615
|
: state.pendingMutationConfirmation
|
|
37532
37616
|
? 'This discards local changes and cannot be undone by Coco.'
|
|
37533
|
-
|
|
37534
|
-
|
|
37535
|
-
|
|
37617
|
+
// Second-stage confirm raised when a safe delete hit an unmerged
|
|
37618
|
+
// branch — name the reason so the force isn't a blind "y again".
|
|
37619
|
+
: state.pendingConfirmationId === 'force-delete-branch'
|
|
37620
|
+
? 'Not fully merged. Force-delete (git branch -D) is irreversible.'
|
|
37621
|
+
: action?.kind === 'ai'
|
|
37622
|
+
? `AI action requires confirmation. Estimated ${action.estimatedTokens || '<unknown>'} tokens.`
|
|
37623
|
+
: 'Destructive Git action requires confirmation.';
|
|
37536
37624
|
return h(Box, {
|
|
37537
37625
|
borderColor: focusBorderColor(theme, focused),
|
|
37538
37626
|
borderStyle: theme.borderStyle,
|
|
@@ -38703,7 +38791,7 @@ function renderReflogSurface(ctx) {
|
|
|
38703
38791
|
* Extracted from `src/commands/log/inkRuntime.ts` as part of phase 5a.1
|
|
38704
38792
|
* of #890. No behavior change.
|
|
38705
38793
|
*/
|
|
38706
|
-
function renderStashSurface(ctx) {
|
|
38794
|
+
function renderStashSurface(ctx, spinnerFrame = 0) {
|
|
38707
38795
|
const { h, components, state, context, contextStatus, bodyRows, width, theme } = ctx;
|
|
38708
38796
|
const { Box, Text } = components;
|
|
38709
38797
|
const focused = state.focus === 'commits';
|
|
@@ -38749,11 +38837,18 @@ function renderStashSurface(ctx) {
|
|
|
38749
38837
|
const rowText = meta
|
|
38750
38838
|
? `${cursor} ${stash.ref.padEnd(11)} ${meta} ${stash.message}`
|
|
38751
38839
|
: `${cursor} ${stash.ref.padEnd(11)} ${stash.message}`;
|
|
38840
|
+
// The `stash@{N}` ref is an identifier, not a status icon, so a
|
|
38841
|
+
// delete-in-flight appends an accent spinner at the row's end
|
|
38842
|
+
// (2 cells reserved from the width budget).
|
|
38843
|
+
const deleting = isPendingDeletion(state.pendingDeletion, 'stash', stash.ref);
|
|
38844
|
+
const spinnerSpan = deleting
|
|
38845
|
+
? h(Text, { color: theme.noColor ? undefined : theme.colors.accent, dimColor: false }, ` ${inlineSpinnerGlyph(spinnerFrame, theme.ascii)}`)
|
|
38846
|
+
: null;
|
|
38752
38847
|
return h(Text, {
|
|
38753
38848
|
key: `stash-${index}`,
|
|
38754
38849
|
bold: isSelected,
|
|
38755
38850
|
dimColor: !isSelected,
|
|
38756
|
-
}, truncateCells(rowText, rowWidth));
|
|
38851
|
+
}, truncateCells(rowText, rowWidth - (deleting ? 2 : 0)), spinnerSpan);
|
|
38757
38852
|
});
|
|
38758
38853
|
const stashHasMoreAbove = startIndex > 0 && stashes.length > 0;
|
|
38759
38854
|
const stashHasMoreBelow = startIndex + listRows < stashes.length;
|
|
@@ -39101,7 +39196,7 @@ function formatHyperlink(text, url, env = process.env) {
|
|
|
39101
39196
|
* Extracted from `src/commands/log/inkRuntime.ts` as part of phase 5a.2
|
|
39102
39197
|
* of #890. No behavior change.
|
|
39103
39198
|
*/
|
|
39104
|
-
function renderTagsSurface(ctx) {
|
|
39199
|
+
function renderTagsSurface(ctx, spinnerFrame = 0) {
|
|
39105
39200
|
const { h, components, state, context, contextStatus, bodyRows, width, theme } = ctx;
|
|
39106
39201
|
const { Box, Text } = components;
|
|
39107
39202
|
const focused = state.focus === 'commits';
|
|
@@ -39142,13 +39237,20 @@ function renderTagsSurface(ctx) {
|
|
|
39142
39237
|
// intact.
|
|
39143
39238
|
const url = buildRefUrl(context.provider?.repository, tag.name);
|
|
39144
39239
|
const namePadded = truncateCells(tag.name, tagNameColWidth).padEnd(tagNameColWidth);
|
|
39145
|
-
|
|
39240
|
+
// Tags have no leading status icon, so a delete-in-flight appends
|
|
39241
|
+
// an accent spinner at the row's end. Reserve its 2 cells from the
|
|
39242
|
+
// truncation budget so it never pushes the row past the panel.
|
|
39243
|
+
const deleting = isPendingDeletion(state.pendingDeletion, 'tag', tag.name);
|
|
39244
|
+
const spinnerSpan = deleting
|
|
39245
|
+
? h(Text, { color: theme.noColor ? undefined : theme.colors.accent, dimColor: false }, ` ${inlineSpinnerGlyph(spinnerFrame, theme.ascii)}`)
|
|
39246
|
+
: null;
|
|
39247
|
+
const lineText = truncateCells(`${cursor} ${namePadded} ${tag.subject}`, Math.max(20, width - 4 - (deleting ? 2 : 0)));
|
|
39146
39248
|
if (!url || lineText.indexOf(namePadded) < 0) {
|
|
39147
39249
|
return h(Text, {
|
|
39148
39250
|
key: `tag-${index}`,
|
|
39149
39251
|
bold: isSelected,
|
|
39150
39252
|
dimColor: !isSelected,
|
|
39151
|
-
}, lineText);
|
|
39253
|
+
}, lineText, spinnerSpan);
|
|
39152
39254
|
}
|
|
39153
39255
|
const linkStart = lineText.indexOf(namePadded);
|
|
39154
39256
|
const before = lineText.slice(0, linkStart);
|
|
@@ -39157,7 +39259,7 @@ function renderTagsSurface(ctx) {
|
|
|
39157
39259
|
key: `tag-${index}`,
|
|
39158
39260
|
bold: isSelected,
|
|
39159
39261
|
dimColor: !isSelected,
|
|
39160
|
-
}, before, formatHyperlink(namePadded, url), after);
|
|
39262
|
+
}, before, formatHyperlink(namePadded, url), after, spinnerSpan);
|
|
39161
39263
|
});
|
|
39162
39264
|
const tagsHasMoreAbove = startIndex > 0 && tags.length > 0;
|
|
39163
39265
|
const tagsHasMoreBelow = startIndex + listRows < tags.length;
|
|
@@ -39184,7 +39286,7 @@ function renderTagsSurface(ctx) {
|
|
|
39184
39286
|
* Extracted from `src/commands/log/inkRuntime.ts` as part of phase 5a.1
|
|
39185
39287
|
* of #890. No behavior change.
|
|
39186
39288
|
*/
|
|
39187
|
-
function renderWorktreesSurface(ctx) {
|
|
39289
|
+
function renderWorktreesSurface(ctx, spinnerFrame = 0) {
|
|
39188
39290
|
const { h, components, state, context, contextStatus, bodyRows, width, theme } = ctx;
|
|
39189
39291
|
const { Box, Text } = components;
|
|
39190
39292
|
const focused = state.focus === 'commits';
|
|
@@ -39220,7 +39322,9 @@ function renderWorktreesSurface(ctx) {
|
|
|
39220
39322
|
const index = startIndex + offset;
|
|
39221
39323
|
const isSelected = index === selected;
|
|
39222
39324
|
const cursor = isSelected ? '>' : ' ';
|
|
39223
|
-
const marker =
|
|
39325
|
+
const marker = isPendingDeletion(state.pendingDeletion, 'worktree', entry.path)
|
|
39326
|
+
? inlineSpinnerGlyph(spinnerFrame, theme.ascii)
|
|
39327
|
+
: entry.current ? '*' : ' ';
|
|
39224
39328
|
const branchLabel = entry.branch ? entry.branch : entry.head || '<detached>';
|
|
39225
39329
|
const stateLabel = entry.dirty ? 'dirty' : 'clean';
|
|
39226
39330
|
const branchPadded = truncateCells(branchLabel, branchColWidth).padEnd(branchColWidth);
|
|
@@ -39290,10 +39394,10 @@ function renderMainPanel(surface, worktreeDiff, worktreeDiffLoading, worktreeHun
|
|
|
39290
39394
|
return renderComposeSurface(surface, spinnerFrame);
|
|
39291
39395
|
}
|
|
39292
39396
|
if (state.activeView === 'branches') {
|
|
39293
|
-
return renderBranchesSurface(surface);
|
|
39397
|
+
return renderBranchesSurface(surface, spinnerFrame);
|
|
39294
39398
|
}
|
|
39295
39399
|
if (state.activeView === 'tags') {
|
|
39296
|
-
return renderTagsSurface(surface);
|
|
39400
|
+
return renderTagsSurface(surface, spinnerFrame);
|
|
39297
39401
|
}
|
|
39298
39402
|
if (state.activeView === 'reflog') {
|
|
39299
39403
|
return renderReflogSurface(surface);
|
|
@@ -39302,10 +39406,10 @@ function renderMainPanel(surface, worktreeDiff, worktreeDiffLoading, worktreeHun
|
|
|
39302
39406
|
return renderBisectSurface(surface, bisectCandidateDetail, bisectCandidateLoading);
|
|
39303
39407
|
}
|
|
39304
39408
|
if (state.activeView === 'stash') {
|
|
39305
|
-
return renderStashSurface(surface);
|
|
39409
|
+
return renderStashSurface(surface, spinnerFrame);
|
|
39306
39410
|
}
|
|
39307
39411
|
if (state.activeView === 'worktrees') {
|
|
39308
|
-
return renderWorktreesSurface(surface);
|
|
39412
|
+
return renderWorktreesSurface(surface, spinnerFrame);
|
|
39309
39413
|
}
|
|
39310
39414
|
if (state.activeView === 'submodules') {
|
|
39311
39415
|
return renderSubmodulesSurface(surface);
|
|
@@ -40678,6 +40782,53 @@ const REMOTE_OP_LOADERS = {
|
|
|
40678
40782
|
'pull-selected-branch': { kind: 'pull', label: 'Pulling branch from remote…' },
|
|
40679
40783
|
'push-selected-branch': { kind: 'push', label: 'Pushing branch to remote…' },
|
|
40680
40784
|
};
|
|
40785
|
+
/**
|
|
40786
|
+
* Resolve which list row a delete workflow is about to act on, so the
|
|
40787
|
+
* runner can mark it pending (inline spinner) for the duration of the
|
|
40788
|
+
* git call. Mirrors the cursored-target resolution inside each delete
|
|
40789
|
+
* handler exactly — same sort, same promoted-filter, same selection
|
|
40790
|
+
* index — so the spinner lands on the row that actually gets deleted.
|
|
40791
|
+
* Returns `undefined` for non-delete workflows (and when nothing is
|
|
40792
|
+
* selected), which the runner treats as "no pending marker".
|
|
40793
|
+
*/
|
|
40794
|
+
function resolvePendingDeletion(id, state, context) {
|
|
40795
|
+
const { filter } = state;
|
|
40796
|
+
if (id === 'delete-branch' || id === 'force-delete-branch') {
|
|
40797
|
+
const all = sortBranches(context.branches?.localBranches || [], state.branchSort);
|
|
40798
|
+
const visible = filter
|
|
40799
|
+
? all.filter((b) => matchesPromotedFilter([b.shortName, b.upstream || ''], filter))
|
|
40800
|
+
: all;
|
|
40801
|
+
const branch = visible[Math.min(state.selectedBranchIndex, visible.length - 1)];
|
|
40802
|
+
return branch ? { kind: 'branch', id: branch.shortName } : undefined;
|
|
40803
|
+
}
|
|
40804
|
+
if (id === 'delete-tag') {
|
|
40805
|
+
const all = sortTags(context.tags?.tags || [], state.tagSort);
|
|
40806
|
+
const visible = filter
|
|
40807
|
+
? all.filter((t) => matchesPromotedFilter([t.name, t.subject], filter))
|
|
40808
|
+
: all;
|
|
40809
|
+
const tag = visible[Math.min(state.selectedTagIndex, visible.length - 1)];
|
|
40810
|
+
return tag ? { kind: 'tag', id: tag.name } : undefined;
|
|
40811
|
+
}
|
|
40812
|
+
if (id === 'drop-stash') {
|
|
40813
|
+
const all = context.stashes?.stashes || [];
|
|
40814
|
+
const visible = filter
|
|
40815
|
+
? all.filter((s) => matchesPromotedFilter([s.ref, s.message], filter))
|
|
40816
|
+
: all;
|
|
40817
|
+
const stash = visible[Math.min(state.selectedStashIndex, visible.length - 1)];
|
|
40818
|
+
return stash ? { kind: 'stash', id: stash.ref } : undefined;
|
|
40819
|
+
}
|
|
40820
|
+
if (id === 'remove-worktree') {
|
|
40821
|
+
const all = context.worktreeList?.worktrees || [];
|
|
40822
|
+
const visible = filter
|
|
40823
|
+
? all.filter((w) => matchesPromotedFilter([w.path, w.branch || ''], filter))
|
|
40824
|
+
: all;
|
|
40825
|
+
const wt = visible.length
|
|
40826
|
+
? visible[Math.min(state.selectedWorktreeListIndex, visible.length - 1)]
|
|
40827
|
+
: all[Math.min(state.selectedWorktreeListIndex, Math.max(0, all.length - 1))];
|
|
40828
|
+
return wt ? { kind: 'worktree', id: wt.path } : undefined;
|
|
40829
|
+
}
|
|
40830
|
+
return undefined;
|
|
40831
|
+
}
|
|
40681
40832
|
function predictNextFilter(action, currentFilter) {
|
|
40682
40833
|
switch (action.type) {
|
|
40683
40834
|
case 'appendFilter':
|
|
@@ -40994,7 +41145,10 @@ function LogInkApp(deps) {
|
|
|
40994
41145
|
state.changelogView.status === 'loading' ||
|
|
40995
41146
|
state.commitCompose.loading ||
|
|
40996
41147
|
Boolean(state.remoteOp) ||
|
|
40997
|
-
Boolean(state.statusLoading)
|
|
41148
|
+
Boolean(state.statusLoading) ||
|
|
41149
|
+
// Keep the shared spinner ticking while a list-item delete is in
|
|
41150
|
+
// flight so its inline pending glyph animates instead of freezing.
|
|
41151
|
+
Boolean(state.pendingDeletion);
|
|
40998
41152
|
React.useEffect(() => {
|
|
40999
41153
|
if (!anyLoading) {
|
|
41000
41154
|
// Reset to 0 so the next loading state starts from a known
|
|
@@ -43151,6 +43305,16 @@ function LogInkApp(deps) {
|
|
|
43151
43305
|
return { ok: false, message: 'No branch selected' };
|
|
43152
43306
|
return deleteBranch(git, branch);
|
|
43153
43307
|
},
|
|
43308
|
+
'force-delete-branch': async () => {
|
|
43309
|
+
const all = sortBranches(context.branches?.localBranches || [], state.branchSort);
|
|
43310
|
+
const visible = state.filter
|
|
43311
|
+
? all.filter((b) => matchesPromotedFilter([b.shortName, b.upstream || ''], state.filter))
|
|
43312
|
+
: all;
|
|
43313
|
+
const branch = visible[Math.min(state.selectedBranchIndex, visible.length - 1)];
|
|
43314
|
+
if (!branch)
|
|
43315
|
+
return { ok: false, message: 'No branch selected' };
|
|
43316
|
+
return deleteBranch(git, branch, true);
|
|
43317
|
+
},
|
|
43154
43318
|
'delete-tag': async () => {
|
|
43155
43319
|
const all = sortTags(context.tags?.tags || [], state.tagSort);
|
|
43156
43320
|
const visible = state.filter
|
|
@@ -43924,9 +44088,26 @@ function LogInkApp(deps) {
|
|
|
43924
44088
|
if (remoteOp) {
|
|
43925
44089
|
dispatch({ type: 'setRemoteOp', value: remoteOp });
|
|
43926
44090
|
}
|
|
44091
|
+
// Mark the cursored row as deleting so it shows an inline pending
|
|
44092
|
+
// spinner while the git call runs. Cleared in `finally` after the
|
|
44093
|
+
// refresh, so a successful delete hands straight off to the row
|
|
44094
|
+
// vanishing, and a failed one (e.g. an unmerged branch) restores
|
|
44095
|
+
// the row's normal icon alongside the error status.
|
|
44096
|
+
const pendingDeletion = resolvePendingDeletion(id, state, context);
|
|
44097
|
+
if (pendingDeletion) {
|
|
44098
|
+
dispatch({ type: 'setPendingDeletion', value: pendingDeletion });
|
|
44099
|
+
}
|
|
43927
44100
|
try {
|
|
43928
44101
|
const result = await handler();
|
|
43929
44102
|
dispatch({ type: 'setStatus', value: result?.message || 'Workflow action complete' });
|
|
44103
|
+
// A safe `delete-branch` (`git branch -d`) refuses branches that
|
|
44104
|
+
// aren't fully merged. Rather than dead-end on git's raw error, raise
|
|
44105
|
+
// a second y-confirm offering the force-delete (`git branch -D`). The
|
|
44106
|
+
// cursor hasn't moved (the delete failed), so the force handler
|
|
44107
|
+
// re-resolves the same branch.
|
|
44108
|
+
if (id === 'delete-branch' && !result?.ok && isBranchNotFullyMergedError(result?.message)) {
|
|
44109
|
+
dispatch({ type: 'setPendingConfirmation', value: 'force-delete-branch' });
|
|
44110
|
+
}
|
|
43930
44111
|
// Refresh history rows AS WELL when the workflow could have
|
|
43931
44112
|
// changed the commits the user sees (#945 follow-up). The
|
|
43932
44113
|
// workflow IDs below all either create/rewrite local commits or
|
|
@@ -44025,6 +44206,12 @@ function LogInkApp(deps) {
|
|
|
44025
44206
|
if (remoteOp) {
|
|
44026
44207
|
dispatch({ type: 'setRemoteOp', value: undefined });
|
|
44027
44208
|
}
|
|
44209
|
+
// Same guarantee for the per-row delete spinner: clear it whether
|
|
44210
|
+
// the delete succeeded, failed, or the refresh threw, so no row is
|
|
44211
|
+
// left spinning forever.
|
|
44212
|
+
if (pendingDeletion) {
|
|
44213
|
+
dispatch({ type: 'setPendingDeletion', value: undefined });
|
|
44214
|
+
}
|
|
44028
44215
|
}
|
|
44029
44216
|
}, [context, dispatch, git, refreshContext, refreshHistoryRows, refreshWorktreeContext,
|
|
44030
44217
|
state.branchSort, state.filter, state.selectedBranchIndex,
|
|
@@ -44884,7 +45071,7 @@ function LogInkApp(deps) {
|
|
|
44884
45071
|
// Panel renderers are thunks so single-pane mode can build only the
|
|
44885
45072
|
// visible pane — the main-panel render in particular is expensive, so
|
|
44886
45073
|
// we don't want to invoke the two hidden ones just to drop them.
|
|
44887
|
-
const sidebarPanel = () => renderSidebar$1(h, { Box, Text }, state, context, contextStatus, layout.sidebarWidth, layout.bodyRows, theme);
|
|
45074
|
+
const sidebarPanel = () => renderSidebar$1(h, { Box, Text }, state, context, contextStatus, layout.sidebarWidth, layout.bodyRows, theme, spinnerFrame);
|
|
44888
45075
|
const mainSurface = {
|
|
44889
45076
|
h,
|
|
44890
45077
|
components: { Box, Text },
|
|
@@ -45195,12 +45382,23 @@ function createLogArgvFromUiArgv(argv) {
|
|
|
45195
45382
|
// starting state. `coco ui --no-all` opts back to
|
|
45196
45383
|
// current-branch-only.
|
|
45197
45384
|
//
|
|
45385
|
+
// `?? true` re-asserts that default for the synthetic-argv path:
|
|
45386
|
+
// bare `coco` routes through defaultRouteHandler → buildSyntheticArgv,
|
|
45387
|
+
// which bypasses yargs and so never applies the `default: true` from
|
|
45388
|
+
// ui/config.ts — leaving `argv.all` undefined (#1169). Without the
|
|
45389
|
+
// fallback the workstation booted in compact (`--first-parent
|
|
45390
|
+
// --no-merges`) mode: fewer commits, branches "ahead" of HEAD hidden,
|
|
45391
|
+
// and cursoring/checking-out a branch whose tip wasn't in the compact
|
|
45392
|
+
// window triggered an anchored-context append that looped the graph
|
|
45393
|
+
// back to the initial commit. Explicit `--no-all` (argv.all === false)
|
|
45394
|
+
// is preserved because `false ?? true === false`.
|
|
45395
|
+
//
|
|
45198
45396
|
// Note: passing `--branch foo` does NOT automatically scope away
|
|
45199
45397
|
// from --all. If the user wants strictly that branch, they pass
|
|
45200
45398
|
// `coco ui --branch foo --no-all`. We considered the implicit
|
|
45201
45399
|
// scope-narrowing but it surprises users who pass `--branch` as
|
|
45202
45400
|
// a "highlight this branch in the all-refs view" hint.
|
|
45203
|
-
all: argv.all,
|
|
45401
|
+
all: argv.all ?? true,
|
|
45204
45402
|
branch: argv.branch,
|
|
45205
45403
|
format: 'table',
|
|
45206
45404
|
interactive: true,
|
|
@@ -50214,6 +50412,11 @@ async function probeIsGitRepo() {
|
|
|
50214
50412
|
* `InitArgv`, `UiArgv`, `WorkspaceArgv`) so we can't just spread the
|
|
50215
50413
|
* raw default argv — we have to project the shared fields and let
|
|
50216
50414
|
* the handler fill in command-specific defaults.
|
|
50415
|
+
*
|
|
50416
|
+
* Exported for testing: this is the path bare `coco` takes to the
|
|
50417
|
+
* workstation, and it intentionally does NOT set `all` — yargs's
|
|
50418
|
+
* `default: true` for `coco ui` never runs here, so the ui→log argv
|
|
50419
|
+
* mapping has to re-assert that default (#1169).
|
|
50217
50420
|
*/
|
|
50218
50421
|
function buildSyntheticArgv(argv) {
|
|
50219
50422
|
return {
|