git-coco 0.62.1 → 0.62.2

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.
@@ -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.62.1";
64
+ const BUILD_VERSION = "0.62.2";
65
65
 
66
66
  const isInteractive = (config) => {
67
67
  return config?.mode === 'interactive' || !!config?.interactive;
@@ -25162,12 +25162,14 @@ function formatSortIndicator(mode, options = {}) {
25162
25162
  }
25163
25163
 
25164
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.
25165
+ * True when `pending` (a `state.pendingItemAction`) targets this exact
25166
+ * row. Action-agnostic on purpose — every surface + the sidebar render
25167
+ * the same spinner whether the row is being deleted or checked out, so
25168
+ * the spinner-swap test stays identical everywhere. Takes the field
25169
+ * value (not the whole state) so it can live next to the type without a
25170
+ * forward reference.
25169
25171
  */
25170
- function isPendingDeletion(pending, kind, id) {
25172
+ function isPendingItemAction(pending, kind, id) {
25171
25173
  return pending?.kind === kind && pending.id === id;
25172
25174
  }
25173
25175
  const DEFAULT_CHANGELOG_VIEW_STATE = {
@@ -26375,10 +26377,11 @@ function applyLogInkAction(state, action) {
26375
26377
  workflowActionId: action.value ? undefined : state.workflowActionId,
26376
26378
  pendingKey: undefined,
26377
26379
  };
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 };
26380
+ case 'setPendingItemAction':
26381
+ // Pure marker for the in-flight row action (delete / checkout);
26382
+ // touches nothing else so the list keeps rendering normally
26383
+ // underneath the one spinner'd row.
26384
+ return { ...state, pendingItemAction: action.value };
26382
26385
  case 'toggleFilterMode':
26383
26386
  return {
26384
26387
  ...state,
@@ -31001,6 +31004,29 @@ function deleteBranch(git, branch, force = false) {
31001
31004
  function isBranchNotFullyMergedError(message) {
31002
31005
  return /not fully merged/i.test(message || '');
31003
31006
  }
31007
+ /**
31008
+ * True when a branch delete was rejected because the branch is checked
31009
+ * out in a worktree. Unlike "not fully merged" there's no force escape
31010
+ * hatch — git refuses `git branch -D` on a worktree-checked-out branch
31011
+ * too — so the UI should surface a clear "free up the worktree first"
31012
+ * message rather than offering a force-delete that would fail the same
31013
+ * way. Matches git's wording: `Cannot delete branch 'x' checked out at
31014
+ * '<path>'` / `used by worktree at '<path>'`.
31015
+ */
31016
+ function isBranchCheckedOutElsewhereError(message) {
31017
+ return /checked out at|used by worktree/i.test(message || '');
31018
+ }
31019
+ /**
31020
+ * Pull the worktree path out of git's "checked out at '<path>'" /
31021
+ * "used by worktree at '<path>'" rejection so the UI can name where the
31022
+ * branch is still in use. Returns undefined when the message doesn't
31023
+ * carry a path (older git phrasings) so callers can fall back to a
31024
+ * generic message.
31025
+ */
31026
+ function parseCheckedOutWorktreePath(message) {
31027
+ const match = /(?:checked out at|used by worktree at) '([^']+)'/i.exec(message || '');
31028
+ return match?.[1];
31029
+ }
31004
31030
  function fetchRemotes(git) {
31005
31031
  return runAction$5(() => git.raw(['fetch', '--all', '--prune']), 'Fetched all remotes');
31006
31032
  }
@@ -34412,7 +34438,7 @@ function renderActiveSidebarContent(h, Text, tab, state, context, contextStatus,
34412
34438
  // shows this spinner in place of its leading marker (branches /
34413
34439
  // worktrees) or appended to the row (tags / stashes, which have no
34414
34440
  // leading status icon). `pending` is the single in-flight target.
34415
- const pending = state.pendingDeletion;
34441
+ const pending = state.pendingItemAction;
34416
34442
  const spin = inlineSpinnerGlyph(spinnerFrame, theme.ascii);
34417
34443
  // Available rows for the active tab's list. The sidebar chrome
34418
34444
  // takes ~10 rows (panel title + spacer + 5 tab headers + 4 inter-tab
@@ -34451,7 +34477,7 @@ function renderActiveSidebarContent(h, Text, tab, state, context, contextStatus,
34451
34477
  return [
34452
34478
  ...headerRows,
34453
34479
  ...renderSelectableSidebarRows(h, Text, sortedBranches, state.selectedBranchIndex, focused, width, theme, (branch) => {
34454
- const glyph = isPendingDeletion(pending, 'branch', branch.shortName)
34480
+ const glyph = isPendingItemAction(pending, 'branch', branch.shortName)
34455
34481
  ? spin
34456
34482
  : branchRowMarker(branch, { ascii: theme.ascii }).glyph;
34457
34483
  return `${glyph} ${branch.shortName}`;
@@ -34470,7 +34496,7 @@ function renderActiveSidebarContent(h, Text, tab, state, context, contextStatus,
34470
34496
  const base = `${truncateCells(tag.name, 16)} ${tag.subject}`;
34471
34497
  // Tags have no leading status icon, so the pending spinner is
34472
34498
  // appended to the row instead of replacing a glyph.
34473
- return isPendingDeletion(pending, 'tag', tag.name) ? `${base} ${spin}` : base;
34499
+ return isPendingItemAction(pending, 'tag', tag.name) ? `${base} ${spin}` : base;
34474
34500
  }, 'tab-tags', visibleListCount);
34475
34501
  }
34476
34502
  if (tab === 'stashes') {
@@ -34485,7 +34511,7 @@ function renderActiveSidebarContent(h, Text, tab, state, context, contextStatus,
34485
34511
  const base = `@{${index}} ${stash.message || '(no message)'}`;
34486
34512
  // `@{N}` is the stash ref, not a status icon, so append the
34487
34513
  // spinner rather than replacing it.
34488
- return isPendingDeletion(pending, 'stash', stash.ref) ? `${base} ${spin}` : base;
34514
+ return isPendingItemAction(pending, 'stash', stash.ref) ? `${base} ${spin}` : base;
34489
34515
  }, 'tab-stashes', visibleListCount);
34490
34516
  }
34491
34517
  // worktrees
@@ -34497,7 +34523,7 @@ function renderActiveSidebarContent(h, Text, tab, state, context, contextStatus,
34497
34523
  return [h(Text, { key: 'tab-worktrees-empty', dimColor: true }, ' No linked worktrees')];
34498
34524
  }
34499
34525
  return renderSelectableSidebarRows(h, Text, worktrees, state.selectedWorktreeListIndex, focused, width, theme, (worktree) => {
34500
- const marker = isPendingDeletion(pending, 'worktree', worktree.path)
34526
+ const marker = isPendingItemAction(pending, 'worktree', worktree.path)
34501
34527
  ? spin
34502
34528
  : worktree.current ? '*' : ' ';
34503
34529
  const wstate = worktree.dirty ? 'dirty' : 'clean';
@@ -34933,7 +34959,7 @@ function renderBranchesSurface(ctx, spinnerFrame = 0) {
34933
34959
  // While this branch's delete is in flight, its sync-state marker
34934
34960
  // is replaced by an inline spinner (accent-coloured) so the row
34935
34961
  // reads as "deleting" until it vanishes on refresh.
34936
- const deleting = isPendingDeletion(state.pendingDeletion, 'branch', branch.shortName);
34962
+ const deleting = isPendingItemAction(state.pendingItemAction, 'branch', branch.shortName);
34937
34963
  const glyph = deleting ? inlineSpinnerGlyph(spinnerFrame, theme.ascii) : marker.glyph;
34938
34964
  const glyphColor = deleting
34939
34965
  ? (theme.noColor ? undefined : theme.colors.accent)
@@ -38823,7 +38849,7 @@ function renderStashSurface(ctx, spinnerFrame = 0) {
38823
38849
  // The `stash@{N}` ref is an identifier, not a status icon, so a
38824
38850
  // delete-in-flight appends an accent spinner at the row's end
38825
38851
  // (2 cells reserved from the width budget).
38826
- const deleting = isPendingDeletion(state.pendingDeletion, 'stash', stash.ref);
38852
+ const deleting = isPendingItemAction(state.pendingItemAction, 'stash', stash.ref);
38827
38853
  const spinnerSpan = deleting
38828
38854
  ? h(Text, { color: theme.noColor ? undefined : theme.colors.accent, dimColor: false }, ` ${inlineSpinnerGlyph(spinnerFrame, theme.ascii)}`)
38829
38855
  : null;
@@ -39223,7 +39249,7 @@ function renderTagsSurface(ctx, spinnerFrame = 0) {
39223
39249
  // Tags have no leading status icon, so a delete-in-flight appends
39224
39250
  // an accent spinner at the row's end. Reserve its 2 cells from the
39225
39251
  // truncation budget so it never pushes the row past the panel.
39226
- const deleting = isPendingDeletion(state.pendingDeletion, 'tag', tag.name);
39252
+ const deleting = isPendingItemAction(state.pendingItemAction, 'tag', tag.name);
39227
39253
  const spinnerSpan = deleting
39228
39254
  ? h(Text, { color: theme.noColor ? undefined : theme.colors.accent, dimColor: false }, ` ${inlineSpinnerGlyph(spinnerFrame, theme.ascii)}`)
39229
39255
  : null;
@@ -39305,7 +39331,7 @@ function renderWorktreesSurface(ctx, spinnerFrame = 0) {
39305
39331
  const index = startIndex + offset;
39306
39332
  const isSelected = index === selected;
39307
39333
  const cursor = isSelected ? '>' : ' ';
39308
- const marker = isPendingDeletion(state.pendingDeletion, 'worktree', entry.path)
39334
+ const marker = isPendingItemAction(state.pendingItemAction, 'worktree', entry.path)
39309
39335
  ? inlineSpinnerGlyph(spinnerFrame, theme.ascii)
39310
39336
  : entry.current ? '*' : ' ';
39311
39337
  const branchLabel = entry.branch ? entry.branch : entry.head || '<detached>';
@@ -40774,15 +40800,28 @@ const REMOTE_OP_LOADERS = {
40774
40800
  * Returns `undefined` for non-delete workflows (and when nothing is
40775
40801
  * selected), which the runner treats as "no pending marker".
40776
40802
  */
40777
- function resolvePendingDeletion(id, state, context) {
40803
+ function resolvePendingItemAction(id, state, context) {
40778
40804
  const { filter } = state;
40805
+ // Checking out a branch gets the same inline spinner on its row as a
40806
+ // delete does — the action just runs `git checkout` instead of
40807
+ // `git branch -d`. Resolved the same way as the delete branch case
40808
+ // (and identically to the checkout-branch handler) so the spinner
40809
+ // lands on exactly the row the user pressed enter on.
40810
+ if (id === 'checkout-branch') {
40811
+ const all = sortBranches(context.branches?.localBranches || [], state.branchSort);
40812
+ const visible = filter
40813
+ ? all.filter((b) => matchesPromotedFilter([b.shortName, b.upstream || ''], filter))
40814
+ : all;
40815
+ const branch = visible[Math.min(state.selectedBranchIndex, visible.length - 1)];
40816
+ return branch ? { kind: 'branch', id: branch.shortName, action: 'checkout' } : undefined;
40817
+ }
40779
40818
  if (id === 'delete-branch' || id === 'force-delete-branch') {
40780
40819
  const all = sortBranches(context.branches?.localBranches || [], state.branchSort);
40781
40820
  const visible = filter
40782
40821
  ? all.filter((b) => matchesPromotedFilter([b.shortName, b.upstream || ''], filter))
40783
40822
  : all;
40784
40823
  const branch = visible[Math.min(state.selectedBranchIndex, visible.length - 1)];
40785
- return branch ? { kind: 'branch', id: branch.shortName } : undefined;
40824
+ return branch ? { kind: 'branch', id: branch.shortName, action: 'delete' } : undefined;
40786
40825
  }
40787
40826
  if (id === 'delete-tag') {
40788
40827
  const all = sortTags(context.tags?.tags || [], state.tagSort);
@@ -40790,7 +40829,7 @@ function resolvePendingDeletion(id, state, context) {
40790
40829
  ? all.filter((t) => matchesPromotedFilter([t.name, t.subject], filter))
40791
40830
  : all;
40792
40831
  const tag = visible[Math.min(state.selectedTagIndex, visible.length - 1)];
40793
- return tag ? { kind: 'tag', id: tag.name } : undefined;
40832
+ return tag ? { kind: 'tag', id: tag.name, action: 'delete' } : undefined;
40794
40833
  }
40795
40834
  if (id === 'drop-stash') {
40796
40835
  const all = context.stashes?.stashes || [];
@@ -40798,7 +40837,7 @@ function resolvePendingDeletion(id, state, context) {
40798
40837
  ? all.filter((s) => matchesPromotedFilter([s.ref, s.message], filter))
40799
40838
  : all;
40800
40839
  const stash = visible[Math.min(state.selectedStashIndex, visible.length - 1)];
40801
- return stash ? { kind: 'stash', id: stash.ref } : undefined;
40840
+ return stash ? { kind: 'stash', id: stash.ref, action: 'delete' } : undefined;
40802
40841
  }
40803
40842
  if (id === 'remove-worktree') {
40804
40843
  const all = context.worktreeList?.worktrees || [];
@@ -40808,7 +40847,7 @@ function resolvePendingDeletion(id, state, context) {
40808
40847
  const wt = visible.length
40809
40848
  ? visible[Math.min(state.selectedWorktreeListIndex, visible.length - 1)]
40810
40849
  : all[Math.min(state.selectedWorktreeListIndex, Math.max(0, all.length - 1))];
40811
- return wt ? { kind: 'worktree', id: wt.path } : undefined;
40850
+ return wt ? { kind: 'worktree', id: wt.path, action: 'delete' } : undefined;
40812
40851
  }
40813
40852
  return undefined;
40814
40853
  }
@@ -41129,9 +41168,10 @@ function LogInkApp(deps) {
41129
41168
  state.commitCompose.loading ||
41130
41169
  Boolean(state.remoteOp) ||
41131
41170
  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);
41171
+ // Keep the shared spinner ticking while a list-item action (delete
41172
+ // or checkout) is in flight so its inline pending glyph animates
41173
+ // instead of freezing.
41174
+ Boolean(state.pendingItemAction);
41135
41175
  React.useEffect(() => {
41136
41176
  if (!anyLoading) {
41137
41177
  // Reset to 0 so the next loading state starts from a known
@@ -44071,14 +44111,15 @@ function LogInkApp(deps) {
44071
44111
  if (remoteOp) {
44072
44112
  dispatch({ type: 'setRemoteOp', value: remoteOp });
44073
44113
  }
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 });
44114
+ // Mark the cursored row as busy so it shows an inline pending
44115
+ // spinner while the git call runs (delete or checkout). Cleared in
44116
+ // `finally` after the refresh, so a successful delete hands straight
44117
+ // off to the row vanishing, a checkout to the sidebar repainting
44118
+ // with the new current branch, and a failure (e.g. an unmerged
44119
+ // branch) restores the row's normal icon alongside the error status.
44120
+ const pendingItemAction = resolvePendingItemAction(id, state, context);
44121
+ if (pendingItemAction) {
44122
+ dispatch({ type: 'setPendingItemAction', value: pendingItemAction });
44082
44123
  }
44083
44124
  try {
44084
44125
  const result = await handler();
@@ -44091,6 +44132,24 @@ function LogInkApp(deps) {
44091
44132
  if (id === 'delete-branch' && !result?.ok && isBranchNotFullyMergedError(result?.message)) {
44092
44133
  dispatch({ type: 'setPendingConfirmation', value: 'force-delete-branch' });
44093
44134
  }
44135
+ // A branch checked out in a worktree can't be deleted — and unlike
44136
+ // the unmerged case, `git branch -D` won't force it either, so we
44137
+ // don't offer a confirmation. Replace git's raw rejection with a
44138
+ // clear "free up the worktree first" message that names where the
44139
+ // branch is still in use.
44140
+ if ((id === 'delete-branch' || id === 'force-delete-branch') &&
44141
+ !result?.ok &&
44142
+ isBranchCheckedOutElsewhereError(result?.message)) {
44143
+ const worktreePath = parseCheckedOutWorktreePath(result?.message);
44144
+ const branchName = pendingItemAction?.id;
44145
+ dispatch({
44146
+ type: 'setStatus',
44147
+ value: worktreePath
44148
+ ? `Can't delete ${branchName ? `'${branchName}'` : 'branch'} — checked out in worktree ${worktreePath}. Switch that worktree off the branch or remove it first.`
44149
+ : `Can't delete ${branchName ? `'${branchName}'` : 'branch'} — it's checked out in another worktree. Switch that worktree off the branch or remove it first.`,
44150
+ kind: 'warning',
44151
+ });
44152
+ }
44094
44153
  // Refresh history rows AS WELL when the workflow could have
44095
44154
  // changed the commits the user sees (#945 follow-up). The
44096
44155
  // workflow IDs below all either create/rewrite local commits or
@@ -44126,15 +44185,18 @@ function LogInkApp(deps) {
44126
44185
  if (result?.ok && historyMutatingIds.has(id)) {
44127
44186
  await refreshHistoryRows();
44128
44187
  }
44129
- // Checkout-branch is the one workflow where we want a *visible*
44130
- // refresh so the user sees the branches sidebar repaint with the
44131
- // new current branch (per #806 follow-up). Snap the cursor to
44132
- // position 0 first so when the refresh completes and the new
44133
- // current branch lands at the top (per #809's pin-current rule),
44134
- // the cursor is already there waiting.
44188
+ // Checkout-branch snaps the cursor to position 0 first so when the
44189
+ // refresh completes and the new current branch lands at the top
44190
+ // (per #809's pin-current rule), the cursor is already there
44191
+ // waiting. The refresh is *silent*: the loud refresh used to blank
44192
+ // every branch name behind a "loading branches…" placeholder (#806),
44193
+ // but the in-flight row now carries its own inline pending spinner
44194
+ // (resolvePendingItemAction → action 'checkout'), so a silent
44195
+ // stale-while-revalidate swap keeps the list readable and just
44196
+ // repaints the current-branch marker once the new context lands.
44135
44197
  if (id === 'checkout-branch' && result?.ok) {
44136
44198
  dispatch({ type: 'resetBranchSelection' });
44137
- await refreshContext();
44199
+ await refreshContext({ silent: true });
44138
44200
  }
44139
44201
  else {
44140
44202
  // Silent refresh so the deleted item disappears from the list
@@ -44189,11 +44251,11 @@ function LogInkApp(deps) {
44189
44251
  if (remoteOp) {
44190
44252
  dispatch({ type: 'setRemoteOp', value: undefined });
44191
44253
  }
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 });
44254
+ // Same guarantee for the per-row pending spinner (delete or
44255
+ // checkout): clear it whether the action succeeded, failed, or the
44256
+ // refresh threw, so no row is left spinning forever.
44257
+ if (pendingItemAction) {
44258
+ dispatch({ type: 'setPendingItemAction', value: undefined });
44197
44259
  }
44198
44260
  }
44199
44261
  }, [context, dispatch, git, refreshContext, refreshHistoryRows, refreshWorktreeContext,
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.62.1";
81
+ const BUILD_VERSION = "0.62.2";
82
82
 
83
83
  const isInteractive = (config) => {
84
84
  return config?.mode === 'interactive' || !!config?.interactive;
@@ -25179,12 +25179,14 @@ function formatSortIndicator(mode, options = {}) {
25179
25179
  }
25180
25180
 
25181
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.
25182
+ * True when `pending` (a `state.pendingItemAction`) targets this exact
25183
+ * row. Action-agnostic on purpose — every surface + the sidebar render
25184
+ * the same spinner whether the row is being deleted or checked out, so
25185
+ * the spinner-swap test stays identical everywhere. Takes the field
25186
+ * value (not the whole state) so it can live next to the type without a
25187
+ * forward reference.
25186
25188
  */
25187
- function isPendingDeletion(pending, kind, id) {
25189
+ function isPendingItemAction(pending, kind, id) {
25188
25190
  return pending?.kind === kind && pending.id === id;
25189
25191
  }
25190
25192
  const DEFAULT_CHANGELOG_VIEW_STATE = {
@@ -26392,10 +26394,11 @@ function applyLogInkAction(state, action) {
26392
26394
  workflowActionId: action.value ? undefined : state.workflowActionId,
26393
26395
  pendingKey: undefined,
26394
26396
  };
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 };
26397
+ case 'setPendingItemAction':
26398
+ // Pure marker for the in-flight row action (delete / checkout);
26399
+ // touches nothing else so the list keeps rendering normally
26400
+ // underneath the one spinner'd row.
26401
+ return { ...state, pendingItemAction: action.value };
26399
26402
  case 'toggleFilterMode':
26400
26403
  return {
26401
26404
  ...state,
@@ -31018,6 +31021,29 @@ function deleteBranch(git, branch, force = false) {
31018
31021
  function isBranchNotFullyMergedError(message) {
31019
31022
  return /not fully merged/i.test(message || '');
31020
31023
  }
31024
+ /**
31025
+ * True when a branch delete was rejected because the branch is checked
31026
+ * out in a worktree. Unlike "not fully merged" there's no force escape
31027
+ * hatch — git refuses `git branch -D` on a worktree-checked-out branch
31028
+ * too — so the UI should surface a clear "free up the worktree first"
31029
+ * message rather than offering a force-delete that would fail the same
31030
+ * way. Matches git's wording: `Cannot delete branch 'x' checked out at
31031
+ * '<path>'` / `used by worktree at '<path>'`.
31032
+ */
31033
+ function isBranchCheckedOutElsewhereError(message) {
31034
+ return /checked out at|used by worktree/i.test(message || '');
31035
+ }
31036
+ /**
31037
+ * Pull the worktree path out of git's "checked out at '<path>'" /
31038
+ * "used by worktree at '<path>'" rejection so the UI can name where the
31039
+ * branch is still in use. Returns undefined when the message doesn't
31040
+ * carry a path (older git phrasings) so callers can fall back to a
31041
+ * generic message.
31042
+ */
31043
+ function parseCheckedOutWorktreePath(message) {
31044
+ const match = /(?:checked out at|used by worktree at) '([^']+)'/i.exec(message || '');
31045
+ return match?.[1];
31046
+ }
31021
31047
  function fetchRemotes(git) {
31022
31048
  return runAction$5(() => git.raw(['fetch', '--all', '--prune']), 'Fetched all remotes');
31023
31049
  }
@@ -34429,7 +34455,7 @@ function renderActiveSidebarContent(h, Text, tab, state, context, contextStatus,
34429
34455
  // shows this spinner in place of its leading marker (branches /
34430
34456
  // worktrees) or appended to the row (tags / stashes, which have no
34431
34457
  // leading status icon). `pending` is the single in-flight target.
34432
- const pending = state.pendingDeletion;
34458
+ const pending = state.pendingItemAction;
34433
34459
  const spin = inlineSpinnerGlyph(spinnerFrame, theme.ascii);
34434
34460
  // Available rows for the active tab's list. The sidebar chrome
34435
34461
  // takes ~10 rows (panel title + spacer + 5 tab headers + 4 inter-tab
@@ -34468,7 +34494,7 @@ function renderActiveSidebarContent(h, Text, tab, state, context, contextStatus,
34468
34494
  return [
34469
34495
  ...headerRows,
34470
34496
  ...renderSelectableSidebarRows(h, Text, sortedBranches, state.selectedBranchIndex, focused, width, theme, (branch) => {
34471
- const glyph = isPendingDeletion(pending, 'branch', branch.shortName)
34497
+ const glyph = isPendingItemAction(pending, 'branch', branch.shortName)
34472
34498
  ? spin
34473
34499
  : branchRowMarker(branch, { ascii: theme.ascii }).glyph;
34474
34500
  return `${glyph} ${branch.shortName}`;
@@ -34487,7 +34513,7 @@ function renderActiveSidebarContent(h, Text, tab, state, context, contextStatus,
34487
34513
  const base = `${truncateCells(tag.name, 16)} ${tag.subject}`;
34488
34514
  // Tags have no leading status icon, so the pending spinner is
34489
34515
  // appended to the row instead of replacing a glyph.
34490
- return isPendingDeletion(pending, 'tag', tag.name) ? `${base} ${spin}` : base;
34516
+ return isPendingItemAction(pending, 'tag', tag.name) ? `${base} ${spin}` : base;
34491
34517
  }, 'tab-tags', visibleListCount);
34492
34518
  }
34493
34519
  if (tab === 'stashes') {
@@ -34502,7 +34528,7 @@ function renderActiveSidebarContent(h, Text, tab, state, context, contextStatus,
34502
34528
  const base = `@{${index}} ${stash.message || '(no message)'}`;
34503
34529
  // `@{N}` is the stash ref, not a status icon, so append the
34504
34530
  // spinner rather than replacing it.
34505
- return isPendingDeletion(pending, 'stash', stash.ref) ? `${base} ${spin}` : base;
34531
+ return isPendingItemAction(pending, 'stash', stash.ref) ? `${base} ${spin}` : base;
34506
34532
  }, 'tab-stashes', visibleListCount);
34507
34533
  }
34508
34534
  // worktrees
@@ -34514,7 +34540,7 @@ function renderActiveSidebarContent(h, Text, tab, state, context, contextStatus,
34514
34540
  return [h(Text, { key: 'tab-worktrees-empty', dimColor: true }, ' No linked worktrees')];
34515
34541
  }
34516
34542
  return renderSelectableSidebarRows(h, Text, worktrees, state.selectedWorktreeListIndex, focused, width, theme, (worktree) => {
34517
- const marker = isPendingDeletion(pending, 'worktree', worktree.path)
34543
+ const marker = isPendingItemAction(pending, 'worktree', worktree.path)
34518
34544
  ? spin
34519
34545
  : worktree.current ? '*' : ' ';
34520
34546
  const wstate = worktree.dirty ? 'dirty' : 'clean';
@@ -34950,7 +34976,7 @@ function renderBranchesSurface(ctx, spinnerFrame = 0) {
34950
34976
  // While this branch's delete is in flight, its sync-state marker
34951
34977
  // is replaced by an inline spinner (accent-coloured) so the row
34952
34978
  // reads as "deleting" until it vanishes on refresh.
34953
- const deleting = isPendingDeletion(state.pendingDeletion, 'branch', branch.shortName);
34979
+ const deleting = isPendingItemAction(state.pendingItemAction, 'branch', branch.shortName);
34954
34980
  const glyph = deleting ? inlineSpinnerGlyph(spinnerFrame, theme.ascii) : marker.glyph;
34955
34981
  const glyphColor = deleting
34956
34982
  ? (theme.noColor ? undefined : theme.colors.accent)
@@ -38840,7 +38866,7 @@ function renderStashSurface(ctx, spinnerFrame = 0) {
38840
38866
  // The `stash@{N}` ref is an identifier, not a status icon, so a
38841
38867
  // delete-in-flight appends an accent spinner at the row's end
38842
38868
  // (2 cells reserved from the width budget).
38843
- const deleting = isPendingDeletion(state.pendingDeletion, 'stash', stash.ref);
38869
+ const deleting = isPendingItemAction(state.pendingItemAction, 'stash', stash.ref);
38844
38870
  const spinnerSpan = deleting
38845
38871
  ? h(Text, { color: theme.noColor ? undefined : theme.colors.accent, dimColor: false }, ` ${inlineSpinnerGlyph(spinnerFrame, theme.ascii)}`)
38846
38872
  : null;
@@ -39240,7 +39266,7 @@ function renderTagsSurface(ctx, spinnerFrame = 0) {
39240
39266
  // Tags have no leading status icon, so a delete-in-flight appends
39241
39267
  // an accent spinner at the row's end. Reserve its 2 cells from the
39242
39268
  // truncation budget so it never pushes the row past the panel.
39243
- const deleting = isPendingDeletion(state.pendingDeletion, 'tag', tag.name);
39269
+ const deleting = isPendingItemAction(state.pendingItemAction, 'tag', tag.name);
39244
39270
  const spinnerSpan = deleting
39245
39271
  ? h(Text, { color: theme.noColor ? undefined : theme.colors.accent, dimColor: false }, ` ${inlineSpinnerGlyph(spinnerFrame, theme.ascii)}`)
39246
39272
  : null;
@@ -39322,7 +39348,7 @@ function renderWorktreesSurface(ctx, spinnerFrame = 0) {
39322
39348
  const index = startIndex + offset;
39323
39349
  const isSelected = index === selected;
39324
39350
  const cursor = isSelected ? '>' : ' ';
39325
- const marker = isPendingDeletion(state.pendingDeletion, 'worktree', entry.path)
39351
+ const marker = isPendingItemAction(state.pendingItemAction, 'worktree', entry.path)
39326
39352
  ? inlineSpinnerGlyph(spinnerFrame, theme.ascii)
39327
39353
  : entry.current ? '*' : ' ';
39328
39354
  const branchLabel = entry.branch ? entry.branch : entry.head || '<detached>';
@@ -40791,15 +40817,28 @@ const REMOTE_OP_LOADERS = {
40791
40817
  * Returns `undefined` for non-delete workflows (and when nothing is
40792
40818
  * selected), which the runner treats as "no pending marker".
40793
40819
  */
40794
- function resolvePendingDeletion(id, state, context) {
40820
+ function resolvePendingItemAction(id, state, context) {
40795
40821
  const { filter } = state;
40822
+ // Checking out a branch gets the same inline spinner on its row as a
40823
+ // delete does — the action just runs `git checkout` instead of
40824
+ // `git branch -d`. Resolved the same way as the delete branch case
40825
+ // (and identically to the checkout-branch handler) so the spinner
40826
+ // lands on exactly the row the user pressed enter on.
40827
+ if (id === 'checkout-branch') {
40828
+ const all = sortBranches(context.branches?.localBranches || [], state.branchSort);
40829
+ const visible = filter
40830
+ ? all.filter((b) => matchesPromotedFilter([b.shortName, b.upstream || ''], filter))
40831
+ : all;
40832
+ const branch = visible[Math.min(state.selectedBranchIndex, visible.length - 1)];
40833
+ return branch ? { kind: 'branch', id: branch.shortName, action: 'checkout' } : undefined;
40834
+ }
40796
40835
  if (id === 'delete-branch' || id === 'force-delete-branch') {
40797
40836
  const all = sortBranches(context.branches?.localBranches || [], state.branchSort);
40798
40837
  const visible = filter
40799
40838
  ? all.filter((b) => matchesPromotedFilter([b.shortName, b.upstream || ''], filter))
40800
40839
  : all;
40801
40840
  const branch = visible[Math.min(state.selectedBranchIndex, visible.length - 1)];
40802
- return branch ? { kind: 'branch', id: branch.shortName } : undefined;
40841
+ return branch ? { kind: 'branch', id: branch.shortName, action: 'delete' } : undefined;
40803
40842
  }
40804
40843
  if (id === 'delete-tag') {
40805
40844
  const all = sortTags(context.tags?.tags || [], state.tagSort);
@@ -40807,7 +40846,7 @@ function resolvePendingDeletion(id, state, context) {
40807
40846
  ? all.filter((t) => matchesPromotedFilter([t.name, t.subject], filter))
40808
40847
  : all;
40809
40848
  const tag = visible[Math.min(state.selectedTagIndex, visible.length - 1)];
40810
- return tag ? { kind: 'tag', id: tag.name } : undefined;
40849
+ return tag ? { kind: 'tag', id: tag.name, action: 'delete' } : undefined;
40811
40850
  }
40812
40851
  if (id === 'drop-stash') {
40813
40852
  const all = context.stashes?.stashes || [];
@@ -40815,7 +40854,7 @@ function resolvePendingDeletion(id, state, context) {
40815
40854
  ? all.filter((s) => matchesPromotedFilter([s.ref, s.message], filter))
40816
40855
  : all;
40817
40856
  const stash = visible[Math.min(state.selectedStashIndex, visible.length - 1)];
40818
- return stash ? { kind: 'stash', id: stash.ref } : undefined;
40857
+ return stash ? { kind: 'stash', id: stash.ref, action: 'delete' } : undefined;
40819
40858
  }
40820
40859
  if (id === 'remove-worktree') {
40821
40860
  const all = context.worktreeList?.worktrees || [];
@@ -40825,7 +40864,7 @@ function resolvePendingDeletion(id, state, context) {
40825
40864
  const wt = visible.length
40826
40865
  ? visible[Math.min(state.selectedWorktreeListIndex, visible.length - 1)]
40827
40866
  : all[Math.min(state.selectedWorktreeListIndex, Math.max(0, all.length - 1))];
40828
- return wt ? { kind: 'worktree', id: wt.path } : undefined;
40867
+ return wt ? { kind: 'worktree', id: wt.path, action: 'delete' } : undefined;
40829
40868
  }
40830
40869
  return undefined;
40831
40870
  }
@@ -41146,9 +41185,10 @@ function LogInkApp(deps) {
41146
41185
  state.commitCompose.loading ||
41147
41186
  Boolean(state.remoteOp) ||
41148
41187
  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);
41188
+ // Keep the shared spinner ticking while a list-item action (delete
41189
+ // or checkout) is in flight so its inline pending glyph animates
41190
+ // instead of freezing.
41191
+ Boolean(state.pendingItemAction);
41152
41192
  React.useEffect(() => {
41153
41193
  if (!anyLoading) {
41154
41194
  // Reset to 0 so the next loading state starts from a known
@@ -44088,14 +44128,15 @@ function LogInkApp(deps) {
44088
44128
  if (remoteOp) {
44089
44129
  dispatch({ type: 'setRemoteOp', value: remoteOp });
44090
44130
  }
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 });
44131
+ // Mark the cursored row as busy so it shows an inline pending
44132
+ // spinner while the git call runs (delete or checkout). Cleared in
44133
+ // `finally` after the refresh, so a successful delete hands straight
44134
+ // off to the row vanishing, a checkout to the sidebar repainting
44135
+ // with the new current branch, and a failure (e.g. an unmerged
44136
+ // branch) restores the row's normal icon alongside the error status.
44137
+ const pendingItemAction = resolvePendingItemAction(id, state, context);
44138
+ if (pendingItemAction) {
44139
+ dispatch({ type: 'setPendingItemAction', value: pendingItemAction });
44099
44140
  }
44100
44141
  try {
44101
44142
  const result = await handler();
@@ -44108,6 +44149,24 @@ function LogInkApp(deps) {
44108
44149
  if (id === 'delete-branch' && !result?.ok && isBranchNotFullyMergedError(result?.message)) {
44109
44150
  dispatch({ type: 'setPendingConfirmation', value: 'force-delete-branch' });
44110
44151
  }
44152
+ // A branch checked out in a worktree can't be deleted — and unlike
44153
+ // the unmerged case, `git branch -D` won't force it either, so we
44154
+ // don't offer a confirmation. Replace git's raw rejection with a
44155
+ // clear "free up the worktree first" message that names where the
44156
+ // branch is still in use.
44157
+ if ((id === 'delete-branch' || id === 'force-delete-branch') &&
44158
+ !result?.ok &&
44159
+ isBranchCheckedOutElsewhereError(result?.message)) {
44160
+ const worktreePath = parseCheckedOutWorktreePath(result?.message);
44161
+ const branchName = pendingItemAction?.id;
44162
+ dispatch({
44163
+ type: 'setStatus',
44164
+ value: worktreePath
44165
+ ? `Can't delete ${branchName ? `'${branchName}'` : 'branch'} — checked out in worktree ${worktreePath}. Switch that worktree off the branch or remove it first.`
44166
+ : `Can't delete ${branchName ? `'${branchName}'` : 'branch'} — it's checked out in another worktree. Switch that worktree off the branch or remove it first.`,
44167
+ kind: 'warning',
44168
+ });
44169
+ }
44111
44170
  // Refresh history rows AS WELL when the workflow could have
44112
44171
  // changed the commits the user sees (#945 follow-up). The
44113
44172
  // workflow IDs below all either create/rewrite local commits or
@@ -44143,15 +44202,18 @@ function LogInkApp(deps) {
44143
44202
  if (result?.ok && historyMutatingIds.has(id)) {
44144
44203
  await refreshHistoryRows();
44145
44204
  }
44146
- // Checkout-branch is the one workflow where we want a *visible*
44147
- // refresh so the user sees the branches sidebar repaint with the
44148
- // new current branch (per #806 follow-up). Snap the cursor to
44149
- // position 0 first so when the refresh completes and the new
44150
- // current branch lands at the top (per #809's pin-current rule),
44151
- // the cursor is already there waiting.
44205
+ // Checkout-branch snaps the cursor to position 0 first so when the
44206
+ // refresh completes and the new current branch lands at the top
44207
+ // (per #809's pin-current rule), the cursor is already there
44208
+ // waiting. The refresh is *silent*: the loud refresh used to blank
44209
+ // every branch name behind a "loading branches…" placeholder (#806),
44210
+ // but the in-flight row now carries its own inline pending spinner
44211
+ // (resolvePendingItemAction → action 'checkout'), so a silent
44212
+ // stale-while-revalidate swap keeps the list readable and just
44213
+ // repaints the current-branch marker once the new context lands.
44152
44214
  if (id === 'checkout-branch' && result?.ok) {
44153
44215
  dispatch({ type: 'resetBranchSelection' });
44154
- await refreshContext();
44216
+ await refreshContext({ silent: true });
44155
44217
  }
44156
44218
  else {
44157
44219
  // Silent refresh so the deleted item disappears from the list
@@ -44206,11 +44268,11 @@ function LogInkApp(deps) {
44206
44268
  if (remoteOp) {
44207
44269
  dispatch({ type: 'setRemoteOp', value: undefined });
44208
44270
  }
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 });
44271
+ // Same guarantee for the per-row pending spinner (delete or
44272
+ // checkout): clear it whether the action succeeded, failed, or the
44273
+ // refresh threw, so no row is left spinning forever.
44274
+ if (pendingItemAction) {
44275
+ dispatch({ type: 'setPendingItemAction', value: undefined });
44214
44276
  }
44215
44277
  }
44216
44278
  }, [context, dispatch, git, refreshContext, refreshHistoryRows, refreshWorktreeContext,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-coco",
3
- "version": "0.62.1",
3
+ "version": "0.62.2",
4
4
  "description": "zero-effort git commits with coco.",
5
5
  "author": "gfargo <ghfargo@gmail.com>",
6
6
  "license": "MIT",