git-coco 0.35.0 → 0.36.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 +1244 -66
- package/dist/index.js +1245 -66
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -36,9 +36,11 @@ require('@langchain/core/utils/env');
|
|
|
36
36
|
require('@langchain/core/utils/async_caller');
|
|
37
37
|
var tiktoken = require('tiktoken');
|
|
38
38
|
var child_process = require('child_process');
|
|
39
|
+
var node_child_process = require('node:child_process');
|
|
39
40
|
var fs$1 = require('node:fs');
|
|
40
41
|
var os$1 = require('node:os');
|
|
41
42
|
var path$1 = require('node:path');
|
|
43
|
+
var crypto = require('node:crypto');
|
|
42
44
|
var readline = require('readline');
|
|
43
45
|
var util$1 = require('util');
|
|
44
46
|
var url = require('url');
|
|
@@ -67,6 +69,7 @@ var path__namespace = /*#__PURE__*/_interopNamespaceDefault(path);
|
|
|
67
69
|
var fs__namespace$1 = /*#__PURE__*/_interopNamespaceDefault(fs$1);
|
|
68
70
|
var os__namespace$1 = /*#__PURE__*/_interopNamespaceDefault(os$1);
|
|
69
71
|
var path__namespace$1 = /*#__PURE__*/_interopNamespaceDefault(path$1);
|
|
72
|
+
var crypto__namespace = /*#__PURE__*/_interopNamespaceDefault(crypto);
|
|
70
73
|
var readline__namespace = /*#__PURE__*/_interopNamespaceDefault(readline);
|
|
71
74
|
|
|
72
75
|
// This file is auto-generated - DO NOT EDIT
|
|
@@ -74,7 +77,7 @@ var readline__namespace = /*#__PURE__*/_interopNamespaceDefault(readline);
|
|
|
74
77
|
/**
|
|
75
78
|
* Current build version from package.json
|
|
76
79
|
*/
|
|
77
|
-
const BUILD_VERSION = "0.
|
|
80
|
+
const BUILD_VERSION = "0.36.0";
|
|
78
81
|
|
|
79
82
|
const isInteractive = (config) => {
|
|
80
83
|
return config?.mode === 'interactive' || !!config?.interactive;
|
|
@@ -13914,13 +13917,18 @@ function applyCommitComposeAction(state, action) {
|
|
|
13914
13917
|
loading: action.value,
|
|
13915
13918
|
};
|
|
13916
13919
|
case 'setDraft':
|
|
13920
|
+
// No `message` here — the loader → filled fields are the confirmation
|
|
13921
|
+
// that the AI generated something. A lingering "AI draft ready for
|
|
13922
|
+
// editing" line in the panel reads as stale state. The runtime still
|
|
13923
|
+
// posts the same string to the footer status line for transient
|
|
13924
|
+
// feedback.
|
|
13917
13925
|
return {
|
|
13918
13926
|
...state,
|
|
13919
13927
|
...splitCommitDraft(action.value),
|
|
13920
13928
|
field: 'summary',
|
|
13921
13929
|
editing: true,
|
|
13922
13930
|
loading: false,
|
|
13923
|
-
message:
|
|
13931
|
+
message: undefined,
|
|
13924
13932
|
details: undefined,
|
|
13925
13933
|
};
|
|
13926
13934
|
case 'setResult':
|
|
@@ -14552,6 +14560,85 @@ function getLogInkWorkflowActions() {
|
|
|
14552
14560
|
kind: 'normal',
|
|
14553
14561
|
requiresConfirmation: false,
|
|
14554
14562
|
},
|
|
14563
|
+
{
|
|
14564
|
+
// Per-view-only: scoped to the history view in inkInput so `c`
|
|
14565
|
+
// doesn't fire elsewhere. Empty key keeps it palette-discoverable
|
|
14566
|
+
// without registering a global hotkey.
|
|
14567
|
+
id: 'cherry-pick-commit',
|
|
14568
|
+
key: '',
|
|
14569
|
+
label: 'Cherry-pick commit',
|
|
14570
|
+
description: 'Apply the selected commit on top of the current branch (after confirmation).',
|
|
14571
|
+
kind: 'destructive',
|
|
14572
|
+
requiresConfirmation: true,
|
|
14573
|
+
},
|
|
14574
|
+
{
|
|
14575
|
+
// Per-view-only: scoped to the commit-diff explore in inkInput.
|
|
14576
|
+
// Routed through the y-confirm path because `git checkout <sha> --
|
|
14577
|
+
// <path>` overwrites the worktree file unconditionally and we
|
|
14578
|
+
// want the user to acknowledge that before discarding any local
|
|
14579
|
+
// edits to the path.
|
|
14580
|
+
id: 'checkout-file-from-commit',
|
|
14581
|
+
key: '',
|
|
14582
|
+
label: 'Cherry-pick file from commit',
|
|
14583
|
+
description: 'Materialize the selected file from this commit into the working tree (after confirmation).',
|
|
14584
|
+
kind: 'destructive',
|
|
14585
|
+
requiresConfirmation: true,
|
|
14586
|
+
},
|
|
14587
|
+
{
|
|
14588
|
+
// Per-view-only: scoped to the stash-diff explorer in inkInput.
|
|
14589
|
+
// Same overwrite rationale as `checkout-file-from-commit` — the
|
|
14590
|
+
// y-confirm path is the dirty-tree warning.
|
|
14591
|
+
id: 'checkout-file-from-stash',
|
|
14592
|
+
key: '',
|
|
14593
|
+
label: 'Cherry-pick file from stash',
|
|
14594
|
+
description: 'Materialize the selected file from this stash into the working tree (after confirmation).',
|
|
14595
|
+
kind: 'destructive',
|
|
14596
|
+
requiresConfirmation: true,
|
|
14597
|
+
},
|
|
14598
|
+
{
|
|
14599
|
+
id: 'open-pr',
|
|
14600
|
+
key: 'O',
|
|
14601
|
+
label: 'Open PR / repo',
|
|
14602
|
+
description: 'Open the current branch\'s pull request in the browser, or the repo page if there\'s no PR.',
|
|
14603
|
+
kind: 'normal',
|
|
14604
|
+
requiresConfirmation: false,
|
|
14605
|
+
},
|
|
14606
|
+
{
|
|
14607
|
+
id: 'fetch-remotes',
|
|
14608
|
+
key: 'F',
|
|
14609
|
+
label: 'Fetch all remotes',
|
|
14610
|
+
description: 'Run `git fetch --all --prune` and silently refresh context.',
|
|
14611
|
+
kind: 'normal',
|
|
14612
|
+
requiresConfirmation: false,
|
|
14613
|
+
},
|
|
14614
|
+
{
|
|
14615
|
+
id: 'pull-current-branch',
|
|
14616
|
+
key: 'U',
|
|
14617
|
+
label: 'Pull current branch',
|
|
14618
|
+
description: 'Run `git pull --ff-only` against the current branch.',
|
|
14619
|
+
kind: 'normal',
|
|
14620
|
+
requiresConfirmation: false,
|
|
14621
|
+
},
|
|
14622
|
+
{
|
|
14623
|
+
id: 'push-current-branch',
|
|
14624
|
+
key: 'P',
|
|
14625
|
+
label: 'Push current branch',
|
|
14626
|
+
description: 'Run `git push` for the current branch.',
|
|
14627
|
+
kind: 'normal',
|
|
14628
|
+
requiresConfirmation: false,
|
|
14629
|
+
},
|
|
14630
|
+
{
|
|
14631
|
+
// Per-view-only — the inkInput handler scopes this to the tags
|
|
14632
|
+
// surface so we don't expose `R` as a remote-delete from elsewhere.
|
|
14633
|
+
// The empty `key` keeps the workflow palette-discoverable but does
|
|
14634
|
+
// not register a global hotkey.
|
|
14635
|
+
id: 'delete-remote-tag',
|
|
14636
|
+
key: '',
|
|
14637
|
+
label: 'Delete remote tag',
|
|
14638
|
+
description: 'Push :tag to origin to delete the selected tag remotely after confirmation.',
|
|
14639
|
+
kind: 'destructive',
|
|
14640
|
+
requiresConfirmation: true,
|
|
14641
|
+
},
|
|
14555
14642
|
{
|
|
14556
14643
|
id: 'stage-file',
|
|
14557
14644
|
key: 'space',
|
|
@@ -14799,6 +14886,13 @@ const LOG_INK_KEY_BINDINGS = [
|
|
|
14799
14886
|
description: 'Push the stash view (gz; gs is reserved for status).',
|
|
14800
14887
|
contexts: ['normal'],
|
|
14801
14888
|
},
|
|
14889
|
+
{
|
|
14890
|
+
id: 'navigateWorktrees',
|
|
14891
|
+
keys: ['gw'],
|
|
14892
|
+
label: 'worktrees',
|
|
14893
|
+
description: 'Push the linked worktrees view.',
|
|
14894
|
+
contexts: ['normal'],
|
|
14895
|
+
},
|
|
14802
14896
|
{
|
|
14803
14897
|
id: 'navigateBack',
|
|
14804
14898
|
keys: ['<', 'esc'],
|
|
@@ -14919,6 +15013,7 @@ const GLOBAL_BINDING_IDS = [
|
|
|
14919
15013
|
'navigateBranches',
|
|
14920
15014
|
'navigateTags',
|
|
14921
15015
|
'navigateStash',
|
|
15016
|
+
'navigateWorktrees',
|
|
14922
15017
|
'navigateBack',
|
|
14923
15018
|
];
|
|
14924
15019
|
const NORMAL_GLOBAL_HINTS = ['g jump', '< back', '? help', ': cmds', 'q quit'];
|
|
@@ -14997,37 +15092,57 @@ function getLogInkFooterHints(options) {
|
|
|
14997
15092
|
};
|
|
14998
15093
|
}
|
|
14999
15094
|
if (options.activeView === 'diff') {
|
|
15095
|
+
if (options.diffSource === 'stash') {
|
|
15096
|
+
return {
|
|
15097
|
+
contextual: ['j/k lines', '[/] file', 'c cherry-pick', 'o edit', 'esc back'],
|
|
15098
|
+
global: NORMAL_GLOBAL_HINTS,
|
|
15099
|
+
};
|
|
15100
|
+
}
|
|
15101
|
+
if (options.diffSource === 'commit') {
|
|
15102
|
+
// Commit-diff explore: read-only diff, but `c` cherry-picks the
|
|
15103
|
+
// cursored file from the commit into the worktree.
|
|
15104
|
+
return {
|
|
15105
|
+
contextual: ['j/k hunks', '[/] file', 'c cherry-pick', 'esc back'],
|
|
15106
|
+
global: NORMAL_GLOBAL_HINTS,
|
|
15107
|
+
};
|
|
15108
|
+
}
|
|
15000
15109
|
return {
|
|
15001
|
-
contextual: ['j/k hunks', 'space stage', 'z revert', 'e/c compose', 'esc files'],
|
|
15110
|
+
contextual: ['j/k hunks', 'space stage', 'z revert', 'o edit', 'e/c compose', 'esc files'],
|
|
15002
15111
|
global: NORMAL_GLOBAL_HINTS,
|
|
15003
15112
|
};
|
|
15004
15113
|
}
|
|
15005
15114
|
if (options.activeView === 'compose') {
|
|
15006
15115
|
return {
|
|
15007
|
-
contextual: ['e edit', '
|
|
15116
|
+
contextual: ['e edit', 'c commit', 'I AI draft', 'gs hunks', 'esc back'],
|
|
15008
15117
|
global: NORMAL_GLOBAL_HINTS,
|
|
15009
15118
|
};
|
|
15010
15119
|
}
|
|
15011
15120
|
if (options.activeView === 'branches') {
|
|
15012
15121
|
return {
|
|
15013
|
-
contextual: ['↑/↓ branches', '
|
|
15122
|
+
contextual: ['↑/↓ branches', 'enter checkout', '+ new', 'D delete', 's sort'],
|
|
15014
15123
|
global: NORMAL_GLOBAL_HINTS,
|
|
15015
15124
|
};
|
|
15016
15125
|
}
|
|
15017
15126
|
if (options.activeView === 'tags') {
|
|
15018
15127
|
return {
|
|
15019
|
-
contextual: ['↑/↓ tags', '
|
|
15128
|
+
contextual: ['↑/↓ tags', '+ new', 'P push', 'T delete', 's sort'],
|
|
15020
15129
|
global: NORMAL_GLOBAL_HINTS,
|
|
15021
15130
|
};
|
|
15022
15131
|
}
|
|
15023
15132
|
if (options.activeView === 'stash') {
|
|
15024
15133
|
return {
|
|
15025
|
-
contextual: ['↑/↓ stashes', '
|
|
15134
|
+
contextual: ['↑/↓ stashes', 'enter diff', 'a apply', 'p pop', 'X drop'],
|
|
15135
|
+
global: NORMAL_GLOBAL_HINTS,
|
|
15136
|
+
};
|
|
15137
|
+
}
|
|
15138
|
+
if (options.activeView === 'worktrees') {
|
|
15139
|
+
return {
|
|
15140
|
+
contextual: ['↑/↓ worktrees', 'W remove', 'esc back'],
|
|
15026
15141
|
global: NORMAL_GLOBAL_HINTS,
|
|
15027
15142
|
};
|
|
15028
15143
|
}
|
|
15029
15144
|
return {
|
|
15030
|
-
contextual: ['↑/↓ move', '/ search', 'gg/G top/bottom'
|
|
15145
|
+
contextual: ['↑/↓ move', 'enter diff', 'c cherry-pick', '/ search', 'gg/G top/bottom'],
|
|
15031
15146
|
global: NORMAL_GLOBAL_HINTS,
|
|
15032
15147
|
};
|
|
15033
15148
|
}
|
|
@@ -15290,10 +15405,12 @@ function getLogInkPaletteExecuteEvents(command, state) {
|
|
|
15290
15405
|
if (command.requiresConfirmation) {
|
|
15291
15406
|
return [action({ type: 'setPendingConfirmation', value: command.id })];
|
|
15292
15407
|
}
|
|
15293
|
-
|
|
15294
|
-
|
|
15295
|
-
|
|
15296
|
-
|
|
15408
|
+
// Non-confirm workflows are dispatched directly through the runtime
|
|
15409
|
+
// workflow runner — same path the keyboard takes. Previously this
|
|
15410
|
+
// emitted `setWorkflowAction` only, which set state but never fired
|
|
15411
|
+
// the action because nothing in the runtime consumes
|
|
15412
|
+
// `workflowActionId`.
|
|
15413
|
+
return [{ type: 'runWorkflowAction', id: command.id }];
|
|
15297
15414
|
}
|
|
15298
15415
|
// Binding-derived commands. Map each LogInkCommandId to the same events
|
|
15299
15416
|
// the keystroke would emit. Order matches the keymap registry.
|
|
@@ -15355,6 +15472,8 @@ function getLogInkPaletteExecuteEvents(command, state) {
|
|
|
15355
15472
|
return [action({ type: 'pushView', value: 'tags' })];
|
|
15356
15473
|
case 'navigateStash':
|
|
15357
15474
|
return [action({ type: 'pushView', value: 'stash' })];
|
|
15475
|
+
case 'navigateWorktrees':
|
|
15476
|
+
return [action({ type: 'pushView', value: 'worktrees' })];
|
|
15358
15477
|
case 'navigateBack':
|
|
15359
15478
|
return [action({ type: 'popView' })];
|
|
15360
15479
|
case 'openSelected': {
|
|
@@ -15453,6 +15572,39 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
|
|
|
15453
15572
|
}
|
|
15454
15573
|
return [{ type: 'exit' }];
|
|
15455
15574
|
}
|
|
15575
|
+
// Input prompt is the most modal — when active, every keystroke routes
|
|
15576
|
+
// into the prompt until Enter (submit) or Esc (cancel). Sits above the
|
|
15577
|
+
// filter/confirmation/compose handlers so a prompt opened from inside
|
|
15578
|
+
// any of those still captures focus cleanly.
|
|
15579
|
+
if (state.inputPrompt) {
|
|
15580
|
+
if (key.escape) {
|
|
15581
|
+
return [
|
|
15582
|
+
action({ type: 'closeInputPrompt' }),
|
|
15583
|
+
action({ type: 'setStatus', value: 'cancelled' }),
|
|
15584
|
+
];
|
|
15585
|
+
}
|
|
15586
|
+
if (key.return) {
|
|
15587
|
+
const value = state.inputPrompt.value.trim();
|
|
15588
|
+
if (!value) {
|
|
15589
|
+
return [action({ type: 'setStatus', value: 'enter a value or press esc to cancel' })];
|
|
15590
|
+
}
|
|
15591
|
+
const id = state.inputPrompt.kind;
|
|
15592
|
+
return [
|
|
15593
|
+
{ type: 'runWorkflowAction', id, payload: value },
|
|
15594
|
+
action({ type: 'closeInputPrompt' }),
|
|
15595
|
+
];
|
|
15596
|
+
}
|
|
15597
|
+
if (key.backspace || key.delete) {
|
|
15598
|
+
return [action({ type: 'backspaceInputPrompt' })];
|
|
15599
|
+
}
|
|
15600
|
+
if (key.ctrl && inputValue === 'u') {
|
|
15601
|
+
return [action({ type: 'clearInputPromptText' })];
|
|
15602
|
+
}
|
|
15603
|
+
if (inputValue && !key.ctrl && !key.meta) {
|
|
15604
|
+
return [action({ type: 'appendInputPrompt', value: inputValue })];
|
|
15605
|
+
}
|
|
15606
|
+
return [];
|
|
15607
|
+
}
|
|
15456
15608
|
if (state.commitCompose.editing) {
|
|
15457
15609
|
if (key.escape) {
|
|
15458
15610
|
return [action({ type: 'commitCompose', action: { type: 'setEditing', value: false } })];
|
|
@@ -15521,7 +15673,7 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
|
|
|
15521
15673
|
// selected item and run the right action function.
|
|
15522
15674
|
if (workflowAction) {
|
|
15523
15675
|
return [
|
|
15524
|
-
{ type: 'runWorkflowAction', id: workflowAction.id },
|
|
15676
|
+
{ type: 'runWorkflowAction', id: workflowAction.id, payload: state.pendingConfirmationPayload },
|
|
15525
15677
|
action({ type: 'setPendingConfirmation', value: undefined }),
|
|
15526
15678
|
];
|
|
15527
15679
|
}
|
|
@@ -15671,6 +15823,12 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
|
|
|
15671
15823
|
action({ type: 'setStatus', value: 'jumped to stash' }),
|
|
15672
15824
|
];
|
|
15673
15825
|
}
|
|
15826
|
+
if (state.pendingKey === 'g' && inputValue === 'w') {
|
|
15827
|
+
return [
|
|
15828
|
+
action({ type: 'pushView', value: 'worktrees' }),
|
|
15829
|
+
action({ type: 'setStatus', value: 'jumped to worktrees' }),
|
|
15830
|
+
];
|
|
15831
|
+
}
|
|
15674
15832
|
if (inputValue === 'g') {
|
|
15675
15833
|
if (state.pendingKey === 'g') {
|
|
15676
15834
|
return [
|
|
@@ -15722,6 +15880,13 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
|
|
|
15722
15880
|
hunkOffsets: context.worktreeHunkOffsets,
|
|
15723
15881
|
})];
|
|
15724
15882
|
}
|
|
15883
|
+
if (state.activeView === 'diff' && state.diffSource === 'stash' && context.stashDiffFileOffsets?.length) {
|
|
15884
|
+
return [action({
|
|
15885
|
+
type: 'jumpCommitDiffHunk',
|
|
15886
|
+
delta: -1,
|
|
15887
|
+
hunkOffsets: context.stashDiffFileOffsets,
|
|
15888
|
+
})];
|
|
15889
|
+
}
|
|
15725
15890
|
if (state.activeView === 'diff' && context.commitDiffHunkOffsets?.length) {
|
|
15726
15891
|
return [action({
|
|
15727
15892
|
type: 'jumpCommitDiffHunk',
|
|
@@ -15739,6 +15904,13 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
|
|
|
15739
15904
|
hunkOffsets: context.worktreeHunkOffsets,
|
|
15740
15905
|
})];
|
|
15741
15906
|
}
|
|
15907
|
+
if (state.activeView === 'diff' && state.diffSource === 'stash' && context.stashDiffFileOffsets?.length) {
|
|
15908
|
+
return [action({
|
|
15909
|
+
type: 'jumpCommitDiffHunk',
|
|
15910
|
+
delta: 1,
|
|
15911
|
+
hunkOffsets: context.stashDiffFileOffsets,
|
|
15912
|
+
})];
|
|
15913
|
+
}
|
|
15742
15914
|
if (state.activeView === 'diff' && context.commitDiffHunkOffsets?.length) {
|
|
15743
15915
|
return [action({
|
|
15744
15916
|
type: 'jumpCommitDiffHunk',
|
|
@@ -15792,6 +15964,9 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
|
|
|
15792
15964
|
if (state.activeView === 'stash' && context.stashCount) {
|
|
15793
15965
|
return [action({ type: 'moveStash', delta: -1, count: context.stashCount })];
|
|
15794
15966
|
}
|
|
15967
|
+
if (state.activeView === 'worktrees' && context.worktreeListCount) {
|
|
15968
|
+
return [action({ type: 'moveWorktreeListEntry', delta: -1, count: context.worktreeListCount })];
|
|
15969
|
+
}
|
|
15795
15970
|
if (state.activeView === 'history' &&
|
|
15796
15971
|
state.focus === 'commits' &&
|
|
15797
15972
|
state.selectedIndex === 0 &&
|
|
@@ -15842,6 +16017,9 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
|
|
|
15842
16017
|
if (state.activeView === 'stash' && context.stashCount) {
|
|
15843
16018
|
return [action({ type: 'moveStash', delta: 1, count: context.stashCount })];
|
|
15844
16019
|
}
|
|
16020
|
+
if (state.activeView === 'worktrees' && context.worktreeListCount) {
|
|
16021
|
+
return [action({ type: 'moveWorktreeListEntry', delta: 1, count: context.worktreeListCount })];
|
|
16022
|
+
}
|
|
15845
16023
|
return [
|
|
15846
16024
|
action(state.focus === 'sidebar'
|
|
15847
16025
|
? { type: 'nextSidebarTab' }
|
|
@@ -15942,12 +16120,183 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
|
|
|
15942
16120
|
})];
|
|
15943
16121
|
}
|
|
15944
16122
|
}
|
|
15945
|
-
|
|
16123
|
+
// Enter on a sidebar tab drills into the corresponding promoted view
|
|
16124
|
+
// (status / branches / tags / stash). Sits above the per-view Enter
|
|
16125
|
+
// handlers so a sidebar-focused Enter never fires checkout-branch /
|
|
16126
|
+
// navigateOpenDiffForCommit / etc. against the (hidden) selection in
|
|
16127
|
+
// the active tab.
|
|
16128
|
+
//
|
|
16129
|
+
// The Enter also moves focus out of the sidebar into the newly opened
|
|
16130
|
+
// list — otherwise ↑/↓ keep cycling sidebar tabs instead of navigating
|
|
16131
|
+
// inside the just-opened view, which made the drill-in feel half-done.
|
|
16132
|
+
if (key.return && state.focus === 'sidebar') {
|
|
16133
|
+
const tabToView = {
|
|
16134
|
+
status: 'status',
|
|
16135
|
+
branches: 'branches',
|
|
16136
|
+
tags: 'tags',
|
|
16137
|
+
stashes: 'stash',
|
|
16138
|
+
worktrees: 'worktrees',
|
|
16139
|
+
};
|
|
16140
|
+
const target = tabToView[state.sidebarTab];
|
|
16141
|
+
if (target) {
|
|
16142
|
+
return [
|
|
16143
|
+
action({ type: 'pushView', value: target }),
|
|
16144
|
+
action({ type: 'setFocus', value: 'commits' }),
|
|
16145
|
+
];
|
|
16146
|
+
}
|
|
16147
|
+
return [action({ type: 'setStatus', value: 'no detail view for this tab' })];
|
|
16148
|
+
}
|
|
16149
|
+
if (key.return && state.activeView === 'status' && state.focus === 'commits' && context.worktreeFileCount) {
|
|
15946
16150
|
return [action({
|
|
15947
16151
|
type: 'navigateOpenDiffForWorktreeFile',
|
|
15948
16152
|
fileIndex: state.selectedWorktreeFileIndex,
|
|
15949
16153
|
})];
|
|
15950
16154
|
}
|
|
16155
|
+
// Enter on a branch row checks the branch out. Non-destructive workflow
|
|
16156
|
+
// action — no confirmation prompt.
|
|
16157
|
+
if (key.return && state.activeView === 'branches' && state.focus === 'commits' && context.branchCount) {
|
|
16158
|
+
return [{ type: 'runWorkflowAction', id: 'checkout-branch' }];
|
|
16159
|
+
}
|
|
16160
|
+
// `+` opens a create-branch / create-tag prompt depending on context.
|
|
16161
|
+
// Works from either the matching promoted view (active branches /
|
|
16162
|
+
// tags surface) or from the sidebar when the corresponding tab is
|
|
16163
|
+
// active — saves a drill-in for "I just want to make a new branch".
|
|
16164
|
+
const wantsCreateBranch = inputValue === '+' && (state.activeView === 'branches' ||
|
|
16165
|
+
(state.focus === 'sidebar' && state.sidebarTab === 'branches'));
|
|
16166
|
+
const wantsCreateTag = inputValue === '+' && (state.activeView === 'tags' ||
|
|
16167
|
+
(state.focus === 'sidebar' && state.sidebarTab === 'tags'));
|
|
16168
|
+
if (wantsCreateBranch) {
|
|
16169
|
+
return [action({
|
|
16170
|
+
type: 'openInputPrompt',
|
|
16171
|
+
kind: 'create-branch',
|
|
16172
|
+
label: 'New branch name',
|
|
16173
|
+
})];
|
|
16174
|
+
}
|
|
16175
|
+
if (wantsCreateTag) {
|
|
16176
|
+
return [action({
|
|
16177
|
+
type: 'openInputPrompt',
|
|
16178
|
+
kind: 'create-tag',
|
|
16179
|
+
label: 'New tag name',
|
|
16180
|
+
})];
|
|
16181
|
+
}
|
|
16182
|
+
// Per-view stash actions: `a` apply (keep the stash), `p` pop (apply
|
|
16183
|
+
// then drop). Drop is the existing destructive `X` workflow which
|
|
16184
|
+
// routes through the y-confirm path. Scoped to the stash view so the
|
|
16185
|
+
// letters stay free elsewhere.
|
|
16186
|
+
if (inputValue === 'a' && state.activeView === 'stash' && context.stashCount) {
|
|
16187
|
+
return [{ type: 'runWorkflowAction', id: 'apply-stash' }];
|
|
16188
|
+
}
|
|
16189
|
+
if (inputValue === 'p' && state.activeView === 'stash' && context.stashCount) {
|
|
16190
|
+
return [{ type: 'runWorkflowAction', id: 'pop-stash' }];
|
|
16191
|
+
}
|
|
16192
|
+
// Per-view tag action: `P` pushes the selected tag to origin. Letter
|
|
16193
|
+
// is scoped to the tags surface so it doesn't collide with `p` for
|
|
16194
|
+
// pop-stash. Note: this also takes precedence over the global
|
|
16195
|
+
// push-current-branch workflow's `P` key.
|
|
16196
|
+
if (inputValue === 'P' && state.activeView === 'tags' && context.tagCount) {
|
|
16197
|
+
return [{ type: 'runWorkflowAction', id: 'push-tag' }];
|
|
16198
|
+
}
|
|
16199
|
+
// Per-view branches actions: `R` renames the selected branch, `u`
|
|
16200
|
+
// sets its upstream. Both open the input prompt so the user can type
|
|
16201
|
+
// the new value. Pre-fills are handled by the prompt's `initial`.
|
|
16202
|
+
if (inputValue === 'R' && state.activeView === 'branches' && context.branchCount) {
|
|
16203
|
+
return [action({
|
|
16204
|
+
type: 'openInputPrompt',
|
|
16205
|
+
kind: 'rename-branch',
|
|
16206
|
+
label: 'Rename branch to',
|
|
16207
|
+
})];
|
|
16208
|
+
}
|
|
16209
|
+
if (inputValue === 'u' && state.activeView === 'branches' && context.branchCount) {
|
|
16210
|
+
return [action({
|
|
16211
|
+
type: 'openInputPrompt',
|
|
16212
|
+
kind: 'set-upstream',
|
|
16213
|
+
label: 'Upstream ref (e.g. origin/main)',
|
|
16214
|
+
})];
|
|
16215
|
+
}
|
|
16216
|
+
// Per-view tag action: `R` deletes the tag from the remote (after
|
|
16217
|
+
// confirmation). Scoped per-view so this letter is free elsewhere
|
|
16218
|
+
// (especially the `R` rename binding on the branches view).
|
|
16219
|
+
if (inputValue === 'R' && state.activeView === 'tags' && context.tagCount) {
|
|
16220
|
+
return [action({ type: 'setPendingConfirmation', value: 'delete-remote-tag' })];
|
|
16221
|
+
}
|
|
16222
|
+
// Global stash hotkey: `S` opens a stash-message prompt and
|
|
16223
|
+
// `createStash` runs once submitted. Available everywhere there's
|
|
16224
|
+
// not a more modal handler in front of it.
|
|
16225
|
+
if (inputValue === 'S') {
|
|
16226
|
+
return [action({
|
|
16227
|
+
type: 'openInputPrompt',
|
|
16228
|
+
kind: 'create-stash',
|
|
16229
|
+
label: 'Stash message',
|
|
16230
|
+
})];
|
|
16231
|
+
}
|
|
16232
|
+
// `o` opens the file under the cursor in $EDITOR. Available on the
|
|
16233
|
+
// status surface (worktree files), the worktree diff (the file being
|
|
16234
|
+
// diffed), and the stash diff (the file the cursor sits in inside
|
|
16235
|
+
// the patch). The runtime suspends Ink, spawns the editor sync, then
|
|
16236
|
+
// re-renders.
|
|
16237
|
+
if (inputValue === 'o' && state.activeView === 'status' && context.worktreeFileCount && context.worktreeSelectedPath) {
|
|
16238
|
+
return [{ type: 'openFileInEditor', path: context.worktreeSelectedPath }];
|
|
16239
|
+
}
|
|
16240
|
+
if (inputValue === 'o' && state.activeView === 'diff' && state.diffSource === 'worktree' && context.worktreeSelectedPath) {
|
|
16241
|
+
return [{ type: 'openFileInEditor', path: context.worktreeSelectedPath }];
|
|
16242
|
+
}
|
|
16243
|
+
if (inputValue === 'o' && state.activeView === 'diff' && state.diffSource === 'stash' && context.stashDiffSelectedPath) {
|
|
16244
|
+
return [{ type: 'openFileInEditor', path: context.stashDiffSelectedPath }];
|
|
16245
|
+
}
|
|
16246
|
+
// `c` on a stash diff cherry-picks the file under the cursor —
|
|
16247
|
+
// materializes that single path from the stash into the working tree
|
|
16248
|
+
// (`git checkout <stashRef> -- <path>`). Routed through the y-confirm
|
|
16249
|
+
// path because the checkout overwrites the worktree file
|
|
16250
|
+
// unconditionally; the prompt is the user's chance to abort if they
|
|
16251
|
+
// have unsaved edits at that path.
|
|
16252
|
+
if (inputValue === 'c' &&
|
|
16253
|
+
state.activeView === 'diff' &&
|
|
16254
|
+
state.diffSource === 'stash' &&
|
|
16255
|
+
context.stashDiffSelectedPath &&
|
|
16256
|
+
state.stashDiffRef) {
|
|
16257
|
+
return [action({
|
|
16258
|
+
type: 'setPendingConfirmation',
|
|
16259
|
+
value: 'checkout-file-from-stash',
|
|
16260
|
+
payload: context.stashDiffSelectedPath,
|
|
16261
|
+
})];
|
|
16262
|
+
}
|
|
16263
|
+
// `c` on a commit-diff explore cherry-picks the cursored file from
|
|
16264
|
+
// that historical commit — `git checkout <sha> -- <path>`. Same
|
|
16265
|
+
// confirmation rationale as the stash variant. The payload encodes
|
|
16266
|
+
// both the sha and the path so the runtime handler doesn't have to
|
|
16267
|
+
// re-resolve either.
|
|
16268
|
+
if (inputValue === 'c' &&
|
|
16269
|
+
state.activeView === 'diff' &&
|
|
16270
|
+
state.diffSource === 'commit' &&
|
|
16271
|
+
context.commitDiffSelectedPath &&
|
|
16272
|
+
context.commitDiffSelectedSha) {
|
|
16273
|
+
return [action({
|
|
16274
|
+
type: 'setPendingConfirmation',
|
|
16275
|
+
value: 'checkout-file-from-commit',
|
|
16276
|
+
payload: `${context.commitDiffSelectedSha} ${context.commitDiffSelectedPath}`,
|
|
16277
|
+
})];
|
|
16278
|
+
}
|
|
16279
|
+
// `c` on the history view cherry-picks the full selected commit on
|
|
16280
|
+
// top of the current branch. Routed through the y-confirm flow since
|
|
16281
|
+
// it can produce conflicts and is a real working-tree mutation.
|
|
16282
|
+
if (inputValue === 'c' &&
|
|
16283
|
+
state.activeView === 'history' &&
|
|
16284
|
+
state.focus === 'commits' &&
|
|
16285
|
+
state.filteredCommits.length > 0 &&
|
|
16286
|
+
!state.pendingCommitFocused) {
|
|
16287
|
+
return [action({ type: 'setPendingConfirmation', value: 'cherry-pick-commit' })];
|
|
16288
|
+
}
|
|
16289
|
+
// Enter on a stash row pushes the diff view scoped to that stash.
|
|
16290
|
+
// The runtime loads `git stash show -p <ref>` once the view is
|
|
16291
|
+
// active. The stash ref is passed via the action so we don't need a
|
|
16292
|
+
// context lookup here.
|
|
16293
|
+
if (key.return && state.activeView === 'stash' && state.focus === 'commits' && context.stashCount && context.stashSelectedRef) {
|
|
16294
|
+
return [action({
|
|
16295
|
+
type: 'navigateOpenDiffForStash',
|
|
16296
|
+
ref: context.stashSelectedRef,
|
|
16297
|
+
stashIndex: state.selectedStashIndex,
|
|
16298
|
+
})];
|
|
16299
|
+
}
|
|
15951
16300
|
if (inputValue === ' ' && state.activeView === 'status' && context.worktreeFileCount) {
|
|
15952
16301
|
return [{ type: 'toggleSelectedFileStage' }];
|
|
15953
16302
|
}
|
|
@@ -15983,10 +16332,10 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
|
|
|
15983
16332
|
return [action({ type: 'setPendingConfirmation', value: workflowAction.id })];
|
|
15984
16333
|
}
|
|
15985
16334
|
if (workflowAction) {
|
|
15986
|
-
|
|
15987
|
-
|
|
15988
|
-
|
|
15989
|
-
];
|
|
16335
|
+
// Non-destructive workflow — fire it directly via the runtime
|
|
16336
|
+
// handler. The handler surfaces success/failure on the status line
|
|
16337
|
+
// and silently refreshes context so the list updates.
|
|
16338
|
+
return [{ type: 'runWorkflowAction', id: workflowAction.id }];
|
|
15990
16339
|
}
|
|
15991
16340
|
return [];
|
|
15992
16341
|
}
|
|
@@ -16002,7 +16351,7 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
|
|
|
16002
16351
|
* fall back to "already seen" so we never block startup.
|
|
16003
16352
|
*/
|
|
16004
16353
|
const MARKER_BASENAME = 'onboarding.seen';
|
|
16005
|
-
function resolveCacheDir() {
|
|
16354
|
+
function resolveCacheDir$1() {
|
|
16006
16355
|
const xdg = process.env.XDG_CACHE_HOME;
|
|
16007
16356
|
if (xdg && xdg.trim().length > 0) {
|
|
16008
16357
|
return path__namespace$1.join(xdg, 'coco');
|
|
@@ -16010,7 +16359,7 @@ function resolveCacheDir() {
|
|
|
16010
16359
|
return path__namespace$1.join(os__namespace$1.homedir(), '.cache', 'coco');
|
|
16011
16360
|
}
|
|
16012
16361
|
function getOnboardingMarkerPath() {
|
|
16013
|
-
return path__namespace$1.join(resolveCacheDir(), MARKER_BASENAME);
|
|
16362
|
+
return path__namespace$1.join(resolveCacheDir$1(), MARKER_BASENAME);
|
|
16014
16363
|
}
|
|
16015
16364
|
function hasSeenOnboarding() {
|
|
16016
16365
|
try {
|
|
@@ -16034,6 +16383,65 @@ function markOnboardingSeen() {
|
|
|
16034
16383
|
}
|
|
16035
16384
|
}
|
|
16036
16385
|
|
|
16386
|
+
/**
|
|
16387
|
+
* Persist which sidebar tab the user last had active, keyed per repo so
|
|
16388
|
+
* switching projects doesn't reset every other repo's preference. The
|
|
16389
|
+
* cache lives next to the onboarding marker (XDG-friendly) and is
|
|
16390
|
+
* best-effort: read/write failures fall back to the default sidebar
|
|
16391
|
+
* tab on next start.
|
|
16392
|
+
*
|
|
16393
|
+
* Repos are keyed by a short hash of their absolute path — no PII in
|
|
16394
|
+
* the cache filename, and re-creating a repo at the same path keeps
|
|
16395
|
+
* the same preference.
|
|
16396
|
+
*/
|
|
16397
|
+
const VALID_TABS = [
|
|
16398
|
+
'status',
|
|
16399
|
+
'branches',
|
|
16400
|
+
'tags',
|
|
16401
|
+
'stashes',
|
|
16402
|
+
'worktrees',
|
|
16403
|
+
];
|
|
16404
|
+
function resolveCacheDir() {
|
|
16405
|
+
const xdg = process.env.XDG_CACHE_HOME;
|
|
16406
|
+
if (xdg && xdg.trim().length > 0) {
|
|
16407
|
+
return path__namespace$1.join(xdg, 'coco');
|
|
16408
|
+
}
|
|
16409
|
+
return path__namespace$1.join(os__namespace$1.homedir(), '.cache', 'coco');
|
|
16410
|
+
}
|
|
16411
|
+
function repoKey(repoPath) {
|
|
16412
|
+
// sha1 is used here as a non-security cache-key derivation — we just
|
|
16413
|
+
// need a deterministic short identifier for the marker filename so
|
|
16414
|
+
// re-creating a repo at the same path keeps the same preference.
|
|
16415
|
+
// No PII or auth context is hashed; no collision-resistance against
|
|
16416
|
+
// an adversary is required. DevSkim DS126858 doesn't apply.
|
|
16417
|
+
// DevSkim: ignore DS126858
|
|
16418
|
+
return crypto__namespace.createHash('sha1').update(repoPath).digest('hex').slice(0, 16);
|
|
16419
|
+
}
|
|
16420
|
+
function getSidebarTabMarkerPath(repoPath) {
|
|
16421
|
+
return path__namespace$1.join(resolveCacheDir(), `sidebar-tab.${repoKey(repoPath)}`);
|
|
16422
|
+
}
|
|
16423
|
+
function getSavedSidebarTab(repoPath) {
|
|
16424
|
+
try {
|
|
16425
|
+
const raw = fs__namespace$1.readFileSync(getSidebarTabMarkerPath(repoPath), 'utf8').trim();
|
|
16426
|
+
return VALID_TABS.includes(raw)
|
|
16427
|
+
? raw
|
|
16428
|
+
: undefined;
|
|
16429
|
+
}
|
|
16430
|
+
catch {
|
|
16431
|
+
return undefined;
|
|
16432
|
+
}
|
|
16433
|
+
}
|
|
16434
|
+
function saveSidebarTab(repoPath, tab) {
|
|
16435
|
+
const marker = getSidebarTabMarkerPath(repoPath);
|
|
16436
|
+
try {
|
|
16437
|
+
fs__namespace$1.mkdirSync(path__namespace$1.dirname(marker), { recursive: true });
|
|
16438
|
+
fs__namespace$1.writeFileSync(marker, tab);
|
|
16439
|
+
}
|
|
16440
|
+
catch {
|
|
16441
|
+
// Best-effort persistence; swallow.
|
|
16442
|
+
}
|
|
16443
|
+
}
|
|
16444
|
+
|
|
16037
16445
|
/**
|
|
16038
16446
|
* Promoted-view selection rectification on filter changes (P4.5).
|
|
16039
16447
|
*
|
|
@@ -16287,7 +16695,12 @@ function getLogInkLayout(input) {
|
|
|
16287
16695
|
const columns = input.columns || LOG_INK_DEFAULT_COLUMNS;
|
|
16288
16696
|
const rows = input.rows || LOG_INK_DEFAULT_ROWS;
|
|
16289
16697
|
const detailWidth = Math.max(30, Math.min(56, Math.floor(columns * 0.34)));
|
|
16290
|
-
|
|
16698
|
+
// Sidebar at rest: 22-34 cells (~24% of width). Focused: 32-50 cells
|
|
16699
|
+
// (~36% of width). The transition is instant per render — focus tab to
|
|
16700
|
+
// expand, focus away to collapse.
|
|
16701
|
+
const sidebarWidth = input.sidebarFocused
|
|
16702
|
+
? Math.max(32, Math.min(50, Math.floor(columns * 0.36)))
|
|
16703
|
+
: Math.max(22, Math.min(34, Math.floor(columns * 0.24)));
|
|
16291
16704
|
return {
|
|
16292
16705
|
bodyRows: Math.max(8, rows - 5),
|
|
16293
16706
|
columns,
|
|
@@ -17027,9 +17440,19 @@ function withPushedView(state, value) {
|
|
|
17027
17440
|
...state,
|
|
17028
17441
|
activeView: value,
|
|
17029
17442
|
viewStack,
|
|
17443
|
+
// The compose + status views' right detail panels already show
|
|
17444
|
+
// worktree info, so keeping the left sidebar on the Status tab
|
|
17445
|
+
// duplicates that information. Auto-switch to Branches when entering
|
|
17446
|
+
// either view; the user can swap back with [/] if they want.
|
|
17447
|
+
//
|
|
17448
|
+
// We update only the rendered `sidebarTab` here, never
|
|
17449
|
+
// `userSidebarTab`, so this auto-switch is invisible to per-repo
|
|
17450
|
+
// persistence and pop-view restores the previous tab.
|
|
17451
|
+
sidebarTab: value === 'compose' || value === 'status' ? 'branches' : state.sidebarTab,
|
|
17030
17452
|
worktreeDiffOffset: value === 'diff' ? state.worktreeDiffOffset : 0,
|
|
17031
17453
|
selectedWorktreeHunkIndex: value === 'diff' ? state.selectedWorktreeHunkIndex : 0,
|
|
17032
17454
|
diffSource: value === 'diff' ? state.diffSource : undefined,
|
|
17455
|
+
stashDiffRef: value === 'diff' ? state.stashDiffRef : undefined,
|
|
17033
17456
|
pendingCommitFocused: value === 'history' ? state.pendingCommitFocused : false,
|
|
17034
17457
|
pendingKey: undefined,
|
|
17035
17458
|
};
|
|
@@ -17044,9 +17467,14 @@ function withPoppedView(state) {
|
|
|
17044
17467
|
...state,
|
|
17045
17468
|
activeView: next,
|
|
17046
17469
|
viewStack,
|
|
17470
|
+
// Restore the user's last explicit tab choice so popping out of
|
|
17471
|
+
// compose / status (which auto-switch the sidebar to Branches)
|
|
17472
|
+
// returns the user to whatever they actually had open before.
|
|
17473
|
+
sidebarTab: state.userSidebarTab,
|
|
17047
17474
|
worktreeDiffOffset: next === 'diff' ? state.worktreeDiffOffset : 0,
|
|
17048
17475
|
selectedWorktreeHunkIndex: next === 'diff' ? state.selectedWorktreeHunkIndex : 0,
|
|
17049
17476
|
diffSource: next === 'diff' ? state.diffSource : undefined,
|
|
17477
|
+
stashDiffRef: next === 'diff' ? state.stashDiffRef : undefined,
|
|
17050
17478
|
pendingCommitFocused: next === 'history' ? state.pendingCommitFocused : false,
|
|
17051
17479
|
pendingKey: undefined,
|
|
17052
17480
|
};
|
|
@@ -17063,6 +17491,7 @@ function withReplacedView(state, value) {
|
|
|
17063
17491
|
worktreeDiffOffset: value === 'diff' ? state.worktreeDiffOffset : 0,
|
|
17064
17492
|
selectedWorktreeHunkIndex: value === 'diff' ? state.selectedWorktreeHunkIndex : 0,
|
|
17065
17493
|
diffSource: value === 'diff' ? state.diffSource : undefined,
|
|
17494
|
+
stashDiffRef: value === 'diff' ? state.stashDiffRef : undefined,
|
|
17066
17495
|
pendingCommitFocused: value === 'history' ? state.pendingCommitFocused : false,
|
|
17067
17496
|
pendingKey: undefined,
|
|
17068
17497
|
};
|
|
@@ -17155,6 +17584,7 @@ function createLogInkState(rows, options = {}) {
|
|
|
17155
17584
|
selectedBranchIndex: 0,
|
|
17156
17585
|
selectedTagIndex: 0,
|
|
17157
17586
|
selectedStashIndex: 0,
|
|
17587
|
+
selectedWorktreeListIndex: 0,
|
|
17158
17588
|
branchSort: DEFAULT_BRANCH_SORT_MODE,
|
|
17159
17589
|
tagSort: DEFAULT_TAG_SORT_MODE,
|
|
17160
17590
|
paletteFilter: '',
|
|
@@ -17170,10 +17600,12 @@ function createLogInkState(rows, options = {}) {
|
|
|
17170
17600
|
showCommandPalette: false,
|
|
17171
17601
|
workflowActionId: undefined,
|
|
17172
17602
|
pendingConfirmationId: undefined,
|
|
17603
|
+
pendingConfirmationPayload: undefined,
|
|
17173
17604
|
pendingMutationConfirmation: undefined,
|
|
17174
17605
|
pendingKey: undefined,
|
|
17175
17606
|
focus: 'commits',
|
|
17176
17607
|
sidebarTab: 'status',
|
|
17608
|
+
userSidebarTab: 'status',
|
|
17177
17609
|
};
|
|
17178
17610
|
}
|
|
17179
17611
|
function getSelectedInkCommit(state) {
|
|
@@ -17280,6 +17712,12 @@ function applyLogInkAction(state, action) {
|
|
|
17280
17712
|
selectedStashIndex: clampIndex$1(state.selectedStashIndex + action.delta, action.count),
|
|
17281
17713
|
pendingKey: undefined,
|
|
17282
17714
|
};
|
|
17715
|
+
case 'moveWorktreeListEntry':
|
|
17716
|
+
return {
|
|
17717
|
+
...state,
|
|
17718
|
+
selectedWorktreeListIndex: clampIndex$1(state.selectedWorktreeListIndex + action.delta, action.count),
|
|
17719
|
+
pendingKey: undefined,
|
|
17720
|
+
};
|
|
17283
17721
|
case 'cycleBranchSort':
|
|
17284
17722
|
return {
|
|
17285
17723
|
...state,
|
|
@@ -17296,6 +17734,30 @@ function applyLogInkAction(state, action) {
|
|
|
17296
17734
|
selectedTagIndex: 0,
|
|
17297
17735
|
pendingKey: undefined,
|
|
17298
17736
|
};
|
|
17737
|
+
case 'openInputPrompt':
|
|
17738
|
+
return {
|
|
17739
|
+
...state,
|
|
17740
|
+
inputPrompt: {
|
|
17741
|
+
kind: action.kind,
|
|
17742
|
+
label: action.label,
|
|
17743
|
+
value: action.initial || '',
|
|
17744
|
+
},
|
|
17745
|
+
pendingKey: undefined,
|
|
17746
|
+
};
|
|
17747
|
+
case 'appendInputPrompt':
|
|
17748
|
+
return state.inputPrompt
|
|
17749
|
+
? { ...state, inputPrompt: { ...state.inputPrompt, value: `${state.inputPrompt.value}${action.value}` } }
|
|
17750
|
+
: state;
|
|
17751
|
+
case 'backspaceInputPrompt':
|
|
17752
|
+
return state.inputPrompt
|
|
17753
|
+
? { ...state, inputPrompt: { ...state.inputPrompt, value: state.inputPrompt.value.slice(0, -1) } }
|
|
17754
|
+
: state;
|
|
17755
|
+
case 'clearInputPromptText':
|
|
17756
|
+
return state.inputPrompt
|
|
17757
|
+
? { ...state, inputPrompt: { ...state.inputPrompt, value: '' } }
|
|
17758
|
+
: state;
|
|
17759
|
+
case 'closeInputPrompt':
|
|
17760
|
+
return { ...state, inputPrompt: undefined, pendingKey: undefined };
|
|
17299
17761
|
case 'moveToBottom':
|
|
17300
17762
|
return {
|
|
17301
17763
|
...state,
|
|
@@ -17314,12 +17776,15 @@ function applyLogInkAction(state, action) {
|
|
|
17314
17776
|
pendingCommitFocused: false,
|
|
17315
17777
|
pendingKey: undefined,
|
|
17316
17778
|
};
|
|
17317
|
-
case 'nextSidebarTab':
|
|
17779
|
+
case 'nextSidebarTab': {
|
|
17780
|
+
const next = cycleValue(SIDEBAR_TABS, state.sidebarTab, 1);
|
|
17318
17781
|
return {
|
|
17319
17782
|
...state,
|
|
17320
|
-
sidebarTab:
|
|
17783
|
+
sidebarTab: next,
|
|
17784
|
+
userSidebarTab: next,
|
|
17321
17785
|
pendingKey: undefined,
|
|
17322
17786
|
};
|
|
17787
|
+
}
|
|
17323
17788
|
case 'page':
|
|
17324
17789
|
return {
|
|
17325
17790
|
...state,
|
|
@@ -17353,12 +17818,15 @@ function applyLogInkAction(state, action) {
|
|
|
17353
17818
|
diffPreviewOffset: nextHunkOffset(state.diffPreviewOffset, action.hunkOffsets, action.delta),
|
|
17354
17819
|
pendingKey: undefined,
|
|
17355
17820
|
};
|
|
17356
|
-
case 'previousSidebarTab':
|
|
17821
|
+
case 'previousSidebarTab': {
|
|
17822
|
+
const previous = cycleValue(SIDEBAR_TABS, state.sidebarTab, -1);
|
|
17357
17823
|
return {
|
|
17358
17824
|
...state,
|
|
17359
|
-
sidebarTab:
|
|
17825
|
+
sidebarTab: previous,
|
|
17826
|
+
userSidebarTab: previous,
|
|
17360
17827
|
pendingKey: undefined,
|
|
17361
17828
|
};
|
|
17829
|
+
}
|
|
17362
17830
|
case 'setFilter':
|
|
17363
17831
|
return withFilter$1(state, action.value, action.promotedSelections);
|
|
17364
17832
|
case 'setActiveView':
|
|
@@ -17406,6 +17874,21 @@ function applyLogInkAction(state, action) {
|
|
|
17406
17874
|
diffSource: 'worktree',
|
|
17407
17875
|
};
|
|
17408
17876
|
}
|
|
17877
|
+
case 'navigateOpenDiffForStash': {
|
|
17878
|
+
const next = withPushedView(state, 'diff');
|
|
17879
|
+
return {
|
|
17880
|
+
...next,
|
|
17881
|
+
diffSource: 'stash',
|
|
17882
|
+
stashDiffRef: action.ref,
|
|
17883
|
+
selectedStashIndex: Math.max(0, action.stashIndex ?? state.selectedStashIndex),
|
|
17884
|
+
// Reset the diff scroll offset so the stash patch always opens
|
|
17885
|
+
// at the top, mirroring `navigateOpenDiffForCommit`. Without
|
|
17886
|
+
// this, opening a stash inherits whatever offset the previous
|
|
17887
|
+
// diff had, landing the user mid-patch.
|
|
17888
|
+
diffPreviewOffset: 0,
|
|
17889
|
+
worktreeDiffOffset: 0,
|
|
17890
|
+
};
|
|
17891
|
+
}
|
|
17409
17892
|
case 'navigateOpenComposeForFile': {
|
|
17410
17893
|
const next = withPushedView(state, 'status');
|
|
17411
17894
|
return {
|
|
@@ -17430,9 +17913,22 @@ function applyLogInkAction(state, action) {
|
|
|
17430
17913
|
return {
|
|
17431
17914
|
...state,
|
|
17432
17915
|
sidebarTab: action.value,
|
|
17916
|
+
userSidebarTab: action.value,
|
|
17433
17917
|
focus: 'sidebar',
|
|
17434
17918
|
pendingKey: undefined,
|
|
17435
17919
|
};
|
|
17920
|
+
case 'restoreSidebarTab':
|
|
17921
|
+
// Mount-time restore from per-repo persistence (#21). Updates the
|
|
17922
|
+
// tab + the user-choice mirror without forcing focus into the
|
|
17923
|
+
// sidebar — that's the focus-steal regression flagged in the PR
|
|
17924
|
+
// review. Users land on commits as usual; their saved tab is
|
|
17925
|
+
// visible in the sidebar but doesn't grab the cursor.
|
|
17926
|
+
return {
|
|
17927
|
+
...state,
|
|
17928
|
+
sidebarTab: action.value,
|
|
17929
|
+
userSidebarTab: action.value,
|
|
17930
|
+
pendingKey: undefined,
|
|
17931
|
+
};
|
|
17436
17932
|
case 'setStatus':
|
|
17437
17933
|
return {
|
|
17438
17934
|
...state,
|
|
@@ -17444,6 +17940,7 @@ function applyLogInkAction(state, action) {
|
|
|
17444
17940
|
...state,
|
|
17445
17941
|
workflowActionId: action.value,
|
|
17446
17942
|
pendingConfirmationId: undefined,
|
|
17943
|
+
pendingConfirmationPayload: undefined,
|
|
17447
17944
|
pendingMutationConfirmation: undefined,
|
|
17448
17945
|
pendingKey: undefined,
|
|
17449
17946
|
};
|
|
@@ -17451,6 +17948,7 @@ function applyLogInkAction(state, action) {
|
|
|
17451
17948
|
return {
|
|
17452
17949
|
...state,
|
|
17453
17950
|
pendingConfirmationId: action.value,
|
|
17951
|
+
pendingConfirmationPayload: action.value ? action.payload : undefined,
|
|
17454
17952
|
workflowActionId: action.value ? undefined : state.workflowActionId,
|
|
17455
17953
|
pendingMutationConfirmation: action.value ? undefined : state.pendingMutationConfirmation,
|
|
17456
17954
|
pendingKey: undefined,
|
|
@@ -17460,6 +17958,7 @@ function applyLogInkAction(state, action) {
|
|
|
17460
17958
|
...state,
|
|
17461
17959
|
pendingMutationConfirmation: action.value,
|
|
17462
17960
|
pendingConfirmationId: action.value ? undefined : state.pendingConfirmationId,
|
|
17961
|
+
pendingConfirmationPayload: action.value ? undefined : state.pendingConfirmationPayload,
|
|
17463
17962
|
workflowActionId: action.value ? undefined : state.workflowActionId,
|
|
17464
17963
|
pendingKey: undefined,
|
|
17465
17964
|
};
|
|
@@ -18281,6 +18780,36 @@ function cherryPickCommit(git, commit) {
|
|
|
18281
18780
|
}
|
|
18282
18781
|
return guardNoInProgressOperation(git).then((blocked) => (blocked || runAction$5(() => git.raw(['cherry-pick', commit.hash]), `Cherry-picked ${commit.shortHash}`)));
|
|
18283
18782
|
}
|
|
18783
|
+
/**
|
|
18784
|
+
* Materialize a single file's contents from a historical commit into the
|
|
18785
|
+
* working tree, leaving every other path untouched. Equivalent to
|
|
18786
|
+
* `git checkout <sha> -- <path>` for additions/modifications. When the
|
|
18787
|
+
* path no longer exists at <sha> (i.e. the commit deleted that file),
|
|
18788
|
+
* mirror the deletion in the worktree via `git rm --force`.
|
|
18789
|
+
*
|
|
18790
|
+
* Important: this overwrites the file in the working tree. The caller
|
|
18791
|
+
* is responsible for confirming with the user when the working tree
|
|
18792
|
+
* already has uncommitted changes to that path.
|
|
18793
|
+
*/
|
|
18794
|
+
async function checkoutFileFromCommit(git, sha, path) {
|
|
18795
|
+
return checkoutOrDeleteFromRef(git, sha, path, sha.slice(0, 7));
|
|
18796
|
+
}
|
|
18797
|
+
async function checkoutOrDeleteFromRef(git, ref, path, label) {
|
|
18798
|
+
const exists = await pathExistsAtRef(git, ref, path);
|
|
18799
|
+
if (exists) {
|
|
18800
|
+
return runAction$5(() => git.raw(['checkout', ref, '--', path]), `Checked out ${path} from ${label}`);
|
|
18801
|
+
}
|
|
18802
|
+
return runAction$5(() => git.raw(['rm', '--force', '--quiet', '--', path]), `Removed ${path} (mirrors deletion from ${label})`);
|
|
18803
|
+
}
|
|
18804
|
+
async function pathExistsAtRef(git, ref, path) {
|
|
18805
|
+
try {
|
|
18806
|
+
await git.raw(['cat-file', '-e', `${ref}:${path}`]);
|
|
18807
|
+
return true;
|
|
18808
|
+
}
|
|
18809
|
+
catch {
|
|
18810
|
+
return false;
|
|
18811
|
+
}
|
|
18812
|
+
}
|
|
18284
18813
|
function revertCommit(git, commit) {
|
|
18285
18814
|
if (!commit) {
|
|
18286
18815
|
return Promise.resolve({
|
|
@@ -18430,6 +18959,20 @@ function popStash(git, stash) {
|
|
|
18430
18959
|
function dropStash(git, stash) {
|
|
18431
18960
|
return runAction$4(() => git.raw(['stash', 'drop', stash.ref]), `Dropped ${stash.ref}`);
|
|
18432
18961
|
}
|
|
18962
|
+
/**
|
|
18963
|
+
* Materialize a single file's contents from a stash into the working
|
|
18964
|
+
* tree, leaving the rest of the stash untouched. Equivalent to
|
|
18965
|
+
* `git checkout <stashRef> -- <path>` for additions/modifications. When
|
|
18966
|
+
* the path doesn't exist at <stashRef> — i.e. the stash recorded a
|
|
18967
|
+
* deletion — mirror that deletion in the worktree.
|
|
18968
|
+
*
|
|
18969
|
+
* Important: this overwrites the file in the working tree. The caller
|
|
18970
|
+
* is responsible for confirming with the user when the working tree
|
|
18971
|
+
* already has uncommitted changes to that path.
|
|
18972
|
+
*/
|
|
18973
|
+
function checkoutFileFromStash(git, stashRef, path) {
|
|
18974
|
+
return checkoutOrDeleteFromRef(git, stashRef, path, stashRef);
|
|
18975
|
+
}
|
|
18433
18976
|
|
|
18434
18977
|
function parseStashSubject(subject) {
|
|
18435
18978
|
const match = subject.match(/^(?:WIP on|On) ([^:]+):\s*(.*)$/);
|
|
@@ -18482,6 +19025,68 @@ async function getStashDiffSummary(git, stashRef) {
|
|
|
18482
19025
|
.map((line) => line.trimEnd())
|
|
18483
19026
|
.filter(Boolean);
|
|
18484
19027
|
}
|
|
19028
|
+
/**
|
|
19029
|
+
* Full unified-patch diff for a stash. Used by the diff surface when
|
|
19030
|
+
* `state.diffSource === 'stash'` to render the stash's changes inline.
|
|
19031
|
+
*
|
|
19032
|
+
* Empty stashes (e.g. created by `git stash --keep-index` against an
|
|
19033
|
+
* already-clean tree) return [] rather than throwing — surfaces fall
|
|
19034
|
+
* back to a "no diff to display" message.
|
|
19035
|
+
*/
|
|
19036
|
+
async function getStashDiff(git, stashRef) {
|
|
19037
|
+
return (await git.raw(['stash', 'show', '-p', stashRef]))
|
|
19038
|
+
.split('\n')
|
|
19039
|
+
.map((line) => line.replace(/\r$/, ''));
|
|
19040
|
+
}
|
|
19041
|
+
/**
|
|
19042
|
+
* Slice a unified-patch into per-file sections. Each entry records the
|
|
19043
|
+
* file path and the offset of its `diff --git` header within `lines`.
|
|
19044
|
+
* Used by the stash explorer to build a per-file cursor + cherry-pick
|
|
19045
|
+
* the file at the cursor.
|
|
19046
|
+
*
|
|
19047
|
+
* Renames / moves return the destination path (the `b/` side); the
|
|
19048
|
+
* action surface treats that as the path to materialize from the stash.
|
|
19049
|
+
*
|
|
19050
|
+
* Path quoting: git wraps paths containing spaces or special characters
|
|
19051
|
+
* in double-quotes (`diff --git "a/path with spaces" "b/path with spaces"`).
|
|
19052
|
+
* The parser handles both the unquoted and quoted forms; without that,
|
|
19053
|
+
* stash-file navigation and cherry-pick silently broke for any file
|
|
19054
|
+
* whose path contained a space.
|
|
19055
|
+
*/
|
|
19056
|
+
function parseStashDiffFiles(lines) {
|
|
19057
|
+
const files = [];
|
|
19058
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
19059
|
+
const line = lines[i];
|
|
19060
|
+
const parsed = parseDiffGitHeader(line);
|
|
19061
|
+
if (parsed) {
|
|
19062
|
+
files.push({ path: parsed.bPath || parsed.aPath, startLine: i });
|
|
19063
|
+
}
|
|
19064
|
+
}
|
|
19065
|
+
return files;
|
|
19066
|
+
}
|
|
19067
|
+
const DIFF_GIT_HEADER = /^diff --git (?:"a\/((?:\\.|[^"\\])+)"|a\/(\S+)) (?:"b\/((?:\\.|[^"\\])+)"|b\/(\S+))$/;
|
|
19068
|
+
function parseDiffGitHeader(line) {
|
|
19069
|
+
const match = line.match(DIFF_GIT_HEADER);
|
|
19070
|
+
if (!match)
|
|
19071
|
+
return undefined;
|
|
19072
|
+
const aPath = unescapeGitQuoted(match[1]) || match[2];
|
|
19073
|
+
const bPath = unescapeGitQuoted(match[3]) || match[4];
|
|
19074
|
+
if (!aPath || !bPath)
|
|
19075
|
+
return undefined;
|
|
19076
|
+
return { aPath, bPath };
|
|
19077
|
+
}
|
|
19078
|
+
function unescapeGitQuoted(value) {
|
|
19079
|
+
if (value === undefined)
|
|
19080
|
+
return undefined;
|
|
19081
|
+
// Git's diff header quoting escapes `"`, `\`, and the usual
|
|
19082
|
+
// C-style sequences. Reverse the most common ones so callers get the
|
|
19083
|
+
// raw on-disk path.
|
|
19084
|
+
return value
|
|
19085
|
+
.replace(/\\\\/g, '\\')
|
|
19086
|
+
.replace(/\\"/g, '"')
|
|
19087
|
+
.replace(/\\t/g, '\t')
|
|
19088
|
+
.replace(/\\n/g, '\n');
|
|
19089
|
+
}
|
|
18485
19090
|
|
|
18486
19091
|
async function runAction$3(action, successMessage) {
|
|
18487
19092
|
try {
|
|
@@ -21086,10 +21691,6 @@ function LogInkApp(deps) {
|
|
|
21086
21691
|
const h = React.createElement;
|
|
21087
21692
|
const { exit } = useApp();
|
|
21088
21693
|
const windowSize = useWindowSize();
|
|
21089
|
-
const layout = getLogInkLayout({
|
|
21090
|
-
columns: windowSize.columns || process.stdout.columns || LOG_INK_DEFAULT_COLUMNS,
|
|
21091
|
-
rows: windowSize.rows || process.stdout.rows || LOG_INK_DEFAULT_ROWS,
|
|
21092
|
-
});
|
|
21093
21694
|
// Bumping this on SIGCONT forces the existing tree to repaint so users
|
|
21094
21695
|
// land on a drawn screen after `fg` instead of an empty alt buffer.
|
|
21095
21696
|
const [, setResumeTick] = React.useState(0);
|
|
@@ -21117,6 +21718,13 @@ function LogInkApp(deps) {
|
|
|
21117
21718
|
const [worktreeDiffLoading, setWorktreeDiffLoading] = React.useState(false);
|
|
21118
21719
|
const [worktreeHunks, setWorktreeHunks] = React.useState(undefined);
|
|
21119
21720
|
const [worktreeHunksLoading, setWorktreeHunksLoading] = React.useState(false);
|
|
21721
|
+
// Stash diff explorer (Enter on a stash row): the runtime fetches
|
|
21722
|
+
// `git stash show -p <ref>` lazily once the diff view becomes active
|
|
21723
|
+
// with diffSource='stash'. Lines are stored as a flat string[] —
|
|
21724
|
+
// renderDiffSurface paints each line through diffLineProps so +/-
|
|
21725
|
+
// colors match the commit-diff path.
|
|
21726
|
+
const [stashDiffLines, setStashDiffLines] = React.useState(undefined);
|
|
21727
|
+
const [stashDiffLoading, setStashDiffLoading] = React.useState(false);
|
|
21120
21728
|
const [hasMoreCommits, setHasMoreCommits] = React.useState(() => (Boolean(logArgv?.interactive && !logArgv.limit) &&
|
|
21121
21729
|
getCommitRows(rows).length >= LOG_INTERACTIVE_DEFAULT_LIMIT));
|
|
21122
21730
|
const [loadingMoreCommits, setLoadingMoreCommits] = React.useState(false);
|
|
@@ -21161,6 +21769,36 @@ function LogInkApp(deps) {
|
|
|
21161
21769
|
const dispatch = React.useCallback((action) => {
|
|
21162
21770
|
setState((current) => applyLogInkAction(current, action));
|
|
21163
21771
|
}, []);
|
|
21772
|
+
// Auto-dismiss status messages after a short window so transient
|
|
21773
|
+
// confirmations ("Pulled current branch", "Edited foo.ts") don't
|
|
21774
|
+
// linger forever. Each new message resets the timer; clearing the
|
|
21775
|
+
// message via setStatus(undefined) cancels it. Doesn't fire while a
|
|
21776
|
+
// modal (input prompt, confirmation, palette) is open — those flows
|
|
21777
|
+
// use the status line as live feedback for the open task.
|
|
21778
|
+
React.useEffect(() => {
|
|
21779
|
+
if (!state.statusMessage)
|
|
21780
|
+
return;
|
|
21781
|
+
if (state.inputPrompt || state.pendingConfirmationId || state.pendingMutationConfirmation || state.showCommandPalette) {
|
|
21782
|
+
return;
|
|
21783
|
+
}
|
|
21784
|
+
// The `setTimeout` callback is a literal arrow function (not a
|
|
21785
|
+
// string), and the delay is a hard-coded constant, so the
|
|
21786
|
+
// eval-injection vector behind DevSkim DS172411 doesn't apply here.
|
|
21787
|
+
// DevSkim: ignore DS172411
|
|
21788
|
+
const handle = setTimeout(() => {
|
|
21789
|
+
if (mountedRef.current) {
|
|
21790
|
+
dispatch({ type: 'setStatus', value: undefined });
|
|
21791
|
+
}
|
|
21792
|
+
}, 4000);
|
|
21793
|
+
return () => clearTimeout(handle);
|
|
21794
|
+
}, [
|
|
21795
|
+
dispatch,
|
|
21796
|
+
state.inputPrompt,
|
|
21797
|
+
state.pendingConfirmationId,
|
|
21798
|
+
state.pendingMutationConfirmation,
|
|
21799
|
+
state.showCommandPalette,
|
|
21800
|
+
state.statusMessage,
|
|
21801
|
+
]);
|
|
21164
21802
|
const refreshContext = React.useCallback(async (options = {}) => {
|
|
21165
21803
|
// Loud refresh (manual `r`): flip everything to 'loading' so the user
|
|
21166
21804
|
// sees the surfaces clear, then settle to 'ready' on completion.
|
|
@@ -21240,6 +21878,61 @@ function LogInkApp(deps) {
|
|
|
21240
21878
|
watcher?.close();
|
|
21241
21879
|
};
|
|
21242
21880
|
}, [git, refreshContext, refreshWorktreeContext]);
|
|
21881
|
+
// Per-repo sidebar tab persistence (#21). Resolve the repo root, look
|
|
21882
|
+
// up the cached tab, and dispatch `restoreSidebarTab` once on mount so
|
|
21883
|
+
// the user lands on whichever tab they were last on for this project.
|
|
21884
|
+
// `restoreSidebarTab` (vs `setSidebarTab`) intentionally does not pull
|
|
21885
|
+
// focus into the sidebar — the user lands on commits, the saved tab
|
|
21886
|
+
// is just visible in the gutter.
|
|
21887
|
+
//
|
|
21888
|
+
// The save effect listens to `userSidebarTab` (the user's explicit
|
|
21889
|
+
// choice mirror), not `sidebarTab`. That way the auto-switch to
|
|
21890
|
+
// Branches when entering compose / status doesn't overwrite the saved
|
|
21891
|
+
// preference.
|
|
21892
|
+
const repoRootRef = React.useRef(undefined);
|
|
21893
|
+
React.useEffect(() => {
|
|
21894
|
+
let cancelled = false;
|
|
21895
|
+
void (async () => {
|
|
21896
|
+
try {
|
|
21897
|
+
const repoRoot = (await git.revparse(['--show-toplevel'])).trim();
|
|
21898
|
+
if (cancelled || !repoRoot)
|
|
21899
|
+
return;
|
|
21900
|
+
repoRootRef.current = repoRoot;
|
|
21901
|
+
const saved = getSavedSidebarTab(repoRoot);
|
|
21902
|
+
if (saved && saved !== state.userSidebarTab) {
|
|
21903
|
+
dispatch({ type: 'restoreSidebarTab', value: saved });
|
|
21904
|
+
}
|
|
21905
|
+
}
|
|
21906
|
+
catch {
|
|
21907
|
+
// Not in a worktree, or revparse failed; nothing to restore.
|
|
21908
|
+
}
|
|
21909
|
+
})();
|
|
21910
|
+
return () => { cancelled = true; };
|
|
21911
|
+
}, [git, dispatch]);
|
|
21912
|
+
React.useEffect(() => {
|
|
21913
|
+
const repoRoot = repoRootRef.current;
|
|
21914
|
+
if (!repoRoot)
|
|
21915
|
+
return;
|
|
21916
|
+
saveSidebarTab(repoRoot, state.userSidebarTab);
|
|
21917
|
+
}, [state.userSidebarTab]);
|
|
21918
|
+
// P-stash-explorer: load `git stash show -p <ref>` once the diff view
|
|
21919
|
+
// becomes active with diffSource='stash'. Best-effort — empty stashes
|
|
21920
|
+
// or read errors fall through to a "no diff" hint at the render site.
|
|
21921
|
+
React.useEffect(() => {
|
|
21922
|
+
if (state.activeView !== 'diff' || state.diffSource !== 'stash' || !state.stashDiffRef) {
|
|
21923
|
+
return;
|
|
21924
|
+
}
|
|
21925
|
+
let active = true;
|
|
21926
|
+
setStashDiffLoading(true);
|
|
21927
|
+
void (async () => {
|
|
21928
|
+
const lines = await safe(getStashDiff(git, state.stashDiffRef));
|
|
21929
|
+
if (active) {
|
|
21930
|
+
setStashDiffLines(lines || []);
|
|
21931
|
+
setStashDiffLoading(false);
|
|
21932
|
+
}
|
|
21933
|
+
})();
|
|
21934
|
+
return () => { active = false; };
|
|
21935
|
+
}, [git, state.activeView, state.diffSource, state.stashDiffRef]);
|
|
21243
21936
|
React.useEffect(() => {
|
|
21244
21937
|
let active = true;
|
|
21245
21938
|
async function loadWorktreeHunks() {
|
|
@@ -21450,13 +22143,96 @@ function LogInkApp(deps) {
|
|
|
21450
22143
|
});
|
|
21451
22144
|
dispatch({ type: 'setStatus', value: result.message });
|
|
21452
22145
|
}, [dispatch]);
|
|
22146
|
+
// Open a file in $EDITOR (or $VISUAL) by suspending Ink's hold on the
|
|
22147
|
+
// terminal, spawning the editor synchronously inheriting stdio, then
|
|
22148
|
+
// restoring the alt screen + raw mode and forcing a re-render. The
|
|
22149
|
+
// dance mirrors the SIGTSTP / SIGCONT path in inkTerminalLifecycle.
|
|
22150
|
+
// Falls back to vi when neither env var is set; surfaces a status
|
|
22151
|
+
// message on missing-binary / non-zero exit so the user isn't left
|
|
22152
|
+
// wondering.
|
|
22153
|
+
const openInEditor = React.useCallback((path) => {
|
|
22154
|
+
if (!path)
|
|
22155
|
+
return;
|
|
22156
|
+
const editorEnv = process.env.VISUAL || process.env.EDITOR || 'vi';
|
|
22157
|
+
// $VISUAL / $EDITOR commonly include flags (`code -w`, `vim -f`,
|
|
22158
|
+
// `emacs -nw`). Tokenize on whitespace so the leading word is the
|
|
22159
|
+
// executable and the rest are passed as arguments — passing the
|
|
22160
|
+
// full string to spawnSync as the executable would fail with
|
|
22161
|
+
// ENOENT for any of those configurations.
|
|
22162
|
+
const editorArgs = editorEnv.trim().split(/\s+/).filter(Boolean);
|
|
22163
|
+
const editor = editorArgs[0] || 'vi';
|
|
22164
|
+
const editorPrefixArgs = editorArgs.slice(1);
|
|
22165
|
+
const out = process.stdout;
|
|
22166
|
+
const stdin = process.stdin;
|
|
22167
|
+
const ENTER_ALT = '\x1b[?1049h';
|
|
22168
|
+
const EXIT_ALT = '\x1b[?1049l';
|
|
22169
|
+
const SHOW_CURSOR = '\x1b[?25h';
|
|
22170
|
+
const HIDE_CURSOR = '\x1b[?25l';
|
|
22171
|
+
try {
|
|
22172
|
+
// Drop into the primary buffer + cooked mode so the editor
|
|
22173
|
+
// doesn't inherit our raw-mode keystrokes.
|
|
22174
|
+
stdin.setRawMode?.(false);
|
|
22175
|
+
out.write(`${SHOW_CURSOR}${EXIT_ALT}`);
|
|
22176
|
+
const result = node_child_process.spawnSync(editor, [...editorPrefixArgs, path], { stdio: 'inherit' });
|
|
22177
|
+
if (result.error) {
|
|
22178
|
+
dispatch({ type: 'setStatus', value: `Failed to launch ${editor}: ${result.error.message}` });
|
|
22179
|
+
}
|
|
22180
|
+
else if (result.signal) {
|
|
22181
|
+
// Editor was killed by a signal (e.g. ^C, SIGTERM). status is
|
|
22182
|
+
// null in this case, so the old `status !== 0` check would
|
|
22183
|
+
// mistakenly fall through to the success branch.
|
|
22184
|
+
dispatch({ type: 'setStatus', value: `${editor} interrupted by ${result.signal}` });
|
|
22185
|
+
}
|
|
22186
|
+
else if (typeof result.status === 'number' && result.status !== 0) {
|
|
22187
|
+
dispatch({ type: 'setStatus', value: `${editor} exited with status ${result.status}` });
|
|
22188
|
+
}
|
|
22189
|
+
else {
|
|
22190
|
+
dispatch({ type: 'setStatus', value: `Edited ${path}` });
|
|
22191
|
+
}
|
|
22192
|
+
}
|
|
22193
|
+
finally {
|
|
22194
|
+
// Re-enter the alt screen + raw mode + hidden cursor; nudge React
|
|
22195
|
+
// so the freshly-restored screen actually paints.
|
|
22196
|
+
out.write(`${ENTER_ALT}${HIDE_CURSOR}`);
|
|
22197
|
+
stdin.setRawMode?.(true);
|
|
22198
|
+
resumeRef?.current?.();
|
|
22199
|
+
}
|
|
22200
|
+
// Worktree status may have changed (e.g. user saved an edit) — silent
|
|
22201
|
+
// refresh so the file row reflects the new staged/unstaged state.
|
|
22202
|
+
void refreshWorktreeContext({ silent: true });
|
|
22203
|
+
}, [dispatch, refreshWorktreeContext, resumeRef]);
|
|
21453
22204
|
// Resolve the destructive-action target from the live filtered+sorted
|
|
21454
22205
|
// list the user is looking at, run the action against it, surface the
|
|
21455
22206
|
// result on the status line, and silently refresh so the deleted item
|
|
21456
22207
|
// disappears. Called from the y-confirm path for delete-branch / delete-
|
|
21457
22208
|
// tag / drop-stash / remove-worktree / abort-operation.
|
|
21458
|
-
const runWorkflowAction = React.useCallback(async (id) => {
|
|
22209
|
+
const runWorkflowAction = React.useCallback(async (id, payload) => {
|
|
21459
22210
|
const handlers = {
|
|
22211
|
+
'create-branch': async () => {
|
|
22212
|
+
const name = payload?.trim();
|
|
22213
|
+
if (!name)
|
|
22214
|
+
return { ok: false, message: 'Branch name required' };
|
|
22215
|
+
const startPoint = context.branches?.currentBranch || 'HEAD';
|
|
22216
|
+
return createBranch(git, name, startPoint);
|
|
22217
|
+
},
|
|
22218
|
+
'create-tag': async () => {
|
|
22219
|
+
const name = payload?.trim();
|
|
22220
|
+
if (!name)
|
|
22221
|
+
return { ok: false, message: 'Tag name required' };
|
|
22222
|
+
return createLightweightTag(git, name, 'HEAD');
|
|
22223
|
+
},
|
|
22224
|
+
'checkout-branch': async () => {
|
|
22225
|
+
const all = sortBranches(context.branches?.localBranches || [], state.branchSort);
|
|
22226
|
+
const visible = state.filter
|
|
22227
|
+
? all.filter((b) => matchesPromotedFilter([b.shortName, b.upstream || ''], state.filter))
|
|
22228
|
+
: all;
|
|
22229
|
+
const branch = visible[Math.min(state.selectedBranchIndex, visible.length - 1)];
|
|
22230
|
+
if (!branch)
|
|
22231
|
+
return { ok: false, message: 'No branch selected' };
|
|
22232
|
+
if (branch.current)
|
|
22233
|
+
return { ok: true, message: `Already on ${branch.shortName}` };
|
|
22234
|
+
return checkoutBranch(git, branch);
|
|
22235
|
+
},
|
|
21460
22236
|
'delete-branch': async () => {
|
|
21461
22237
|
const all = sortBranches(context.branches?.localBranches || [], state.branchSort);
|
|
21462
22238
|
const visible = state.filter
|
|
@@ -21477,6 +22253,16 @@ function LogInkApp(deps) {
|
|
|
21477
22253
|
return { ok: false, message: 'No tag selected' };
|
|
21478
22254
|
return deleteLocalTag(git, tag.name);
|
|
21479
22255
|
},
|
|
22256
|
+
'push-tag': async () => {
|
|
22257
|
+
const all = sortTags(context.tags?.tags || [], state.tagSort);
|
|
22258
|
+
const visible = state.filter
|
|
22259
|
+
? all.filter((t) => matchesPromotedFilter([t.name, t.subject], state.filter))
|
|
22260
|
+
: all;
|
|
22261
|
+
const tag = visible[Math.min(state.selectedTagIndex, visible.length - 1)];
|
|
22262
|
+
if (!tag)
|
|
22263
|
+
return { ok: false, message: 'No tag selected' };
|
|
22264
|
+
return pushTag(git, tag.name);
|
|
22265
|
+
},
|
|
21480
22266
|
'drop-stash': async () => {
|
|
21481
22267
|
const all = context.stashes?.stashes || [];
|
|
21482
22268
|
const visible = state.filter
|
|
@@ -21487,14 +22273,82 @@ function LogInkApp(deps) {
|
|
|
21487
22273
|
return { ok: false, message: 'No stash selected' };
|
|
21488
22274
|
return dropStash(git, stash);
|
|
21489
22275
|
},
|
|
22276
|
+
'apply-stash': async () => {
|
|
22277
|
+
const all = context.stashes?.stashes || [];
|
|
22278
|
+
const visible = state.filter
|
|
22279
|
+
? all.filter((s) => matchesPromotedFilter([s.ref, s.message], state.filter))
|
|
22280
|
+
: all;
|
|
22281
|
+
const stash = visible[Math.min(state.selectedStashIndex, visible.length - 1)];
|
|
22282
|
+
if (!stash)
|
|
22283
|
+
return { ok: false, message: 'No stash selected' };
|
|
22284
|
+
return applyStash(git, stash);
|
|
22285
|
+
},
|
|
22286
|
+
'pop-stash': async () => {
|
|
22287
|
+
const all = context.stashes?.stashes || [];
|
|
22288
|
+
const visible = state.filter
|
|
22289
|
+
? all.filter((s) => matchesPromotedFilter([s.ref, s.message], state.filter))
|
|
22290
|
+
: all;
|
|
22291
|
+
const stash = visible[Math.min(state.selectedStashIndex, visible.length - 1)];
|
|
22292
|
+
if (!stash)
|
|
22293
|
+
return { ok: false, message: 'No stash selected' };
|
|
22294
|
+
return popStash(git, stash);
|
|
22295
|
+
},
|
|
22296
|
+
'checkout-file-from-stash': async () => {
|
|
22297
|
+
const path = payload?.trim();
|
|
22298
|
+
const ref = state.stashDiffRef;
|
|
22299
|
+
if (!path)
|
|
22300
|
+
return { ok: false, message: 'No stash file under cursor' };
|
|
22301
|
+
if (!ref)
|
|
22302
|
+
return { ok: false, message: 'No stash ref active' };
|
|
22303
|
+
return checkoutFileFromStash(git, ref, path);
|
|
22304
|
+
},
|
|
22305
|
+
'cherry-pick-commit': async () => {
|
|
22306
|
+
const commit = getSelectedInkCommit(state);
|
|
22307
|
+
if (!commit)
|
|
22308
|
+
return { ok: false, message: 'No commit selected' };
|
|
22309
|
+
return cherryPickCommit(git, {
|
|
22310
|
+
hash: commit.hash,
|
|
22311
|
+
shortHash: commit.shortHash,
|
|
22312
|
+
message: commit.message,
|
|
22313
|
+
});
|
|
22314
|
+
},
|
|
22315
|
+
'checkout-file-from-commit': async () => {
|
|
22316
|
+
// payload is "<sha> <path>" so we pass both through a single
|
|
22317
|
+
// string field on the action.
|
|
22318
|
+
const trimmed = payload?.trim();
|
|
22319
|
+
if (!trimmed)
|
|
22320
|
+
return { ok: false, message: 'No commit file under cursor' };
|
|
22321
|
+
const spaceIndex = trimmed.indexOf(' ');
|
|
22322
|
+
if (spaceIndex < 0)
|
|
22323
|
+
return { ok: false, message: 'Malformed commit file payload' };
|
|
22324
|
+
const sha = trimmed.slice(0, spaceIndex);
|
|
22325
|
+
const path = trimmed.slice(spaceIndex + 1);
|
|
22326
|
+
if (!sha || !path)
|
|
22327
|
+
return { ok: false, message: 'No commit file under cursor' };
|
|
22328
|
+
return checkoutFileFromCommit(git, sha, path);
|
|
22329
|
+
},
|
|
21490
22330
|
'remove-worktree': async () => {
|
|
21491
22331
|
const all = context.worktreeList?.worktrees || [];
|
|
21492
|
-
//
|
|
21493
|
-
//
|
|
21494
|
-
|
|
21495
|
-
|
|
21496
|
-
|
|
21497
|
-
|
|
22332
|
+
// Resolve the target from the visible (filtered) list so a
|
|
22333
|
+
// hidden filtered-out worktree can never be the action target.
|
|
22334
|
+
// Falls back to the cursor against the unfiltered list when the
|
|
22335
|
+
// action is invoked from the palette without ever visiting the
|
|
22336
|
+
// worktrees view.
|
|
22337
|
+
const visible = state.filter
|
|
22338
|
+
? all.filter((w) => matchesPromotedFilter([w.path, w.branch || ''], state.filter))
|
|
22339
|
+
: all;
|
|
22340
|
+
const cursorTarget = visible.length
|
|
22341
|
+
? visible[Math.min(state.selectedWorktreeListIndex, visible.length - 1)]
|
|
22342
|
+
: all[Math.min(state.selectedWorktreeListIndex, Math.max(0, all.length - 1))];
|
|
22343
|
+
if (!cursorTarget)
|
|
22344
|
+
return { ok: false, message: 'No worktree selected' };
|
|
22345
|
+
if (cursorTarget.current) {
|
|
22346
|
+
return {
|
|
22347
|
+
ok: false,
|
|
22348
|
+
message: 'Cannot remove the current worktree — switch to another worktree first.',
|
|
22349
|
+
};
|
|
22350
|
+
}
|
|
22351
|
+
return removeWorktree(git, cursorTarget);
|
|
21498
22352
|
},
|
|
21499
22353
|
'abort-operation': async () => {
|
|
21500
22354
|
const operation = context.operation?.operation;
|
|
@@ -21503,6 +22357,64 @@ function LogInkApp(deps) {
|
|
|
21503
22357
|
}
|
|
21504
22358
|
return abortOperation(git, operation);
|
|
21505
22359
|
},
|
|
22360
|
+
'open-pr': async () => {
|
|
22361
|
+
const repo = context.provider?.repository;
|
|
22362
|
+
if (!repo || repo.provider !== 'github' || !repo.owner || !repo.name) {
|
|
22363
|
+
return { ok: false, message: 'No GitHub remote detected for this repo' };
|
|
22364
|
+
}
|
|
22365
|
+
const pr = context.provider?.currentPullRequest || context.pullRequest?.currentPullRequest;
|
|
22366
|
+
if (pr) {
|
|
22367
|
+
return openProviderUrl(repo, { type: 'pull-request', number: pr.number });
|
|
22368
|
+
}
|
|
22369
|
+
// No PR — fall back to opening the repo page so the user can
|
|
22370
|
+
// create one or browse from there.
|
|
22371
|
+
return openProviderUrl(repo, { type: 'repo' });
|
|
22372
|
+
},
|
|
22373
|
+
'fetch-remotes': async () => fetchRemotes(git),
|
|
22374
|
+
'pull-current-branch': async () => pullCurrentBranch(git),
|
|
22375
|
+
'push-current-branch': async () => pushCurrentBranch(git),
|
|
22376
|
+
'rename-branch': async () => {
|
|
22377
|
+
const newName = payload?.trim();
|
|
22378
|
+
if (!newName)
|
|
22379
|
+
return { ok: false, message: 'New branch name required' };
|
|
22380
|
+
const all = sortBranches(context.branches?.localBranches || [], state.branchSort);
|
|
22381
|
+
const visible = state.filter
|
|
22382
|
+
? all.filter((b) => matchesPromotedFilter([b.shortName, b.upstream || ''], state.filter))
|
|
22383
|
+
: all;
|
|
22384
|
+
const branch = visible[Math.min(state.selectedBranchIndex, visible.length - 1)];
|
|
22385
|
+
if (!branch)
|
|
22386
|
+
return { ok: false, message: 'No branch selected' };
|
|
22387
|
+
return renameBranch(git, branch.shortName, newName);
|
|
22388
|
+
},
|
|
22389
|
+
'set-upstream': async () => {
|
|
22390
|
+
const upstream = payload?.trim();
|
|
22391
|
+
if (!upstream)
|
|
22392
|
+
return { ok: false, message: 'Upstream ref required' };
|
|
22393
|
+
const all = sortBranches(context.branches?.localBranches || [], state.branchSort);
|
|
22394
|
+
const visible = state.filter
|
|
22395
|
+
? all.filter((b) => matchesPromotedFilter([b.shortName, b.upstream || ''], state.filter))
|
|
22396
|
+
: all;
|
|
22397
|
+
const branch = visible[Math.min(state.selectedBranchIndex, visible.length - 1)];
|
|
22398
|
+
if (!branch)
|
|
22399
|
+
return { ok: false, message: 'No branch selected' };
|
|
22400
|
+
return setUpstream(git, branch.shortName, upstream);
|
|
22401
|
+
},
|
|
22402
|
+
'delete-remote-tag': async () => {
|
|
22403
|
+
const all = sortTags(context.tags?.tags || [], state.tagSort);
|
|
22404
|
+
const visible = state.filter
|
|
22405
|
+
? all.filter((t) => matchesPromotedFilter([t.name, t.subject], state.filter))
|
|
22406
|
+
: all;
|
|
22407
|
+
const tag = visible[Math.min(state.selectedTagIndex, visible.length - 1)];
|
|
22408
|
+
if (!tag)
|
|
22409
|
+
return { ok: false, message: 'No tag selected' };
|
|
22410
|
+
return deleteRemoteTag(git, tag.name);
|
|
22411
|
+
},
|
|
22412
|
+
'create-stash': async () => {
|
|
22413
|
+
const message = payload?.trim();
|
|
22414
|
+
if (!message)
|
|
22415
|
+
return { ok: false, message: 'Stash message required' };
|
|
22416
|
+
return createStash(git, message);
|
|
22417
|
+
},
|
|
21506
22418
|
};
|
|
21507
22419
|
const handler = handlers[id];
|
|
21508
22420
|
if (!handler) {
|
|
@@ -21515,7 +22427,8 @@ function LogInkApp(deps) {
|
|
|
21515
22427
|
// flickering the surfaces through a 'loading' phase.
|
|
21516
22428
|
await refreshContext({ silent: true });
|
|
21517
22429
|
}, [context, dispatch, git, refreshContext, state.branchSort, state.filter, state.selectedBranchIndex,
|
|
21518
|
-
state.selectedStashIndex, state.selectedTagIndex, state.
|
|
22430
|
+
state.selectedStashIndex, state.selectedTagIndex, state.selectedWorktreeListIndex, state.stashDiffRef,
|
|
22431
|
+
state.tagSort]);
|
|
21519
22432
|
React.useEffect(() => {
|
|
21520
22433
|
let active = true;
|
|
21521
22434
|
async function loadPreview() {
|
|
@@ -21621,14 +22534,53 @@ function LogInkApp(deps) {
|
|
|
21621
22534
|
.filter((tag) => matchesPromotedFilter([tag.name, tag.subject], state.filter))
|
|
21622
22535
|
.length
|
|
21623
22536
|
: context.tags?.tags.length;
|
|
21624
|
-
const
|
|
22537
|
+
const visibleStashes = state.filter
|
|
21625
22538
|
? (context.stashes?.stashes || [])
|
|
21626
22539
|
.filter((stash) => matchesPromotedFilter([stash.ref, stash.message], state.filter))
|
|
22540
|
+
: (context.stashes?.stashes || []);
|
|
22541
|
+
const stashVisibleCount = visibleStashes.length;
|
|
22542
|
+
const stashSelectedRef = visibleStashes[Math.min(state.selectedStashIndex, Math.max(0, visibleStashes.length - 1))]?.ref;
|
|
22543
|
+
// The worktrees promoted view is filterable; mirror the branches /
|
|
22544
|
+
// tags / stash pattern and feed the filtered count into the input
|
|
22545
|
+
// dispatcher so ↑/↓ stay synchronized with the visible rows.
|
|
22546
|
+
const worktreeVisibleCount = state.filter
|
|
22547
|
+
? (context.worktreeList?.worktrees || [])
|
|
22548
|
+
.filter((w) => matchesPromotedFilter([w.path, w.branch || ''], state.filter))
|
|
21627
22549
|
.length
|
|
21628
|
-
: context.
|
|
22550
|
+
: context.worktreeList?.worktrees.length;
|
|
22551
|
+
// When the diff view is showing a stash patch, swap the previewLineCount
|
|
22552
|
+
// to the stash diff length so the existing pageDetailPreview path
|
|
22553
|
+
// (j/k, PgUp/PgDn) scrolls through it without a parallel pipeline.
|
|
22554
|
+
const diffPreviewLineCount = state.diffSource === 'stash'
|
|
22555
|
+
? stashDiffLines?.length
|
|
22556
|
+
: filePreview?.hunks.length;
|
|
22557
|
+
// Parse the active stash diff into per-file sections so `]`/`[` can
|
|
22558
|
+
// jump between files and `c` knows which path the cursor is on for
|
|
22559
|
+
// a file-level cherry-pick.
|
|
22560
|
+
const stashDiffFiles = state.diffSource === 'stash' && stashDiffLines
|
|
22561
|
+
? parseStashDiffFiles(stashDiffLines)
|
|
22562
|
+
: [];
|
|
22563
|
+
const stashDiffFileOffsets = stashDiffFiles.map((file) => file.startLine);
|
|
22564
|
+
const stashDiffSelectedPath = (() => {
|
|
22565
|
+
if (state.diffSource !== 'stash' || stashDiffFiles.length === 0)
|
|
22566
|
+
return undefined;
|
|
22567
|
+
const offset = state.diffPreviewOffset;
|
|
22568
|
+
// Walk backwards to the most recent file header at or before the
|
|
22569
|
+
// current cursor offset.
|
|
22570
|
+
let current = stashDiffFiles[0];
|
|
22571
|
+
for (const file of stashDiffFiles) {
|
|
22572
|
+
if (file.startLine <= offset) {
|
|
22573
|
+
current = file;
|
|
22574
|
+
}
|
|
22575
|
+
else {
|
|
22576
|
+
break;
|
|
22577
|
+
}
|
|
22578
|
+
}
|
|
22579
|
+
return current.path;
|
|
22580
|
+
})();
|
|
21629
22581
|
getLogInkInputEvents(state, inputValue, key, {
|
|
21630
22582
|
detailFileCount: detail?.files.length,
|
|
21631
|
-
previewLineCount:
|
|
22583
|
+
previewLineCount: diffPreviewLineCount,
|
|
21632
22584
|
worktreeDiffLineCount: worktreeDiff?.lines.length,
|
|
21633
22585
|
worktreeFileCount: context.worktree?.files.length,
|
|
21634
22586
|
worktreeHunkOffsets: worktreeDiff?.hunkOffsets,
|
|
@@ -21636,6 +22588,17 @@ function LogInkApp(deps) {
|
|
|
21636
22588
|
branchCount: branchVisibleCount,
|
|
21637
22589
|
tagCount: tagVisibleCount,
|
|
21638
22590
|
stashCount: stashVisibleCount,
|
|
22591
|
+
stashSelectedRef,
|
|
22592
|
+
stashDiffFileOffsets: stashDiffFileOffsets.length ? stashDiffFileOffsets : undefined,
|
|
22593
|
+
stashDiffSelectedPath,
|
|
22594
|
+
worktreeListCount: worktreeVisibleCount,
|
|
22595
|
+
worktreeSelectedPath: context.worktree?.files[state.selectedWorktreeFileIndex]?.path,
|
|
22596
|
+
commitDiffSelectedPath: state.diffSource === 'commit'
|
|
22597
|
+
? selectedDetailFile?.path
|
|
22598
|
+
: undefined,
|
|
22599
|
+
commitDiffSelectedSha: state.diffSource === 'commit'
|
|
22600
|
+
? selected?.hash
|
|
22601
|
+
: undefined,
|
|
21639
22602
|
worktreeDirty,
|
|
21640
22603
|
}).forEach((event) => {
|
|
21641
22604
|
if (event.type === 'exit') {
|
|
@@ -21663,7 +22626,10 @@ function LogInkApp(deps) {
|
|
|
21663
22626
|
void runAiCommitDraft();
|
|
21664
22627
|
}
|
|
21665
22628
|
else if (event.type === 'runWorkflowAction') {
|
|
21666
|
-
void runWorkflowAction(event.id);
|
|
22629
|
+
void runWorkflowAction(event.id, event.payload);
|
|
22630
|
+
}
|
|
22631
|
+
else if (event.type === 'openFileInEditor') {
|
|
22632
|
+
openInEditor(event.path);
|
|
21667
22633
|
}
|
|
21668
22634
|
else {
|
|
21669
22635
|
// P4.5: enrich filter-mutating actions with a precomputed
|
|
@@ -21677,6 +22643,13 @@ function LogInkApp(deps) {
|
|
|
21677
22643
|
}
|
|
21678
22644
|
});
|
|
21679
22645
|
});
|
|
22646
|
+
// Layout depends on focus (sidebar grows when focused), so it's
|
|
22647
|
+
// computed here — after state is in scope but before the render path.
|
|
22648
|
+
const layout = getLogInkLayout({
|
|
22649
|
+
columns: windowSize.columns || process.stdout.columns || LOG_INK_DEFAULT_COLUMNS,
|
|
22650
|
+
rows: windowSize.rows || process.stdout.rows || LOG_INK_DEFAULT_ROWS,
|
|
22651
|
+
sidebarFocused: state.focus === 'sidebar',
|
|
22652
|
+
});
|
|
21680
22653
|
if (layout.tooSmall) {
|
|
21681
22654
|
return h(Box, {
|
|
21682
22655
|
flexDirection: 'column',
|
|
@@ -21690,7 +22663,7 @@ function LogInkApp(deps) {
|
|
|
21690
22663
|
if (showOnboarding) {
|
|
21691
22664
|
return renderOnboardingOverlay(h, { Box, Text }, layout.rows, layout.columns, theme, appLabel);
|
|
21692
22665
|
}
|
|
21693
|
-
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, 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, theme, idleTip));
|
|
22666
|
+
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, theme, idleTip));
|
|
21694
22667
|
}
|
|
21695
22668
|
function renderHeader(h, components, state, context, contextStatus, columns, theme, appLabel) {
|
|
21696
22669
|
const { Box, Text } = components;
|
|
@@ -21746,28 +22719,96 @@ function renderHeader(h, components, state, context, contextStatus, columns, the
|
|
|
21746
22719
|
function renderSidebar(h, components, state, context, contextStatus, width, theme) {
|
|
21747
22720
|
const { Box, Text } = components;
|
|
21748
22721
|
const focused = state.focus === 'sidebar';
|
|
21749
|
-
const lines = sidebarLines(context, contextStatus, state.sidebarTab, width - 4, state, theme);
|
|
21750
22722
|
const tabs = getLogInkSidebarTabs();
|
|
22723
|
+
// Accordion layout — every tab's title is visible on its own line, but
|
|
22724
|
+
// only the active tab expands its content underneath. Switching tabs
|
|
22725
|
+
// (1-5 / [/]) collapses the previous and expands the next.
|
|
22726
|
+
const tabBlocks = tabs.flatMap((tab, tabIndex) => {
|
|
22727
|
+
const isActive = tab === state.sidebarTab;
|
|
22728
|
+
const count = sidebarTabCount(tab, context);
|
|
22729
|
+
const labelWithCount = count !== undefined
|
|
22730
|
+
? `${sidebarTabLabel(tab)} (${count})`
|
|
22731
|
+
: sidebarTabLabel(tab);
|
|
22732
|
+
const headerText = isActive ? `[${labelWithCount}]` : labelWithCount;
|
|
22733
|
+
const blocks = [];
|
|
22734
|
+
if (tabIndex > 0) {
|
|
22735
|
+
blocks.push(h(Text, { key: `tab-spacer-${tab}` }, ''));
|
|
22736
|
+
}
|
|
22737
|
+
blocks.push(h(Text, {
|
|
22738
|
+
key: `tab-header-${tab}`,
|
|
22739
|
+
bold: isActive,
|
|
22740
|
+
dimColor: !isActive,
|
|
22741
|
+
}, headerText));
|
|
22742
|
+
if (isActive) {
|
|
22743
|
+
blocks.push(...renderActiveSidebarContent(h, Text, tab, state, context, contextStatus, width, theme));
|
|
22744
|
+
}
|
|
22745
|
+
return blocks;
|
|
22746
|
+
});
|
|
21751
22747
|
return h(Box, {
|
|
21752
22748
|
borderColor: focusBorderColor(theme, focused),
|
|
21753
22749
|
borderStyle: theme.borderStyle,
|
|
21754
22750
|
flexDirection: 'column',
|
|
21755
22751
|
width,
|
|
21756
22752
|
paddingX: 1,
|
|
21757
|
-
}, h(Text, { bold: true }, panelTitle('Repository', focused)), h(Text,
|
|
21758
|
-
|
|
21759
|
-
|
|
21760
|
-
|
|
21761
|
-
|
|
21762
|
-
|
|
21763
|
-
|
|
22753
|
+
}, h(Text, { bold: true }, panelTitle('Repository', focused)), h(Text, undefined, ''), ...tabBlocks);
|
|
22754
|
+
}
|
|
22755
|
+
/**
|
|
22756
|
+
* Render the indented body of the active sidebar tab. The status tab
|
|
22757
|
+
* colours its summary counts (warning / danger / muted) and per-file
|
|
22758
|
+
* rows so they read as the same severity scale used in the main status
|
|
22759
|
+
* surface; every other tab falls through to `sidebarLines` for its
|
|
22760
|
+
* string-based summary.
|
|
22761
|
+
*/
|
|
22762
|
+
function renderActiveSidebarContent(h, Text, tab, state, context, contextStatus, width, theme) {
|
|
22763
|
+
if (tab === 'status') {
|
|
22764
|
+
return renderActiveStatusTabContent(h, Text, context, contextStatus, width, theme);
|
|
22765
|
+
}
|
|
22766
|
+
const lines = sidebarLines(context, contextStatus, tab, width - 6, state, theme);
|
|
22767
|
+
return lines.map((line, index) => h(Text, {
|
|
22768
|
+
key: `tab-content-${tab}-${index}`,
|
|
22769
|
+
dimColor: !line.trim(),
|
|
22770
|
+
}, truncate$1(` ${line}`, width - 4)));
|
|
22771
|
+
}
|
|
22772
|
+
function renderActiveStatusTabContent(h, Text, context, contextStatus, width, theme) {
|
|
22773
|
+
if (isLogInkContextKeyLoading(contextStatus, 'worktree')) {
|
|
22774
|
+
return [h(Text, { key: 'tab-status-loading', dimColor: true }, ' Loading status…')];
|
|
22775
|
+
}
|
|
22776
|
+
const worktree = context.worktree;
|
|
22777
|
+
if (!worktree) {
|
|
22778
|
+
return [h(Text, { key: 'tab-status-empty', dimColor: true }, ' Status unavailable')];
|
|
22779
|
+
}
|
|
22780
|
+
const colorOf = (state) => {
|
|
22781
|
+
if (theme.noColor)
|
|
22782
|
+
return undefined;
|
|
22783
|
+
if (state === 'staged')
|
|
22784
|
+
return theme.colors.warning;
|
|
22785
|
+
if (state === 'unstaged')
|
|
22786
|
+
return theme.colors.danger;
|
|
22787
|
+
return theme.colors.muted;
|
|
22788
|
+
};
|
|
22789
|
+
const summaryRow = (count, label, key, kind) => h(Text, { key }, ' ', h(Text, { color: colorOf(kind), bold: count > 0 }, `${count} ${label}`));
|
|
22790
|
+
const fileRows = worktree.files.slice(0, 12).map((file, index) => {
|
|
22791
|
+
const codes = `${file.indexStatus}${file.worktreeStatus}`;
|
|
22792
|
+
return h(Text, {
|
|
22793
|
+
key: `tab-status-file-${index}`,
|
|
22794
|
+
color: colorOf(file.state),
|
|
22795
|
+
}, truncate$1(` ${codes} ${file.path}`, width - 4));
|
|
22796
|
+
});
|
|
22797
|
+
return [
|
|
22798
|
+
summaryRow(worktree.stagedCount, 'staged', 'tab-status-staged', 'staged'),
|
|
22799
|
+
summaryRow(worktree.unstagedCount, 'unstaged', 'tab-status-unstaged', 'unstaged'),
|
|
22800
|
+
summaryRow(worktree.untrackedCount, 'untracked', 'tab-status-untracked', 'untracked'),
|
|
22801
|
+
...(fileRows.length
|
|
22802
|
+
? [h(Text, { key: 'tab-status-spacer' }, ''), ...fileRows]
|
|
22803
|
+
: []),
|
|
22804
|
+
];
|
|
21764
22805
|
}
|
|
21765
|
-
function renderMainPanel(h, components, state, context, contextStatus, worktreeDiff, worktreeDiffLoading, worktreeHunks, worktreeHunksLoading, filePreview, filePreviewLoading, commitDiffHunkOffsets, selectedDetailFile, bodyRows, width, theme, hasMoreCommits, loadingMoreCommits) {
|
|
22806
|
+
function renderMainPanel(h, components, state, context, contextStatus, worktreeDiff, worktreeDiffLoading, worktreeHunks, worktreeHunksLoading, filePreview, filePreviewLoading, commitDiffHunkOffsets, selectedDetailFile, stashDiffLines, stashDiffLoading, bodyRows, width, theme, hasMoreCommits, loadingMoreCommits) {
|
|
21766
22807
|
if (state.activeView === 'status') {
|
|
21767
22808
|
return renderStatusSurface(h, components, state, context, contextStatus, bodyRows, width, theme);
|
|
21768
22809
|
}
|
|
21769
22810
|
if (state.activeView === 'diff') {
|
|
21770
|
-
return renderDiffSurface(h, components, state, context, contextStatus, worktreeDiff, worktreeDiffLoading, worktreeHunks, worktreeHunksLoading, filePreview, filePreviewLoading, commitDiffHunkOffsets, selectedDetailFile, bodyRows, width, theme);
|
|
22811
|
+
return renderDiffSurface(h, components, state, context, contextStatus, worktreeDiff, worktreeDiffLoading, worktreeHunks, worktreeHunksLoading, filePreview, filePreviewLoading, commitDiffHunkOffsets, selectedDetailFile, stashDiffLines, stashDiffLoading, bodyRows, width, theme);
|
|
21771
22812
|
}
|
|
21772
22813
|
if (state.activeView === 'compose') {
|
|
21773
22814
|
return renderComposeSurface(h, components, state, context, contextStatus, bodyRows, width, theme);
|
|
@@ -21781,6 +22822,9 @@ function renderMainPanel(h, components, state, context, contextStatus, worktreeD
|
|
|
21781
22822
|
if (state.activeView === 'stash') {
|
|
21782
22823
|
return renderStashSurface(h, components, state, context, contextStatus, bodyRows, width, theme);
|
|
21783
22824
|
}
|
|
22825
|
+
if (state.activeView === 'worktrees') {
|
|
22826
|
+
return renderWorktreesSurface(h, components, state, context, contextStatus, bodyRows, width, theme);
|
|
22827
|
+
}
|
|
21784
22828
|
return renderHistoryPanel(h, components, state, context, bodyRows, width, theme, hasMoreCommits, loadingMoreCommits);
|
|
21785
22829
|
}
|
|
21786
22830
|
function renderHistoryPanel(h, components, state, context, bodyRows, width, theme, hasMoreCommits, loadingMoreCommits) {
|
|
@@ -21990,15 +23034,25 @@ function renderComposeSurface(h, components, state, context, contextStatus, body
|
|
|
21990
23034
|
key: `compose-body-${index}`,
|
|
21991
23035
|
dimColor: line === '<empty>',
|
|
21992
23036
|
}, ` ${line}${bodyCursor && isLast ? bodyCursor : ''}`);
|
|
21993
|
-
}),
|
|
21994
|
-
|
|
23037
|
+
}),
|
|
23038
|
+
// Loading indicator + post-action message belong inline with the draft
|
|
23039
|
+
// (they describe what just happened to the fields above). The state-
|
|
23040
|
+
// line ("Editing — Enter switches summary↔body…" / "Press e to edit
|
|
23041
|
+
// …") is footer-style guidance and now sits at the very bottom of the
|
|
23042
|
+
// pane so it doesn't visually separate the body from any
|
|
23043
|
+
// result/details.
|
|
23044
|
+
...(compose.loading
|
|
23045
|
+
? [
|
|
23046
|
+
h(Text, undefined, ''),
|
|
23047
|
+
h(Text, {
|
|
21995
23048
|
key: 'compose-loading',
|
|
21996
23049
|
bold: true,
|
|
21997
23050
|
color: theme.noColor ? undefined : theme.colors.accent,
|
|
21998
23051
|
}, theme.ascii
|
|
21999
23052
|
? '[...] Generating AI commit draft (this can take a moment)'
|
|
22000
|
-
: '⏳ Generating AI commit draft… (this can take a moment)')
|
|
22001
|
-
|
|
23053
|
+
: '⏳ Generating AI commit draft… (this can take a moment)'),
|
|
23054
|
+
]
|
|
23055
|
+
: []), ...(compose.message ? [h(Text, undefined, ''), h(Text, { key: 'compose-msg' }, truncate$1(compose.message, 140))] : []), ...(compose.details || []).map((line, index) => h(Text, {
|
|
22002
23056
|
key: `compose-detail-${index}`,
|
|
22003
23057
|
dimColor: true,
|
|
22004
23058
|
}, truncate$1(` ${line}`, 140))), ...(!hasStagedFiles && noStagedHint
|
|
@@ -22006,7 +23060,7 @@ function renderComposeSurface(h, components, state, context, contextStatus, body
|
|
|
22006
23060
|
h(Text, { key: 'compose-no-staged-spacer' }, ''),
|
|
22007
23061
|
h(Text, { key: 'compose-no-staged', dimColor: true }, truncate$1(noStagedHint, 140)),
|
|
22008
23062
|
]
|
|
22009
|
-
: []));
|
|
23063
|
+
: []), h(Box, { flexGrow: 1 }), h(Text, { key: 'compose-stateline', dimColor: true }, truncate$1(stateLine, width - 4)));
|
|
22010
23064
|
}
|
|
22011
23065
|
function matchesPromotedFilter(haystacks, filter) {
|
|
22012
23066
|
if (!filter.trim()) {
|
|
@@ -22160,6 +23214,48 @@ function renderStashSurface(h, components, state, context, contextStatus, bodyRo
|
|
|
22160
23214
|
width,
|
|
22161
23215
|
}, h(Box, { justifyContent: 'space-between' }, h(Text, { bold: true }, panelTitle('Stash', focused)), h(Text, { dimColor: true }, headerRight)), ...renderPromotedFilterAffordance(h, Text, state, theme), ...lines);
|
|
22162
23216
|
}
|
|
23217
|
+
function renderWorktreesSurface(h, components, state, context, contextStatus, bodyRows, width, theme) {
|
|
23218
|
+
const { Box, Text } = components;
|
|
23219
|
+
const focused = state.focus === 'commits';
|
|
23220
|
+
const loading = isLogInkContextKeyLoading(contextStatus, 'worktreeList');
|
|
23221
|
+
const allWorktrees = context.worktreeList?.worktrees || [];
|
|
23222
|
+
const worktrees = state.filter
|
|
23223
|
+
? allWorktrees.filter((entry) => matchesPromotedFilter([entry.path, entry.branch || '', entry.head || ''], state.filter))
|
|
23224
|
+
: allWorktrees;
|
|
23225
|
+
const selected = Math.max(0, Math.min(state.selectedWorktreeListIndex, Math.max(0, worktrees.length - 1)));
|
|
23226
|
+
const listRows = Math.max(4, bodyRows - 4);
|
|
23227
|
+
const startIndex = Math.max(0, selected - Math.floor(listRows / 2));
|
|
23228
|
+
const visible = worktrees.slice(startIndex, startIndex + listRows);
|
|
23229
|
+
const filterLabel = state.filter ? ` | filter: ${state.filter}` : '';
|
|
23230
|
+
const headerRight = loading
|
|
23231
|
+
? 'loading worktrees'
|
|
23232
|
+
: `${worktrees.length}/${allWorktrees.length} worktrees${filterLabel}`;
|
|
23233
|
+
const lines = loading
|
|
23234
|
+
? [h(Text, { key: 'worktrees-loading', dimColor: true }, formatLogInkLoading({ resource: 'worktrees' }))]
|
|
23235
|
+
: worktrees.length === 0
|
|
23236
|
+
? [h(Text, { key: 'worktrees-empty', dimColor: true }, 'No linked worktrees.')]
|
|
23237
|
+
: visible.map((entry, offset) => {
|
|
23238
|
+
const index = startIndex + offset;
|
|
23239
|
+
const isSelected = index === selected;
|
|
23240
|
+
const cursor = isSelected ? '>' : ' ';
|
|
23241
|
+
const marker = entry.current ? '*' : ' ';
|
|
23242
|
+
const branchLabel = entry.branch ? entry.branch : entry.head || '<detached>';
|
|
23243
|
+
const stateLabel = entry.dirty ? 'dirty' : 'clean';
|
|
23244
|
+
return h(Text, {
|
|
23245
|
+
key: `worktree-${index}`,
|
|
23246
|
+
bold: isSelected,
|
|
23247
|
+
dimColor: !isSelected && !entry.current,
|
|
23248
|
+
}, truncate$1(`${cursor} ${marker} ${branchLabel.padEnd(28)} ${stateLabel.padEnd(6)} ${entry.path}`, width - 4));
|
|
23249
|
+
});
|
|
23250
|
+
return h(Box, {
|
|
23251
|
+
borderColor: focusBorderColor(theme, focused),
|
|
23252
|
+
borderStyle: theme.borderStyle,
|
|
23253
|
+
flexDirection: 'column',
|
|
23254
|
+
flexShrink: 0,
|
|
23255
|
+
paddingX: 1,
|
|
23256
|
+
width,
|
|
23257
|
+
}, h(Box, { justifyContent: 'space-between' }, h(Text, { bold: true }, panelTitle('Worktrees', focused)), h(Text, { dimColor: true }, headerRight)), ...renderPromotedFilterAffordance(h, Text, state, theme), ...lines);
|
|
23258
|
+
}
|
|
22163
23259
|
/**
|
|
22164
23260
|
* Filter input cursor for the promoted views (branches/tags/stash).
|
|
22165
23261
|
* History already shows the same `filter: foo_` affordance in its header
|
|
@@ -22178,12 +23274,67 @@ function renderPromotedFilterAffordance(h, Text, state, theme) {
|
|
|
22178
23274
|
h(Text, { key: 'promoted-filter-input', color: accent }, `filter: ${state.filter}_`),
|
|
22179
23275
|
];
|
|
22180
23276
|
}
|
|
22181
|
-
function renderDiffSurface(h, components, state, context, contextStatus, worktreeDiff, worktreeDiffLoading, worktreeHunks, worktreeHunksLoading, filePreview, filePreviewLoading, commitDiffHunkOffsets, selectedDetailFile, bodyRows, width, theme) {
|
|
23277
|
+
function renderDiffSurface(h, components, state, context, contextStatus, worktreeDiff, worktreeDiffLoading, worktreeHunks, worktreeHunksLoading, filePreview, filePreviewLoading, commitDiffHunkOffsets, selectedDetailFile, stashDiffLines, stashDiffLoading, bodyRows, width, theme) {
|
|
22182
23278
|
const { Box, Text } = components;
|
|
22183
23279
|
const focused = state.focus === 'commits';
|
|
22184
23280
|
const worktree = context.worktree;
|
|
22185
23281
|
const worktreeFile = worktree?.files[state.selectedWorktreeFileIndex];
|
|
22186
23282
|
const visibleRows = Math.max(4, bodyRows - 4);
|
|
23283
|
+
// Stash diff branch: when the user opened the diff via Enter on a stash
|
|
23284
|
+
// row, render the stash patch text directly. The patch is parsed into
|
|
23285
|
+
// per-file sections so `]` / `[` jumps between files and `c`
|
|
23286
|
+
// cherry-picks the file at the cursor.
|
|
23287
|
+
if (state.diffSource === 'stash') {
|
|
23288
|
+
const lines = stashDiffLines || [];
|
|
23289
|
+
const visibleLines = lines.slice(state.diffPreviewOffset, state.diffPreviewOffset + visibleRows);
|
|
23290
|
+
const stashFiles = parseStashDiffFiles(lines);
|
|
23291
|
+
const fileCount = stashFiles.length;
|
|
23292
|
+
const currentFile = (() => {
|
|
23293
|
+
if (fileCount === 0)
|
|
23294
|
+
return undefined;
|
|
23295
|
+
let current = stashFiles[0];
|
|
23296
|
+
for (const file of stashFiles) {
|
|
23297
|
+
if (file.startLine <= state.diffPreviewOffset) {
|
|
23298
|
+
current = file;
|
|
23299
|
+
}
|
|
23300
|
+
else {
|
|
23301
|
+
break;
|
|
23302
|
+
}
|
|
23303
|
+
}
|
|
23304
|
+
return current;
|
|
23305
|
+
})();
|
|
23306
|
+
const currentFileIndex = currentFile
|
|
23307
|
+
? Math.max(0, stashFiles.findIndex((file) => file.startLine === currentFile.startLine))
|
|
23308
|
+
: -1;
|
|
23309
|
+
const headerLines = stashDiffLoading
|
|
23310
|
+
? [`Loading diff for ${state.stashDiffRef || 'stash'}...`]
|
|
23311
|
+
: lines.length
|
|
23312
|
+
? [
|
|
23313
|
+
`Stash: ${state.stashDiffRef || ''}`,
|
|
23314
|
+
fileCount > 0 && currentFile
|
|
23315
|
+
? `File ${currentFileIndex + 1}/${fileCount}: ${currentFile.path}`
|
|
23316
|
+
: 'No files in this stash.',
|
|
23317
|
+
`Lines ${Math.min(state.diffPreviewOffset + 1, lines.length)}-${Math.min(state.diffPreviewOffset + visibleLines.length, lines.length)}/${lines.length}`,
|
|
23318
|
+
'',
|
|
23319
|
+
]
|
|
23320
|
+
: ['No diff to display for this stash.'];
|
|
23321
|
+
return h(Box, {
|
|
23322
|
+
borderColor: focusBorderColor(theme, focused),
|
|
23323
|
+
borderStyle: theme.borderStyle,
|
|
23324
|
+
flexDirection: 'column',
|
|
23325
|
+
flexShrink: 0,
|
|
23326
|
+
paddingX: 1,
|
|
23327
|
+
width,
|
|
23328
|
+
}, h(Box, { justifyContent: 'space-between' }, h(Text, { bold: true }, panelTitle('Stash diff', focused)), h(Text, { dimColor: true }, state.stashDiffRef || 'no stash')), ...headerLines.map((line, index) => h(Text, {
|
|
23329
|
+
key: `stash-diff-header-${index}`,
|
|
23330
|
+
dimColor: index > 0,
|
|
23331
|
+
}, truncate$1(line, width - 4))), ...(stashDiffLoading || !lines.length
|
|
23332
|
+
? []
|
|
23333
|
+
: visibleLines.map((line, index) => h(Text, {
|
|
23334
|
+
key: `stash-diff-line-${state.diffPreviewOffset + index}`,
|
|
23335
|
+
...diffLineProps(line, theme),
|
|
23336
|
+
}, truncate$1(line, width - 4)))));
|
|
23337
|
+
}
|
|
22187
23338
|
// diffSource disambiguates: 'commit' was set when the user opened the
|
|
22188
23339
|
// diff via history → Enter (read-only commit-diff explore), 'worktree'
|
|
22189
23340
|
// was set when they came from status → Enter (stage / hunk / revert).
|
|
@@ -22277,6 +23428,9 @@ function renderDetailPanel(h, components, state, context, contextStatus, detail,
|
|
|
22277
23428
|
if (state.showCommandPalette) {
|
|
22278
23429
|
return renderCommandPalette(h, components, state, width, theme, focused);
|
|
22279
23430
|
}
|
|
23431
|
+
if (state.inputPrompt) {
|
|
23432
|
+
return renderInputPromptPanel(h, components, state, width, theme, focused);
|
|
23433
|
+
}
|
|
22280
23434
|
if (state.pendingConfirmationId || state.pendingMutationConfirmation) {
|
|
22281
23435
|
return renderConfirmationPanel(h, components, state, width, theme, focused);
|
|
22282
23436
|
}
|
|
@@ -22676,16 +23830,40 @@ function renderCommitPanel(h, components, state, context, contextStatus, width,
|
|
|
22676
23830
|
}, h(Text, { bold: true }, panelTitle('Commit', focused)), ...headerLines.map((line, index) => h(Text, {
|
|
22677
23831
|
key: `commit-header-${index}`,
|
|
22678
23832
|
dimColor: index < 2 || line.startsWith(' ') || line === '<empty>',
|
|
22679
|
-
}, truncate$1(line, width - 4))),
|
|
22680
|
-
|
|
22681
|
-
|
|
22682
|
-
|
|
22683
|
-
|
|
22684
|
-
|
|
22685
|
-
|
|
23833
|
+
}, truncate$1(line, width - 4))),
|
|
23834
|
+
// Loading indicator + commit result/details stay inline with the body
|
|
23835
|
+
// (they describe what just happened to the fields above). The action
|
|
23836
|
+
// hint ("e edit | c commit | I AI draft") moves to the bottom of the
|
|
23837
|
+
// pane to read as footer guidance, matching the compose surface.
|
|
23838
|
+
...(loading
|
|
23839
|
+
? [h(Text, {
|
|
23840
|
+
key: 'commit-loading',
|
|
23841
|
+
bold: true,
|
|
23842
|
+
color: theme.noColor ? undefined : theme.colors.accent,
|
|
23843
|
+
}, truncate$1(theme.ascii ? '[...] Generating AI draft' : '⏳ Generating AI draft…', width - 4))]
|
|
23844
|
+
: []), ...trailerLines.map((line, index) => h(Text, {
|
|
22686
23845
|
key: `commit-trailer-${index}`,
|
|
22687
23846
|
dimColor: line.startsWith(' '),
|
|
22688
|
-
}, truncate$1(line, width - 4))))
|
|
23847
|
+
}, truncate$1(line, width - 4))), h(Box, { flexGrow: 1 }), loading
|
|
23848
|
+
? null
|
|
23849
|
+
: h(Text, { key: 'commit-state', dimColor: true }, truncate$1(stateLine, width - 4)));
|
|
23850
|
+
}
|
|
23851
|
+
function renderInputPromptPanel(h, components, state, width, theme, focused) {
|
|
23852
|
+
const { Box, Text } = components;
|
|
23853
|
+
const prompt = state.inputPrompt;
|
|
23854
|
+
if (!prompt) {
|
|
23855
|
+
return h(Box, { width });
|
|
23856
|
+
}
|
|
23857
|
+
return h(Box, {
|
|
23858
|
+
borderColor: focusBorderColor(theme, focused),
|
|
23859
|
+
borderStyle: theme.borderStyle,
|
|
23860
|
+
flexDirection: 'column',
|
|
23861
|
+
width,
|
|
23862
|
+
paddingX: 1,
|
|
23863
|
+
}, h(Text, { bold: true }, panelTitle('Prompt', focused)), h(Text, { dimColor: true }, truncate$1(prompt.label, width - 4)), h(Text, undefined, ''), h(Text, {
|
|
23864
|
+
bold: true,
|
|
23865
|
+
color: theme.noColor ? undefined : theme.colors.accent,
|
|
23866
|
+
}, truncate$1(`${prompt.value}_`, width - 4)), h(Text, undefined, ''), h(Text, { dimColor: true }, 'Enter to submit · Esc to cancel · Ctrl+u to clear'));
|
|
22689
23867
|
}
|
|
22690
23868
|
function renderConfirmationPanel(h, components, state, width, theme, focused) {
|
|
22691
23869
|
const { Box, Text } = components;
|
|
@@ -22863,6 +24041,7 @@ function renderFooter(h, components, state, theme, idleTip) {
|
|
|
22863
24041
|
const { Box, Text } = components;
|
|
22864
24042
|
const hints = getLogInkFooterHints({
|
|
22865
24043
|
activeView: state.activeView,
|
|
24044
|
+
diffSource: state.diffSource,
|
|
22866
24045
|
filterMode: state.filterMode,
|
|
22867
24046
|
focus: state.focus,
|
|
22868
24047
|
pendingKey: state.pendingKey,
|