git-coco 0.58.0 → 0.58.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/README.md CHANGED
@@ -19,6 +19,8 @@ An AI-powered git assistant that generates meaningful commit messages, creates c
19
19
  - 🔧 **Commitlint Integration** - Seamless integration with your existing commitlint configuration
20
20
  - 🏠 **Local AI Support** - Run completely offline with Ollama (no API costs, full privacy)
21
21
  - 🖥️ **Coco UI Git Workstation** - Sixteen top-level views (history, status, diff, compose, branches, tags, stash, worktrees, pull-request, PR triage, issues, conflicts, reflog, bisect, submodules, changelog) reachable via `g`-prefixed chords, with an interactive command palette (`:`), global search (`/`), and one-keystroke workflows: `S` split staged changes, `L` generate a changelog, `C` create a PR seeded from changelog, `E` open the commit draft in `$EDITOR`
22
+ - 🗂️ **Multi-Repo Workspace** - `coco workspace` (alias `ws`) scans your current directory for git repos and gives you a sortable, filterable overview — branch, dirty count, ahead/behind, open PR count — then `Enter` drills into any one as a full `coco ui` session
23
+ - 🎨 **49 Color Themes** - Catppuccin, Gruvbox, Dracula, Tokyo Night, Solarized, and more (17 light) — browse and **live-preview** them with the in-app theme picker (`gC`), then apply with one keystroke; or set one via `coco ui --theme <name>` / config. `NO_COLOR` honored
22
24
  - 🎯 **`--repo <dir>` global flag** - Drive any coco command against any repository without `cd`-ing first
23
25
  - 📦 **Package Manager Friendly** - Works with npm, yarn, and pnpm
24
26
  - 👥 **Team Ready** - Shared configurations and enterprise deployment
@@ -48,6 +50,9 @@ coco commit -i
48
50
  - **`coco review`** - AI-powered code review of your changes
49
51
  - **`coco log`** - Explore commit history with graph, filters, JSON output, and commit details
50
52
  - **`coco ui`** - Open the full-screen Git workstation TUI
53
+ - **`coco workspace`** (alias `ws`) - Multi-repo overview TUI; drill into any repo as a `coco ui` session
54
+ - **`coco issues`** / **`coco prs`** - List GitHub issues / pull requests (stdout or interactive triage)
55
+ - **`coco doctor`** - Diagnose your environment, config, and provider setup
51
56
  - **`coco init`** - Interactive setup wizard
52
57
 
53
58
  > **Smart default (0.57.0+):** running `coco` with **no subcommand** routes by environment — `coco ui` inside a git repo, `coco workspace` outside one, or `coco init` on a fresh install. It no longer defaults to `commit`; use `coco commit` for messages (or `--commit` / `COCO_DEFAULT=commit` to restore the old default).
@@ -111,10 +116,11 @@ coco log --format json
111
116
  ```text
112
117
  g h history g c compose g x conflicts
113
118
  g s status g b branches g r reflog
114
- g d diff g t tags < back
115
- g z stash Esc back / close modal
116
- g w worktrees ? help overlay
117
- g p pull request : command palette
119
+ g d diff g t tags g C theme picker
120
+ g w worktrees g z stash < back
121
+ g p pull request Esc back / close modal
122
+ ? help overlay
123
+ : command palette
118
124
  ```
119
125
 
120
126
  The command palette (`:`) is an interactive launcher with fuzzy filter and recently-used at the top — every keybinding and workflow action is reachable from there. `/` searches the active view (history, branches, tags, stash, or reflog). On branches, tags, and history, press `m` to mark a ref as the compare base — then `Enter` on a second ref opens a `git diff <base>..<head>` view. See the [Coco UI](https://github.com/gfargo/coco/wiki/Coco-UI) and [TUI Navigation](https://github.com/gfargo/coco/wiki/TUI-Navigation) wiki pages for the full keymap.
@@ -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.58.0";
64
+ const BUILD_VERSION = "0.58.1";
65
65
 
66
66
  const isInteractive = (config) => {
67
67
  return config?.mode === 'interactive' || !!config?.interactive;
@@ -25305,6 +25305,11 @@ function applyLogInkAction(state, action) {
25305
25305
  bootLoading: action.value,
25306
25306
  pendingKey: undefined,
25307
25307
  };
25308
+ case 'setRemoteOp':
25309
+ return {
25310
+ ...state,
25311
+ remoteOp: action.value,
25312
+ };
25308
25313
  case 'moveTag':
25309
25314
  return {
25310
25315
  ...state,
@@ -29964,6 +29969,35 @@ async function runAction$5(action, successMessage) {
29964
29969
  };
29965
29970
  }
29966
29971
  }
29972
+ /** Configured remote names (best-effort; `[]` if the call fails). */
29973
+ async function listRemotes(git) {
29974
+ try {
29975
+ return (await git.getRemotes()).map((remote) => remote.name).filter(Boolean);
29976
+ }
29977
+ catch {
29978
+ return [];
29979
+ }
29980
+ }
29981
+ /**
29982
+ * Remote to push a not-yet-tracked branch to: `origin` when it exists,
29983
+ * else the first configured remote, else `undefined` (no remotes).
29984
+ */
29985
+ async function resolveDefaultRemote(git) {
29986
+ const remotes = await listRemotes(git);
29987
+ if (remotes.length === 0)
29988
+ return undefined;
29989
+ return remotes.includes('origin') ? 'origin' : remotes[0];
29990
+ }
29991
+ /** Whether the remote-tracking ref `refs/remotes/<remote>/<branch>` exists locally. */
29992
+ async function remoteBranchExists(git, remote, branch) {
29993
+ try {
29994
+ await git.raw(['show-ref', '--verify', '--quiet', `refs/remotes/${remote}/${branch}`]);
29995
+ return true;
29996
+ }
29997
+ catch {
29998
+ return false;
29999
+ }
30000
+ }
29967
30001
  function checkoutBranch(git, branch) {
29968
30002
  const refs = getBranchActionRefs(branch);
29969
30003
  if (branch.type === 'remote') {
@@ -29998,11 +30032,58 @@ function fetchRemotes(git) {
29998
30032
  function pullCurrentBranch(git) {
29999
30033
  return runAction$5(() => git.raw(['pull', '--ff-only']), 'Pulled current branch');
30000
30034
  }
30001
- function pushCurrentBranch(git) {
30002
- return runAction$5(() => git.raw(['push']), 'Pushed current branch');
30003
- }
30004
- function setUpstream(git, localBranch, upstreamBranch) {
30005
- return runAction$5(() => git.raw(['branch', '--set-upstream-to', upstreamBranch, localBranch]), `Set ${localBranch} upstream to ${upstreamBranch}`);
30035
+ async function pushCurrentBranch(git) {
30036
+ const hasUpstream = await git
30037
+ .raw(['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{upstream}'])
30038
+ .then(() => true)
30039
+ .catch(() => false);
30040
+ if (hasUpstream) {
30041
+ return runAction$5(() => git.raw(['push']), 'Pushed current branch');
30042
+ }
30043
+ // No upstream yet — push with `-u` to create the remote branch AND set
30044
+ // tracking, instead of failing with git's bare "has no upstream" error.
30045
+ const remote = await resolveDefaultRemote(git);
30046
+ if (!remote) {
30047
+ return { ok: false, message: 'No upstream and no remote configured — add one with `git remote add origin <url>`.' };
30048
+ }
30049
+ const current = (await git.raw(['rev-parse', '--abbrev-ref', 'HEAD'])).trim();
30050
+ return runAction$5(() => git.raw(['push', '-u', remote, current]), `Pushed ${current} and set upstream to ${remote}/${current}`);
30051
+ }
30052
+ /**
30053
+ * Set (or create) the upstream for a local branch from a user-typed target.
30054
+ *
30055
+ * The target may be a bare branch name (`main` → `<default-remote>/main`) or
30056
+ * a `remote/branch` ref (`origin/main`). If that remote-tracking branch
30057
+ * already exists, we just link to it (`git branch --set-upstream-to`). If it
30058
+ * does NOT exist yet — the common "I just created this branch" case — we
30059
+ * `git push -u` to create the remote branch and set tracking in one step.
30060
+ * The old behavior ran `--set-upstream-to <bare-name>`, which silently
30061
+ * resolved `main` to the *local* branch and left push still complaining.
30062
+ */
30063
+ async function setUpstream(git, localBranch, target) {
30064
+ const cleaned = target.trim();
30065
+ if (!cleaned)
30066
+ return { ok: false, message: 'Upstream ref required' };
30067
+ const remotes = await listRemotes(git);
30068
+ const slash = cleaned.indexOf('/');
30069
+ let remote;
30070
+ let remoteBranch;
30071
+ if (slash > 0 && remotes.includes(cleaned.slice(0, slash))) {
30072
+ remote = cleaned.slice(0, slash);
30073
+ remoteBranch = cleaned.slice(slash + 1);
30074
+ }
30075
+ else {
30076
+ remote = remotes.includes('origin') ? 'origin' : remotes[0];
30077
+ remoteBranch = cleaned;
30078
+ }
30079
+ if (!remote) {
30080
+ return { ok: false, message: 'No remote configured — add one with `git remote add origin <url>` first.' };
30081
+ }
30082
+ if (await remoteBranchExists(git, remote, remoteBranch)) {
30083
+ return runAction$5(() => git.raw(['branch', '--set-upstream-to', `${remote}/${remoteBranch}`, localBranch]), `Set ${localBranch} to track ${remote}/${remoteBranch}`);
30084
+ }
30085
+ // Remote branch doesn't exist yet — push it and set upstream in one step.
30086
+ return runAction$5(() => git.raw(['push', '-u', remote, `${localBranch}:${remoteBranch}`]), `Pushed ${localBranch} → ${remote}/${remoteBranch} and set upstream`);
30006
30087
  }
30007
30088
  /**
30008
30089
  * Push an arbitrary local branch (need not be the current branch) to
@@ -30013,18 +30094,21 @@ function setUpstream(git, localBranch, upstreamBranch) {
30013
30094
  * Pairs with `pushCurrentBranch` (no-arg variant); the workstation
30014
30095
  * dispatcher picks one or the other based on where the cursor is.
30015
30096
  */
30016
- function pushBranch(git, branch) {
30097
+ async function pushBranch(git, branch) {
30017
30098
  if (branch.type !== 'local') {
30018
- return Promise.resolve({
30019
- ok: false,
30020
- message: 'Only local branches can be pushed.',
30021
- });
30099
+ return { ok: false, message: 'Only local branches can be pushed.' };
30022
30100
  }
30023
30101
  if (!branch.upstream || !branch.remote) {
30024
- return Promise.resolve({
30025
- ok: false,
30026
- message: `${branch.shortName} has no upstream — checkout the branch and run \`git push -u <remote> ${branch.shortName}\` first.`,
30027
- });
30102
+ // No upstream yet — push with `-u` to create the remote branch AND set
30103
+ // tracking, rather than refusing and sending the user to the shell.
30104
+ const remote = await resolveDefaultRemote(git);
30105
+ if (!remote) {
30106
+ return {
30107
+ ok: false,
30108
+ message: `${branch.shortName} has no upstream and no remote is configured — add one with \`git remote add origin <url>\`.`,
30109
+ };
30110
+ }
30111
+ return runAction$5(() => git.raw(['push', '-u', remote, branch.shortName]), `Pushed ${branch.shortName} and set upstream to ${remote}/${branch.shortName}`);
30028
30112
  }
30029
30113
  return runAction$5(() => git.raw(['push', branch.remote, branch.shortName]), `Pushed ${branch.shortName} to ${branch.upstream}`);
30030
30114
  }
@@ -35396,9 +35480,55 @@ function renderPendingCommitRow(h, Text, worktree, selected, theme) {
35396
35480
  backgroundColor: selected && !theme.noColor ? theme.colors.selection : undefined,
35397
35481
  }, truncateCells(label, 140));
35398
35482
  }
35399
- function renderHistoryPanel(h, components, state, context, bodyRows, width, theme, hasMoreCommits, loadingMoreCommits, density, rowMode, dateBucketingEnabled = false, now = getRenderNow()) {
35483
+ /**
35484
+ * Full-panel loader shown over the history surface while a remote
35485
+ * operation (fetch / pull / push) is in flight. Same bordered frame
35486
+ * and `Commits` title row as the real panel so the swap in/out is
35487
+ * seamless: a centered spinner + label + a travelling arrow track
35488
+ * give the user an unmistakable "we're talking to the remote" beat in
35489
+ * place of a frozen, soon-to-abruptly-repaint commit list.
35490
+ */
35491
+ function renderRemoteOpLoader(h, components, state, width, bodyRows, theme, focused, spinnerFrame) {
35492
+ const { Box, Text } = components;
35493
+ const op = state.remoteOp;
35494
+ if (!op) {
35495
+ return h(Box, { width });
35496
+ }
35497
+ const spinner = pickSpinnerFrame(spinnerFrame);
35498
+ // Directional glyph hints which way the bits are flowing.
35499
+ const glyph = op.kind === 'push' ? '↑' : op.kind === 'pull' ? '↓' : '↕';
35500
+ // A single glyph "travels" along a dotted track each tick so the
35501
+ // motion reads even on terminals that render braille spinners poorly.
35502
+ const trackWidth = 9;
35503
+ const pos = Math.max(0, spinnerFrame) % trackWidth;
35504
+ const track = Array.from({ length: trackWidth }, (_, i) => (i === pos ? glyph : '·')).join(' ');
35505
+ const accent = theme.noColor ? undefined : theme.colors.accent;
35506
+ const innerHeight = Math.max(3, bodyRows - 2);
35507
+ return h(Box, {
35508
+ borderColor: focusBorderColor(theme, focused),
35509
+ borderStyle: theme.borderStyle,
35510
+ flexDirection: 'column',
35511
+ flexShrink: 0,
35512
+ paddingX: 1,
35513
+ width,
35514
+ }, h(Box, { justifyContent: 'space-between' }, h(Text, { bold: true }, panelTitle('Commits', focused)), h(Text, { dimColor: true }, `${op.kind} in progress`)), h(Box, {
35515
+ flexDirection: 'column',
35516
+ alignItems: 'center',
35517
+ justifyContent: 'center',
35518
+ height: innerHeight,
35519
+ }, h(Text, { color: accent, bold: true }, `${spinner} ${op.label}`), h(Text, undefined, ''), h(Text, { color: accent }, track), h(Text, undefined, ''), h(Text, { dimColor: true }, 'Talking to the remote — history refreshes automatically.')));
35520
+ }
35521
+ function renderHistoryPanel(h, components, state, context, bodyRows, width, theme, hasMoreCommits, loadingMoreCommits, density, rowMode, dateBucketingEnabled = false, now = getRenderNow(), spinnerFrame = 0) {
35400
35522
  const { Box, Text } = components;
35401
35523
  const focused = state.focus === 'commits';
35524
+ // Remote op in flight (fetch / pull / push) → swap the commit list
35525
+ // for a centered, animated loader. Keeping the same bordered panel
35526
+ // (same width, same title row) means that when the op completes and
35527
+ // `remoteOp` clears, the fresh rows paint in place without the panel
35528
+ // jumping — smoothing over the "frozen list → sudden repaint" feel.
35529
+ if (state.remoteOp) {
35530
+ return renderRemoteOpLoader(h, components, state, width, bodyRows, theme, focused, spinnerFrame);
35531
+ }
35402
35532
  const worktree = context.worktree;
35403
35533
  // Distinct remote names seen across the repo's remote-tracking
35404
35534
  // branches — `['origin']` for a typical fork, `['origin', 'upstream']`
@@ -37279,7 +37409,7 @@ function renderMainPanel(h, components, state, context, contextStatus, worktreeD
37279
37409
  if (state.activeView === 'changelog') {
37280
37410
  return renderChangelogSurface(h, components, state, context, contextStatus, bodyRows, width, theme);
37281
37411
  }
37282
- return renderHistoryPanel(h, components, state, context, bodyRows, width, theme, hasMoreCommits, loadingMoreCommits, density, rowMode, dateBucketingEnabled);
37412
+ return renderHistoryPanel(h, components, state, context, bodyRows, width, theme, hasMoreCommits, loadingMoreCommits, density, rowMode, dateBucketingEnabled, undefined, spinnerFrame);
37283
37413
  }
37284
37414
 
37285
37415
  /**
@@ -38541,6 +38671,18 @@ function loadLogInkContextEntries(git) {
38541
38671
  },
38542
38672
  ];
38543
38673
  }
38674
+ // Workflow action ids that hit the network (fetch / pull / push) →
38675
+ // the loader copy shown over the history surface while they run. Any
38676
+ // id NOT in this map runs without the full-screen loader (local-only
38677
+ // mutations repaint fast enough that a loader would just flicker).
38678
+ const REMOTE_OP_LOADERS = {
38679
+ 'fetch-remotes': { kind: 'fetch', label: 'Fetching all remotes…' },
38680
+ 'pull-current-branch': { kind: 'pull', label: 'Pulling from origin…' },
38681
+ 'push-current-branch': { kind: 'push', label: 'Pushing to origin…' },
38682
+ 'fetch-selected-branch': { kind: 'fetch', label: 'Fetching branch from remote…' },
38683
+ 'pull-selected-branch': { kind: 'pull', label: 'Pulling branch from remote…' },
38684
+ 'push-selected-branch': { kind: 'push', label: 'Pushing branch to remote…' },
38685
+ };
38544
38686
  function predictNextFilter(action, currentFilter) {
38545
38687
  switch (action.type) {
38546
38688
  case 'appendFilter':
@@ -38847,6 +38989,7 @@ function LogInkApp(deps) {
38847
38989
  state.splitPlan?.status === 'applying' ||
38848
38990
  state.changelogView.status === 'loading' ||
38849
38991
  state.commitCompose.loading ||
38992
+ Boolean(state.remoteOp) ||
38850
38993
  Boolean(state.statusLoading);
38851
38994
  React.useEffect(() => {
38852
38995
  if (!anyLoading) {
@@ -41637,76 +41780,96 @@ function LogInkApp(deps) {
41637
41780
  dispatch({ type: 'setStatus', value: `Workflow action ${id} not yet wired`, kind: 'warning' });
41638
41781
  return;
41639
41782
  }
41640
- const result = await handler();
41641
- dispatch({ type: 'setStatus', value: result?.message || 'Workflow action complete' });
41642
- // Refresh history rows AS WELL when the workflow could have
41643
- // changed the commits the user sees (#945 follow-up). The
41644
- // workflow IDs below all either create/rewrite local commits or
41645
- // change which branch's history is being viewed without this
41646
- // the history pane shows stale data even after the operation
41647
- // succeeds. Cheap one-off `git log` call; doesn't fire on
41648
- // metadata-only mutations (delete-tag, set-upstream, etc.).
41649
- const historyMutatingIds = new Set([
41650
- 'checkout-branch',
41651
- 'continue-operation',
41652
- 'pull-current-branch',
41653
- 'cherry-pick-commit',
41654
- 'revert-commit',
41655
- 'reset-hard-to-commit',
41656
- 'reset-soft-to-commit',
41657
- 'reset-mixed-to-commit',
41658
- 'interactive-rebase-to-commit',
41659
- 'bisect-good',
41660
- 'bisect-bad',
41661
- 'bisect-skip',
41662
- 'bisect-reset',
41663
- ]);
41664
- if (result?.ok && historyMutatingIds.has(id)) {
41665
- await refreshHistoryRows();
41666
- }
41667
- // Checkout-branch is the one workflow where we want a *visible*
41668
- // refresh so the user sees the branches sidebar repaint with the
41669
- // new current branch (per #806 follow-up). Snap the cursor to
41670
- // position 0 first so when the refresh completes and the new
41671
- // current branch lands at the top (per #809's pin-current rule),
41672
- // the cursor is already there waiting.
41673
- if (id === 'checkout-branch' && result?.ok) {
41674
- dispatch({ type: 'resetBranchSelection' });
41675
- await refreshContext();
41783
+ // Remote network ops (fetch / pull / push) get a full-screen
41784
+ // history loader while in flight so the commit list doesn't sit
41785
+ // frozen and then abruptly repaint when the call returns. Cleared
41786
+ // in `finally` *after* the post-op refresh below so the loader
41787
+ // hands straight off to the freshly-fetched rows instead of
41788
+ // flashing the stale list for a frame in between.
41789
+ const remoteOp = REMOTE_OP_LOADERS[id];
41790
+ if (remoteOp) {
41791
+ dispatch({ type: 'setRemoteOp', value: remoteOp });
41676
41792
  }
41677
- else {
41678
- // Silent refresh so the deleted item disappears from the list
41679
- // without flickering the surfaces through a 'loading' phase.
41680
- await refreshContext({ silent: true });
41681
- }
41682
- // Stash workflow follow-up. Two distinct behaviours.
41683
- //
41684
- // **apply / pop**: the user brought stashed content back into the
41685
- // worktree, but the sidebar still has them on the stash view.
41686
- // Expected next move is "look at what landed in my worktree", so
41687
- // jump them to history view (where the worktree counts in the
41688
- // sidebar are visible) AND refresh worktree context explicitly so
41689
- // the staged / unstaged / untracked numbers reflect the changes.
41690
- //
41691
- // **drop**: the silent context refresh above already re-fetched
41692
- // the stash list, BUT users reported it feeling like nothing
41693
- // happened. Fix two things: refresh worktree alongside (drops can
41694
- // affect untracked files when the stash held `-u` state), and
41695
- // surface the new stash count on the status line so there's
41696
- // unambiguous feedback that the drop landed and the list shrank.
41697
- if (result?.ok && (id === 'apply-stash' || id === 'pop-stash')) {
41698
- dispatch({ type: 'pushView', value: 'history' });
41699
- await refreshWorktreeContext();
41793
+ try {
41794
+ const result = await handler();
41795
+ dispatch({ type: 'setStatus', value: result?.message || 'Workflow action complete' });
41796
+ // Refresh history rows AS WELL when the workflow could have
41797
+ // changed the commits the user sees (#945 follow-up). The
41798
+ // workflow IDs below all either create/rewrite local commits or
41799
+ // change which branch's history is being viewed — without this
41800
+ // the history pane shows stale data even after the operation
41801
+ // succeeds. Cheap one-off `git log` call; doesn't fire on
41802
+ // metadata-only mutations (delete-tag, set-upstream, etc.).
41803
+ const historyMutatingIds = new Set([
41804
+ 'checkout-branch',
41805
+ 'continue-operation',
41806
+ 'pull-current-branch',
41807
+ 'cherry-pick-commit',
41808
+ 'revert-commit',
41809
+ 'reset-hard-to-commit',
41810
+ 'reset-soft-to-commit',
41811
+ 'reset-mixed-to-commit',
41812
+ 'interactive-rebase-to-commit',
41813
+ 'bisect-good',
41814
+ 'bisect-bad',
41815
+ 'bisect-skip',
41816
+ 'bisect-reset',
41817
+ ]);
41818
+ if (result?.ok && historyMutatingIds.has(id)) {
41819
+ await refreshHistoryRows();
41820
+ }
41821
+ // Checkout-branch is the one workflow where we want a *visible*
41822
+ // refresh so the user sees the branches sidebar repaint with the
41823
+ // new current branch (per #806 follow-up). Snap the cursor to
41824
+ // position 0 first so when the refresh completes and the new
41825
+ // current branch lands at the top (per #809's pin-current rule),
41826
+ // the cursor is already there waiting.
41827
+ if (id === 'checkout-branch' && result?.ok) {
41828
+ dispatch({ type: 'resetBranchSelection' });
41829
+ await refreshContext();
41830
+ }
41831
+ else {
41832
+ // Silent refresh so the deleted item disappears from the list
41833
+ // without flickering the surfaces through a 'loading' phase.
41834
+ await refreshContext({ silent: true });
41835
+ }
41836
+ // Stash workflow follow-up. Two distinct behaviours.
41837
+ //
41838
+ // **apply / pop**: the user brought stashed content back into the
41839
+ // worktree, but the sidebar still has them on the stash view.
41840
+ // Expected next move is "look at what landed in my worktree", so
41841
+ // jump them to history view (where the worktree counts in the
41842
+ // sidebar are visible) AND refresh worktree context explicitly so
41843
+ // the staged / unstaged / untracked numbers reflect the changes.
41844
+ //
41845
+ // **drop**: the silent context refresh above already re-fetched
41846
+ // the stash list, BUT users reported it feeling like nothing
41847
+ // happened. Fix two things: refresh worktree alongside (drops can
41848
+ // affect untracked files when the stash held `-u` state), and
41849
+ // surface the new stash count on the status line so there's
41850
+ // unambiguous feedback that the drop landed and the list shrank.
41851
+ if (result?.ok && (id === 'apply-stash' || id === 'pop-stash')) {
41852
+ dispatch({ type: 'pushView', value: 'history' });
41853
+ await refreshWorktreeContext();
41854
+ }
41855
+ if (result?.ok && id === 'drop-stash') {
41856
+ // Explicit worktree refresh in case the dropped stash carried
41857
+ // untracked-file state that's now collected.
41858
+ await refreshWorktreeContext();
41859
+ // The silent context refresh already replaced `context.stashes`;
41860
+ // reading the count back here would be stale because closures
41861
+ // capture the pre-refresh value. Status message stays generic
41862
+ // ("Dropped stash@{N}") — the visible list shrinking is the
41863
+ // unambiguous signal that the operation landed.
41864
+ }
41700
41865
  }
41701
- if (result?.ok && id === 'drop-stash') {
41702
- // Explicit worktree refresh in case the dropped stash carried
41703
- // untracked-file state that's now collected.
41704
- await refreshWorktreeContext();
41705
- // The silent context refresh already replaced `context.stashes`;
41706
- // reading the count back here would be stale because closures
41707
- // capture the pre-refresh value. Status message stays generic
41708
- // ("Dropped stash@{N}") — the visible list shrinking is the
41709
- // unambiguous signal that the operation landed.
41866
+ finally {
41867
+ // Always clear the loader even if a refresh threw — so a
41868
+ // failed fetch/pull can't leave the history surface stuck behind
41869
+ // the spinner.
41870
+ if (remoteOp) {
41871
+ dispatch({ type: 'setRemoteOp', value: undefined });
41872
+ }
41710
41873
  }
41711
41874
  }, [context, dispatch, git, refreshContext, refreshHistoryRows, refreshWorktreeContext,
41712
41875
  state.branchSort, state.filter, state.selectedBranchIndex,
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.58.0";
81
+ const BUILD_VERSION = "0.58.1";
82
82
 
83
83
  const isInteractive = (config) => {
84
84
  return config?.mode === 'interactive' || !!config?.interactive;
@@ -25322,6 +25322,11 @@ function applyLogInkAction(state, action) {
25322
25322
  bootLoading: action.value,
25323
25323
  pendingKey: undefined,
25324
25324
  };
25325
+ case 'setRemoteOp':
25326
+ return {
25327
+ ...state,
25328
+ remoteOp: action.value,
25329
+ };
25325
25330
  case 'moveTag':
25326
25331
  return {
25327
25332
  ...state,
@@ -29981,6 +29986,35 @@ async function runAction$5(action, successMessage) {
29981
29986
  };
29982
29987
  }
29983
29988
  }
29989
+ /** Configured remote names (best-effort; `[]` if the call fails). */
29990
+ async function listRemotes(git) {
29991
+ try {
29992
+ return (await git.getRemotes()).map((remote) => remote.name).filter(Boolean);
29993
+ }
29994
+ catch {
29995
+ return [];
29996
+ }
29997
+ }
29998
+ /**
29999
+ * Remote to push a not-yet-tracked branch to: `origin` when it exists,
30000
+ * else the first configured remote, else `undefined` (no remotes).
30001
+ */
30002
+ async function resolveDefaultRemote(git) {
30003
+ const remotes = await listRemotes(git);
30004
+ if (remotes.length === 0)
30005
+ return undefined;
30006
+ return remotes.includes('origin') ? 'origin' : remotes[0];
30007
+ }
30008
+ /** Whether the remote-tracking ref `refs/remotes/<remote>/<branch>` exists locally. */
30009
+ async function remoteBranchExists(git, remote, branch) {
30010
+ try {
30011
+ await git.raw(['show-ref', '--verify', '--quiet', `refs/remotes/${remote}/${branch}`]);
30012
+ return true;
30013
+ }
30014
+ catch {
30015
+ return false;
30016
+ }
30017
+ }
29984
30018
  function checkoutBranch(git, branch) {
29985
30019
  const refs = getBranchActionRefs(branch);
29986
30020
  if (branch.type === 'remote') {
@@ -30015,11 +30049,58 @@ function fetchRemotes(git) {
30015
30049
  function pullCurrentBranch(git) {
30016
30050
  return runAction$5(() => git.raw(['pull', '--ff-only']), 'Pulled current branch');
30017
30051
  }
30018
- function pushCurrentBranch(git) {
30019
- return runAction$5(() => git.raw(['push']), 'Pushed current branch');
30020
- }
30021
- function setUpstream(git, localBranch, upstreamBranch) {
30022
- return runAction$5(() => git.raw(['branch', '--set-upstream-to', upstreamBranch, localBranch]), `Set ${localBranch} upstream to ${upstreamBranch}`);
30052
+ async function pushCurrentBranch(git) {
30053
+ const hasUpstream = await git
30054
+ .raw(['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{upstream}'])
30055
+ .then(() => true)
30056
+ .catch(() => false);
30057
+ if (hasUpstream) {
30058
+ return runAction$5(() => git.raw(['push']), 'Pushed current branch');
30059
+ }
30060
+ // No upstream yet — push with `-u` to create the remote branch AND set
30061
+ // tracking, instead of failing with git's bare "has no upstream" error.
30062
+ const remote = await resolveDefaultRemote(git);
30063
+ if (!remote) {
30064
+ return { ok: false, message: 'No upstream and no remote configured — add one with `git remote add origin <url>`.' };
30065
+ }
30066
+ const current = (await git.raw(['rev-parse', '--abbrev-ref', 'HEAD'])).trim();
30067
+ return runAction$5(() => git.raw(['push', '-u', remote, current]), `Pushed ${current} and set upstream to ${remote}/${current}`);
30068
+ }
30069
+ /**
30070
+ * Set (or create) the upstream for a local branch from a user-typed target.
30071
+ *
30072
+ * The target may be a bare branch name (`main` → `<default-remote>/main`) or
30073
+ * a `remote/branch` ref (`origin/main`). If that remote-tracking branch
30074
+ * already exists, we just link to it (`git branch --set-upstream-to`). If it
30075
+ * does NOT exist yet — the common "I just created this branch" case — we
30076
+ * `git push -u` to create the remote branch and set tracking in one step.
30077
+ * The old behavior ran `--set-upstream-to <bare-name>`, which silently
30078
+ * resolved `main` to the *local* branch and left push still complaining.
30079
+ */
30080
+ async function setUpstream(git, localBranch, target) {
30081
+ const cleaned = target.trim();
30082
+ if (!cleaned)
30083
+ return { ok: false, message: 'Upstream ref required' };
30084
+ const remotes = await listRemotes(git);
30085
+ const slash = cleaned.indexOf('/');
30086
+ let remote;
30087
+ let remoteBranch;
30088
+ if (slash > 0 && remotes.includes(cleaned.slice(0, slash))) {
30089
+ remote = cleaned.slice(0, slash);
30090
+ remoteBranch = cleaned.slice(slash + 1);
30091
+ }
30092
+ else {
30093
+ remote = remotes.includes('origin') ? 'origin' : remotes[0];
30094
+ remoteBranch = cleaned;
30095
+ }
30096
+ if (!remote) {
30097
+ return { ok: false, message: 'No remote configured — add one with `git remote add origin <url>` first.' };
30098
+ }
30099
+ if (await remoteBranchExists(git, remote, remoteBranch)) {
30100
+ return runAction$5(() => git.raw(['branch', '--set-upstream-to', `${remote}/${remoteBranch}`, localBranch]), `Set ${localBranch} to track ${remote}/${remoteBranch}`);
30101
+ }
30102
+ // Remote branch doesn't exist yet — push it and set upstream in one step.
30103
+ return runAction$5(() => git.raw(['push', '-u', remote, `${localBranch}:${remoteBranch}`]), `Pushed ${localBranch} → ${remote}/${remoteBranch} and set upstream`);
30023
30104
  }
30024
30105
  /**
30025
30106
  * Push an arbitrary local branch (need not be the current branch) to
@@ -30030,18 +30111,21 @@ function setUpstream(git, localBranch, upstreamBranch) {
30030
30111
  * Pairs with `pushCurrentBranch` (no-arg variant); the workstation
30031
30112
  * dispatcher picks one or the other based on where the cursor is.
30032
30113
  */
30033
- function pushBranch(git, branch) {
30114
+ async function pushBranch(git, branch) {
30034
30115
  if (branch.type !== 'local') {
30035
- return Promise.resolve({
30036
- ok: false,
30037
- message: 'Only local branches can be pushed.',
30038
- });
30116
+ return { ok: false, message: 'Only local branches can be pushed.' };
30039
30117
  }
30040
30118
  if (!branch.upstream || !branch.remote) {
30041
- return Promise.resolve({
30042
- ok: false,
30043
- message: `${branch.shortName} has no upstream — checkout the branch and run \`git push -u <remote> ${branch.shortName}\` first.`,
30044
- });
30119
+ // No upstream yet — push with `-u` to create the remote branch AND set
30120
+ // tracking, rather than refusing and sending the user to the shell.
30121
+ const remote = await resolveDefaultRemote(git);
30122
+ if (!remote) {
30123
+ return {
30124
+ ok: false,
30125
+ message: `${branch.shortName} has no upstream and no remote is configured — add one with \`git remote add origin <url>\`.`,
30126
+ };
30127
+ }
30128
+ return runAction$5(() => git.raw(['push', '-u', remote, branch.shortName]), `Pushed ${branch.shortName} and set upstream to ${remote}/${branch.shortName}`);
30045
30129
  }
30046
30130
  return runAction$5(() => git.raw(['push', branch.remote, branch.shortName]), `Pushed ${branch.shortName} to ${branch.upstream}`);
30047
30131
  }
@@ -35413,9 +35497,55 @@ function renderPendingCommitRow(h, Text, worktree, selected, theme) {
35413
35497
  backgroundColor: selected && !theme.noColor ? theme.colors.selection : undefined,
35414
35498
  }, truncateCells(label, 140));
35415
35499
  }
35416
- function renderHistoryPanel(h, components, state, context, bodyRows, width, theme, hasMoreCommits, loadingMoreCommits, density, rowMode, dateBucketingEnabled = false, now = getRenderNow()) {
35500
+ /**
35501
+ * Full-panel loader shown over the history surface while a remote
35502
+ * operation (fetch / pull / push) is in flight. Same bordered frame
35503
+ * and `Commits` title row as the real panel so the swap in/out is
35504
+ * seamless: a centered spinner + label + a travelling arrow track
35505
+ * give the user an unmistakable "we're talking to the remote" beat in
35506
+ * place of a frozen, soon-to-abruptly-repaint commit list.
35507
+ */
35508
+ function renderRemoteOpLoader(h, components, state, width, bodyRows, theme, focused, spinnerFrame) {
35509
+ const { Box, Text } = components;
35510
+ const op = state.remoteOp;
35511
+ if (!op) {
35512
+ return h(Box, { width });
35513
+ }
35514
+ const spinner = pickSpinnerFrame(spinnerFrame);
35515
+ // Directional glyph hints which way the bits are flowing.
35516
+ const glyph = op.kind === 'push' ? '↑' : op.kind === 'pull' ? '↓' : '↕';
35517
+ // A single glyph "travels" along a dotted track each tick so the
35518
+ // motion reads even on terminals that render braille spinners poorly.
35519
+ const trackWidth = 9;
35520
+ const pos = Math.max(0, spinnerFrame) % trackWidth;
35521
+ const track = Array.from({ length: trackWidth }, (_, i) => (i === pos ? glyph : '·')).join(' ');
35522
+ const accent = theme.noColor ? undefined : theme.colors.accent;
35523
+ const innerHeight = Math.max(3, bodyRows - 2);
35524
+ return h(Box, {
35525
+ borderColor: focusBorderColor(theme, focused),
35526
+ borderStyle: theme.borderStyle,
35527
+ flexDirection: 'column',
35528
+ flexShrink: 0,
35529
+ paddingX: 1,
35530
+ width,
35531
+ }, h(Box, { justifyContent: 'space-between' }, h(Text, { bold: true }, panelTitle('Commits', focused)), h(Text, { dimColor: true }, `${op.kind} in progress`)), h(Box, {
35532
+ flexDirection: 'column',
35533
+ alignItems: 'center',
35534
+ justifyContent: 'center',
35535
+ height: innerHeight,
35536
+ }, h(Text, { color: accent, bold: true }, `${spinner} ${op.label}`), h(Text, undefined, ''), h(Text, { color: accent }, track), h(Text, undefined, ''), h(Text, { dimColor: true }, 'Talking to the remote — history refreshes automatically.')));
35537
+ }
35538
+ function renderHistoryPanel(h, components, state, context, bodyRows, width, theme, hasMoreCommits, loadingMoreCommits, density, rowMode, dateBucketingEnabled = false, now = getRenderNow(), spinnerFrame = 0) {
35417
35539
  const { Box, Text } = components;
35418
35540
  const focused = state.focus === 'commits';
35541
+ // Remote op in flight (fetch / pull / push) → swap the commit list
35542
+ // for a centered, animated loader. Keeping the same bordered panel
35543
+ // (same width, same title row) means that when the op completes and
35544
+ // `remoteOp` clears, the fresh rows paint in place without the panel
35545
+ // jumping — smoothing over the "frozen list → sudden repaint" feel.
35546
+ if (state.remoteOp) {
35547
+ return renderRemoteOpLoader(h, components, state, width, bodyRows, theme, focused, spinnerFrame);
35548
+ }
35419
35549
  const worktree = context.worktree;
35420
35550
  // Distinct remote names seen across the repo's remote-tracking
35421
35551
  // branches — `['origin']` for a typical fork, `['origin', 'upstream']`
@@ -37296,7 +37426,7 @@ function renderMainPanel(h, components, state, context, contextStatus, worktreeD
37296
37426
  if (state.activeView === 'changelog') {
37297
37427
  return renderChangelogSurface(h, components, state, context, contextStatus, bodyRows, width, theme);
37298
37428
  }
37299
- return renderHistoryPanel(h, components, state, context, bodyRows, width, theme, hasMoreCommits, loadingMoreCommits, density, rowMode, dateBucketingEnabled);
37429
+ return renderHistoryPanel(h, components, state, context, bodyRows, width, theme, hasMoreCommits, loadingMoreCommits, density, rowMode, dateBucketingEnabled, undefined, spinnerFrame);
37300
37430
  }
37301
37431
 
37302
37432
  /**
@@ -38558,6 +38688,18 @@ function loadLogInkContextEntries(git) {
38558
38688
  },
38559
38689
  ];
38560
38690
  }
38691
+ // Workflow action ids that hit the network (fetch / pull / push) →
38692
+ // the loader copy shown over the history surface while they run. Any
38693
+ // id NOT in this map runs without the full-screen loader (local-only
38694
+ // mutations repaint fast enough that a loader would just flicker).
38695
+ const REMOTE_OP_LOADERS = {
38696
+ 'fetch-remotes': { kind: 'fetch', label: 'Fetching all remotes…' },
38697
+ 'pull-current-branch': { kind: 'pull', label: 'Pulling from origin…' },
38698
+ 'push-current-branch': { kind: 'push', label: 'Pushing to origin…' },
38699
+ 'fetch-selected-branch': { kind: 'fetch', label: 'Fetching branch from remote…' },
38700
+ 'pull-selected-branch': { kind: 'pull', label: 'Pulling branch from remote…' },
38701
+ 'push-selected-branch': { kind: 'push', label: 'Pushing branch to remote…' },
38702
+ };
38561
38703
  function predictNextFilter(action, currentFilter) {
38562
38704
  switch (action.type) {
38563
38705
  case 'appendFilter':
@@ -38864,6 +39006,7 @@ function LogInkApp(deps) {
38864
39006
  state.splitPlan?.status === 'applying' ||
38865
39007
  state.changelogView.status === 'loading' ||
38866
39008
  state.commitCompose.loading ||
39009
+ Boolean(state.remoteOp) ||
38867
39010
  Boolean(state.statusLoading);
38868
39011
  React.useEffect(() => {
38869
39012
  if (!anyLoading) {
@@ -41654,76 +41797,96 @@ function LogInkApp(deps) {
41654
41797
  dispatch({ type: 'setStatus', value: `Workflow action ${id} not yet wired`, kind: 'warning' });
41655
41798
  return;
41656
41799
  }
41657
- const result = await handler();
41658
- dispatch({ type: 'setStatus', value: result?.message || 'Workflow action complete' });
41659
- // Refresh history rows AS WELL when the workflow could have
41660
- // changed the commits the user sees (#945 follow-up). The
41661
- // workflow IDs below all either create/rewrite local commits or
41662
- // change which branch's history is being viewed without this
41663
- // the history pane shows stale data even after the operation
41664
- // succeeds. Cheap one-off `git log` call; doesn't fire on
41665
- // metadata-only mutations (delete-tag, set-upstream, etc.).
41666
- const historyMutatingIds = new Set([
41667
- 'checkout-branch',
41668
- 'continue-operation',
41669
- 'pull-current-branch',
41670
- 'cherry-pick-commit',
41671
- 'revert-commit',
41672
- 'reset-hard-to-commit',
41673
- 'reset-soft-to-commit',
41674
- 'reset-mixed-to-commit',
41675
- 'interactive-rebase-to-commit',
41676
- 'bisect-good',
41677
- 'bisect-bad',
41678
- 'bisect-skip',
41679
- 'bisect-reset',
41680
- ]);
41681
- if (result?.ok && historyMutatingIds.has(id)) {
41682
- await refreshHistoryRows();
41683
- }
41684
- // Checkout-branch is the one workflow where we want a *visible*
41685
- // refresh so the user sees the branches sidebar repaint with the
41686
- // new current branch (per #806 follow-up). Snap the cursor to
41687
- // position 0 first so when the refresh completes and the new
41688
- // current branch lands at the top (per #809's pin-current rule),
41689
- // the cursor is already there waiting.
41690
- if (id === 'checkout-branch' && result?.ok) {
41691
- dispatch({ type: 'resetBranchSelection' });
41692
- await refreshContext();
41800
+ // Remote network ops (fetch / pull / push) get a full-screen
41801
+ // history loader while in flight so the commit list doesn't sit
41802
+ // frozen and then abruptly repaint when the call returns. Cleared
41803
+ // in `finally` *after* the post-op refresh below so the loader
41804
+ // hands straight off to the freshly-fetched rows instead of
41805
+ // flashing the stale list for a frame in between.
41806
+ const remoteOp = REMOTE_OP_LOADERS[id];
41807
+ if (remoteOp) {
41808
+ dispatch({ type: 'setRemoteOp', value: remoteOp });
41693
41809
  }
41694
- else {
41695
- // Silent refresh so the deleted item disappears from the list
41696
- // without flickering the surfaces through a 'loading' phase.
41697
- await refreshContext({ silent: true });
41698
- }
41699
- // Stash workflow follow-up. Two distinct behaviours.
41700
- //
41701
- // **apply / pop**: the user brought stashed content back into the
41702
- // worktree, but the sidebar still has them on the stash view.
41703
- // Expected next move is "look at what landed in my worktree", so
41704
- // jump them to history view (where the worktree counts in the
41705
- // sidebar are visible) AND refresh worktree context explicitly so
41706
- // the staged / unstaged / untracked numbers reflect the changes.
41707
- //
41708
- // **drop**: the silent context refresh above already re-fetched
41709
- // the stash list, BUT users reported it feeling like nothing
41710
- // happened. Fix two things: refresh worktree alongside (drops can
41711
- // affect untracked files when the stash held `-u` state), and
41712
- // surface the new stash count on the status line so there's
41713
- // unambiguous feedback that the drop landed and the list shrank.
41714
- if (result?.ok && (id === 'apply-stash' || id === 'pop-stash')) {
41715
- dispatch({ type: 'pushView', value: 'history' });
41716
- await refreshWorktreeContext();
41810
+ try {
41811
+ const result = await handler();
41812
+ dispatch({ type: 'setStatus', value: result?.message || 'Workflow action complete' });
41813
+ // Refresh history rows AS WELL when the workflow could have
41814
+ // changed the commits the user sees (#945 follow-up). The
41815
+ // workflow IDs below all either create/rewrite local commits or
41816
+ // change which branch's history is being viewed — without this
41817
+ // the history pane shows stale data even after the operation
41818
+ // succeeds. Cheap one-off `git log` call; doesn't fire on
41819
+ // metadata-only mutations (delete-tag, set-upstream, etc.).
41820
+ const historyMutatingIds = new Set([
41821
+ 'checkout-branch',
41822
+ 'continue-operation',
41823
+ 'pull-current-branch',
41824
+ 'cherry-pick-commit',
41825
+ 'revert-commit',
41826
+ 'reset-hard-to-commit',
41827
+ 'reset-soft-to-commit',
41828
+ 'reset-mixed-to-commit',
41829
+ 'interactive-rebase-to-commit',
41830
+ 'bisect-good',
41831
+ 'bisect-bad',
41832
+ 'bisect-skip',
41833
+ 'bisect-reset',
41834
+ ]);
41835
+ if (result?.ok && historyMutatingIds.has(id)) {
41836
+ await refreshHistoryRows();
41837
+ }
41838
+ // Checkout-branch is the one workflow where we want a *visible*
41839
+ // refresh so the user sees the branches sidebar repaint with the
41840
+ // new current branch (per #806 follow-up). Snap the cursor to
41841
+ // position 0 first so when the refresh completes and the new
41842
+ // current branch lands at the top (per #809's pin-current rule),
41843
+ // the cursor is already there waiting.
41844
+ if (id === 'checkout-branch' && result?.ok) {
41845
+ dispatch({ type: 'resetBranchSelection' });
41846
+ await refreshContext();
41847
+ }
41848
+ else {
41849
+ // Silent refresh so the deleted item disappears from the list
41850
+ // without flickering the surfaces through a 'loading' phase.
41851
+ await refreshContext({ silent: true });
41852
+ }
41853
+ // Stash workflow follow-up. Two distinct behaviours.
41854
+ //
41855
+ // **apply / pop**: the user brought stashed content back into the
41856
+ // worktree, but the sidebar still has them on the stash view.
41857
+ // Expected next move is "look at what landed in my worktree", so
41858
+ // jump them to history view (where the worktree counts in the
41859
+ // sidebar are visible) AND refresh worktree context explicitly so
41860
+ // the staged / unstaged / untracked numbers reflect the changes.
41861
+ //
41862
+ // **drop**: the silent context refresh above already re-fetched
41863
+ // the stash list, BUT users reported it feeling like nothing
41864
+ // happened. Fix two things: refresh worktree alongside (drops can
41865
+ // affect untracked files when the stash held `-u` state), and
41866
+ // surface the new stash count on the status line so there's
41867
+ // unambiguous feedback that the drop landed and the list shrank.
41868
+ if (result?.ok && (id === 'apply-stash' || id === 'pop-stash')) {
41869
+ dispatch({ type: 'pushView', value: 'history' });
41870
+ await refreshWorktreeContext();
41871
+ }
41872
+ if (result?.ok && id === 'drop-stash') {
41873
+ // Explicit worktree refresh in case the dropped stash carried
41874
+ // untracked-file state that's now collected.
41875
+ await refreshWorktreeContext();
41876
+ // The silent context refresh already replaced `context.stashes`;
41877
+ // reading the count back here would be stale because closures
41878
+ // capture the pre-refresh value. Status message stays generic
41879
+ // ("Dropped stash@{N}") — the visible list shrinking is the
41880
+ // unambiguous signal that the operation landed.
41881
+ }
41717
41882
  }
41718
- if (result?.ok && id === 'drop-stash') {
41719
- // Explicit worktree refresh in case the dropped stash carried
41720
- // untracked-file state that's now collected.
41721
- await refreshWorktreeContext();
41722
- // The silent context refresh already replaced `context.stashes`;
41723
- // reading the count back here would be stale because closures
41724
- // capture the pre-refresh value. Status message stays generic
41725
- // ("Dropped stash@{N}") — the visible list shrinking is the
41726
- // unambiguous signal that the operation landed.
41883
+ finally {
41884
+ // Always clear the loader even if a refresh threw — so a
41885
+ // failed fetch/pull can't leave the history surface stuck behind
41886
+ // the spinner.
41887
+ if (remoteOp) {
41888
+ dispatch({ type: 'setRemoteOp', value: undefined });
41889
+ }
41727
41890
  }
41728
41891
  }, [context, dispatch, git, refreshContext, refreshHistoryRows, refreshWorktreeContext,
41729
41892
  state.branchSort, state.filter, state.selectedBranchIndex,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-coco",
3
- "version": "0.58.0",
3
+ "version": "0.58.1",
4
4
  "description": "zero-effort git commits with coco.",
5
5
  "author": "gfargo <ghfargo@gmail.com>",
6
6
  "license": "MIT",
@@ -79,7 +79,7 @@
79
79
  "@types/ini": "^4.1.1",
80
80
  "@types/jest": "^30.0.0",
81
81
  "@types/node": "^25.0.10",
82
- "@types/react": "19.2.14",
82
+ "@types/react": "19.2.15",
83
83
  "@types/yargs": "^17.0.33",
84
84
  "@typescript-eslint/eslint-plugin": "^7.13.1",
85
85
  "@typescript-eslint/parser": "^7.13.1",
@@ -114,13 +114,13 @@
114
114
  "chalk": "4.1.2",
115
115
  "diff": "9.0.0",
116
116
  "ini": "7.0.0",
117
- "ink": "7.0.3",
117
+ "ink": "7.0.5",
118
118
  "minimatch": "^10.2.5",
119
119
  "ora": "5.4.1",
120
120
  "p-queue": "5.0.0",
121
121
  "performance-now": "2.1.0",
122
122
  "pretty-ms": "7.0.1",
123
- "react": "19.2.5",
123
+ "react": "19.2.6",
124
124
  "react-devtools-core": "7.0.1",
125
125
  "simple-git": "3.36.0",
126
126
  "tiktoken": "^1.0.21",