git-coco 0.38.0 → 0.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.esm.mjs +577 -130
- package/dist/index.js +577 -130
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -78,7 +78,7 @@ var readline__namespace = /*#__PURE__*/_interopNamespaceDefault(readline);
|
|
|
78
78
|
/**
|
|
79
79
|
* Current build version from package.json
|
|
80
80
|
*/
|
|
81
|
-
const BUILD_VERSION = "0.
|
|
81
|
+
const BUILD_VERSION = "0.39.0";
|
|
82
82
|
|
|
83
83
|
const isInteractive = (config) => {
|
|
84
84
|
return config?.mode === 'interactive' || !!config?.interactive;
|
|
@@ -14772,84 +14772,6 @@ function formatInkRefLabels(refs) {
|
|
|
14772
14772
|
return refs.length ? ` ${refs.map((ref) => `[${ref}]`).join(' ')}` : '';
|
|
14773
14773
|
}
|
|
14774
14774
|
|
|
14775
|
-
function countLabel(count, singular, plural = `${singular}s`) {
|
|
14776
|
-
return `${count} ${count === 1 ? singular : plural}`;
|
|
14777
|
-
}
|
|
14778
|
-
function getLogInkWorkflowSections(context) {
|
|
14779
|
-
const currentBranch = context.branches?.currentBranch || context.provider?.currentBranch || '<detached>';
|
|
14780
|
-
const dirty = context.branches?.dirty ? 'dirty worktree' : 'clean worktree';
|
|
14781
|
-
const loading = context.contextLoading;
|
|
14782
|
-
const currentPullRequest = context.provider?.currentPullRequest || context.pullRequest?.currentPullRequest;
|
|
14783
|
-
const repository = context.provider?.repository;
|
|
14784
|
-
const repoName = repository?.owner && repository.name
|
|
14785
|
-
? `${repository.owner}/${repository.name}`
|
|
14786
|
-
: repository?.message || 'local repository';
|
|
14787
|
-
const operation = context.operation;
|
|
14788
|
-
const worktree = context.worktree;
|
|
14789
|
-
return [
|
|
14790
|
-
{
|
|
14791
|
-
title: 'Branch',
|
|
14792
|
-
lines: [
|
|
14793
|
-
`Current: ${currentBranch}`,
|
|
14794
|
-
`State: ${dirty}`,
|
|
14795
|
-
loading && !context.branches
|
|
14796
|
-
? 'Branch data loading'
|
|
14797
|
-
: context.branches
|
|
14798
|
-
? `${countLabel(context.branches.localBranches.length, 'local branch', 'local branches')} | ${countLabel(context.branches.remoteBranches.length, 'remote branch', 'remote branches')}`
|
|
14799
|
-
: 'Branch data unavailable',
|
|
14800
|
-
],
|
|
14801
|
-
},
|
|
14802
|
-
{
|
|
14803
|
-
title: 'Provider / PR',
|
|
14804
|
-
lines: [
|
|
14805
|
-
`Repository: ${repoName}`,
|
|
14806
|
-
loading && !context.provider && !context.pullRequest
|
|
14807
|
-
? 'Provider and pull request data loading'
|
|
14808
|
-
: currentPullRequest
|
|
14809
|
-
? `PR #${currentPullRequest.number} ${currentPullRequest.state}${currentPullRequest.isDraft ? ' draft' : ''}`
|
|
14810
|
-
: 'No pull request detected for current branch',
|
|
14811
|
-
context.provider?.authenticated === false ? 'Provider auth: offline' : 'Provider auth: available',
|
|
14812
|
-
],
|
|
14813
|
-
},
|
|
14814
|
-
{
|
|
14815
|
-
title: 'Status',
|
|
14816
|
-
lines: loading && !worktree
|
|
14817
|
-
? ['Status data loading']
|
|
14818
|
-
: worktree
|
|
14819
|
-
? [
|
|
14820
|
-
`${countLabel(worktree.stagedCount, 'staged file')}`,
|
|
14821
|
-
`${countLabel(worktree.unstagedCount, 'unstaged file')}`,
|
|
14822
|
-
`${countLabel(worktree.untrackedCount, 'untracked file')}`,
|
|
14823
|
-
]
|
|
14824
|
-
: ['Status data unavailable'],
|
|
14825
|
-
},
|
|
14826
|
-
{
|
|
14827
|
-
title: 'Tags / Stashes / Worktrees',
|
|
14828
|
-
lines: [
|
|
14829
|
-
loading && !context.tags ? 'Tags loading' : context.tags ? countLabel(context.tags.tags.length, 'tag') : 'Tags unavailable',
|
|
14830
|
-
loading && !context.stashes ? 'Stashes loading' : context.stashes ? countLabel(context.stashes.stashes.length, 'stash', 'stashes') : 'Stashes unavailable',
|
|
14831
|
-
context.worktreeList
|
|
14832
|
-
? countLabel(context.worktreeList.worktrees.length, 'worktree')
|
|
14833
|
-
: loading
|
|
14834
|
-
? 'Worktrees loading'
|
|
14835
|
-
: 'Worktrees unavailable',
|
|
14836
|
-
],
|
|
14837
|
-
},
|
|
14838
|
-
{
|
|
14839
|
-
title: 'Operation / AI',
|
|
14840
|
-
lines: [
|
|
14841
|
-
loading && !operation
|
|
14842
|
-
? 'Operation data loading'
|
|
14843
|
-
: operation?.operation
|
|
14844
|
-
? `${operation.operation} in progress with ${countLabel(operation.conflictedFiles.length, 'conflict')}`
|
|
14845
|
-
: 'No merge, rebase, cherry-pick, or revert in progress',
|
|
14846
|
-
context.selectedCommit
|
|
14847
|
-
? `AI actions target ${context.selectedCommit.shortHash}; estimates shown before execution`
|
|
14848
|
-
: 'AI actions require a selected commit',
|
|
14849
|
-
],
|
|
14850
|
-
},
|
|
14851
|
-
];
|
|
14852
|
-
}
|
|
14853
14775
|
function getLogInkWorkflowActions() {
|
|
14854
14776
|
return [
|
|
14855
14777
|
{
|
|
@@ -15093,6 +15015,38 @@ function getLogInkWorkflowActions() {
|
|
|
15093
15015
|
kind: 'destructive',
|
|
15094
15016
|
requiresConfirmation: true,
|
|
15095
15017
|
},
|
|
15018
|
+
{
|
|
15019
|
+
// Per-view-only: scoped to the history view in inkInput (key `B`).
|
|
15020
|
+
// The prompt itself is the affirmative gate — the user has to
|
|
15021
|
+
// type a branch name before anything happens — so this skips the
|
|
15022
|
+
// y-confirm path. Empty key keeps it palette-discoverable; the
|
|
15023
|
+
// palette path can't synthesize a branch name and surfaces a
|
|
15024
|
+
// hint instead.
|
|
15025
|
+
//
|
|
15026
|
+
// Distinct from `create-branch` (palette / `+` on branches view),
|
|
15027
|
+
// which uses `git switch -c` and switches onto the new branch.
|
|
15028
|
+
// This workflow uses `git branch <name> <sha>` and stays put —
|
|
15029
|
+
// GitKraken's "create branch here" semantic.
|
|
15030
|
+
id: 'create-branch-here',
|
|
15031
|
+
key: '',
|
|
15032
|
+
label: 'Create branch from commit',
|
|
15033
|
+
description: 'Create a branch pointed at the cursored commit (does not switch).',
|
|
15034
|
+
kind: 'normal',
|
|
15035
|
+
requiresConfirmation: false,
|
|
15036
|
+
},
|
|
15037
|
+
{
|
|
15038
|
+
// Per-view-only: scoped to the history view in inkInput via the
|
|
15039
|
+
// `gT` chord (bare `T` is taken by delete-tag on the tags view).
|
|
15040
|
+
// Same prompt-as-confirmation pattern as create-branch-here.
|
|
15041
|
+
// Lightweight tag — annotated tags remain available through the
|
|
15042
|
+
// existing `+` flow on the tags view.
|
|
15043
|
+
id: 'create-tag-here',
|
|
15044
|
+
key: '',
|
|
15045
|
+
label: 'Create tag at commit',
|
|
15046
|
+
description: 'Create a lightweight tag at the cursored commit.',
|
|
15047
|
+
kind: 'normal',
|
|
15048
|
+
requiresConfirmation: false,
|
|
15049
|
+
},
|
|
15096
15050
|
{
|
|
15097
15051
|
// Per-view-only: scoped to the history view in inkInput. `i`
|
|
15098
15052
|
// (lowercase) is used instead of `I` so the existing `I`
|
|
@@ -15105,6 +15059,38 @@ function getLogInkWorkflowActions() {
|
|
|
15105
15059
|
kind: 'destructive',
|
|
15106
15060
|
requiresConfirmation: true,
|
|
15107
15061
|
},
|
|
15062
|
+
{
|
|
15063
|
+
// Per-view-only: scoped to the history view in inkInput (key `B`).
|
|
15064
|
+
// The prompt itself is the affirmative gate — the user has to
|
|
15065
|
+
// type a branch name before anything happens — so this skips the
|
|
15066
|
+
// y-confirm path. Empty key keeps it palette-discoverable; the
|
|
15067
|
+
// palette path can't synthesize a branch name and surfaces a
|
|
15068
|
+
// hint instead.
|
|
15069
|
+
//
|
|
15070
|
+
// Distinct from `create-branch` (palette / `+` on branches view),
|
|
15071
|
+
// which uses `git switch -c` and switches onto the new branch.
|
|
15072
|
+
// This workflow uses `git branch <name> <sha>` and stays put —
|
|
15073
|
+
// GitKraken's "create branch here" semantic.
|
|
15074
|
+
id: 'create-branch-here',
|
|
15075
|
+
key: '',
|
|
15076
|
+
label: 'Create branch from commit',
|
|
15077
|
+
description: 'Create a branch pointed at the cursored commit (does not switch).',
|
|
15078
|
+
kind: 'normal',
|
|
15079
|
+
requiresConfirmation: false,
|
|
15080
|
+
},
|
|
15081
|
+
{
|
|
15082
|
+
// Per-view-only: scoped to the history view in inkInput via the
|
|
15083
|
+
// `gT` chord (bare `T` is taken by delete-tag on the tags view).
|
|
15084
|
+
// Same prompt-as-confirmation pattern as create-branch-here.
|
|
15085
|
+
// Lightweight tag — annotated tags remain available through the
|
|
15086
|
+
// existing `+` flow on the tags view.
|
|
15087
|
+
id: 'create-tag-here',
|
|
15088
|
+
key: '',
|
|
15089
|
+
label: 'Create tag at commit',
|
|
15090
|
+
description: 'Create a lightweight tag at the cursored commit.',
|
|
15091
|
+
kind: 'normal',
|
|
15092
|
+
requiresConfirmation: false,
|
|
15093
|
+
},
|
|
15108
15094
|
{
|
|
15109
15095
|
id: 'ai-commit-summary',
|
|
15110
15096
|
key: 'I',
|
|
@@ -15639,9 +15625,12 @@ function getLogInkFooterHints(options) {
|
|
|
15639
15625
|
// History view default hints. Mutating ops (`c` cherry-pick, `R`
|
|
15640
15626
|
// revert, `Z` reset, `i` interactive-rebase) all route through a
|
|
15641
15627
|
// y-confirm or mode prompt — none fire silently from the keystroke.
|
|
15642
|
-
//
|
|
15643
|
-
//
|
|
15644
|
-
|
|
15628
|
+
// `B` create-branch-here and `gT` create-tag-here use a prompt as
|
|
15629
|
+
// the affirmative gate (typing the name is the confirmation).
|
|
15630
|
+
// Grouped into compact `c/R/Z/i mutate` and `B/gT new` chips so
|
|
15631
|
+
// the footer stays scannable; full descriptions live in `?` help
|
|
15632
|
+
// and the palette.
|
|
15633
|
+
contextual: ['↑/↓ move', 'enter diff', 'c/R/Z/i mutate', 'B/gT new', 'y/Y yank', '/ search', 'gg/G top/bottom'],
|
|
15645
15634
|
global: NORMAL_GLOBAL_HINTS,
|
|
15646
15635
|
};
|
|
15647
15636
|
}
|
|
@@ -15952,23 +15941,31 @@ function cycleBranchSort(mode) {
|
|
|
15952
15941
|
return BRANCH_SORT_MODES[(index + 1) % BRANCH_SORT_MODES.length];
|
|
15953
15942
|
}
|
|
15954
15943
|
function sortBranches(branches, mode) {
|
|
15955
|
-
|
|
15944
|
+
// Pin the current branch at index 0 regardless of sort mode (#806
|
|
15945
|
+
// follow-up). Lands the user's cursor on the active branch by
|
|
15946
|
+
// default and keeps the most-relevant row glued to the top of the
|
|
15947
|
+
// list as they cycle sorts.
|
|
15948
|
+
const current = branches.find((entry) => entry.current);
|
|
15949
|
+
const rest = branches.filter((entry) => !entry.current);
|
|
15950
|
+
const sortedRest = rest.slice();
|
|
15956
15951
|
switch (mode) {
|
|
15957
15952
|
case 'name':
|
|
15958
|
-
|
|
15953
|
+
sortedRest.sort((a, b) => a.shortName.localeCompare(b.shortName));
|
|
15954
|
+
break;
|
|
15959
15955
|
case 'recent':
|
|
15960
15956
|
// ISO-shaped dates compare byte-for-byte; descending so the freshest
|
|
15961
15957
|
// branch sits at the top.
|
|
15962
|
-
|
|
15958
|
+
sortedRest.sort((a, b) => (b.date || '').localeCompare(a.date || '') ||
|
|
15963
15959
|
a.shortName.localeCompare(b.shortName));
|
|
15960
|
+
break;
|
|
15964
15961
|
case 'ahead':
|
|
15965
15962
|
// ahead-first; ties broken by behind, then by name. Keeps "this branch
|
|
15966
15963
|
// has unmerged work" in the user's first scroll.
|
|
15967
|
-
|
|
15964
|
+
sortedRest.sort((a, b) => b.ahead - a.ahead || b.behind - a.behind ||
|
|
15968
15965
|
a.shortName.localeCompare(b.shortName));
|
|
15969
|
-
|
|
15970
|
-
return copy;
|
|
15966
|
+
break;
|
|
15971
15967
|
}
|
|
15968
|
+
return current ? [current, ...sortedRest] : sortedRest;
|
|
15972
15969
|
}
|
|
15973
15970
|
const TAG_SORT_MODES = ['recent', 'name'];
|
|
15974
15971
|
const DEFAULT_TAG_SORT_MODE = 'recent';
|
|
@@ -16303,6 +16300,7 @@ function createLogInkState(rows, options = {}) {
|
|
|
16303
16300
|
userSidebarTab: 'status',
|
|
16304
16301
|
statusFilterMask: { ...DEFAULT_LOG_INK_STATUS_FILTER_MASK },
|
|
16305
16302
|
diffViewMode: 'unified',
|
|
16303
|
+
inspectorTab: 'inspector',
|
|
16306
16304
|
};
|
|
16307
16305
|
}
|
|
16308
16306
|
function getSelectedInkCommit(state) {
|
|
@@ -16360,6 +16358,28 @@ function applyLogInkAction(state, action) {
|
|
|
16360
16358
|
pendingCommitFocused: false,
|
|
16361
16359
|
pendingKey: undefined,
|
|
16362
16360
|
};
|
|
16361
|
+
case 'selectCommitByHash': {
|
|
16362
|
+
// Locates a commit by its full or short hash within the active
|
|
16363
|
+
// filtered list and snaps the cursor to it. Used by the
|
|
16364
|
+
// branch/tag auto-jump effect (#806 follow-up): cursoring a
|
|
16365
|
+
// branch in the sidebar tracks the history view to that
|
|
16366
|
+
// branch's tip without the user manually scrolling. No-op when
|
|
16367
|
+
// the hash isn't in the loaded list (the runtime surfaces a
|
|
16368
|
+
// status hint in that case).
|
|
16369
|
+
const target = action.hash;
|
|
16370
|
+
const index = state.filteredCommits.findIndex((commit) => commit.hash === target || commit.shortHash === target);
|
|
16371
|
+
if (index < 0) {
|
|
16372
|
+
return state;
|
|
16373
|
+
}
|
|
16374
|
+
return {
|
|
16375
|
+
...state,
|
|
16376
|
+
selectedIndex: index,
|
|
16377
|
+
selectedFileIndex: 0,
|
|
16378
|
+
diffPreviewOffset: 0,
|
|
16379
|
+
pendingCommitFocused: false,
|
|
16380
|
+
pendingKey: undefined,
|
|
16381
|
+
};
|
|
16382
|
+
}
|
|
16363
16383
|
case 'focusPendingCommit':
|
|
16364
16384
|
return {
|
|
16365
16385
|
...state,
|
|
@@ -16399,6 +16419,34 @@ function applyLogInkAction(state, action) {
|
|
|
16399
16419
|
selectedBranchIndex: clampIndex$1(state.selectedBranchIndex + action.delta, action.count),
|
|
16400
16420
|
pendingKey: undefined,
|
|
16401
16421
|
};
|
|
16422
|
+
case 'resetBranchSelection':
|
|
16423
|
+
// Snap the branches sidebar / view cursor back to position 0.
|
|
16424
|
+
// Used after a successful checkout (#806 follow-up): combined
|
|
16425
|
+
// with the "current branch pinned at top" rule from #809, this
|
|
16426
|
+
// lands the user's cursor on the just-checked-out branch.
|
|
16427
|
+
return {
|
|
16428
|
+
...state,
|
|
16429
|
+
selectedBranchIndex: 0,
|
|
16430
|
+
pendingKey: undefined,
|
|
16431
|
+
};
|
|
16432
|
+
case 'setInspectorTab':
|
|
16433
|
+
return {
|
|
16434
|
+
...state,
|
|
16435
|
+
inspectorTab: action.value,
|
|
16436
|
+
pendingKey: undefined,
|
|
16437
|
+
};
|
|
16438
|
+
case 'cycleInspectorTab': {
|
|
16439
|
+
// Two-tab toggle — `delta` is symmetrical so direction does not
|
|
16440
|
+
// matter, but we keep the action shape consistent with the
|
|
16441
|
+
// sidebar's `nextSidebarTab` / `previousSidebarTab` so callers
|
|
16442
|
+
// can mirror the sidebar pattern verbatim.
|
|
16443
|
+
const next = state.inspectorTab === 'inspector' ? 'actions' : 'inspector';
|
|
16444
|
+
return {
|
|
16445
|
+
...state,
|
|
16446
|
+
inspectorTab: next,
|
|
16447
|
+
pendingKey: undefined,
|
|
16448
|
+
};
|
|
16449
|
+
}
|
|
16402
16450
|
case 'moveTag':
|
|
16403
16451
|
return {
|
|
16404
16452
|
...state,
|
|
@@ -17484,6 +17532,30 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
|
|
|
17484
17532
|
action({ type: 'setStatus', value: 'gH applies a hunk in commit-diff or stash-diff view' }),
|
|
17485
17533
|
];
|
|
17486
17534
|
}
|
|
17535
|
+
// `gT` chord: create a lightweight tag at the cursored commit on the
|
|
17536
|
+
// history view. Bare `T` is taken (delete-tag on the tags view) so we
|
|
17537
|
+
// use the chord. Mirrors `gH` exactly — uppercase letter after the
|
|
17538
|
+
// `g` chord prefix, distinct from the lowercase `gt` chord which
|
|
17539
|
+
// jumps to the tags view. The prompt is the affirmative gate.
|
|
17540
|
+
if (state.pendingKey === 'g' && inputValue === 'T') {
|
|
17541
|
+
if (state.activeView === 'history' &&
|
|
17542
|
+
state.focus === 'commits' &&
|
|
17543
|
+
state.filteredCommits.length > 0 &&
|
|
17544
|
+
!state.pendingCommitFocused) {
|
|
17545
|
+
return [
|
|
17546
|
+
action({ type: 'setPendingKey', value: undefined }),
|
|
17547
|
+
action({
|
|
17548
|
+
type: 'openInputPrompt',
|
|
17549
|
+
kind: 'create-tag-here',
|
|
17550
|
+
label: 'New tag name (at cursored commit)',
|
|
17551
|
+
}),
|
|
17552
|
+
];
|
|
17553
|
+
}
|
|
17554
|
+
return [
|
|
17555
|
+
action({ type: 'setPendingKey', value: undefined }),
|
|
17556
|
+
action({ type: 'setStatus', value: 'gT creates a tag at the cursored commit on the history view' }),
|
|
17557
|
+
];
|
|
17558
|
+
}
|
|
17487
17559
|
if (inputValue === 'g') {
|
|
17488
17560
|
if (state.pendingKey === 'g') {
|
|
17489
17561
|
return [
|
|
@@ -17565,6 +17637,13 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
|
|
|
17565
17637
|
hunkOffsets: context.commitDiffHunkOffsets,
|
|
17566
17638
|
})];
|
|
17567
17639
|
}
|
|
17640
|
+
// Inspector focused: cycle the inspector tab. The renderer only
|
|
17641
|
+
// honors the tab field on short terminals (where the inspector
|
|
17642
|
+
// collapses into a tabbed layout), but we let the user pre-set
|
|
17643
|
+
// their preference on tall terminals too.
|
|
17644
|
+
if (state.focus === 'detail') {
|
|
17645
|
+
return [action({ type: 'cycleInspectorTab', delta: -1 })];
|
|
17646
|
+
}
|
|
17568
17647
|
return [action({ type: 'previousSidebarTab' })];
|
|
17569
17648
|
}
|
|
17570
17649
|
if (inputValue === ']') {
|
|
@@ -17589,6 +17668,9 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
|
|
|
17589
17668
|
hunkOffsets: context.commitDiffHunkOffsets,
|
|
17590
17669
|
})];
|
|
17591
17670
|
}
|
|
17671
|
+
if (state.focus === 'detail') {
|
|
17672
|
+
return [action({ type: 'cycleInspectorTab', delta: 1 })];
|
|
17673
|
+
}
|
|
17592
17674
|
return [action({ type: 'nextSidebarTab' })];
|
|
17593
17675
|
}
|
|
17594
17676
|
// Status surface intercepts 1/2/3 before the sidebar-tab numeric
|
|
@@ -18088,6 +18170,22 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
|
|
|
18088
18170
|
!state.pendingCommitFocused) {
|
|
18089
18171
|
return [action({ type: 'setPendingConfirmation', value: 'interactive-rebase' })];
|
|
18090
18172
|
}
|
|
18173
|
+
// `B` opens a create-branch prompt rooted at the cursored commit
|
|
18174
|
+
// (`git branch <name> <sha>` — does NOT switch to the new branch).
|
|
18175
|
+
// The prompt itself is the affirmative gate, so no separate y-confirm.
|
|
18176
|
+
// Bare uppercase `B` since the lowercase `b` is used by the `gb`
|
|
18177
|
+
// chord prefix and we want a single keystroke for this common op.
|
|
18178
|
+
if (inputValue === 'B' &&
|
|
18179
|
+
state.activeView === 'history' &&
|
|
18180
|
+
state.focus === 'commits' &&
|
|
18181
|
+
state.filteredCommits.length > 0 &&
|
|
18182
|
+
!state.pendingCommitFocused) {
|
|
18183
|
+
return [action({
|
|
18184
|
+
type: 'openInputPrompt',
|
|
18185
|
+
kind: 'create-branch-here',
|
|
18186
|
+
label: 'New branch name (at cursored commit)',
|
|
18187
|
+
})];
|
|
18188
|
+
}
|
|
18091
18189
|
// `y` / `Y` yank the contextually relevant identifier from the active
|
|
18092
18190
|
// view to the system clipboard:
|
|
18093
18191
|
// history → cursored commit hash (Y for short hash)
|
|
@@ -18714,10 +18812,25 @@ const LOG_INK_MIN_COLUMNS = 80;
|
|
|
18714
18812
|
const LOG_INK_MIN_ROWS = 24;
|
|
18715
18813
|
const LOG_INK_DEFAULT_COLUMNS = 120;
|
|
18716
18814
|
const LOG_INK_DEFAULT_ROWS = 40;
|
|
18815
|
+
/**
|
|
18816
|
+
* Terminal-row threshold below which the inspector switches to a
|
|
18817
|
+
* tabbed layout (commit-detail vs actions). Picked empirically: at
|
|
18818
|
+
* 28 rows the inspector's full stack (~30 rows when fully populated)
|
|
18819
|
+
* starts clipping the actions section; below that, the tabbed mode
|
|
18820
|
+
* gives both views their own air.
|
|
18821
|
+
*/
|
|
18822
|
+
const INSPECTOR_TABBED_BELOW_ROWS = 28;
|
|
18717
18823
|
function getLogInkLayout(input) {
|
|
18718
18824
|
const columns = input.columns || LOG_INK_DEFAULT_COLUMNS;
|
|
18719
18825
|
const rows = input.rows || LOG_INK_DEFAULT_ROWS;
|
|
18720
|
-
|
|
18826
|
+
// Inspector width — at rest 20-32 cells (~22% of width), focused
|
|
18827
|
+
// 36-60 cells (~40% of width). Narrow rest state keeps the commit
|
|
18828
|
+
// graph dominant; focus expansion gives the inspector room for long
|
|
18829
|
+
// commit bodies / file lists / action labels. Mirrors the sidebar
|
|
18830
|
+
// pattern (sidebarFocused above): instant transition per render.
|
|
18831
|
+
const detailWidth = input.inspectorFocused
|
|
18832
|
+
? Math.max(36, Math.min(60, Math.floor(columns * 0.40)))
|
|
18833
|
+
: Math.max(20, Math.min(32, Math.floor(columns * 0.22)));
|
|
18721
18834
|
// Sidebar at rest: 22-34 cells (~24% of width). Focused: 32-50 cells
|
|
18722
18835
|
// (~36% of width). The transition is instant per render — focus tab to
|
|
18723
18836
|
// expand, focus away to collapse.
|
|
@@ -18732,6 +18845,7 @@ function getLogInkLayout(input) {
|
|
|
18732
18845
|
rows,
|
|
18733
18846
|
sidebarWidth,
|
|
18734
18847
|
tooSmall: columns < LOG_INK_MIN_COLUMNS || rows < LOG_INK_MIN_ROWS,
|
|
18848
|
+
inspectorTabbed: rows < INSPECTOR_TABBED_BELOW_ROWS,
|
|
18735
18849
|
};
|
|
18736
18850
|
}
|
|
18737
18851
|
|
|
@@ -20227,6 +20341,63 @@ function resetToCommit(git, commit, mode) {
|
|
|
20227
20341
|
: result.details,
|
|
20228
20342
|
}));
|
|
20229
20343
|
}
|
|
20344
|
+
/**
|
|
20345
|
+
* Create a new local branch pointed at <commit>, without switching to it.
|
|
20346
|
+
*
|
|
20347
|
+
* This is the "create branch from cursored commit" history action — the
|
|
20348
|
+
* user types the new branch name into an input prompt and we run
|
|
20349
|
+
* `git branch <name> <sha>` (NOT `git switch -c`, which is what
|
|
20350
|
+
* `branchActions.createBranch` does for the create-branch-at-HEAD flow).
|
|
20351
|
+
* The split exists because GitKraken-style "create branch here" is
|
|
20352
|
+
* specifically about marking a historical commit, not about switching
|
|
20353
|
+
* onto a new working branch.
|
|
20354
|
+
*
|
|
20355
|
+
* Note for the inspector follow-up: workflow surfacing is driven by the
|
|
20356
|
+
* registry in `inkWorkflows.ts`, not a hardcoded action list — adding
|
|
20357
|
+
* `create-branch-here` there is enough for the inspector / palette to
|
|
20358
|
+
* pick this up.
|
|
20359
|
+
*/
|
|
20360
|
+
function createBranchFromCommit(git, name, commit) {
|
|
20361
|
+
const trimmedName = name.trim();
|
|
20362
|
+
if (!commit) {
|
|
20363
|
+
return Promise.resolve({
|
|
20364
|
+
ok: false,
|
|
20365
|
+
message: 'No commit selected.',
|
|
20366
|
+
});
|
|
20367
|
+
}
|
|
20368
|
+
if (!trimmedName) {
|
|
20369
|
+
return Promise.resolve({
|
|
20370
|
+
ok: false,
|
|
20371
|
+
message: 'Branch name required.',
|
|
20372
|
+
});
|
|
20373
|
+
}
|
|
20374
|
+
return guardNoInProgressOperation(git).then((blocked) => (blocked || runAction$5(() => git.raw(['branch', trimmedName, commit.hash]), `Created branch ${trimmedName} at ${commit.shortHash}`)));
|
|
20375
|
+
}
|
|
20376
|
+
/**
|
|
20377
|
+
* Create a lightweight tag pointed at <commit>.
|
|
20378
|
+
*
|
|
20379
|
+
* Mirrors `createBranchFromCommit` for the tag side: the user types a
|
|
20380
|
+
* tag name into an input prompt and we run `git tag <name> <sha>`
|
|
20381
|
+
* (lightweight, no `-a`/`-m`). Annotated tags remain available through
|
|
20382
|
+
* the existing `+` flow on the tags view; this is the per-commit
|
|
20383
|
+
* shortcut.
|
|
20384
|
+
*/
|
|
20385
|
+
function createTagAtCommit(git, name, commit) {
|
|
20386
|
+
const trimmedName = name.trim();
|
|
20387
|
+
if (!commit) {
|
|
20388
|
+
return Promise.resolve({
|
|
20389
|
+
ok: false,
|
|
20390
|
+
message: 'No commit selected.',
|
|
20391
|
+
});
|
|
20392
|
+
}
|
|
20393
|
+
if (!trimmedName) {
|
|
20394
|
+
return Promise.resolve({
|
|
20395
|
+
ok: false,
|
|
20396
|
+
message: 'Tag name required.',
|
|
20397
|
+
});
|
|
20398
|
+
}
|
|
20399
|
+
return guardNoInProgressOperation(git).then((blocked) => (blocked || runAction$5(() => git.raw(['tag', trimmedName, commit.hash]), `Created tag ${trimmedName} at ${commit.shortHash}`)));
|
|
20400
|
+
}
|
|
20230
20401
|
function startInteractiveRebase(git, commit) {
|
|
20231
20402
|
if (!commit) {
|
|
20232
20403
|
return Promise.resolve({
|
|
@@ -22953,6 +23124,88 @@ function formatPullRequestStateLine(pr) {
|
|
|
22953
23124
|
return parts.join(' · ');
|
|
22954
23125
|
}
|
|
22955
23126
|
|
|
23127
|
+
/**
|
|
23128
|
+
* Hardcoded per-entity action lists surfaced inside the right-hand
|
|
23129
|
+
* inspector panel. The inspector used to repeat the repo / branch /
|
|
23130
|
+
* status content the top header and left sidebar already show; we drop
|
|
23131
|
+
* that trailer in favor of an actionable cheat-sheet so the user knows
|
|
23132
|
+
* exactly which keystrokes apply to whatever they have under the cursor.
|
|
23133
|
+
*
|
|
23134
|
+
* Why hardcoded instead of introspecting `LOG_INK_KEY_BINDINGS`:
|
|
23135
|
+
* - Most per-entity actions live in `inkInput.ts` as direct keystroke
|
|
23136
|
+
* handlers (e.g. `c` cherry-pick, `R` revert) rather than as
|
|
23137
|
+
* globally-registered bindings, so the registry would be a partial
|
|
23138
|
+
* view at best.
|
|
23139
|
+
* - The bindings registry's `contexts` model (normal / search / focus
|
|
23140
|
+
* name) does not cleanly map to inspector entity types like "branch"
|
|
23141
|
+
* or "tag". Filtering it would mean replicating the same per-view
|
|
23142
|
+
* scoping logic the input dispatcher already encodes.
|
|
23143
|
+
* - New per-entity actions are added infrequently — the maintenance
|
|
23144
|
+
* cost of mirroring them here is low and keeps this file the single
|
|
23145
|
+
* source of truth for "what shows in the inspector".
|
|
23146
|
+
*
|
|
23147
|
+
* If you wire up a new per-entity keystroke in `inkInput.ts` — for
|
|
23148
|
+
* example a "create branch from this commit" or "create tag from this
|
|
23149
|
+
* commit" action — add the matching row to the relevant array below so
|
|
23150
|
+
* it shows up in the inspector automatically.
|
|
23151
|
+
*/
|
|
23152
|
+
const HISTORY_COMMIT_ACTIONS = [
|
|
23153
|
+
{ key: 'enter', label: 'Open diff' },
|
|
23154
|
+
{ key: 'c', label: 'Cherry-pick' },
|
|
23155
|
+
{ key: 'R', label: 'Revert', destructive: true },
|
|
23156
|
+
{ key: 'Z', label: 'Reset to commit', destructive: true },
|
|
23157
|
+
{ key: 'i', label: 'Interactive rebase', destructive: true },
|
|
23158
|
+
{ key: 'y', label: 'Yank hash' },
|
|
23159
|
+
{ key: 'Y', label: 'Yank short hash' },
|
|
23160
|
+
{ key: 'O', label: 'Open in browser' },
|
|
23161
|
+
];
|
|
23162
|
+
const BRANCH_ACTIONS = [
|
|
23163
|
+
{ key: 'enter', label: 'Checkout' },
|
|
23164
|
+
{ key: '+', label: 'New branch' },
|
|
23165
|
+
{ key: 'R', label: 'Rename' },
|
|
23166
|
+
{ key: 'u', label: 'Set upstream' },
|
|
23167
|
+
{ key: 'D', label: 'Delete', destructive: true },
|
|
23168
|
+
{ key: 'P', label: 'Push current' },
|
|
23169
|
+
{ key: 'F', label: 'Fetch all' },
|
|
23170
|
+
{ key: 'y', label: 'Yank name' },
|
|
23171
|
+
];
|
|
23172
|
+
const TAG_ACTIONS = [
|
|
23173
|
+
{ key: '+', label: 'New tag' },
|
|
23174
|
+
{ key: 'P', label: 'Push tag' },
|
|
23175
|
+
{ key: 'T', label: 'Delete', destructive: true },
|
|
23176
|
+
{ key: 'R', label: 'Delete remote', destructive: true },
|
|
23177
|
+
{ key: 'y', label: 'Yank name' },
|
|
23178
|
+
];
|
|
23179
|
+
const STASH_ACTIONS = [
|
|
23180
|
+
{ key: 'enter', label: 'Open diff' },
|
|
23181
|
+
{ key: 'a', label: 'Apply' },
|
|
23182
|
+
{ key: 'p', label: 'Pop' },
|
|
23183
|
+
{ key: 'X', label: 'Drop', destructive: true },
|
|
23184
|
+
{ key: 'y', label: 'Yank ref' },
|
|
23185
|
+
];
|
|
23186
|
+
const WORKTREE_ACTIONS = [
|
|
23187
|
+
{ key: 'W', label: 'Remove', destructive: true },
|
|
23188
|
+
{ key: 'y', label: 'Yank path' },
|
|
23189
|
+
];
|
|
23190
|
+
function getInspectorActions(context) {
|
|
23191
|
+
switch (context) {
|
|
23192
|
+
case 'history-commit':
|
|
23193
|
+
return HISTORY_COMMIT_ACTIONS;
|
|
23194
|
+
case 'branch':
|
|
23195
|
+
return BRANCH_ACTIONS;
|
|
23196
|
+
case 'tag':
|
|
23197
|
+
return TAG_ACTIONS;
|
|
23198
|
+
case 'stash':
|
|
23199
|
+
return STASH_ACTIONS;
|
|
23200
|
+
case 'worktree':
|
|
23201
|
+
return WORKTREE_ACTIONS;
|
|
23202
|
+
default: {
|
|
23203
|
+
const exhaustive = context;
|
|
23204
|
+
throw new Error(`Unhandled inspector action context: ${String(exhaustive)}`);
|
|
23205
|
+
}
|
|
23206
|
+
}
|
|
23207
|
+
}
|
|
23208
|
+
|
|
22956
23209
|
function sectionLines(title, diff) {
|
|
22957
23210
|
const lines = diff.split('\n').map((line) => line.trimEnd());
|
|
22958
23211
|
return [
|
|
@@ -23752,6 +24005,80 @@ function LogInkApp(deps) {
|
|
|
23752
24005
|
active = false;
|
|
23753
24006
|
};
|
|
23754
24007
|
}, [git, selected?.hash]);
|
|
24008
|
+
// #806 follow-up — auto-jump the history view to whichever branch /
|
|
24009
|
+
// tag the user is currently cursoring in the sidebar (or the
|
|
24010
|
+
// dedicated branches / tags view). Debounced so cursor-scrolling
|
|
24011
|
+
// through a long branch list doesn't dispatch on every keystroke.
|
|
24012
|
+
// No-op when the cursored ref's tip isn't in the loaded commit
|
|
24013
|
+
// window (under compact mode the cursored branch's tip may not be
|
|
24014
|
+
// fetched yet); a status hint surfaces in that case so the user
|
|
24015
|
+
// knows to toggle full graph or load older commits.
|
|
24016
|
+
React.useEffect(() => {
|
|
24017
|
+
const onBranchTab = state.activeView === 'branches' ||
|
|
24018
|
+
(state.focus === 'sidebar' && state.sidebarTab === 'branches');
|
|
24019
|
+
const onTagTab = state.activeView === 'tags' ||
|
|
24020
|
+
(state.focus === 'sidebar' && state.sidebarTab === 'tags');
|
|
24021
|
+
if (!onBranchTab && !onTagTab)
|
|
24022
|
+
return;
|
|
24023
|
+
let cancelled = false;
|
|
24024
|
+
const timer = setTimeout(() => {
|
|
24025
|
+
if (cancelled)
|
|
24026
|
+
return;
|
|
24027
|
+
let targetHash;
|
|
24028
|
+
let targetLabel;
|
|
24029
|
+
if (onBranchTab) {
|
|
24030
|
+
const all = sortBranches(context.branches?.localBranches || [], state.branchSort);
|
|
24031
|
+
const visible = state.filter
|
|
24032
|
+
? all.filter((b) => matchesPromotedFilter([b.shortName, b.upstream || ''], state.filter))
|
|
24033
|
+
: all;
|
|
24034
|
+
const branch = visible[Math.min(state.selectedBranchIndex, Math.max(0, visible.length - 1))];
|
|
24035
|
+
if (branch) {
|
|
24036
|
+
targetHash = branch.hash;
|
|
24037
|
+
targetLabel = `branch ${branch.shortName}`;
|
|
24038
|
+
}
|
|
24039
|
+
}
|
|
24040
|
+
else if (onTagTab) {
|
|
24041
|
+
const all = sortTags(context.tags?.tags || [], state.tagSort);
|
|
24042
|
+
const visible = state.filter
|
|
24043
|
+
? all.filter((t) => matchesPromotedFilter([t.name, t.subject], state.filter))
|
|
24044
|
+
: all;
|
|
24045
|
+
const tag = visible[Math.min(state.selectedTagIndex, Math.max(0, visible.length - 1))];
|
|
24046
|
+
if (tag) {
|
|
24047
|
+
targetHash = tag.hash;
|
|
24048
|
+
targetLabel = `tag ${tag.name}`;
|
|
24049
|
+
}
|
|
24050
|
+
}
|
|
24051
|
+
if (!targetHash)
|
|
24052
|
+
return;
|
|
24053
|
+
const loaded = state.filteredCommits.some((commit) => commit.hash === targetHash || commit.shortHash === targetHash);
|
|
24054
|
+
if (loaded) {
|
|
24055
|
+
dispatch({ type: 'selectCommitByHash', hash: targetHash });
|
|
24056
|
+
// Confirmation status message so the user gets feedback even
|
|
24057
|
+
// when the dedicated branches / tags view is occupying the
|
|
24058
|
+
// main panel and the history cursor moves invisibly behind it.
|
|
24059
|
+
dispatch({
|
|
24060
|
+
type: 'setStatus',
|
|
24061
|
+
value: `Synced history to ${targetLabel} tip`,
|
|
24062
|
+
});
|
|
24063
|
+
}
|
|
24064
|
+
else {
|
|
24065
|
+
dispatch({
|
|
24066
|
+
type: 'setStatus',
|
|
24067
|
+
value: `${targetLabel} tip not in loaded window — press \\ for full graph or Ctrl+L to load more`,
|
|
24068
|
+
});
|
|
24069
|
+
}
|
|
24070
|
+
}, 150);
|
|
24071
|
+
return () => {
|
|
24072
|
+
cancelled = true;
|
|
24073
|
+
clearTimeout(timer);
|
|
24074
|
+
};
|
|
24075
|
+
}, [
|
|
24076
|
+
dispatch, context.branches, context.tags,
|
|
24077
|
+
state.activeView, state.focus, state.sidebarTab,
|
|
24078
|
+
state.selectedBranchIndex, state.selectedTagIndex,
|
|
24079
|
+
state.branchSort, state.tagSort, state.filter,
|
|
24080
|
+
state.filteredCommits,
|
|
24081
|
+
]);
|
|
23755
24082
|
React.useEffect(() => {
|
|
23756
24083
|
let active = true;
|
|
23757
24084
|
async function loadWorktreeDiff() {
|
|
@@ -24127,6 +24454,30 @@ function LogInkApp(deps) {
|
|
|
24127
24454
|
message: commit.message,
|
|
24128
24455
|
});
|
|
24129
24456
|
},
|
|
24457
|
+
'create-branch-here': async () => {
|
|
24458
|
+
const commit = getSelectedInkCommit(state);
|
|
24459
|
+
const name = payload?.trim();
|
|
24460
|
+
if (!commit)
|
|
24461
|
+
return { ok: false, message: 'No commit selected' };
|
|
24462
|
+
if (!name)
|
|
24463
|
+
return { ok: false, message: 'Branch name required' };
|
|
24464
|
+
return createBranchFromCommit(git, name, {
|
|
24465
|
+
hash: commit.hash,
|
|
24466
|
+
shortHash: commit.shortHash,
|
|
24467
|
+
});
|
|
24468
|
+
},
|
|
24469
|
+
'create-tag-here': async () => {
|
|
24470
|
+
const commit = getSelectedInkCommit(state);
|
|
24471
|
+
const name = payload?.trim();
|
|
24472
|
+
if (!commit)
|
|
24473
|
+
return { ok: false, message: 'No commit selected' };
|
|
24474
|
+
if (!name)
|
|
24475
|
+
return { ok: false, message: 'Tag name required' };
|
|
24476
|
+
return createTagAtCommit(git, name, {
|
|
24477
|
+
hash: commit.hash,
|
|
24478
|
+
shortHash: commit.shortHash,
|
|
24479
|
+
});
|
|
24480
|
+
},
|
|
24130
24481
|
'checkout-file-from-commit': async () => {
|
|
24131
24482
|
// payload is "<sha> <path>" so we pass both through a single
|
|
24132
24483
|
// string field on the action.
|
|
@@ -24179,6 +24530,17 @@ function LogInkApp(deps) {
|
|
|
24179
24530
|
if (!repo || repo.provider !== 'github' || !repo.owner || !repo.name) {
|
|
24180
24531
|
return { ok: false, message: 'No GitHub remote detected for this repo' };
|
|
24181
24532
|
}
|
|
24533
|
+
// History view: prefer the cursored commit's URL so `O` from
|
|
24534
|
+
// a commit context lands the user on the commit page rather
|
|
24535
|
+
// than the repo root or the current PR. The user-visible
|
|
24536
|
+
// intent of `O` is "open whatever I'm cursoring on the web";
|
|
24537
|
+
// a commit is what the cursor is on in the history view.
|
|
24538
|
+
if (state.activeView === 'history') {
|
|
24539
|
+
const commit = getSelectedInkCommit(state);
|
|
24540
|
+
if (commit) {
|
|
24541
|
+
return openProviderUrl(repo, { type: 'commit', commit: commit.hash });
|
|
24542
|
+
}
|
|
24543
|
+
}
|
|
24182
24544
|
const pr = context.provider?.currentPullRequest || context.pullRequest?.currentPullRequest;
|
|
24183
24545
|
if (pr) {
|
|
24184
24546
|
return openProviderUrl(repo, { type: 'pull-request', number: pr.number });
|
|
@@ -24266,9 +24628,21 @@ function LogInkApp(deps) {
|
|
|
24266
24628
|
}
|
|
24267
24629
|
const result = await handler();
|
|
24268
24630
|
dispatch({ type: 'setStatus', value: result?.message || 'Workflow action complete' });
|
|
24269
|
-
//
|
|
24270
|
-
//
|
|
24271
|
-
|
|
24631
|
+
// Checkout-branch is the one workflow where we want a *visible*
|
|
24632
|
+
// refresh so the user sees the branches sidebar repaint with the
|
|
24633
|
+
// new current branch (per #806 follow-up). Snap the cursor to
|
|
24634
|
+
// position 0 first so when the refresh completes and the new
|
|
24635
|
+
// current branch lands at the top (per #809's pin-current rule),
|
|
24636
|
+
// the cursor is already there waiting.
|
|
24637
|
+
if (id === 'checkout-branch' && result?.ok) {
|
|
24638
|
+
dispatch({ type: 'resetBranchSelection' });
|
|
24639
|
+
await refreshContext();
|
|
24640
|
+
}
|
|
24641
|
+
else {
|
|
24642
|
+
// Silent refresh so the deleted item disappears from the list
|
|
24643
|
+
// without flickering the surfaces through a 'loading' phase.
|
|
24644
|
+
await refreshContext({ silent: true });
|
|
24645
|
+
}
|
|
24272
24646
|
}, [context, dispatch, git, refreshContext, state.branchSort, state.filter, state.selectedBranchIndex,
|
|
24273
24647
|
state.selectedStashIndex, state.selectedTagIndex, state.selectedWorktreeListIndex, state.stashDiffRef,
|
|
24274
24648
|
state.tagSort]);
|
|
@@ -24750,6 +25124,7 @@ function LogInkApp(deps) {
|
|
|
24750
25124
|
columns: windowSize.columns || process.stdout.columns || LOG_INK_DEFAULT_COLUMNS,
|
|
24751
25125
|
rows: windowSize.rows || process.stdout.rows || LOG_INK_DEFAULT_ROWS,
|
|
24752
25126
|
sidebarFocused: state.focus === 'sidebar',
|
|
25127
|
+
inspectorFocused: state.focus === 'detail',
|
|
24753
25128
|
});
|
|
24754
25129
|
if (layout.tooSmall) {
|
|
24755
25130
|
return h(Box, {
|
|
@@ -24764,7 +25139,7 @@ function LogInkApp(deps) {
|
|
|
24764
25139
|
if (showOnboarding) {
|
|
24765
25140
|
return renderOnboardingOverlay(h, { Box, Text }, layout.rows, layout.columns, theme, appLabel);
|
|
24766
25141
|
}
|
|
24767
|
-
return h(Box, { flexDirection: 'column', height: layout.rows }, renderHeader(h, { Box, Text }, state, context, contextStatus, layout.columns, theme, appLabel), h(Box, { flexDirection: 'row', height: layout.bodyRows }, renderSidebar(h, { Box, Text }, state, context, contextStatus, layout.sidebarWidth, theme), renderMainPanel(h, { Box, Text }, state, context, contextStatus, worktreeDiff, worktreeDiffLoading, worktreeHunks, worktreeHunksLoading, filePreview, filePreviewLoading, commitDiffHunkOffsets, selectedDetailFile, stashDiffLines, stashDiffLoading, layout.bodyRows, layout.mainPanelWidth, theme, hasMoreCommits, loadingMoreCommits), renderDetailPanel(h, { Box, Text }, state, context, contextStatus, detail, detailLoading, filePreview, filePreviewLoading, layout.detailWidth, theme)), renderFooter(h, { Box, Text }, state, context, theme, idleTip));
|
|
25142
|
+
return h(Box, { flexDirection: 'column', height: layout.rows }, renderHeader(h, { Box, Text }, state, context, contextStatus, layout.columns, theme, appLabel), h(Box, { flexDirection: 'row', height: layout.bodyRows }, renderSidebar(h, { Box, Text }, state, context, contextStatus, layout.sidebarWidth, layout.bodyRows, theme), renderMainPanel(h, { Box, Text }, state, context, contextStatus, worktreeDiff, worktreeDiffLoading, worktreeHunks, worktreeHunksLoading, filePreview, filePreviewLoading, commitDiffHunkOffsets, selectedDetailFile, stashDiffLines, stashDiffLoading, layout.bodyRows, layout.mainPanelWidth, theme, hasMoreCommits, loadingMoreCommits), renderDetailPanel(h, { Box, Text }, state, context, contextStatus, detail, detailLoading, filePreview, filePreviewLoading, layout.detailWidth, layout.inspectorTabbed, theme)), renderFooter(h, { Box, Text }, state, context, theme, idleTip));
|
|
24768
25143
|
}
|
|
24769
25144
|
function renderHeader(h, components, state, context, contextStatus, columns, theme, appLabel) {
|
|
24770
25145
|
const { Box, Text } = components;
|
|
@@ -24817,7 +25192,7 @@ function renderHeader(h, components, state, context, contextStatus, columns, the
|
|
|
24817
25192
|
? h(Text, { bold: true, color: theme.colors.accent }, `${prLabel}${titleSuffix}`)
|
|
24818
25193
|
: undefined, h(Text, { bold: true, color: modeColor }, ` ${mode}`), search ? h(Text, { dimColor: true }, ` ${truncate$1(search, 36)}`) : undefined);
|
|
24819
25194
|
}
|
|
24820
|
-
function renderSidebar(h, components, state, context, contextStatus, width, theme) {
|
|
25195
|
+
function renderSidebar(h, components, state, context, contextStatus, width, bodyRows, theme) {
|
|
24821
25196
|
const { Box, Text } = components;
|
|
24822
25197
|
const focused = state.focus === 'sidebar';
|
|
24823
25198
|
const tabs = getLogInkSidebarTabs();
|
|
@@ -24841,7 +25216,7 @@ function renderSidebar(h, components, state, context, contextStatus, width, them
|
|
|
24841
25216
|
dimColor: !isActive,
|
|
24842
25217
|
}, headerText));
|
|
24843
25218
|
if (isActive) {
|
|
24844
|
-
blocks.push(...renderActiveSidebarContent(h, Text, tab, state, context, contextStatus, width, theme));
|
|
25219
|
+
blocks.push(...renderActiveSidebarContent(h, Text, tab, state, context, contextStatus, width, bodyRows, theme));
|
|
24845
25220
|
}
|
|
24846
25221
|
return blocks;
|
|
24847
25222
|
});
|
|
@@ -24860,7 +25235,15 @@ function renderSidebar(h, components, state, context, contextStatus, width, them
|
|
|
24860
25235
|
* surface; every other tab falls through to `sidebarLines` for its
|
|
24861
25236
|
* string-based summary.
|
|
24862
25237
|
*/
|
|
24863
|
-
function renderActiveSidebarContent(h, Text, tab, state, context, contextStatus, width, theme) {
|
|
25238
|
+
function renderActiveSidebarContent(h, Text, tab, state, context, contextStatus, width, bodyRows, theme) {
|
|
25239
|
+
// Available rows for the active tab's list. The sidebar chrome
|
|
25240
|
+
// takes ~10 rows (panel title + spacer + 5 tab headers + 4 inter-tab
|
|
25241
|
+
// spacers); the branches tab eats 3 more for its summary header
|
|
25242
|
+
// (Current / Worktree / spacer). Floor of 8 keeps short terminals
|
|
25243
|
+
// usable; tall terminals (40+ rows) get noticeably more items.
|
|
25244
|
+
const sidebarChrome = 10;
|
|
25245
|
+
const branchHeaderRows = tab === 'branches' ? 3 : 0;
|
|
25246
|
+
const visibleListCount = Math.max(8, bodyRows - sidebarChrome - branchHeaderRows);
|
|
24864
25247
|
if (tab === 'status') {
|
|
24865
25248
|
return renderActiveStatusTabContent(h, Text, context, contextStatus, width, theme);
|
|
24866
25249
|
}
|
|
@@ -24885,7 +25268,7 @@ function renderActiveSidebarContent(h, Text, tab, state, context, contextStatus,
|
|
|
24885
25268
|
];
|
|
24886
25269
|
return [
|
|
24887
25270
|
...headerRows,
|
|
24888
|
-
...renderSelectableSidebarRows(h, Text, sortedBranches, state.selectedBranchIndex, focused, width, theme, (branch) => `${branchRowMarker(branch, { ascii: theme.ascii })} ${branch.shortName}`, 'tab-branches'),
|
|
25271
|
+
...renderSelectableSidebarRows(h, Text, sortedBranches, state.selectedBranchIndex, focused, width, theme, (branch) => `${branchRowMarker(branch, { ascii: theme.ascii })} ${branch.shortName}`, 'tab-branches', visibleListCount),
|
|
24889
25272
|
];
|
|
24890
25273
|
}
|
|
24891
25274
|
if (tab === 'tags') {
|
|
@@ -24896,7 +25279,7 @@ function renderActiveSidebarContent(h, Text, tab, state, context, contextStatus,
|
|
|
24896
25279
|
if (tags.length === 0) {
|
|
24897
25280
|
return [h(Text, { key: 'tab-tags-empty', dimColor: true }, ' No tags found')];
|
|
24898
25281
|
}
|
|
24899
|
-
return renderSelectableSidebarRows(h, Text, tags, state.selectedTagIndex, focused, width, theme, (tag) => `${truncate$1(tag.name, 16)} ${tag.subject}`, 'tab-tags');
|
|
25282
|
+
return renderSelectableSidebarRows(h, Text, tags, state.selectedTagIndex, focused, width, theme, (tag) => `${truncate$1(tag.name, 16)} ${tag.subject}`, 'tab-tags', visibleListCount);
|
|
24900
25283
|
}
|
|
24901
25284
|
if (tab === 'stashes') {
|
|
24902
25285
|
if (isLogInkContextKeyLoading(contextStatus, 'stashes')) {
|
|
@@ -24906,7 +25289,7 @@ function renderActiveSidebarContent(h, Text, tab, state, context, contextStatus,
|
|
|
24906
25289
|
if (stashes.length === 0) {
|
|
24907
25290
|
return [h(Text, { key: 'tab-stashes-empty', dimColor: true }, ' No stashes found')];
|
|
24908
25291
|
}
|
|
24909
|
-
return renderSelectableSidebarRows(h, Text, stashes, state.selectedStashIndex, focused, width, theme, (stash, index) => `@{${index}} ${stash.message || '(no message)'}`, 'tab-stashes');
|
|
25292
|
+
return renderSelectableSidebarRows(h, Text, stashes, state.selectedStashIndex, focused, width, theme, (stash, index) => `@{${index}} ${stash.message || '(no message)'}`, 'tab-stashes', visibleListCount);
|
|
24910
25293
|
}
|
|
24911
25294
|
// worktrees
|
|
24912
25295
|
if (isLogInkContextKeyLoading(contextStatus, 'worktreeList')) {
|
|
@@ -24920,7 +25303,7 @@ function renderActiveSidebarContent(h, Text, tab, state, context, contextStatus,
|
|
|
24920
25303
|
const marker = worktree.current ? '*' : ' ';
|
|
24921
25304
|
const wstate = worktree.dirty ? 'dirty' : 'clean';
|
|
24922
25305
|
return `${marker} ${worktree.branch || worktree.path} ${wstate}`;
|
|
24923
|
-
}, 'tab-worktrees');
|
|
25306
|
+
}, 'tab-worktrees', visibleListCount);
|
|
24924
25307
|
}
|
|
24925
25308
|
/**
|
|
24926
25309
|
* Render a sliding-window list of selectable sidebar rows. The cursor
|
|
@@ -24929,10 +25312,10 @@ function renderActiveSidebarContent(h, Text, tab, state, context, contextStatus,
|
|
|
24929
25312
|
* Sliding window keeps the cursor in view as the user navigates a long
|
|
24930
25313
|
* list; truncation hints surface the count of hidden rows.
|
|
24931
25314
|
*/
|
|
24932
|
-
function renderSelectableSidebarRows(h, Text, items, selectedIndex, focused, width, theme, toRowText, keyPrefix) {
|
|
25315
|
+
function renderSelectableSidebarRows(h, Text, items, selectedIndex, focused, width, theme, toRowText, keyPrefix, visibleCount) {
|
|
24933
25316
|
if (items.length === 0)
|
|
24934
25317
|
return [];
|
|
24935
|
-
const window = getSidebarVisibleWindow(items.length, selectedIndex);
|
|
25318
|
+
const window = getSidebarVisibleWindow(items.length, selectedIndex, visibleCount);
|
|
24936
25319
|
const elements = [];
|
|
24937
25320
|
if (window.truncatedAbove > 0) {
|
|
24938
25321
|
elements.push(h(Text, {
|
|
@@ -25833,7 +26216,7 @@ function renderDiffSurface(h, components, state, context, contextStatus, worktre
|
|
|
25833
26216
|
}, truncate$1(line, 140)))
|
|
25834
26217
|
: []));
|
|
25835
26218
|
}
|
|
25836
|
-
function renderDetailPanel(h, components, state, context, contextStatus, detail, loading, filePreview, filePreviewLoading, width, theme) {
|
|
26219
|
+
function renderDetailPanel(h, components, state, context, contextStatus, detail, loading, filePreview, filePreviewLoading, width, tabbed, theme) {
|
|
25837
26220
|
const focused = state.focus === 'detail';
|
|
25838
26221
|
if (state.showHelp) {
|
|
25839
26222
|
return renderHelpPanel(h, components, state, width, theme, focused);
|
|
@@ -25890,16 +26273,11 @@ function renderDetailPanel(h, components, state, context, contextStatus, detail,
|
|
|
25890
26273
|
if (state.activeView === 'stash') {
|
|
25891
26274
|
return renderStashPreviewPanel(h, components, state, context, contextStatus, width, theme, focused);
|
|
25892
26275
|
}
|
|
25893
|
-
return renderHistoryInspector(h, components, state, context, contextStatus, detail, loading, filePreview, filePreviewLoading, width, theme, focused);
|
|
26276
|
+
return renderHistoryInspector(h, components, state, context, contextStatus, detail, loading, filePreview, filePreviewLoading, width, tabbed, theme, focused);
|
|
25894
26277
|
}
|
|
25895
|
-
function renderHistoryInspector(h, components, state, context,
|
|
26278
|
+
function renderHistoryInspector(h, components, state, context, _contextStatus, detail, loading, _filePreview, _filePreviewLoading, width, tabbed, theme, focused) {
|
|
25896
26279
|
const { Box, Text } = components;
|
|
25897
26280
|
const selected = getSelectedInkCommit(state);
|
|
25898
|
-
const workflowSections = getLogInkWorkflowSections({
|
|
25899
|
-
...context,
|
|
25900
|
-
contextLoading: isLogInkContextLoading(contextStatus),
|
|
25901
|
-
selectedCommit: selected,
|
|
25902
|
-
});
|
|
25903
26281
|
if (!detail) {
|
|
25904
26282
|
const fallbackLines = [
|
|
25905
26283
|
selected?.message || 'No commit selected.',
|
|
@@ -25915,7 +26293,7 @@ function renderHistoryInspector(h, components, state, context, contextStatus, de
|
|
|
25915
26293
|
}, h(Text, { bold: true }, panelTitle('Inspector', focused)), ...fallbackLines.map((line, index) => h(Text, {
|
|
25916
26294
|
key: `detail-${index}`,
|
|
25917
26295
|
dimColor: index > 1,
|
|
25918
|
-
}, truncate$1(line, width - 4))));
|
|
26296
|
+
}, truncate$1(line, width - 4))), ...renderInspectorActionsSection(h, Text, 'history-commit', width, theme));
|
|
25919
26297
|
}
|
|
25920
26298
|
const statLine = `${detail.stats.filesChanged} files +${detail.stats.insertions}/-${detail.stats.deletions}`;
|
|
25921
26299
|
// P5.1 — link the commit hash and each ref out to GitHub when we know
|
|
@@ -25926,18 +26304,26 @@ function renderHistoryInspector(h, components, state, context, contextStatus, de
|
|
|
25926
26304
|
const refNodes = detail.refs.length
|
|
25927
26305
|
? renderInspectorRefs(h, Text, detail.refs, repository)
|
|
25928
26306
|
: null;
|
|
26307
|
+
// Inspector reorder (PR — drop duplicative Workflows trailer):
|
|
26308
|
+
// 1. Commit message (the headline of what you're looking at)
|
|
26309
|
+
// 2. Metadata (hash / author / date / refs / stats)
|
|
26310
|
+
// 3. Body preview (up to 8 lines now that the trailer is gone)
|
|
26311
|
+
// 4. Changed files list (cursored entry highlights)
|
|
26312
|
+
// 5. Actions cheat-sheet (per-entity keystrokes; destructive marked)
|
|
26313
|
+
// The Workflows: trailer that used to repeat the repo / branch /
|
|
26314
|
+
// status from the top header and left sidebar is intentionally gone.
|
|
25929
26315
|
const headerNodes = [
|
|
25930
26316
|
h(Text, { key: 'detail-msg' }, truncate$1(detail.message, width - 4)),
|
|
25931
26317
|
h(Text, { key: 'detail-spacer-1' }, ''),
|
|
25932
26318
|
h(Text, { key: 'detail-commit', dimColor: true }, 'Commit: ', commitLink),
|
|
25933
26319
|
h(Text, { key: 'detail-author', dimColor: true }, truncate$1(`Author: ${detail.author}`, width - 4)),
|
|
25934
|
-
h(Text, { key: 'detail-date', dimColor: true }, truncate$1(`Date:
|
|
26320
|
+
h(Text, { key: 'detail-date', dimColor: true }, truncate$1(`Date: ${detail.date}`, width - 4)),
|
|
25935
26321
|
refNodes
|
|
25936
|
-
? h(Text, { key: 'detail-refs', dimColor: true }, 'Refs:
|
|
25937
|
-
: h(Text, { key: 'detail-refs', dimColor: true }, 'Refs:
|
|
25938
|
-
h(Text, { key: 'detail-stat', dimColor: true }, truncate$1(statLine
|
|
26322
|
+
? h(Text, { key: 'detail-refs', dimColor: true }, 'Refs: ', ...refNodes)
|
|
26323
|
+
: h(Text, { key: 'detail-refs', dimColor: true }, 'Refs: none'),
|
|
26324
|
+
h(Text, { key: 'detail-stat', dimColor: true }, truncate$1(`Stats: ${statLine}`, width - 4)),
|
|
25939
26325
|
h(Text, { key: 'detail-spacer-2' }, ''),
|
|
25940
|
-
...(detail.body ? detail.body.split('\n').slice(0,
|
|
26326
|
+
...(detail.body ? detail.body.split('\n').slice(0, 8) : ['No commit body.']).map((line, index) => h(Text, {
|
|
25941
26327
|
key: `detail-body-${index}`,
|
|
25942
26328
|
dimColor: true,
|
|
25943
26329
|
}, truncate$1(line, width - 4))),
|
|
@@ -25946,24 +26332,85 @@ function renderHistoryInspector(h, components, state, context, contextStatus, de
|
|
|
25946
26332
|
];
|
|
25947
26333
|
const fileListMaxRows = Math.max(4, Math.min(detail.files.length, 10));
|
|
25948
26334
|
const fileListNodes = renderCommitFileList(h, Text, detail.files, state.selectedFileIndex, focused, fileListMaxRows, width, theme);
|
|
25949
|
-
|
|
25950
|
-
|
|
25951
|
-
|
|
25952
|
-
|
|
25953
|
-
|
|
25954
|
-
|
|
25955
|
-
|
|
25956
|
-
|
|
26335
|
+
// Tabbed mode (#806 follow-up — short terminals): render only the
|
|
26336
|
+
// active inspector tab with a `[Inspector] Actions` header so the
|
|
26337
|
+
// user knows what they're seeing and how to switch (`[/]` while
|
|
26338
|
+
// focus is on the inspector). Tall terminals stack both sections
|
|
26339
|
+
// as before.
|
|
26340
|
+
if (tabbed) {
|
|
26341
|
+
const activeTab = state.inspectorTab;
|
|
26342
|
+
const tabHeader = h(Box, { key: 'inspector-tabs', flexDirection: 'row' }, h(Text, {
|
|
26343
|
+
bold: activeTab === 'inspector',
|
|
26344
|
+
dimColor: activeTab !== 'inspector',
|
|
26345
|
+
}, activeTab === 'inspector' ? '[Inspector]' : ' Inspector '), ' ', h(Text, {
|
|
26346
|
+
bold: activeTab === 'actions',
|
|
26347
|
+
dimColor: activeTab !== 'actions',
|
|
26348
|
+
}, activeTab === 'actions' ? '[Actions]' : ' Actions '));
|
|
26349
|
+
return h(Box, {
|
|
26350
|
+
borderColor: focusBorderColor(theme, focused),
|
|
26351
|
+
borderStyle: theme.borderStyle,
|
|
26352
|
+
flexDirection: 'column',
|
|
26353
|
+
width,
|
|
26354
|
+
paddingX: 1,
|
|
26355
|
+
}, h(Text, { bold: true }, panelTitle('Inspector', focused)), tabHeader, h(Text, { key: 'inspector-tabs-spacer' }, ''), ...(activeTab === 'inspector'
|
|
26356
|
+
? [...headerNodes, ...fileListNodes]
|
|
26357
|
+
: renderInspectorActionsSection(h, Text, 'history-commit', width, theme)));
|
|
26358
|
+
}
|
|
25957
26359
|
return h(Box, {
|
|
25958
26360
|
borderColor: focusBorderColor(theme, focused),
|
|
25959
26361
|
borderStyle: theme.borderStyle,
|
|
25960
26362
|
flexDirection: 'column',
|
|
25961
26363
|
width,
|
|
25962
26364
|
paddingX: 1,
|
|
25963
|
-
}, h(Text, { bold: true }, panelTitle('Inspector', focused)), ...headerNodes, ...fileListNodes, ...
|
|
25964
|
-
|
|
25965
|
-
|
|
25966
|
-
|
|
26365
|
+
}, h(Text, { bold: true }, panelTitle('Inspector', focused)), ...headerNodes, ...fileListNodes, ...renderInspectorActionsSection(h, Text, 'history-commit', width, theme));
|
|
26366
|
+
}
|
|
26367
|
+
/**
|
|
26368
|
+
* Render the trailing "Actions:" section that surfaces which keystrokes
|
|
26369
|
+
* apply to whatever the inspector is focused on. Keys are colored with
|
|
26370
|
+
* `theme.colors.accent` so they pop as the actionable element. Destructive
|
|
26371
|
+
* actions get the danger color plus a `[!]` marker so they don't blend
|
|
26372
|
+
* into the cherry-pick / yank rows.
|
|
26373
|
+
*
|
|
26374
|
+
* Truncates labels when the inspector is narrow (down to the 26-cell
|
|
26375
|
+
* minimum from `getLogInkLayout`) so an overflowing label never wraps and
|
|
26376
|
+
* collides with the next row.
|
|
26377
|
+
*/
|
|
26378
|
+
function renderInspectorActionsSection(h, Text, context, width, theme) {
|
|
26379
|
+
const actions = getInspectorActions(context);
|
|
26380
|
+
if (!actions.length)
|
|
26381
|
+
return [];
|
|
26382
|
+
// Width budget for each row: subtract padding + " " gutter, the key
|
|
26383
|
+
// column (left-padded to 5 cells so labels align), the " " gap
|
|
26384
|
+
// between key and label, and the optional " [!]" suffix (5 cells).
|
|
26385
|
+
const KEY_COLUMN = 5;
|
|
26386
|
+
const GAP = ' ';
|
|
26387
|
+
const DESTRUCTIVE_SUFFIX = ' [!]';
|
|
26388
|
+
const labelBudget = Math.max(4, width - 4 /* border + padX */ - KEY_COLUMN - GAP.length - DESTRUCTIVE_SUFFIX.length);
|
|
26389
|
+
const nodes = [
|
|
26390
|
+
h(Text, { key: 'actions-spacer' }, ''),
|
|
26391
|
+
h(Text, { key: 'actions-title' }, 'Actions:'),
|
|
26392
|
+
...actions.map((action, index) => {
|
|
26393
|
+
const keyCell = action.key.padEnd(KEY_COLUMN);
|
|
26394
|
+
const label = truncate$1(action.label, labelBudget);
|
|
26395
|
+
const children = [
|
|
26396
|
+
h(Text, {
|
|
26397
|
+
key: `actions-${index}-key`,
|
|
26398
|
+
color: action.destructive ? theme.colors.danger : theme.colors.accent,
|
|
26399
|
+
}, keyCell),
|
|
26400
|
+
GAP,
|
|
26401
|
+
label,
|
|
26402
|
+
];
|
|
26403
|
+
if (action.destructive) {
|
|
26404
|
+
children.push(h(Text, {
|
|
26405
|
+
key: `actions-${index}-mark`,
|
|
26406
|
+
color: theme.colors.danger,
|
|
26407
|
+
dimColor: false,
|
|
26408
|
+
}, DESTRUCTIVE_SUFFIX));
|
|
26409
|
+
}
|
|
26410
|
+
return h(Text, { key: `actions-${index}` }, ...children);
|
|
26411
|
+
}),
|
|
26412
|
+
];
|
|
26413
|
+
return nodes;
|
|
25967
26414
|
}
|
|
25968
26415
|
/**
|
|
25969
26416
|
* Build a commit URL for the repo when GitHub provider info is available.
|
|
@@ -26537,7 +26984,7 @@ async function startCocoUiFromLogArgv(logArgv, options = {}) {
|
|
|
26537
26984
|
const git = options.git || getRepo();
|
|
26538
26985
|
const rows = options.rows || (await getLogRows(git, logArgv));
|
|
26539
26986
|
await startInkInteractiveLog(git, rows, {}, {
|
|
26540
|
-
appLabel: 'coco
|
|
26987
|
+
appLabel: 'coco',
|
|
26541
26988
|
idleTips: config.logTui?.idleTips,
|
|
26542
26989
|
initialView: 'history',
|
|
26543
26990
|
logArgv,
|
|
@@ -26550,7 +26997,7 @@ async function startCocoUi(argv) {
|
|
|
26550
26997
|
const logArgv = createLogArgvFromUiArgv(argv);
|
|
26551
26998
|
const rows = await getLogRows(git, logArgv);
|
|
26552
26999
|
await startInkInteractiveLog(git, rows, {}, {
|
|
26553
|
-
appLabel: 'coco
|
|
27000
|
+
appLabel: 'coco',
|
|
26554
27001
|
idleTips: config.logTui?.idleTips,
|
|
26555
27002
|
initialView: argv.view || 'history',
|
|
26556
27003
|
logArgv,
|