git-coco 0.62.3 → 0.62.4

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.3";
64
+ const BUILD_VERSION = "0.62.4";
65
65
 
66
66
  const isInteractive = (config) => {
67
67
  return config?.mode === 'interactive' || !!config?.interactive;
@@ -26368,6 +26368,8 @@ function applyLogInkAction(state, action) {
26368
26368
  pendingMutationConfirmation: action.value ? undefined : state.pendingMutationConfirmation,
26369
26369
  pendingKey: undefined,
26370
26370
  };
26371
+ case 'setWorktreeCheckoutConflict':
26372
+ return { ...state, worktreeCheckoutConflict: action.value, pendingKey: undefined };
26371
26373
  case 'setPendingMutationConfirmation':
26372
26374
  return {
26373
26375
  ...state,
@@ -27740,7 +27742,45 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
27740
27742
  return [];
27741
27743
  }
27742
27744
  if (state.pendingConfirmationId) {
27745
+ // Worktree-conflict removal options (#1175): alongside the y-switch,
27746
+ // `r` removes the conflicting worktree and checks the branch out
27747
+ // here, `x` removes the worktree AND deletes the branch. Both defer
27748
+ // to the runtime (it owns the git ops + the conflict context); the
27749
+ // runtime clears the conflict state once it resolves.
27750
+ if (state.pendingConfirmationId === 'switch-to-conflicting-worktree' && state.worktreeCheckoutConflict) {
27751
+ if (inputValue === 'r') {
27752
+ return [
27753
+ { type: 'runWorkflowAction', id: 'conflict-remove-worktree-checkout' },
27754
+ action({ type: 'setPendingConfirmation', value: undefined }),
27755
+ ];
27756
+ }
27757
+ if (inputValue === 'x') {
27758
+ return [
27759
+ { type: 'runWorkflowAction', id: 'conflict-remove-worktree-branch' },
27760
+ action({ type: 'setPendingConfirmation', value: undefined }),
27761
+ ];
27762
+ }
27763
+ }
27743
27764
  if (inputValue === 'y') {
27765
+ // Worktree-conflict switch (#1175): the branch is already checked
27766
+ // out elsewhere, so "switch" just opens that worktree as a nested
27767
+ // repo frame (same mechanism as drilling into a submodule) — no
27768
+ // git mutation, hence handled here rather than via the runtime.
27769
+ if (state.pendingConfirmationId === 'switch-to-conflicting-worktree') {
27770
+ const conflict = state.worktreeCheckoutConflict;
27771
+ if (conflict) {
27772
+ return [
27773
+ action({ type: 'pushRepoFrame', label: conflict.branch, workdir: conflict.worktreePath }),
27774
+ action({ type: 'setStatus', value: `Switched to worktree ${conflict.worktreePath} (${conflict.branch})` }),
27775
+ action({ type: 'setPendingConfirmation', value: undefined }),
27776
+ action({ type: 'setWorktreeCheckoutConflict', value: undefined }),
27777
+ ];
27778
+ }
27779
+ return [
27780
+ action({ type: 'setPendingConfirmation', value: undefined }),
27781
+ action({ type: 'setWorktreeCheckoutConflict', value: undefined }),
27782
+ ];
27783
+ }
27744
27784
  const workflowAction = getLogInkWorkflowActionById(state.pendingConfirmationId);
27745
27785
  if (workflowAction?.id === 'ai-commit-summary') {
27746
27786
  return [
@@ -27766,6 +27806,11 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
27766
27806
  if (inputValue === 'n' || key.escape) {
27767
27807
  return [
27768
27808
  action({ type: 'setPendingConfirmation', value: undefined }),
27809
+ // Drop any worktree-conflict context so the prompt doesn't
27810
+ // linger after the user declines to switch.
27811
+ ...(state.worktreeCheckoutConflict
27812
+ ? [action({ type: 'setWorktreeCheckoutConflict', value: undefined })]
27813
+ : []),
27769
27814
  action({ type: 'setStatus', value: 'workflow action cancelled' }),
27770
27815
  ];
27771
27816
  }
@@ -37640,25 +37685,36 @@ function renderConfirmationPanel(h, components, state, width, theme, focused) {
37640
37685
  : state.pendingMutationConfirmation === 'discard-draft'
37641
37686
  ? 'Quit and discard the in-progress commit draft'
37642
37687
  : undefined;
37643
- const label = action?.label || mutationLabel || 'Workflow action';
37688
+ // Worktree-conflict switch (#1175): a checkout was rejected because
37689
+ // the branch is checked out elsewhere — name the branch + worktree so
37690
+ // the prompt explains what "y" does (jump into that worktree).
37691
+ const conflict = state.worktreeCheckoutConflict;
37692
+ const isWorktreeConflict = state.pendingConfirmationId === 'switch-to-conflicting-worktree';
37693
+ const label = isWorktreeConflict && conflict
37694
+ ? `Switch to the worktree where '${conflict.branch}' is checked out?`
37695
+ : action?.label || mutationLabel || 'Workflow action';
37644
37696
  const warning = state.pendingMutationConfirmation === 'discard-draft'
37645
37697
  ? 'You have an unsaved commit draft. Press y to discard it and quit.'
37646
37698
  : state.pendingMutationConfirmation
37647
37699
  ? 'This discards local changes and cannot be undone by Coco.'
37648
- // Second-stage confirm raised when a safe delete hit an unmerged
37649
- // branch — name the reason so the force isn't a blind "y again".
37650
- : state.pendingConfirmationId === 'force-delete-branch'
37651
- ? 'Not fully merged. Force-delete (git branch -D) is irreversible.'
37652
- : action?.kind === 'ai'
37653
- ? `AI action requires confirmation. Estimated ${action.estimatedTokens || '<unknown>'} tokens.`
37654
- : 'Destructive Git action requires confirmation.';
37700
+ : isWorktreeConflict && conflict
37701
+ ? `'${conflict.branch}' is checked out at ${conflict.worktreePath}.${conflict.dirty ? ' That worktree has uncommitted changes removal will be refused until it is clean or stashed.' : ''}`
37702
+ // Second-stage confirm raised when a safe delete hit an unmerged
37703
+ // branch name the reason so the force isn't a blind "y again".
37704
+ : state.pendingConfirmationId === 'force-delete-branch'
37705
+ ? 'Not fully merged. Force-delete (git branch -D) is irreversible.'
37706
+ : action?.kind === 'ai'
37707
+ ? `AI action requires confirmation. Estimated ${action.estimatedTokens || '<unknown>'} tokens.`
37708
+ : 'Destructive Git action requires confirmation.';
37655
37709
  return h(Box, {
37656
37710
  borderColor: focusBorderColor(theme, focused),
37657
37711
  borderStyle: theme.borderStyle,
37658
37712
  flexDirection: 'column',
37659
37713
  width,
37660
37714
  paddingX: 1,
37661
- }, h(Text, { bold: true }, panelTitle('Confirm', focused)), h(Text, undefined, truncateCells(label, width - 4)), h(Text, { dimColor: true }, truncateCells(warning, width - 4)), h(Text, undefined, ''), h(Text, undefined, 'Press y to confirm or n/Esc to cancel.'));
37715
+ }, h(Text, { bold: true }, panelTitle('Confirm', focused)), h(Text, undefined, truncateCells(label, width - 4)), h(Text, { dimColor: true }, truncateCells(warning, width - 4)), h(Text, undefined, ''), h(Text, undefined, truncateCells(isWorktreeConflict
37716
+ ? 'y switch · r remove worktree & check out here · x remove worktree & delete branch · n/Esc cancel'
37717
+ : 'Press y to confirm or n/Esc to cancel.', width - 4)));
37662
37718
  }
37663
37719
  /**
37664
37720
  * First-launch onboarding overlay (P1.3). Shown once per machine, gated
@@ -43772,6 +43828,42 @@ function LogInkApp(deps) {
43772
43828
  // path on the wrong target.
43773
43829
  return removeWorktreeAndBranch(git, cursorTarget, context.branches?.localBranches || []);
43774
43830
  },
43831
+ // Worktree-checkout-conflict resolutions (#1175). Unlike the
43832
+ // cursor-targeted handlers above, these act on the worktree
43833
+ // captured in `state.worktreeCheckoutConflict` (the one git named
43834
+ // when it refused the checkout), not the worktrees-view cursor.
43835
+ 'conflict-remove-worktree-checkout': async () => {
43836
+ const conflict = state.worktreeCheckoutConflict;
43837
+ dispatch({ type: 'setWorktreeCheckoutConflict', value: undefined });
43838
+ if (!conflict)
43839
+ return { ok: false, message: 'No worktree conflict to resolve.' };
43840
+ const worktree = context.worktreeList?.worktrees?.find((w) => w.path === conflict.worktreePath);
43841
+ if (!worktree)
43842
+ return { ok: false, message: `Worktree ${conflict.worktreePath} not found.` };
43843
+ // removeWorktree refuses a dirty / current worktree and returns
43844
+ // a clear message — surface it rather than forcing.
43845
+ const removed = await removeWorktree(git, worktree);
43846
+ if (!removed.ok)
43847
+ return removed;
43848
+ const branch = (context.branches?.localBranches || []).find((b) => b.type === 'local' && b.shortName === conflict.branch);
43849
+ if (!branch) {
43850
+ return { ok: true, message: `Removed worktree ${worktree.path}; branch ${conflict.branch} not found to check out.` };
43851
+ }
43852
+ const checkout = await checkoutBranch(git, branch);
43853
+ return checkout.ok
43854
+ ? { ok: true, message: `Removed worktree ${worktree.path} and checked out ${conflict.branch}` }
43855
+ : { ok: false, message: `Removed worktree ${worktree.path}, but checkout failed: ${checkout.message}` };
43856
+ },
43857
+ 'conflict-remove-worktree-branch': async () => {
43858
+ const conflict = state.worktreeCheckoutConflict;
43859
+ dispatch({ type: 'setWorktreeCheckoutConflict', value: undefined });
43860
+ if (!conflict)
43861
+ return { ok: false, message: 'No worktree conflict to resolve.' };
43862
+ const worktree = context.worktreeList?.worktrees?.find((w) => w.path === conflict.worktreePath);
43863
+ if (!worktree)
43864
+ return { ok: false, message: `Worktree ${conflict.worktreePath} not found.` };
43865
+ return removeWorktreeAndBranch(git, worktree, context.branches?.localBranches || []);
43866
+ },
43775
43867
  'abort-operation': async () => {
43776
43868
  const operation = context.operation?.operation;
43777
43869
  if (!operation) {
@@ -44234,6 +44326,30 @@ function LogInkApp(deps) {
44234
44326
  kind: 'warning',
44235
44327
  });
44236
44328
  }
44329
+ // Checking out a branch that's already checked out in another
44330
+ // worktree is rejected by git ("already checked out at <path>").
44331
+ // Rather than dead-end on that, capture the conflict and raise a
44332
+ // y-confirm offering to switch into that worktree — the branch IS
44333
+ // checked out, just elsewhere (#1175).
44334
+ if (id === 'checkout-branch' && !result?.ok && isBranchCheckedOutElsewhereError(result?.message)) {
44335
+ const worktreePath = parseCheckedOutWorktreePath(result?.message);
44336
+ const branchName = pendingItemAction?.id;
44337
+ if (worktreePath && branchName) {
44338
+ const worktree = context.worktreeList?.worktrees?.find((w) => w.path === worktreePath);
44339
+ dispatch({
44340
+ type: 'setWorktreeCheckoutConflict',
44341
+ value: { branch: branchName, worktreePath, dirty: worktree?.dirty ?? false },
44342
+ });
44343
+ dispatch({ type: 'setPendingConfirmation', value: 'switch-to-conflicting-worktree' });
44344
+ }
44345
+ else {
44346
+ dispatch({
44347
+ type: 'setStatus',
44348
+ value: `'${branchName ?? 'branch'}' is already checked out in another worktree.`,
44349
+ kind: 'warning',
44350
+ });
44351
+ }
44352
+ }
44237
44353
  // Refresh history rows AS WELL when the workflow could have
44238
44354
  // changed the commits the user sees (#945 follow-up). The
44239
44355
  // workflow IDs below all either create/rewrite local commits or
@@ -44243,6 +44359,10 @@ function LogInkApp(deps) {
44243
44359
  // metadata-only mutations (delete-tag, set-upstream, etc.).
44244
44360
  const historyMutatingIds = new Set([
44245
44361
  'checkout-branch',
44362
+ // Resolving a checkout conflict changes HEAD (checkout) and/or the
44363
+ // ref set (branch delete), so the graph needs a refresh.
44364
+ 'conflict-remove-worktree-checkout',
44365
+ 'conflict-remove-worktree-branch',
44246
44366
  'continue-operation',
44247
44367
  'pull-current-branch',
44248
44368
  // Fetch / pull / push bring in new commits and move
@@ -44278,7 +44398,7 @@ function LogInkApp(deps) {
44278
44398
  // (resolvePendingItemAction → action 'checkout'), so a silent
44279
44399
  // stale-while-revalidate swap keeps the list readable and just
44280
44400
  // repaints the current-branch marker once the new context lands.
44281
- if (id === 'checkout-branch' && result?.ok) {
44401
+ if ((id === 'checkout-branch' || id === 'conflict-remove-worktree-checkout') && result?.ok) {
44282
44402
  dispatch({ type: 'resetBranchSelection' });
44283
44403
  await refreshContext({ silent: true });
44284
44404
  }
@@ -44345,7 +44465,7 @@ function LogInkApp(deps) {
44345
44465
  }, [context, dispatch, git, refreshContext, refreshHistoryRows, refreshWorktreeContext,
44346
44466
  state.branchSort, state.filter, state.selectedBranchIndex,
44347
44467
  state.selectedStashIndex, state.selectedTagIndex, state.selectedWorktreeListIndex, state.stashDiffRef,
44348
- state.statusFilterMask, state.tagSort]);
44468
+ state.statusFilterMask, state.tagSort, state.worktreeCheckoutConflict]);
44349
44469
  // Resolve the active view's "yank target" (commit hash / branch /
44350
44470
  // tag / stash ref / file path) against the live filtered+sorted list,
44351
44471
  // copy it to the system clipboard, and surface the result on the
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.3";
81
+ const BUILD_VERSION = "0.62.4";
82
82
 
83
83
  const isInteractive = (config) => {
84
84
  return config?.mode === 'interactive' || !!config?.interactive;
@@ -26385,6 +26385,8 @@ function applyLogInkAction(state, action) {
26385
26385
  pendingMutationConfirmation: action.value ? undefined : state.pendingMutationConfirmation,
26386
26386
  pendingKey: undefined,
26387
26387
  };
26388
+ case 'setWorktreeCheckoutConflict':
26389
+ return { ...state, worktreeCheckoutConflict: action.value, pendingKey: undefined };
26388
26390
  case 'setPendingMutationConfirmation':
26389
26391
  return {
26390
26392
  ...state,
@@ -27757,7 +27759,45 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
27757
27759
  return [];
27758
27760
  }
27759
27761
  if (state.pendingConfirmationId) {
27762
+ // Worktree-conflict removal options (#1175): alongside the y-switch,
27763
+ // `r` removes the conflicting worktree and checks the branch out
27764
+ // here, `x` removes the worktree AND deletes the branch. Both defer
27765
+ // to the runtime (it owns the git ops + the conflict context); the
27766
+ // runtime clears the conflict state once it resolves.
27767
+ if (state.pendingConfirmationId === 'switch-to-conflicting-worktree' && state.worktreeCheckoutConflict) {
27768
+ if (inputValue === 'r') {
27769
+ return [
27770
+ { type: 'runWorkflowAction', id: 'conflict-remove-worktree-checkout' },
27771
+ action({ type: 'setPendingConfirmation', value: undefined }),
27772
+ ];
27773
+ }
27774
+ if (inputValue === 'x') {
27775
+ return [
27776
+ { type: 'runWorkflowAction', id: 'conflict-remove-worktree-branch' },
27777
+ action({ type: 'setPendingConfirmation', value: undefined }),
27778
+ ];
27779
+ }
27780
+ }
27760
27781
  if (inputValue === 'y') {
27782
+ // Worktree-conflict switch (#1175): the branch is already checked
27783
+ // out elsewhere, so "switch" just opens that worktree as a nested
27784
+ // repo frame (same mechanism as drilling into a submodule) — no
27785
+ // git mutation, hence handled here rather than via the runtime.
27786
+ if (state.pendingConfirmationId === 'switch-to-conflicting-worktree') {
27787
+ const conflict = state.worktreeCheckoutConflict;
27788
+ if (conflict) {
27789
+ return [
27790
+ action({ type: 'pushRepoFrame', label: conflict.branch, workdir: conflict.worktreePath }),
27791
+ action({ type: 'setStatus', value: `Switched to worktree ${conflict.worktreePath} (${conflict.branch})` }),
27792
+ action({ type: 'setPendingConfirmation', value: undefined }),
27793
+ action({ type: 'setWorktreeCheckoutConflict', value: undefined }),
27794
+ ];
27795
+ }
27796
+ return [
27797
+ action({ type: 'setPendingConfirmation', value: undefined }),
27798
+ action({ type: 'setWorktreeCheckoutConflict', value: undefined }),
27799
+ ];
27800
+ }
27761
27801
  const workflowAction = getLogInkWorkflowActionById(state.pendingConfirmationId);
27762
27802
  if (workflowAction?.id === 'ai-commit-summary') {
27763
27803
  return [
@@ -27783,6 +27823,11 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
27783
27823
  if (inputValue === 'n' || key.escape) {
27784
27824
  return [
27785
27825
  action({ type: 'setPendingConfirmation', value: undefined }),
27826
+ // Drop any worktree-conflict context so the prompt doesn't
27827
+ // linger after the user declines to switch.
27828
+ ...(state.worktreeCheckoutConflict
27829
+ ? [action({ type: 'setWorktreeCheckoutConflict', value: undefined })]
27830
+ : []),
27786
27831
  action({ type: 'setStatus', value: 'workflow action cancelled' }),
27787
27832
  ];
27788
27833
  }
@@ -37657,25 +37702,36 @@ function renderConfirmationPanel(h, components, state, width, theme, focused) {
37657
37702
  : state.pendingMutationConfirmation === 'discard-draft'
37658
37703
  ? 'Quit and discard the in-progress commit draft'
37659
37704
  : undefined;
37660
- const label = action?.label || mutationLabel || 'Workflow action';
37705
+ // Worktree-conflict switch (#1175): a checkout was rejected because
37706
+ // the branch is checked out elsewhere — name the branch + worktree so
37707
+ // the prompt explains what "y" does (jump into that worktree).
37708
+ const conflict = state.worktreeCheckoutConflict;
37709
+ const isWorktreeConflict = state.pendingConfirmationId === 'switch-to-conflicting-worktree';
37710
+ const label = isWorktreeConflict && conflict
37711
+ ? `Switch to the worktree where '${conflict.branch}' is checked out?`
37712
+ : action?.label || mutationLabel || 'Workflow action';
37661
37713
  const warning = state.pendingMutationConfirmation === 'discard-draft'
37662
37714
  ? 'You have an unsaved commit draft. Press y to discard it and quit.'
37663
37715
  : state.pendingMutationConfirmation
37664
37716
  ? 'This discards local changes and cannot be undone by Coco.'
37665
- // Second-stage confirm raised when a safe delete hit an unmerged
37666
- // branch — name the reason so the force isn't a blind "y again".
37667
- : state.pendingConfirmationId === 'force-delete-branch'
37668
- ? 'Not fully merged. Force-delete (git branch -D) is irreversible.'
37669
- : action?.kind === 'ai'
37670
- ? `AI action requires confirmation. Estimated ${action.estimatedTokens || '<unknown>'} tokens.`
37671
- : 'Destructive Git action requires confirmation.';
37717
+ : isWorktreeConflict && conflict
37718
+ ? `'${conflict.branch}' is checked out at ${conflict.worktreePath}.${conflict.dirty ? ' That worktree has uncommitted changes removal will be refused until it is clean or stashed.' : ''}`
37719
+ // Second-stage confirm raised when a safe delete hit an unmerged
37720
+ // branch name the reason so the force isn't a blind "y again".
37721
+ : state.pendingConfirmationId === 'force-delete-branch'
37722
+ ? 'Not fully merged. Force-delete (git branch -D) is irreversible.'
37723
+ : action?.kind === 'ai'
37724
+ ? `AI action requires confirmation. Estimated ${action.estimatedTokens || '<unknown>'} tokens.`
37725
+ : 'Destructive Git action requires confirmation.';
37672
37726
  return h(Box, {
37673
37727
  borderColor: focusBorderColor(theme, focused),
37674
37728
  borderStyle: theme.borderStyle,
37675
37729
  flexDirection: 'column',
37676
37730
  width,
37677
37731
  paddingX: 1,
37678
- }, h(Text, { bold: true }, panelTitle('Confirm', focused)), h(Text, undefined, truncateCells(label, width - 4)), h(Text, { dimColor: true }, truncateCells(warning, width - 4)), h(Text, undefined, ''), h(Text, undefined, 'Press y to confirm or n/Esc to cancel.'));
37732
+ }, h(Text, { bold: true }, panelTitle('Confirm', focused)), h(Text, undefined, truncateCells(label, width - 4)), h(Text, { dimColor: true }, truncateCells(warning, width - 4)), h(Text, undefined, ''), h(Text, undefined, truncateCells(isWorktreeConflict
37733
+ ? 'y switch · r remove worktree & check out here · x remove worktree & delete branch · n/Esc cancel'
37734
+ : 'Press y to confirm or n/Esc to cancel.', width - 4)));
37679
37735
  }
37680
37736
  /**
37681
37737
  * First-launch onboarding overlay (P1.3). Shown once per machine, gated
@@ -43789,6 +43845,42 @@ function LogInkApp(deps) {
43789
43845
  // path on the wrong target.
43790
43846
  return removeWorktreeAndBranch(git, cursorTarget, context.branches?.localBranches || []);
43791
43847
  },
43848
+ // Worktree-checkout-conflict resolutions (#1175). Unlike the
43849
+ // cursor-targeted handlers above, these act on the worktree
43850
+ // captured in `state.worktreeCheckoutConflict` (the one git named
43851
+ // when it refused the checkout), not the worktrees-view cursor.
43852
+ 'conflict-remove-worktree-checkout': async () => {
43853
+ const conflict = state.worktreeCheckoutConflict;
43854
+ dispatch({ type: 'setWorktreeCheckoutConflict', value: undefined });
43855
+ if (!conflict)
43856
+ return { ok: false, message: 'No worktree conflict to resolve.' };
43857
+ const worktree = context.worktreeList?.worktrees?.find((w) => w.path === conflict.worktreePath);
43858
+ if (!worktree)
43859
+ return { ok: false, message: `Worktree ${conflict.worktreePath} not found.` };
43860
+ // removeWorktree refuses a dirty / current worktree and returns
43861
+ // a clear message — surface it rather than forcing.
43862
+ const removed = await removeWorktree(git, worktree);
43863
+ if (!removed.ok)
43864
+ return removed;
43865
+ const branch = (context.branches?.localBranches || []).find((b) => b.type === 'local' && b.shortName === conflict.branch);
43866
+ if (!branch) {
43867
+ return { ok: true, message: `Removed worktree ${worktree.path}; branch ${conflict.branch} not found to check out.` };
43868
+ }
43869
+ const checkout = await checkoutBranch(git, branch);
43870
+ return checkout.ok
43871
+ ? { ok: true, message: `Removed worktree ${worktree.path} and checked out ${conflict.branch}` }
43872
+ : { ok: false, message: `Removed worktree ${worktree.path}, but checkout failed: ${checkout.message}` };
43873
+ },
43874
+ 'conflict-remove-worktree-branch': async () => {
43875
+ const conflict = state.worktreeCheckoutConflict;
43876
+ dispatch({ type: 'setWorktreeCheckoutConflict', value: undefined });
43877
+ if (!conflict)
43878
+ return { ok: false, message: 'No worktree conflict to resolve.' };
43879
+ const worktree = context.worktreeList?.worktrees?.find((w) => w.path === conflict.worktreePath);
43880
+ if (!worktree)
43881
+ return { ok: false, message: `Worktree ${conflict.worktreePath} not found.` };
43882
+ return removeWorktreeAndBranch(git, worktree, context.branches?.localBranches || []);
43883
+ },
43792
43884
  'abort-operation': async () => {
43793
43885
  const operation = context.operation?.operation;
43794
43886
  if (!operation) {
@@ -44251,6 +44343,30 @@ function LogInkApp(deps) {
44251
44343
  kind: 'warning',
44252
44344
  });
44253
44345
  }
44346
+ // Checking out a branch that's already checked out in another
44347
+ // worktree is rejected by git ("already checked out at <path>").
44348
+ // Rather than dead-end on that, capture the conflict and raise a
44349
+ // y-confirm offering to switch into that worktree — the branch IS
44350
+ // checked out, just elsewhere (#1175).
44351
+ if (id === 'checkout-branch' && !result?.ok && isBranchCheckedOutElsewhereError(result?.message)) {
44352
+ const worktreePath = parseCheckedOutWorktreePath(result?.message);
44353
+ const branchName = pendingItemAction?.id;
44354
+ if (worktreePath && branchName) {
44355
+ const worktree = context.worktreeList?.worktrees?.find((w) => w.path === worktreePath);
44356
+ dispatch({
44357
+ type: 'setWorktreeCheckoutConflict',
44358
+ value: { branch: branchName, worktreePath, dirty: worktree?.dirty ?? false },
44359
+ });
44360
+ dispatch({ type: 'setPendingConfirmation', value: 'switch-to-conflicting-worktree' });
44361
+ }
44362
+ else {
44363
+ dispatch({
44364
+ type: 'setStatus',
44365
+ value: `'${branchName ?? 'branch'}' is already checked out in another worktree.`,
44366
+ kind: 'warning',
44367
+ });
44368
+ }
44369
+ }
44254
44370
  // Refresh history rows AS WELL when the workflow could have
44255
44371
  // changed the commits the user sees (#945 follow-up). The
44256
44372
  // workflow IDs below all either create/rewrite local commits or
@@ -44260,6 +44376,10 @@ function LogInkApp(deps) {
44260
44376
  // metadata-only mutations (delete-tag, set-upstream, etc.).
44261
44377
  const historyMutatingIds = new Set([
44262
44378
  'checkout-branch',
44379
+ // Resolving a checkout conflict changes HEAD (checkout) and/or the
44380
+ // ref set (branch delete), so the graph needs a refresh.
44381
+ 'conflict-remove-worktree-checkout',
44382
+ 'conflict-remove-worktree-branch',
44263
44383
  'continue-operation',
44264
44384
  'pull-current-branch',
44265
44385
  // Fetch / pull / push bring in new commits and move
@@ -44295,7 +44415,7 @@ function LogInkApp(deps) {
44295
44415
  // (resolvePendingItemAction → action 'checkout'), so a silent
44296
44416
  // stale-while-revalidate swap keeps the list readable and just
44297
44417
  // repaints the current-branch marker once the new context lands.
44298
- if (id === 'checkout-branch' && result?.ok) {
44418
+ if ((id === 'checkout-branch' || id === 'conflict-remove-worktree-checkout') && result?.ok) {
44299
44419
  dispatch({ type: 'resetBranchSelection' });
44300
44420
  await refreshContext({ silent: true });
44301
44421
  }
@@ -44362,7 +44482,7 @@ function LogInkApp(deps) {
44362
44482
  }, [context, dispatch, git, refreshContext, refreshHistoryRows, refreshWorktreeContext,
44363
44483
  state.branchSort, state.filter, state.selectedBranchIndex,
44364
44484
  state.selectedStashIndex, state.selectedTagIndex, state.selectedWorktreeListIndex, state.stashDiffRef,
44365
- state.statusFilterMask, state.tagSort]);
44485
+ state.statusFilterMask, state.tagSort, state.worktreeCheckoutConflict]);
44366
44486
  // Resolve the active view's "yank target" (commit hash / branch /
44367
44487
  // tag / stash ref / file path) against the live filtered+sorted list,
44368
44488
  // copy it to the system clipboard, and surface the result on the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-coco",
3
- "version": "0.62.3",
3
+ "version": "0.62.4",
4
4
  "description": "zero-effort git commits with coco.",
5
5
  "author": "gfargo <ghfargo@gmail.com>",
6
6
  "license": "MIT",