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.esm.mjs
CHANGED
|
@@ -37,9 +37,11 @@ import '@langchain/core/utils/env';
|
|
|
37
37
|
import '@langchain/core/utils/async_caller';
|
|
38
38
|
import { encoding_for_model } from 'tiktoken';
|
|
39
39
|
import { spawn, exec, execFile } from 'child_process';
|
|
40
|
+
import { spawnSync } from 'node:child_process';
|
|
40
41
|
import * as fs$1 from 'node:fs';
|
|
41
42
|
import * as os$1 from 'node:os';
|
|
42
43
|
import * as path$1 from 'node:path';
|
|
44
|
+
import * as crypto from 'node:crypto';
|
|
43
45
|
import * as readline from 'readline';
|
|
44
46
|
import readline__default from 'readline';
|
|
45
47
|
import { promisify } from 'util';
|
|
@@ -50,7 +52,7 @@ import { pathToFileURL } from 'url';
|
|
|
50
52
|
/**
|
|
51
53
|
* Current build version from package.json
|
|
52
54
|
*/
|
|
53
|
-
const BUILD_VERSION = "0.
|
|
55
|
+
const BUILD_VERSION = "0.36.0";
|
|
54
56
|
|
|
55
57
|
const isInteractive = (config) => {
|
|
56
58
|
return config?.mode === 'interactive' || !!config?.interactive;
|
|
@@ -13890,13 +13892,18 @@ function applyCommitComposeAction(state, action) {
|
|
|
13890
13892
|
loading: action.value,
|
|
13891
13893
|
};
|
|
13892
13894
|
case 'setDraft':
|
|
13895
|
+
// No `message` here — the loader → filled fields are the confirmation
|
|
13896
|
+
// that the AI generated something. A lingering "AI draft ready for
|
|
13897
|
+
// editing" line in the panel reads as stale state. The runtime still
|
|
13898
|
+
// posts the same string to the footer status line for transient
|
|
13899
|
+
// feedback.
|
|
13893
13900
|
return {
|
|
13894
13901
|
...state,
|
|
13895
13902
|
...splitCommitDraft(action.value),
|
|
13896
13903
|
field: 'summary',
|
|
13897
13904
|
editing: true,
|
|
13898
13905
|
loading: false,
|
|
13899
|
-
message:
|
|
13906
|
+
message: undefined,
|
|
13900
13907
|
details: undefined,
|
|
13901
13908
|
};
|
|
13902
13909
|
case 'setResult':
|
|
@@ -14528,6 +14535,85 @@ function getLogInkWorkflowActions() {
|
|
|
14528
14535
|
kind: 'normal',
|
|
14529
14536
|
requiresConfirmation: false,
|
|
14530
14537
|
},
|
|
14538
|
+
{
|
|
14539
|
+
// Per-view-only: scoped to the history view in inkInput so `c`
|
|
14540
|
+
// doesn't fire elsewhere. Empty key keeps it palette-discoverable
|
|
14541
|
+
// without registering a global hotkey.
|
|
14542
|
+
id: 'cherry-pick-commit',
|
|
14543
|
+
key: '',
|
|
14544
|
+
label: 'Cherry-pick commit',
|
|
14545
|
+
description: 'Apply the selected commit on top of the current branch (after confirmation).',
|
|
14546
|
+
kind: 'destructive',
|
|
14547
|
+
requiresConfirmation: true,
|
|
14548
|
+
},
|
|
14549
|
+
{
|
|
14550
|
+
// Per-view-only: scoped to the commit-diff explore in inkInput.
|
|
14551
|
+
// Routed through the y-confirm path because `git checkout <sha> --
|
|
14552
|
+
// <path>` overwrites the worktree file unconditionally and we
|
|
14553
|
+
// want the user to acknowledge that before discarding any local
|
|
14554
|
+
// edits to the path.
|
|
14555
|
+
id: 'checkout-file-from-commit',
|
|
14556
|
+
key: '',
|
|
14557
|
+
label: 'Cherry-pick file from commit',
|
|
14558
|
+
description: 'Materialize the selected file from this commit into the working tree (after confirmation).',
|
|
14559
|
+
kind: 'destructive',
|
|
14560
|
+
requiresConfirmation: true,
|
|
14561
|
+
},
|
|
14562
|
+
{
|
|
14563
|
+
// Per-view-only: scoped to the stash-diff explorer in inkInput.
|
|
14564
|
+
// Same overwrite rationale as `checkout-file-from-commit` — the
|
|
14565
|
+
// y-confirm path is the dirty-tree warning.
|
|
14566
|
+
id: 'checkout-file-from-stash',
|
|
14567
|
+
key: '',
|
|
14568
|
+
label: 'Cherry-pick file from stash',
|
|
14569
|
+
description: 'Materialize the selected file from this stash into the working tree (after confirmation).',
|
|
14570
|
+
kind: 'destructive',
|
|
14571
|
+
requiresConfirmation: true,
|
|
14572
|
+
},
|
|
14573
|
+
{
|
|
14574
|
+
id: 'open-pr',
|
|
14575
|
+
key: 'O',
|
|
14576
|
+
label: 'Open PR / repo',
|
|
14577
|
+
description: 'Open the current branch\'s pull request in the browser, or the repo page if there\'s no PR.',
|
|
14578
|
+
kind: 'normal',
|
|
14579
|
+
requiresConfirmation: false,
|
|
14580
|
+
},
|
|
14581
|
+
{
|
|
14582
|
+
id: 'fetch-remotes',
|
|
14583
|
+
key: 'F',
|
|
14584
|
+
label: 'Fetch all remotes',
|
|
14585
|
+
description: 'Run `git fetch --all --prune` and silently refresh context.',
|
|
14586
|
+
kind: 'normal',
|
|
14587
|
+
requiresConfirmation: false,
|
|
14588
|
+
},
|
|
14589
|
+
{
|
|
14590
|
+
id: 'pull-current-branch',
|
|
14591
|
+
key: 'U',
|
|
14592
|
+
label: 'Pull current branch',
|
|
14593
|
+
description: 'Run `git pull --ff-only` against the current branch.',
|
|
14594
|
+
kind: 'normal',
|
|
14595
|
+
requiresConfirmation: false,
|
|
14596
|
+
},
|
|
14597
|
+
{
|
|
14598
|
+
id: 'push-current-branch',
|
|
14599
|
+
key: 'P',
|
|
14600
|
+
label: 'Push current branch',
|
|
14601
|
+
description: 'Run `git push` for the current branch.',
|
|
14602
|
+
kind: 'normal',
|
|
14603
|
+
requiresConfirmation: false,
|
|
14604
|
+
},
|
|
14605
|
+
{
|
|
14606
|
+
// Per-view-only — the inkInput handler scopes this to the tags
|
|
14607
|
+
// surface so we don't expose `R` as a remote-delete from elsewhere.
|
|
14608
|
+
// The empty `key` keeps the workflow palette-discoverable but does
|
|
14609
|
+
// not register a global hotkey.
|
|
14610
|
+
id: 'delete-remote-tag',
|
|
14611
|
+
key: '',
|
|
14612
|
+
label: 'Delete remote tag',
|
|
14613
|
+
description: 'Push :tag to origin to delete the selected tag remotely after confirmation.',
|
|
14614
|
+
kind: 'destructive',
|
|
14615
|
+
requiresConfirmation: true,
|
|
14616
|
+
},
|
|
14531
14617
|
{
|
|
14532
14618
|
id: 'stage-file',
|
|
14533
14619
|
key: 'space',
|
|
@@ -14775,6 +14861,13 @@ const LOG_INK_KEY_BINDINGS = [
|
|
|
14775
14861
|
description: 'Push the stash view (gz; gs is reserved for status).',
|
|
14776
14862
|
contexts: ['normal'],
|
|
14777
14863
|
},
|
|
14864
|
+
{
|
|
14865
|
+
id: 'navigateWorktrees',
|
|
14866
|
+
keys: ['gw'],
|
|
14867
|
+
label: 'worktrees',
|
|
14868
|
+
description: 'Push the linked worktrees view.',
|
|
14869
|
+
contexts: ['normal'],
|
|
14870
|
+
},
|
|
14778
14871
|
{
|
|
14779
14872
|
id: 'navigateBack',
|
|
14780
14873
|
keys: ['<', 'esc'],
|
|
@@ -14895,6 +14988,7 @@ const GLOBAL_BINDING_IDS = [
|
|
|
14895
14988
|
'navigateBranches',
|
|
14896
14989
|
'navigateTags',
|
|
14897
14990
|
'navigateStash',
|
|
14991
|
+
'navigateWorktrees',
|
|
14898
14992
|
'navigateBack',
|
|
14899
14993
|
];
|
|
14900
14994
|
const NORMAL_GLOBAL_HINTS = ['g jump', '< back', '? help', ': cmds', 'q quit'];
|
|
@@ -14973,37 +15067,57 @@ function getLogInkFooterHints(options) {
|
|
|
14973
15067
|
};
|
|
14974
15068
|
}
|
|
14975
15069
|
if (options.activeView === 'diff') {
|
|
15070
|
+
if (options.diffSource === 'stash') {
|
|
15071
|
+
return {
|
|
15072
|
+
contextual: ['j/k lines', '[/] file', 'c cherry-pick', 'o edit', 'esc back'],
|
|
15073
|
+
global: NORMAL_GLOBAL_HINTS,
|
|
15074
|
+
};
|
|
15075
|
+
}
|
|
15076
|
+
if (options.diffSource === 'commit') {
|
|
15077
|
+
// Commit-diff explore: read-only diff, but `c` cherry-picks the
|
|
15078
|
+
// cursored file from the commit into the worktree.
|
|
15079
|
+
return {
|
|
15080
|
+
contextual: ['j/k hunks', '[/] file', 'c cherry-pick', 'esc back'],
|
|
15081
|
+
global: NORMAL_GLOBAL_HINTS,
|
|
15082
|
+
};
|
|
15083
|
+
}
|
|
14976
15084
|
return {
|
|
14977
|
-
contextual: ['j/k hunks', 'space stage', 'z revert', 'e/c compose', 'esc files'],
|
|
15085
|
+
contextual: ['j/k hunks', 'space stage', 'z revert', 'o edit', 'e/c compose', 'esc files'],
|
|
14978
15086
|
global: NORMAL_GLOBAL_HINTS,
|
|
14979
15087
|
};
|
|
14980
15088
|
}
|
|
14981
15089
|
if (options.activeView === 'compose') {
|
|
14982
15090
|
return {
|
|
14983
|
-
contextual: ['e edit', '
|
|
15091
|
+
contextual: ['e edit', 'c commit', 'I AI draft', 'gs hunks', 'esc back'],
|
|
14984
15092
|
global: NORMAL_GLOBAL_HINTS,
|
|
14985
15093
|
};
|
|
14986
15094
|
}
|
|
14987
15095
|
if (options.activeView === 'branches') {
|
|
14988
15096
|
return {
|
|
14989
|
-
contextual: ['↑/↓ branches', '
|
|
15097
|
+
contextual: ['↑/↓ branches', 'enter checkout', '+ new', 'D delete', 's sort'],
|
|
14990
15098
|
global: NORMAL_GLOBAL_HINTS,
|
|
14991
15099
|
};
|
|
14992
15100
|
}
|
|
14993
15101
|
if (options.activeView === 'tags') {
|
|
14994
15102
|
return {
|
|
14995
|
-
contextual: ['↑/↓ tags', '
|
|
15103
|
+
contextual: ['↑/↓ tags', '+ new', 'P push', 'T delete', 's sort'],
|
|
14996
15104
|
global: NORMAL_GLOBAL_HINTS,
|
|
14997
15105
|
};
|
|
14998
15106
|
}
|
|
14999
15107
|
if (options.activeView === 'stash') {
|
|
15000
15108
|
return {
|
|
15001
|
-
contextual: ['↑/↓ stashes', '
|
|
15109
|
+
contextual: ['↑/↓ stashes', 'enter diff', 'a apply', 'p pop', 'X drop'],
|
|
15110
|
+
global: NORMAL_GLOBAL_HINTS,
|
|
15111
|
+
};
|
|
15112
|
+
}
|
|
15113
|
+
if (options.activeView === 'worktrees') {
|
|
15114
|
+
return {
|
|
15115
|
+
contextual: ['↑/↓ worktrees', 'W remove', 'esc back'],
|
|
15002
15116
|
global: NORMAL_GLOBAL_HINTS,
|
|
15003
15117
|
};
|
|
15004
15118
|
}
|
|
15005
15119
|
return {
|
|
15006
|
-
contextual: ['↑/↓ move', '/ search', 'gg/G top/bottom'
|
|
15120
|
+
contextual: ['↑/↓ move', 'enter diff', 'c cherry-pick', '/ search', 'gg/G top/bottom'],
|
|
15007
15121
|
global: NORMAL_GLOBAL_HINTS,
|
|
15008
15122
|
};
|
|
15009
15123
|
}
|
|
@@ -15266,10 +15380,12 @@ function getLogInkPaletteExecuteEvents(command, state) {
|
|
|
15266
15380
|
if (command.requiresConfirmation) {
|
|
15267
15381
|
return [action({ type: 'setPendingConfirmation', value: command.id })];
|
|
15268
15382
|
}
|
|
15269
|
-
|
|
15270
|
-
|
|
15271
|
-
|
|
15272
|
-
|
|
15383
|
+
// Non-confirm workflows are dispatched directly through the runtime
|
|
15384
|
+
// workflow runner — same path the keyboard takes. Previously this
|
|
15385
|
+
// emitted `setWorkflowAction` only, which set state but never fired
|
|
15386
|
+
// the action because nothing in the runtime consumes
|
|
15387
|
+
// `workflowActionId`.
|
|
15388
|
+
return [{ type: 'runWorkflowAction', id: command.id }];
|
|
15273
15389
|
}
|
|
15274
15390
|
// Binding-derived commands. Map each LogInkCommandId to the same events
|
|
15275
15391
|
// the keystroke would emit. Order matches the keymap registry.
|
|
@@ -15331,6 +15447,8 @@ function getLogInkPaletteExecuteEvents(command, state) {
|
|
|
15331
15447
|
return [action({ type: 'pushView', value: 'tags' })];
|
|
15332
15448
|
case 'navigateStash':
|
|
15333
15449
|
return [action({ type: 'pushView', value: 'stash' })];
|
|
15450
|
+
case 'navigateWorktrees':
|
|
15451
|
+
return [action({ type: 'pushView', value: 'worktrees' })];
|
|
15334
15452
|
case 'navigateBack':
|
|
15335
15453
|
return [action({ type: 'popView' })];
|
|
15336
15454
|
case 'openSelected': {
|
|
@@ -15429,6 +15547,39 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
|
|
|
15429
15547
|
}
|
|
15430
15548
|
return [{ type: 'exit' }];
|
|
15431
15549
|
}
|
|
15550
|
+
// Input prompt is the most modal — when active, every keystroke routes
|
|
15551
|
+
// into the prompt until Enter (submit) or Esc (cancel). Sits above the
|
|
15552
|
+
// filter/confirmation/compose handlers so a prompt opened from inside
|
|
15553
|
+
// any of those still captures focus cleanly.
|
|
15554
|
+
if (state.inputPrompt) {
|
|
15555
|
+
if (key.escape) {
|
|
15556
|
+
return [
|
|
15557
|
+
action({ type: 'closeInputPrompt' }),
|
|
15558
|
+
action({ type: 'setStatus', value: 'cancelled' }),
|
|
15559
|
+
];
|
|
15560
|
+
}
|
|
15561
|
+
if (key.return) {
|
|
15562
|
+
const value = state.inputPrompt.value.trim();
|
|
15563
|
+
if (!value) {
|
|
15564
|
+
return [action({ type: 'setStatus', value: 'enter a value or press esc to cancel' })];
|
|
15565
|
+
}
|
|
15566
|
+
const id = state.inputPrompt.kind;
|
|
15567
|
+
return [
|
|
15568
|
+
{ type: 'runWorkflowAction', id, payload: value },
|
|
15569
|
+
action({ type: 'closeInputPrompt' }),
|
|
15570
|
+
];
|
|
15571
|
+
}
|
|
15572
|
+
if (key.backspace || key.delete) {
|
|
15573
|
+
return [action({ type: 'backspaceInputPrompt' })];
|
|
15574
|
+
}
|
|
15575
|
+
if (key.ctrl && inputValue === 'u') {
|
|
15576
|
+
return [action({ type: 'clearInputPromptText' })];
|
|
15577
|
+
}
|
|
15578
|
+
if (inputValue && !key.ctrl && !key.meta) {
|
|
15579
|
+
return [action({ type: 'appendInputPrompt', value: inputValue })];
|
|
15580
|
+
}
|
|
15581
|
+
return [];
|
|
15582
|
+
}
|
|
15432
15583
|
if (state.commitCompose.editing) {
|
|
15433
15584
|
if (key.escape) {
|
|
15434
15585
|
return [action({ type: 'commitCompose', action: { type: 'setEditing', value: false } })];
|
|
@@ -15497,7 +15648,7 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
|
|
|
15497
15648
|
// selected item and run the right action function.
|
|
15498
15649
|
if (workflowAction) {
|
|
15499
15650
|
return [
|
|
15500
|
-
{ type: 'runWorkflowAction', id: workflowAction.id },
|
|
15651
|
+
{ type: 'runWorkflowAction', id: workflowAction.id, payload: state.pendingConfirmationPayload },
|
|
15501
15652
|
action({ type: 'setPendingConfirmation', value: undefined }),
|
|
15502
15653
|
];
|
|
15503
15654
|
}
|
|
@@ -15647,6 +15798,12 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
|
|
|
15647
15798
|
action({ type: 'setStatus', value: 'jumped to stash' }),
|
|
15648
15799
|
];
|
|
15649
15800
|
}
|
|
15801
|
+
if (state.pendingKey === 'g' && inputValue === 'w') {
|
|
15802
|
+
return [
|
|
15803
|
+
action({ type: 'pushView', value: 'worktrees' }),
|
|
15804
|
+
action({ type: 'setStatus', value: 'jumped to worktrees' }),
|
|
15805
|
+
];
|
|
15806
|
+
}
|
|
15650
15807
|
if (inputValue === 'g') {
|
|
15651
15808
|
if (state.pendingKey === 'g') {
|
|
15652
15809
|
return [
|
|
@@ -15698,6 +15855,13 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
|
|
|
15698
15855
|
hunkOffsets: context.worktreeHunkOffsets,
|
|
15699
15856
|
})];
|
|
15700
15857
|
}
|
|
15858
|
+
if (state.activeView === 'diff' && state.diffSource === 'stash' && context.stashDiffFileOffsets?.length) {
|
|
15859
|
+
return [action({
|
|
15860
|
+
type: 'jumpCommitDiffHunk',
|
|
15861
|
+
delta: -1,
|
|
15862
|
+
hunkOffsets: context.stashDiffFileOffsets,
|
|
15863
|
+
})];
|
|
15864
|
+
}
|
|
15701
15865
|
if (state.activeView === 'diff' && context.commitDiffHunkOffsets?.length) {
|
|
15702
15866
|
return [action({
|
|
15703
15867
|
type: 'jumpCommitDiffHunk',
|
|
@@ -15715,6 +15879,13 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
|
|
|
15715
15879
|
hunkOffsets: context.worktreeHunkOffsets,
|
|
15716
15880
|
})];
|
|
15717
15881
|
}
|
|
15882
|
+
if (state.activeView === 'diff' && state.diffSource === 'stash' && context.stashDiffFileOffsets?.length) {
|
|
15883
|
+
return [action({
|
|
15884
|
+
type: 'jumpCommitDiffHunk',
|
|
15885
|
+
delta: 1,
|
|
15886
|
+
hunkOffsets: context.stashDiffFileOffsets,
|
|
15887
|
+
})];
|
|
15888
|
+
}
|
|
15718
15889
|
if (state.activeView === 'diff' && context.commitDiffHunkOffsets?.length) {
|
|
15719
15890
|
return [action({
|
|
15720
15891
|
type: 'jumpCommitDiffHunk',
|
|
@@ -15768,6 +15939,9 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
|
|
|
15768
15939
|
if (state.activeView === 'stash' && context.stashCount) {
|
|
15769
15940
|
return [action({ type: 'moveStash', delta: -1, count: context.stashCount })];
|
|
15770
15941
|
}
|
|
15942
|
+
if (state.activeView === 'worktrees' && context.worktreeListCount) {
|
|
15943
|
+
return [action({ type: 'moveWorktreeListEntry', delta: -1, count: context.worktreeListCount })];
|
|
15944
|
+
}
|
|
15771
15945
|
if (state.activeView === 'history' &&
|
|
15772
15946
|
state.focus === 'commits' &&
|
|
15773
15947
|
state.selectedIndex === 0 &&
|
|
@@ -15818,6 +15992,9 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
|
|
|
15818
15992
|
if (state.activeView === 'stash' && context.stashCount) {
|
|
15819
15993
|
return [action({ type: 'moveStash', delta: 1, count: context.stashCount })];
|
|
15820
15994
|
}
|
|
15995
|
+
if (state.activeView === 'worktrees' && context.worktreeListCount) {
|
|
15996
|
+
return [action({ type: 'moveWorktreeListEntry', delta: 1, count: context.worktreeListCount })];
|
|
15997
|
+
}
|
|
15821
15998
|
return [
|
|
15822
15999
|
action(state.focus === 'sidebar'
|
|
15823
16000
|
? { type: 'nextSidebarTab' }
|
|
@@ -15918,12 +16095,183 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
|
|
|
15918
16095
|
})];
|
|
15919
16096
|
}
|
|
15920
16097
|
}
|
|
15921
|
-
|
|
16098
|
+
// Enter on a sidebar tab drills into the corresponding promoted view
|
|
16099
|
+
// (status / branches / tags / stash). Sits above the per-view Enter
|
|
16100
|
+
// handlers so a sidebar-focused Enter never fires checkout-branch /
|
|
16101
|
+
// navigateOpenDiffForCommit / etc. against the (hidden) selection in
|
|
16102
|
+
// the active tab.
|
|
16103
|
+
//
|
|
16104
|
+
// The Enter also moves focus out of the sidebar into the newly opened
|
|
16105
|
+
// list — otherwise ↑/↓ keep cycling sidebar tabs instead of navigating
|
|
16106
|
+
// inside the just-opened view, which made the drill-in feel half-done.
|
|
16107
|
+
if (key.return && state.focus === 'sidebar') {
|
|
16108
|
+
const tabToView = {
|
|
16109
|
+
status: 'status',
|
|
16110
|
+
branches: 'branches',
|
|
16111
|
+
tags: 'tags',
|
|
16112
|
+
stashes: 'stash',
|
|
16113
|
+
worktrees: 'worktrees',
|
|
16114
|
+
};
|
|
16115
|
+
const target = tabToView[state.sidebarTab];
|
|
16116
|
+
if (target) {
|
|
16117
|
+
return [
|
|
16118
|
+
action({ type: 'pushView', value: target }),
|
|
16119
|
+
action({ type: 'setFocus', value: 'commits' }),
|
|
16120
|
+
];
|
|
16121
|
+
}
|
|
16122
|
+
return [action({ type: 'setStatus', value: 'no detail view for this tab' })];
|
|
16123
|
+
}
|
|
16124
|
+
if (key.return && state.activeView === 'status' && state.focus === 'commits' && context.worktreeFileCount) {
|
|
15922
16125
|
return [action({
|
|
15923
16126
|
type: 'navigateOpenDiffForWorktreeFile',
|
|
15924
16127
|
fileIndex: state.selectedWorktreeFileIndex,
|
|
15925
16128
|
})];
|
|
15926
16129
|
}
|
|
16130
|
+
// Enter on a branch row checks the branch out. Non-destructive workflow
|
|
16131
|
+
// action — no confirmation prompt.
|
|
16132
|
+
if (key.return && state.activeView === 'branches' && state.focus === 'commits' && context.branchCount) {
|
|
16133
|
+
return [{ type: 'runWorkflowAction', id: 'checkout-branch' }];
|
|
16134
|
+
}
|
|
16135
|
+
// `+` opens a create-branch / create-tag prompt depending on context.
|
|
16136
|
+
// Works from either the matching promoted view (active branches /
|
|
16137
|
+
// tags surface) or from the sidebar when the corresponding tab is
|
|
16138
|
+
// active — saves a drill-in for "I just want to make a new branch".
|
|
16139
|
+
const wantsCreateBranch = inputValue === '+' && (state.activeView === 'branches' ||
|
|
16140
|
+
(state.focus === 'sidebar' && state.sidebarTab === 'branches'));
|
|
16141
|
+
const wantsCreateTag = inputValue === '+' && (state.activeView === 'tags' ||
|
|
16142
|
+
(state.focus === 'sidebar' && state.sidebarTab === 'tags'));
|
|
16143
|
+
if (wantsCreateBranch) {
|
|
16144
|
+
return [action({
|
|
16145
|
+
type: 'openInputPrompt',
|
|
16146
|
+
kind: 'create-branch',
|
|
16147
|
+
label: 'New branch name',
|
|
16148
|
+
})];
|
|
16149
|
+
}
|
|
16150
|
+
if (wantsCreateTag) {
|
|
16151
|
+
return [action({
|
|
16152
|
+
type: 'openInputPrompt',
|
|
16153
|
+
kind: 'create-tag',
|
|
16154
|
+
label: 'New tag name',
|
|
16155
|
+
})];
|
|
16156
|
+
}
|
|
16157
|
+
// Per-view stash actions: `a` apply (keep the stash), `p` pop (apply
|
|
16158
|
+
// then drop). Drop is the existing destructive `X` workflow which
|
|
16159
|
+
// routes through the y-confirm path. Scoped to the stash view so the
|
|
16160
|
+
// letters stay free elsewhere.
|
|
16161
|
+
if (inputValue === 'a' && state.activeView === 'stash' && context.stashCount) {
|
|
16162
|
+
return [{ type: 'runWorkflowAction', id: 'apply-stash' }];
|
|
16163
|
+
}
|
|
16164
|
+
if (inputValue === 'p' && state.activeView === 'stash' && context.stashCount) {
|
|
16165
|
+
return [{ type: 'runWorkflowAction', id: 'pop-stash' }];
|
|
16166
|
+
}
|
|
16167
|
+
// Per-view tag action: `P` pushes the selected tag to origin. Letter
|
|
16168
|
+
// is scoped to the tags surface so it doesn't collide with `p` for
|
|
16169
|
+
// pop-stash. Note: this also takes precedence over the global
|
|
16170
|
+
// push-current-branch workflow's `P` key.
|
|
16171
|
+
if (inputValue === 'P' && state.activeView === 'tags' && context.tagCount) {
|
|
16172
|
+
return [{ type: 'runWorkflowAction', id: 'push-tag' }];
|
|
16173
|
+
}
|
|
16174
|
+
// Per-view branches actions: `R` renames the selected branch, `u`
|
|
16175
|
+
// sets its upstream. Both open the input prompt so the user can type
|
|
16176
|
+
// the new value. Pre-fills are handled by the prompt's `initial`.
|
|
16177
|
+
if (inputValue === 'R' && state.activeView === 'branches' && context.branchCount) {
|
|
16178
|
+
return [action({
|
|
16179
|
+
type: 'openInputPrompt',
|
|
16180
|
+
kind: 'rename-branch',
|
|
16181
|
+
label: 'Rename branch to',
|
|
16182
|
+
})];
|
|
16183
|
+
}
|
|
16184
|
+
if (inputValue === 'u' && state.activeView === 'branches' && context.branchCount) {
|
|
16185
|
+
return [action({
|
|
16186
|
+
type: 'openInputPrompt',
|
|
16187
|
+
kind: 'set-upstream',
|
|
16188
|
+
label: 'Upstream ref (e.g. origin/main)',
|
|
16189
|
+
})];
|
|
16190
|
+
}
|
|
16191
|
+
// Per-view tag action: `R` deletes the tag from the remote (after
|
|
16192
|
+
// confirmation). Scoped per-view so this letter is free elsewhere
|
|
16193
|
+
// (especially the `R` rename binding on the branches view).
|
|
16194
|
+
if (inputValue === 'R' && state.activeView === 'tags' && context.tagCount) {
|
|
16195
|
+
return [action({ type: 'setPendingConfirmation', value: 'delete-remote-tag' })];
|
|
16196
|
+
}
|
|
16197
|
+
// Global stash hotkey: `S` opens a stash-message prompt and
|
|
16198
|
+
// `createStash` runs once submitted. Available everywhere there's
|
|
16199
|
+
// not a more modal handler in front of it.
|
|
16200
|
+
if (inputValue === 'S') {
|
|
16201
|
+
return [action({
|
|
16202
|
+
type: 'openInputPrompt',
|
|
16203
|
+
kind: 'create-stash',
|
|
16204
|
+
label: 'Stash message',
|
|
16205
|
+
})];
|
|
16206
|
+
}
|
|
16207
|
+
// `o` opens the file under the cursor in $EDITOR. Available on the
|
|
16208
|
+
// status surface (worktree files), the worktree diff (the file being
|
|
16209
|
+
// diffed), and the stash diff (the file the cursor sits in inside
|
|
16210
|
+
// the patch). The runtime suspends Ink, spawns the editor sync, then
|
|
16211
|
+
// re-renders.
|
|
16212
|
+
if (inputValue === 'o' && state.activeView === 'status' && context.worktreeFileCount && context.worktreeSelectedPath) {
|
|
16213
|
+
return [{ type: 'openFileInEditor', path: context.worktreeSelectedPath }];
|
|
16214
|
+
}
|
|
16215
|
+
if (inputValue === 'o' && state.activeView === 'diff' && state.diffSource === 'worktree' && context.worktreeSelectedPath) {
|
|
16216
|
+
return [{ type: 'openFileInEditor', path: context.worktreeSelectedPath }];
|
|
16217
|
+
}
|
|
16218
|
+
if (inputValue === 'o' && state.activeView === 'diff' && state.diffSource === 'stash' && context.stashDiffSelectedPath) {
|
|
16219
|
+
return [{ type: 'openFileInEditor', path: context.stashDiffSelectedPath }];
|
|
16220
|
+
}
|
|
16221
|
+
// `c` on a stash diff cherry-picks the file under the cursor —
|
|
16222
|
+
// materializes that single path from the stash into the working tree
|
|
16223
|
+
// (`git checkout <stashRef> -- <path>`). Routed through the y-confirm
|
|
16224
|
+
// path because the checkout overwrites the worktree file
|
|
16225
|
+
// unconditionally; the prompt is the user's chance to abort if they
|
|
16226
|
+
// have unsaved edits at that path.
|
|
16227
|
+
if (inputValue === 'c' &&
|
|
16228
|
+
state.activeView === 'diff' &&
|
|
16229
|
+
state.diffSource === 'stash' &&
|
|
16230
|
+
context.stashDiffSelectedPath &&
|
|
16231
|
+
state.stashDiffRef) {
|
|
16232
|
+
return [action({
|
|
16233
|
+
type: 'setPendingConfirmation',
|
|
16234
|
+
value: 'checkout-file-from-stash',
|
|
16235
|
+
payload: context.stashDiffSelectedPath,
|
|
16236
|
+
})];
|
|
16237
|
+
}
|
|
16238
|
+
// `c` on a commit-diff explore cherry-picks the cursored file from
|
|
16239
|
+
// that historical commit — `git checkout <sha> -- <path>`. Same
|
|
16240
|
+
// confirmation rationale as the stash variant. The payload encodes
|
|
16241
|
+
// both the sha and the path so the runtime handler doesn't have to
|
|
16242
|
+
// re-resolve either.
|
|
16243
|
+
if (inputValue === 'c' &&
|
|
16244
|
+
state.activeView === 'diff' &&
|
|
16245
|
+
state.diffSource === 'commit' &&
|
|
16246
|
+
context.commitDiffSelectedPath &&
|
|
16247
|
+
context.commitDiffSelectedSha) {
|
|
16248
|
+
return [action({
|
|
16249
|
+
type: 'setPendingConfirmation',
|
|
16250
|
+
value: 'checkout-file-from-commit',
|
|
16251
|
+
payload: `${context.commitDiffSelectedSha} ${context.commitDiffSelectedPath}`,
|
|
16252
|
+
})];
|
|
16253
|
+
}
|
|
16254
|
+
// `c` on the history view cherry-picks the full selected commit on
|
|
16255
|
+
// top of the current branch. Routed through the y-confirm flow since
|
|
16256
|
+
// it can produce conflicts and is a real working-tree mutation.
|
|
16257
|
+
if (inputValue === 'c' &&
|
|
16258
|
+
state.activeView === 'history' &&
|
|
16259
|
+
state.focus === 'commits' &&
|
|
16260
|
+
state.filteredCommits.length > 0 &&
|
|
16261
|
+
!state.pendingCommitFocused) {
|
|
16262
|
+
return [action({ type: 'setPendingConfirmation', value: 'cherry-pick-commit' })];
|
|
16263
|
+
}
|
|
16264
|
+
// Enter on a stash row pushes the diff view scoped to that stash.
|
|
16265
|
+
// The runtime loads `git stash show -p <ref>` once the view is
|
|
16266
|
+
// active. The stash ref is passed via the action so we don't need a
|
|
16267
|
+
// context lookup here.
|
|
16268
|
+
if (key.return && state.activeView === 'stash' && state.focus === 'commits' && context.stashCount && context.stashSelectedRef) {
|
|
16269
|
+
return [action({
|
|
16270
|
+
type: 'navigateOpenDiffForStash',
|
|
16271
|
+
ref: context.stashSelectedRef,
|
|
16272
|
+
stashIndex: state.selectedStashIndex,
|
|
16273
|
+
})];
|
|
16274
|
+
}
|
|
15927
16275
|
if (inputValue === ' ' && state.activeView === 'status' && context.worktreeFileCount) {
|
|
15928
16276
|
return [{ type: 'toggleSelectedFileStage' }];
|
|
15929
16277
|
}
|
|
@@ -15959,10 +16307,10 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
|
|
|
15959
16307
|
return [action({ type: 'setPendingConfirmation', value: workflowAction.id })];
|
|
15960
16308
|
}
|
|
15961
16309
|
if (workflowAction) {
|
|
15962
|
-
|
|
15963
|
-
|
|
15964
|
-
|
|
15965
|
-
];
|
|
16310
|
+
// Non-destructive workflow — fire it directly via the runtime
|
|
16311
|
+
// handler. The handler surfaces success/failure on the status line
|
|
16312
|
+
// and silently refreshes context so the list updates.
|
|
16313
|
+
return [{ type: 'runWorkflowAction', id: workflowAction.id }];
|
|
15966
16314
|
}
|
|
15967
16315
|
return [];
|
|
15968
16316
|
}
|
|
@@ -15978,7 +16326,7 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
|
|
|
15978
16326
|
* fall back to "already seen" so we never block startup.
|
|
15979
16327
|
*/
|
|
15980
16328
|
const MARKER_BASENAME = 'onboarding.seen';
|
|
15981
|
-
function resolveCacheDir() {
|
|
16329
|
+
function resolveCacheDir$1() {
|
|
15982
16330
|
const xdg = process.env.XDG_CACHE_HOME;
|
|
15983
16331
|
if (xdg && xdg.trim().length > 0) {
|
|
15984
16332
|
return path$1.join(xdg, 'coco');
|
|
@@ -15986,7 +16334,7 @@ function resolveCacheDir() {
|
|
|
15986
16334
|
return path$1.join(os$1.homedir(), '.cache', 'coco');
|
|
15987
16335
|
}
|
|
15988
16336
|
function getOnboardingMarkerPath() {
|
|
15989
|
-
return path$1.join(resolveCacheDir(), MARKER_BASENAME);
|
|
16337
|
+
return path$1.join(resolveCacheDir$1(), MARKER_BASENAME);
|
|
15990
16338
|
}
|
|
15991
16339
|
function hasSeenOnboarding() {
|
|
15992
16340
|
try {
|
|
@@ -16010,6 +16358,65 @@ function markOnboardingSeen() {
|
|
|
16010
16358
|
}
|
|
16011
16359
|
}
|
|
16012
16360
|
|
|
16361
|
+
/**
|
|
16362
|
+
* Persist which sidebar tab the user last had active, keyed per repo so
|
|
16363
|
+
* switching projects doesn't reset every other repo's preference. The
|
|
16364
|
+
* cache lives next to the onboarding marker (XDG-friendly) and is
|
|
16365
|
+
* best-effort: read/write failures fall back to the default sidebar
|
|
16366
|
+
* tab on next start.
|
|
16367
|
+
*
|
|
16368
|
+
* Repos are keyed by a short hash of their absolute path — no PII in
|
|
16369
|
+
* the cache filename, and re-creating a repo at the same path keeps
|
|
16370
|
+
* the same preference.
|
|
16371
|
+
*/
|
|
16372
|
+
const VALID_TABS = [
|
|
16373
|
+
'status',
|
|
16374
|
+
'branches',
|
|
16375
|
+
'tags',
|
|
16376
|
+
'stashes',
|
|
16377
|
+
'worktrees',
|
|
16378
|
+
];
|
|
16379
|
+
function resolveCacheDir() {
|
|
16380
|
+
const xdg = process.env.XDG_CACHE_HOME;
|
|
16381
|
+
if (xdg && xdg.trim().length > 0) {
|
|
16382
|
+
return path$1.join(xdg, 'coco');
|
|
16383
|
+
}
|
|
16384
|
+
return path$1.join(os$1.homedir(), '.cache', 'coco');
|
|
16385
|
+
}
|
|
16386
|
+
function repoKey(repoPath) {
|
|
16387
|
+
// sha1 is used here as a non-security cache-key derivation — we just
|
|
16388
|
+
// need a deterministic short identifier for the marker filename so
|
|
16389
|
+
// re-creating a repo at the same path keeps the same preference.
|
|
16390
|
+
// No PII or auth context is hashed; no collision-resistance against
|
|
16391
|
+
// an adversary is required. DevSkim DS126858 doesn't apply.
|
|
16392
|
+
// DevSkim: ignore DS126858
|
|
16393
|
+
return crypto.createHash('sha1').update(repoPath).digest('hex').slice(0, 16);
|
|
16394
|
+
}
|
|
16395
|
+
function getSidebarTabMarkerPath(repoPath) {
|
|
16396
|
+
return path$1.join(resolveCacheDir(), `sidebar-tab.${repoKey(repoPath)}`);
|
|
16397
|
+
}
|
|
16398
|
+
function getSavedSidebarTab(repoPath) {
|
|
16399
|
+
try {
|
|
16400
|
+
const raw = fs$1.readFileSync(getSidebarTabMarkerPath(repoPath), 'utf8').trim();
|
|
16401
|
+
return VALID_TABS.includes(raw)
|
|
16402
|
+
? raw
|
|
16403
|
+
: undefined;
|
|
16404
|
+
}
|
|
16405
|
+
catch {
|
|
16406
|
+
return undefined;
|
|
16407
|
+
}
|
|
16408
|
+
}
|
|
16409
|
+
function saveSidebarTab(repoPath, tab) {
|
|
16410
|
+
const marker = getSidebarTabMarkerPath(repoPath);
|
|
16411
|
+
try {
|
|
16412
|
+
fs$1.mkdirSync(path$1.dirname(marker), { recursive: true });
|
|
16413
|
+
fs$1.writeFileSync(marker, tab);
|
|
16414
|
+
}
|
|
16415
|
+
catch {
|
|
16416
|
+
// Best-effort persistence; swallow.
|
|
16417
|
+
}
|
|
16418
|
+
}
|
|
16419
|
+
|
|
16013
16420
|
/**
|
|
16014
16421
|
* Promoted-view selection rectification on filter changes (P4.5).
|
|
16015
16422
|
*
|
|
@@ -16263,7 +16670,12 @@ function getLogInkLayout(input) {
|
|
|
16263
16670
|
const columns = input.columns || LOG_INK_DEFAULT_COLUMNS;
|
|
16264
16671
|
const rows = input.rows || LOG_INK_DEFAULT_ROWS;
|
|
16265
16672
|
const detailWidth = Math.max(30, Math.min(56, Math.floor(columns * 0.34)));
|
|
16266
|
-
|
|
16673
|
+
// Sidebar at rest: 22-34 cells (~24% of width). Focused: 32-50 cells
|
|
16674
|
+
// (~36% of width). The transition is instant per render — focus tab to
|
|
16675
|
+
// expand, focus away to collapse.
|
|
16676
|
+
const sidebarWidth = input.sidebarFocused
|
|
16677
|
+
? Math.max(32, Math.min(50, Math.floor(columns * 0.36)))
|
|
16678
|
+
: Math.max(22, Math.min(34, Math.floor(columns * 0.24)));
|
|
16267
16679
|
return {
|
|
16268
16680
|
bodyRows: Math.max(8, rows - 5),
|
|
16269
16681
|
columns,
|
|
@@ -17003,9 +17415,19 @@ function withPushedView(state, value) {
|
|
|
17003
17415
|
...state,
|
|
17004
17416
|
activeView: value,
|
|
17005
17417
|
viewStack,
|
|
17418
|
+
// The compose + status views' right detail panels already show
|
|
17419
|
+
// worktree info, so keeping the left sidebar on the Status tab
|
|
17420
|
+
// duplicates that information. Auto-switch to Branches when entering
|
|
17421
|
+
// either view; the user can swap back with [/] if they want.
|
|
17422
|
+
//
|
|
17423
|
+
// We update only the rendered `sidebarTab` here, never
|
|
17424
|
+
// `userSidebarTab`, so this auto-switch is invisible to per-repo
|
|
17425
|
+
// persistence and pop-view restores the previous tab.
|
|
17426
|
+
sidebarTab: value === 'compose' || value === 'status' ? 'branches' : state.sidebarTab,
|
|
17006
17427
|
worktreeDiffOffset: value === 'diff' ? state.worktreeDiffOffset : 0,
|
|
17007
17428
|
selectedWorktreeHunkIndex: value === 'diff' ? state.selectedWorktreeHunkIndex : 0,
|
|
17008
17429
|
diffSource: value === 'diff' ? state.diffSource : undefined,
|
|
17430
|
+
stashDiffRef: value === 'diff' ? state.stashDiffRef : undefined,
|
|
17009
17431
|
pendingCommitFocused: value === 'history' ? state.pendingCommitFocused : false,
|
|
17010
17432
|
pendingKey: undefined,
|
|
17011
17433
|
};
|
|
@@ -17020,9 +17442,14 @@ function withPoppedView(state) {
|
|
|
17020
17442
|
...state,
|
|
17021
17443
|
activeView: next,
|
|
17022
17444
|
viewStack,
|
|
17445
|
+
// Restore the user's last explicit tab choice so popping out of
|
|
17446
|
+
// compose / status (which auto-switch the sidebar to Branches)
|
|
17447
|
+
// returns the user to whatever they actually had open before.
|
|
17448
|
+
sidebarTab: state.userSidebarTab,
|
|
17023
17449
|
worktreeDiffOffset: next === 'diff' ? state.worktreeDiffOffset : 0,
|
|
17024
17450
|
selectedWorktreeHunkIndex: next === 'diff' ? state.selectedWorktreeHunkIndex : 0,
|
|
17025
17451
|
diffSource: next === 'diff' ? state.diffSource : undefined,
|
|
17452
|
+
stashDiffRef: next === 'diff' ? state.stashDiffRef : undefined,
|
|
17026
17453
|
pendingCommitFocused: next === 'history' ? state.pendingCommitFocused : false,
|
|
17027
17454
|
pendingKey: undefined,
|
|
17028
17455
|
};
|
|
@@ -17039,6 +17466,7 @@ function withReplacedView(state, value) {
|
|
|
17039
17466
|
worktreeDiffOffset: value === 'diff' ? state.worktreeDiffOffset : 0,
|
|
17040
17467
|
selectedWorktreeHunkIndex: value === 'diff' ? state.selectedWorktreeHunkIndex : 0,
|
|
17041
17468
|
diffSource: value === 'diff' ? state.diffSource : undefined,
|
|
17469
|
+
stashDiffRef: value === 'diff' ? state.stashDiffRef : undefined,
|
|
17042
17470
|
pendingCommitFocused: value === 'history' ? state.pendingCommitFocused : false,
|
|
17043
17471
|
pendingKey: undefined,
|
|
17044
17472
|
};
|
|
@@ -17131,6 +17559,7 @@ function createLogInkState(rows, options = {}) {
|
|
|
17131
17559
|
selectedBranchIndex: 0,
|
|
17132
17560
|
selectedTagIndex: 0,
|
|
17133
17561
|
selectedStashIndex: 0,
|
|
17562
|
+
selectedWorktreeListIndex: 0,
|
|
17134
17563
|
branchSort: DEFAULT_BRANCH_SORT_MODE,
|
|
17135
17564
|
tagSort: DEFAULT_TAG_SORT_MODE,
|
|
17136
17565
|
paletteFilter: '',
|
|
@@ -17146,10 +17575,12 @@ function createLogInkState(rows, options = {}) {
|
|
|
17146
17575
|
showCommandPalette: false,
|
|
17147
17576
|
workflowActionId: undefined,
|
|
17148
17577
|
pendingConfirmationId: undefined,
|
|
17578
|
+
pendingConfirmationPayload: undefined,
|
|
17149
17579
|
pendingMutationConfirmation: undefined,
|
|
17150
17580
|
pendingKey: undefined,
|
|
17151
17581
|
focus: 'commits',
|
|
17152
17582
|
sidebarTab: 'status',
|
|
17583
|
+
userSidebarTab: 'status',
|
|
17153
17584
|
};
|
|
17154
17585
|
}
|
|
17155
17586
|
function getSelectedInkCommit(state) {
|
|
@@ -17256,6 +17687,12 @@ function applyLogInkAction(state, action) {
|
|
|
17256
17687
|
selectedStashIndex: clampIndex$1(state.selectedStashIndex + action.delta, action.count),
|
|
17257
17688
|
pendingKey: undefined,
|
|
17258
17689
|
};
|
|
17690
|
+
case 'moveWorktreeListEntry':
|
|
17691
|
+
return {
|
|
17692
|
+
...state,
|
|
17693
|
+
selectedWorktreeListIndex: clampIndex$1(state.selectedWorktreeListIndex + action.delta, action.count),
|
|
17694
|
+
pendingKey: undefined,
|
|
17695
|
+
};
|
|
17259
17696
|
case 'cycleBranchSort':
|
|
17260
17697
|
return {
|
|
17261
17698
|
...state,
|
|
@@ -17272,6 +17709,30 @@ function applyLogInkAction(state, action) {
|
|
|
17272
17709
|
selectedTagIndex: 0,
|
|
17273
17710
|
pendingKey: undefined,
|
|
17274
17711
|
};
|
|
17712
|
+
case 'openInputPrompt':
|
|
17713
|
+
return {
|
|
17714
|
+
...state,
|
|
17715
|
+
inputPrompt: {
|
|
17716
|
+
kind: action.kind,
|
|
17717
|
+
label: action.label,
|
|
17718
|
+
value: action.initial || '',
|
|
17719
|
+
},
|
|
17720
|
+
pendingKey: undefined,
|
|
17721
|
+
};
|
|
17722
|
+
case 'appendInputPrompt':
|
|
17723
|
+
return state.inputPrompt
|
|
17724
|
+
? { ...state, inputPrompt: { ...state.inputPrompt, value: `${state.inputPrompt.value}${action.value}` } }
|
|
17725
|
+
: state;
|
|
17726
|
+
case 'backspaceInputPrompt':
|
|
17727
|
+
return state.inputPrompt
|
|
17728
|
+
? { ...state, inputPrompt: { ...state.inputPrompt, value: state.inputPrompt.value.slice(0, -1) } }
|
|
17729
|
+
: state;
|
|
17730
|
+
case 'clearInputPromptText':
|
|
17731
|
+
return state.inputPrompt
|
|
17732
|
+
? { ...state, inputPrompt: { ...state.inputPrompt, value: '' } }
|
|
17733
|
+
: state;
|
|
17734
|
+
case 'closeInputPrompt':
|
|
17735
|
+
return { ...state, inputPrompt: undefined, pendingKey: undefined };
|
|
17275
17736
|
case 'moveToBottom':
|
|
17276
17737
|
return {
|
|
17277
17738
|
...state,
|
|
@@ -17290,12 +17751,15 @@ function applyLogInkAction(state, action) {
|
|
|
17290
17751
|
pendingCommitFocused: false,
|
|
17291
17752
|
pendingKey: undefined,
|
|
17292
17753
|
};
|
|
17293
|
-
case 'nextSidebarTab':
|
|
17754
|
+
case 'nextSidebarTab': {
|
|
17755
|
+
const next = cycleValue(SIDEBAR_TABS, state.sidebarTab, 1);
|
|
17294
17756
|
return {
|
|
17295
17757
|
...state,
|
|
17296
|
-
sidebarTab:
|
|
17758
|
+
sidebarTab: next,
|
|
17759
|
+
userSidebarTab: next,
|
|
17297
17760
|
pendingKey: undefined,
|
|
17298
17761
|
};
|
|
17762
|
+
}
|
|
17299
17763
|
case 'page':
|
|
17300
17764
|
return {
|
|
17301
17765
|
...state,
|
|
@@ -17329,12 +17793,15 @@ function applyLogInkAction(state, action) {
|
|
|
17329
17793
|
diffPreviewOffset: nextHunkOffset(state.diffPreviewOffset, action.hunkOffsets, action.delta),
|
|
17330
17794
|
pendingKey: undefined,
|
|
17331
17795
|
};
|
|
17332
|
-
case 'previousSidebarTab':
|
|
17796
|
+
case 'previousSidebarTab': {
|
|
17797
|
+
const previous = cycleValue(SIDEBAR_TABS, state.sidebarTab, -1);
|
|
17333
17798
|
return {
|
|
17334
17799
|
...state,
|
|
17335
|
-
sidebarTab:
|
|
17800
|
+
sidebarTab: previous,
|
|
17801
|
+
userSidebarTab: previous,
|
|
17336
17802
|
pendingKey: undefined,
|
|
17337
17803
|
};
|
|
17804
|
+
}
|
|
17338
17805
|
case 'setFilter':
|
|
17339
17806
|
return withFilter$1(state, action.value, action.promotedSelections);
|
|
17340
17807
|
case 'setActiveView':
|
|
@@ -17382,6 +17849,21 @@ function applyLogInkAction(state, action) {
|
|
|
17382
17849
|
diffSource: 'worktree',
|
|
17383
17850
|
};
|
|
17384
17851
|
}
|
|
17852
|
+
case 'navigateOpenDiffForStash': {
|
|
17853
|
+
const next = withPushedView(state, 'diff');
|
|
17854
|
+
return {
|
|
17855
|
+
...next,
|
|
17856
|
+
diffSource: 'stash',
|
|
17857
|
+
stashDiffRef: action.ref,
|
|
17858
|
+
selectedStashIndex: Math.max(0, action.stashIndex ?? state.selectedStashIndex),
|
|
17859
|
+
// Reset the diff scroll offset so the stash patch always opens
|
|
17860
|
+
// at the top, mirroring `navigateOpenDiffForCommit`. Without
|
|
17861
|
+
// this, opening a stash inherits whatever offset the previous
|
|
17862
|
+
// diff had, landing the user mid-patch.
|
|
17863
|
+
diffPreviewOffset: 0,
|
|
17864
|
+
worktreeDiffOffset: 0,
|
|
17865
|
+
};
|
|
17866
|
+
}
|
|
17385
17867
|
case 'navigateOpenComposeForFile': {
|
|
17386
17868
|
const next = withPushedView(state, 'status');
|
|
17387
17869
|
return {
|
|
@@ -17406,9 +17888,22 @@ function applyLogInkAction(state, action) {
|
|
|
17406
17888
|
return {
|
|
17407
17889
|
...state,
|
|
17408
17890
|
sidebarTab: action.value,
|
|
17891
|
+
userSidebarTab: action.value,
|
|
17409
17892
|
focus: 'sidebar',
|
|
17410
17893
|
pendingKey: undefined,
|
|
17411
17894
|
};
|
|
17895
|
+
case 'restoreSidebarTab':
|
|
17896
|
+
// Mount-time restore from per-repo persistence (#21). Updates the
|
|
17897
|
+
// tab + the user-choice mirror without forcing focus into the
|
|
17898
|
+
// sidebar — that's the focus-steal regression flagged in the PR
|
|
17899
|
+
// review. Users land on commits as usual; their saved tab is
|
|
17900
|
+
// visible in the sidebar but doesn't grab the cursor.
|
|
17901
|
+
return {
|
|
17902
|
+
...state,
|
|
17903
|
+
sidebarTab: action.value,
|
|
17904
|
+
userSidebarTab: action.value,
|
|
17905
|
+
pendingKey: undefined,
|
|
17906
|
+
};
|
|
17412
17907
|
case 'setStatus':
|
|
17413
17908
|
return {
|
|
17414
17909
|
...state,
|
|
@@ -17420,6 +17915,7 @@ function applyLogInkAction(state, action) {
|
|
|
17420
17915
|
...state,
|
|
17421
17916
|
workflowActionId: action.value,
|
|
17422
17917
|
pendingConfirmationId: undefined,
|
|
17918
|
+
pendingConfirmationPayload: undefined,
|
|
17423
17919
|
pendingMutationConfirmation: undefined,
|
|
17424
17920
|
pendingKey: undefined,
|
|
17425
17921
|
};
|
|
@@ -17427,6 +17923,7 @@ function applyLogInkAction(state, action) {
|
|
|
17427
17923
|
return {
|
|
17428
17924
|
...state,
|
|
17429
17925
|
pendingConfirmationId: action.value,
|
|
17926
|
+
pendingConfirmationPayload: action.value ? action.payload : undefined,
|
|
17430
17927
|
workflowActionId: action.value ? undefined : state.workflowActionId,
|
|
17431
17928
|
pendingMutationConfirmation: action.value ? undefined : state.pendingMutationConfirmation,
|
|
17432
17929
|
pendingKey: undefined,
|
|
@@ -17436,6 +17933,7 @@ function applyLogInkAction(state, action) {
|
|
|
17436
17933
|
...state,
|
|
17437
17934
|
pendingMutationConfirmation: action.value,
|
|
17438
17935
|
pendingConfirmationId: action.value ? undefined : state.pendingConfirmationId,
|
|
17936
|
+
pendingConfirmationPayload: action.value ? undefined : state.pendingConfirmationPayload,
|
|
17439
17937
|
workflowActionId: action.value ? undefined : state.workflowActionId,
|
|
17440
17938
|
pendingKey: undefined,
|
|
17441
17939
|
};
|
|
@@ -18257,6 +18755,36 @@ function cherryPickCommit(git, commit) {
|
|
|
18257
18755
|
}
|
|
18258
18756
|
return guardNoInProgressOperation(git).then((blocked) => (blocked || runAction$5(() => git.raw(['cherry-pick', commit.hash]), `Cherry-picked ${commit.shortHash}`)));
|
|
18259
18757
|
}
|
|
18758
|
+
/**
|
|
18759
|
+
* Materialize a single file's contents from a historical commit into the
|
|
18760
|
+
* working tree, leaving every other path untouched. Equivalent to
|
|
18761
|
+
* `git checkout <sha> -- <path>` for additions/modifications. When the
|
|
18762
|
+
* path no longer exists at <sha> (i.e. the commit deleted that file),
|
|
18763
|
+
* mirror the deletion in the worktree via `git rm --force`.
|
|
18764
|
+
*
|
|
18765
|
+
* Important: this overwrites the file in the working tree. The caller
|
|
18766
|
+
* is responsible for confirming with the user when the working tree
|
|
18767
|
+
* already has uncommitted changes to that path.
|
|
18768
|
+
*/
|
|
18769
|
+
async function checkoutFileFromCommit(git, sha, path) {
|
|
18770
|
+
return checkoutOrDeleteFromRef(git, sha, path, sha.slice(0, 7));
|
|
18771
|
+
}
|
|
18772
|
+
async function checkoutOrDeleteFromRef(git, ref, path, label) {
|
|
18773
|
+
const exists = await pathExistsAtRef(git, ref, path);
|
|
18774
|
+
if (exists) {
|
|
18775
|
+
return runAction$5(() => git.raw(['checkout', ref, '--', path]), `Checked out ${path} from ${label}`);
|
|
18776
|
+
}
|
|
18777
|
+
return runAction$5(() => git.raw(['rm', '--force', '--quiet', '--', path]), `Removed ${path} (mirrors deletion from ${label})`);
|
|
18778
|
+
}
|
|
18779
|
+
async function pathExistsAtRef(git, ref, path) {
|
|
18780
|
+
try {
|
|
18781
|
+
await git.raw(['cat-file', '-e', `${ref}:${path}`]);
|
|
18782
|
+
return true;
|
|
18783
|
+
}
|
|
18784
|
+
catch {
|
|
18785
|
+
return false;
|
|
18786
|
+
}
|
|
18787
|
+
}
|
|
18260
18788
|
function revertCommit(git, commit) {
|
|
18261
18789
|
if (!commit) {
|
|
18262
18790
|
return Promise.resolve({
|
|
@@ -18406,6 +18934,20 @@ function popStash(git, stash) {
|
|
|
18406
18934
|
function dropStash(git, stash) {
|
|
18407
18935
|
return runAction$4(() => git.raw(['stash', 'drop', stash.ref]), `Dropped ${stash.ref}`);
|
|
18408
18936
|
}
|
|
18937
|
+
/**
|
|
18938
|
+
* Materialize a single file's contents from a stash into the working
|
|
18939
|
+
* tree, leaving the rest of the stash untouched. Equivalent to
|
|
18940
|
+
* `git checkout <stashRef> -- <path>` for additions/modifications. When
|
|
18941
|
+
* the path doesn't exist at <stashRef> — i.e. the stash recorded a
|
|
18942
|
+
* deletion — mirror that deletion in the worktree.
|
|
18943
|
+
*
|
|
18944
|
+
* Important: this overwrites the file in the working tree. The caller
|
|
18945
|
+
* is responsible for confirming with the user when the working tree
|
|
18946
|
+
* already has uncommitted changes to that path.
|
|
18947
|
+
*/
|
|
18948
|
+
function checkoutFileFromStash(git, stashRef, path) {
|
|
18949
|
+
return checkoutOrDeleteFromRef(git, stashRef, path, stashRef);
|
|
18950
|
+
}
|
|
18409
18951
|
|
|
18410
18952
|
function parseStashSubject(subject) {
|
|
18411
18953
|
const match = subject.match(/^(?:WIP on|On) ([^:]+):\s*(.*)$/);
|
|
@@ -18458,6 +19000,68 @@ async function getStashDiffSummary(git, stashRef) {
|
|
|
18458
19000
|
.map((line) => line.trimEnd())
|
|
18459
19001
|
.filter(Boolean);
|
|
18460
19002
|
}
|
|
19003
|
+
/**
|
|
19004
|
+
* Full unified-patch diff for a stash. Used by the diff surface when
|
|
19005
|
+
* `state.diffSource === 'stash'` to render the stash's changes inline.
|
|
19006
|
+
*
|
|
19007
|
+
* Empty stashes (e.g. created by `git stash --keep-index` against an
|
|
19008
|
+
* already-clean tree) return [] rather than throwing — surfaces fall
|
|
19009
|
+
* back to a "no diff to display" message.
|
|
19010
|
+
*/
|
|
19011
|
+
async function getStashDiff(git, stashRef) {
|
|
19012
|
+
return (await git.raw(['stash', 'show', '-p', stashRef]))
|
|
19013
|
+
.split('\n')
|
|
19014
|
+
.map((line) => line.replace(/\r$/, ''));
|
|
19015
|
+
}
|
|
19016
|
+
/**
|
|
19017
|
+
* Slice a unified-patch into per-file sections. Each entry records the
|
|
19018
|
+
* file path and the offset of its `diff --git` header within `lines`.
|
|
19019
|
+
* Used by the stash explorer to build a per-file cursor + cherry-pick
|
|
19020
|
+
* the file at the cursor.
|
|
19021
|
+
*
|
|
19022
|
+
* Renames / moves return the destination path (the `b/` side); the
|
|
19023
|
+
* action surface treats that as the path to materialize from the stash.
|
|
19024
|
+
*
|
|
19025
|
+
* Path quoting: git wraps paths containing spaces or special characters
|
|
19026
|
+
* in double-quotes (`diff --git "a/path with spaces" "b/path with spaces"`).
|
|
19027
|
+
* The parser handles both the unquoted and quoted forms; without that,
|
|
19028
|
+
* stash-file navigation and cherry-pick silently broke for any file
|
|
19029
|
+
* whose path contained a space.
|
|
19030
|
+
*/
|
|
19031
|
+
function parseStashDiffFiles(lines) {
|
|
19032
|
+
const files = [];
|
|
19033
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
19034
|
+
const line = lines[i];
|
|
19035
|
+
const parsed = parseDiffGitHeader(line);
|
|
19036
|
+
if (parsed) {
|
|
19037
|
+
files.push({ path: parsed.bPath || parsed.aPath, startLine: i });
|
|
19038
|
+
}
|
|
19039
|
+
}
|
|
19040
|
+
return files;
|
|
19041
|
+
}
|
|
19042
|
+
const DIFF_GIT_HEADER = /^diff --git (?:"a\/((?:\\.|[^"\\])+)"|a\/(\S+)) (?:"b\/((?:\\.|[^"\\])+)"|b\/(\S+))$/;
|
|
19043
|
+
function parseDiffGitHeader(line) {
|
|
19044
|
+
const match = line.match(DIFF_GIT_HEADER);
|
|
19045
|
+
if (!match)
|
|
19046
|
+
return undefined;
|
|
19047
|
+
const aPath = unescapeGitQuoted(match[1]) || match[2];
|
|
19048
|
+
const bPath = unescapeGitQuoted(match[3]) || match[4];
|
|
19049
|
+
if (!aPath || !bPath)
|
|
19050
|
+
return undefined;
|
|
19051
|
+
return { aPath, bPath };
|
|
19052
|
+
}
|
|
19053
|
+
function unescapeGitQuoted(value) {
|
|
19054
|
+
if (value === undefined)
|
|
19055
|
+
return undefined;
|
|
19056
|
+
// Git's diff header quoting escapes `"`, `\`, and the usual
|
|
19057
|
+
// C-style sequences. Reverse the most common ones so callers get the
|
|
19058
|
+
// raw on-disk path.
|
|
19059
|
+
return value
|
|
19060
|
+
.replace(/\\\\/g, '\\')
|
|
19061
|
+
.replace(/\\"/g, '"')
|
|
19062
|
+
.replace(/\\t/g, '\t')
|
|
19063
|
+
.replace(/\\n/g, '\n');
|
|
19064
|
+
}
|
|
18461
19065
|
|
|
18462
19066
|
async function runAction$3(action, successMessage) {
|
|
18463
19067
|
try {
|
|
@@ -21062,10 +21666,6 @@ function LogInkApp(deps) {
|
|
|
21062
21666
|
const h = React.createElement;
|
|
21063
21667
|
const { exit } = useApp();
|
|
21064
21668
|
const windowSize = useWindowSize();
|
|
21065
|
-
const layout = getLogInkLayout({
|
|
21066
|
-
columns: windowSize.columns || process.stdout.columns || LOG_INK_DEFAULT_COLUMNS,
|
|
21067
|
-
rows: windowSize.rows || process.stdout.rows || LOG_INK_DEFAULT_ROWS,
|
|
21068
|
-
});
|
|
21069
21669
|
// Bumping this on SIGCONT forces the existing tree to repaint so users
|
|
21070
21670
|
// land on a drawn screen after `fg` instead of an empty alt buffer.
|
|
21071
21671
|
const [, setResumeTick] = React.useState(0);
|
|
@@ -21093,6 +21693,13 @@ function LogInkApp(deps) {
|
|
|
21093
21693
|
const [worktreeDiffLoading, setWorktreeDiffLoading] = React.useState(false);
|
|
21094
21694
|
const [worktreeHunks, setWorktreeHunks] = React.useState(undefined);
|
|
21095
21695
|
const [worktreeHunksLoading, setWorktreeHunksLoading] = React.useState(false);
|
|
21696
|
+
// Stash diff explorer (Enter on a stash row): the runtime fetches
|
|
21697
|
+
// `git stash show -p <ref>` lazily once the diff view becomes active
|
|
21698
|
+
// with diffSource='stash'. Lines are stored as a flat string[] —
|
|
21699
|
+
// renderDiffSurface paints each line through diffLineProps so +/-
|
|
21700
|
+
// colors match the commit-diff path.
|
|
21701
|
+
const [stashDiffLines, setStashDiffLines] = React.useState(undefined);
|
|
21702
|
+
const [stashDiffLoading, setStashDiffLoading] = React.useState(false);
|
|
21096
21703
|
const [hasMoreCommits, setHasMoreCommits] = React.useState(() => (Boolean(logArgv?.interactive && !logArgv.limit) &&
|
|
21097
21704
|
getCommitRows(rows).length >= LOG_INTERACTIVE_DEFAULT_LIMIT));
|
|
21098
21705
|
const [loadingMoreCommits, setLoadingMoreCommits] = React.useState(false);
|
|
@@ -21137,6 +21744,36 @@ function LogInkApp(deps) {
|
|
|
21137
21744
|
const dispatch = React.useCallback((action) => {
|
|
21138
21745
|
setState((current) => applyLogInkAction(current, action));
|
|
21139
21746
|
}, []);
|
|
21747
|
+
// Auto-dismiss status messages after a short window so transient
|
|
21748
|
+
// confirmations ("Pulled current branch", "Edited foo.ts") don't
|
|
21749
|
+
// linger forever. Each new message resets the timer; clearing the
|
|
21750
|
+
// message via setStatus(undefined) cancels it. Doesn't fire while a
|
|
21751
|
+
// modal (input prompt, confirmation, palette) is open — those flows
|
|
21752
|
+
// use the status line as live feedback for the open task.
|
|
21753
|
+
React.useEffect(() => {
|
|
21754
|
+
if (!state.statusMessage)
|
|
21755
|
+
return;
|
|
21756
|
+
if (state.inputPrompt || state.pendingConfirmationId || state.pendingMutationConfirmation || state.showCommandPalette) {
|
|
21757
|
+
return;
|
|
21758
|
+
}
|
|
21759
|
+
// The `setTimeout` callback is a literal arrow function (not a
|
|
21760
|
+
// string), and the delay is a hard-coded constant, so the
|
|
21761
|
+
// eval-injection vector behind DevSkim DS172411 doesn't apply here.
|
|
21762
|
+
// DevSkim: ignore DS172411
|
|
21763
|
+
const handle = setTimeout(() => {
|
|
21764
|
+
if (mountedRef.current) {
|
|
21765
|
+
dispatch({ type: 'setStatus', value: undefined });
|
|
21766
|
+
}
|
|
21767
|
+
}, 4000);
|
|
21768
|
+
return () => clearTimeout(handle);
|
|
21769
|
+
}, [
|
|
21770
|
+
dispatch,
|
|
21771
|
+
state.inputPrompt,
|
|
21772
|
+
state.pendingConfirmationId,
|
|
21773
|
+
state.pendingMutationConfirmation,
|
|
21774
|
+
state.showCommandPalette,
|
|
21775
|
+
state.statusMessage,
|
|
21776
|
+
]);
|
|
21140
21777
|
const refreshContext = React.useCallback(async (options = {}) => {
|
|
21141
21778
|
// Loud refresh (manual `r`): flip everything to 'loading' so the user
|
|
21142
21779
|
// sees the surfaces clear, then settle to 'ready' on completion.
|
|
@@ -21216,6 +21853,61 @@ function LogInkApp(deps) {
|
|
|
21216
21853
|
watcher?.close();
|
|
21217
21854
|
};
|
|
21218
21855
|
}, [git, refreshContext, refreshWorktreeContext]);
|
|
21856
|
+
// Per-repo sidebar tab persistence (#21). Resolve the repo root, look
|
|
21857
|
+
// up the cached tab, and dispatch `restoreSidebarTab` once on mount so
|
|
21858
|
+
// the user lands on whichever tab they were last on for this project.
|
|
21859
|
+
// `restoreSidebarTab` (vs `setSidebarTab`) intentionally does not pull
|
|
21860
|
+
// focus into the sidebar — the user lands on commits, the saved tab
|
|
21861
|
+
// is just visible in the gutter.
|
|
21862
|
+
//
|
|
21863
|
+
// The save effect listens to `userSidebarTab` (the user's explicit
|
|
21864
|
+
// choice mirror), not `sidebarTab`. That way the auto-switch to
|
|
21865
|
+
// Branches when entering compose / status doesn't overwrite the saved
|
|
21866
|
+
// preference.
|
|
21867
|
+
const repoRootRef = React.useRef(undefined);
|
|
21868
|
+
React.useEffect(() => {
|
|
21869
|
+
let cancelled = false;
|
|
21870
|
+
void (async () => {
|
|
21871
|
+
try {
|
|
21872
|
+
const repoRoot = (await git.revparse(['--show-toplevel'])).trim();
|
|
21873
|
+
if (cancelled || !repoRoot)
|
|
21874
|
+
return;
|
|
21875
|
+
repoRootRef.current = repoRoot;
|
|
21876
|
+
const saved = getSavedSidebarTab(repoRoot);
|
|
21877
|
+
if (saved && saved !== state.userSidebarTab) {
|
|
21878
|
+
dispatch({ type: 'restoreSidebarTab', value: saved });
|
|
21879
|
+
}
|
|
21880
|
+
}
|
|
21881
|
+
catch {
|
|
21882
|
+
// Not in a worktree, or revparse failed; nothing to restore.
|
|
21883
|
+
}
|
|
21884
|
+
})();
|
|
21885
|
+
return () => { cancelled = true; };
|
|
21886
|
+
}, [git, dispatch]);
|
|
21887
|
+
React.useEffect(() => {
|
|
21888
|
+
const repoRoot = repoRootRef.current;
|
|
21889
|
+
if (!repoRoot)
|
|
21890
|
+
return;
|
|
21891
|
+
saveSidebarTab(repoRoot, state.userSidebarTab);
|
|
21892
|
+
}, [state.userSidebarTab]);
|
|
21893
|
+
// P-stash-explorer: load `git stash show -p <ref>` once the diff view
|
|
21894
|
+
// becomes active with diffSource='stash'. Best-effort — empty stashes
|
|
21895
|
+
// or read errors fall through to a "no diff" hint at the render site.
|
|
21896
|
+
React.useEffect(() => {
|
|
21897
|
+
if (state.activeView !== 'diff' || state.diffSource !== 'stash' || !state.stashDiffRef) {
|
|
21898
|
+
return;
|
|
21899
|
+
}
|
|
21900
|
+
let active = true;
|
|
21901
|
+
setStashDiffLoading(true);
|
|
21902
|
+
void (async () => {
|
|
21903
|
+
const lines = await safe(getStashDiff(git, state.stashDiffRef));
|
|
21904
|
+
if (active) {
|
|
21905
|
+
setStashDiffLines(lines || []);
|
|
21906
|
+
setStashDiffLoading(false);
|
|
21907
|
+
}
|
|
21908
|
+
})();
|
|
21909
|
+
return () => { active = false; };
|
|
21910
|
+
}, [git, state.activeView, state.diffSource, state.stashDiffRef]);
|
|
21219
21911
|
React.useEffect(() => {
|
|
21220
21912
|
let active = true;
|
|
21221
21913
|
async function loadWorktreeHunks() {
|
|
@@ -21426,13 +22118,96 @@ function LogInkApp(deps) {
|
|
|
21426
22118
|
});
|
|
21427
22119
|
dispatch({ type: 'setStatus', value: result.message });
|
|
21428
22120
|
}, [dispatch]);
|
|
22121
|
+
// Open a file in $EDITOR (or $VISUAL) by suspending Ink's hold on the
|
|
22122
|
+
// terminal, spawning the editor synchronously inheriting stdio, then
|
|
22123
|
+
// restoring the alt screen + raw mode and forcing a re-render. The
|
|
22124
|
+
// dance mirrors the SIGTSTP / SIGCONT path in inkTerminalLifecycle.
|
|
22125
|
+
// Falls back to vi when neither env var is set; surfaces a status
|
|
22126
|
+
// message on missing-binary / non-zero exit so the user isn't left
|
|
22127
|
+
// wondering.
|
|
22128
|
+
const openInEditor = React.useCallback((path) => {
|
|
22129
|
+
if (!path)
|
|
22130
|
+
return;
|
|
22131
|
+
const editorEnv = process.env.VISUAL || process.env.EDITOR || 'vi';
|
|
22132
|
+
// $VISUAL / $EDITOR commonly include flags (`code -w`, `vim -f`,
|
|
22133
|
+
// `emacs -nw`). Tokenize on whitespace so the leading word is the
|
|
22134
|
+
// executable and the rest are passed as arguments — passing the
|
|
22135
|
+
// full string to spawnSync as the executable would fail with
|
|
22136
|
+
// ENOENT for any of those configurations.
|
|
22137
|
+
const editorArgs = editorEnv.trim().split(/\s+/).filter(Boolean);
|
|
22138
|
+
const editor = editorArgs[0] || 'vi';
|
|
22139
|
+
const editorPrefixArgs = editorArgs.slice(1);
|
|
22140
|
+
const out = process.stdout;
|
|
22141
|
+
const stdin = process.stdin;
|
|
22142
|
+
const ENTER_ALT = '\x1b[?1049h';
|
|
22143
|
+
const EXIT_ALT = '\x1b[?1049l';
|
|
22144
|
+
const SHOW_CURSOR = '\x1b[?25h';
|
|
22145
|
+
const HIDE_CURSOR = '\x1b[?25l';
|
|
22146
|
+
try {
|
|
22147
|
+
// Drop into the primary buffer + cooked mode so the editor
|
|
22148
|
+
// doesn't inherit our raw-mode keystrokes.
|
|
22149
|
+
stdin.setRawMode?.(false);
|
|
22150
|
+
out.write(`${SHOW_CURSOR}${EXIT_ALT}`);
|
|
22151
|
+
const result = spawnSync(editor, [...editorPrefixArgs, path], { stdio: 'inherit' });
|
|
22152
|
+
if (result.error) {
|
|
22153
|
+
dispatch({ type: 'setStatus', value: `Failed to launch ${editor}: ${result.error.message}` });
|
|
22154
|
+
}
|
|
22155
|
+
else if (result.signal) {
|
|
22156
|
+
// Editor was killed by a signal (e.g. ^C, SIGTERM). status is
|
|
22157
|
+
// null in this case, so the old `status !== 0` check would
|
|
22158
|
+
// mistakenly fall through to the success branch.
|
|
22159
|
+
dispatch({ type: 'setStatus', value: `${editor} interrupted by ${result.signal}` });
|
|
22160
|
+
}
|
|
22161
|
+
else if (typeof result.status === 'number' && result.status !== 0) {
|
|
22162
|
+
dispatch({ type: 'setStatus', value: `${editor} exited with status ${result.status}` });
|
|
22163
|
+
}
|
|
22164
|
+
else {
|
|
22165
|
+
dispatch({ type: 'setStatus', value: `Edited ${path}` });
|
|
22166
|
+
}
|
|
22167
|
+
}
|
|
22168
|
+
finally {
|
|
22169
|
+
// Re-enter the alt screen + raw mode + hidden cursor; nudge React
|
|
22170
|
+
// so the freshly-restored screen actually paints.
|
|
22171
|
+
out.write(`${ENTER_ALT}${HIDE_CURSOR}`);
|
|
22172
|
+
stdin.setRawMode?.(true);
|
|
22173
|
+
resumeRef?.current?.();
|
|
22174
|
+
}
|
|
22175
|
+
// Worktree status may have changed (e.g. user saved an edit) — silent
|
|
22176
|
+
// refresh so the file row reflects the new staged/unstaged state.
|
|
22177
|
+
void refreshWorktreeContext({ silent: true });
|
|
22178
|
+
}, [dispatch, refreshWorktreeContext, resumeRef]);
|
|
21429
22179
|
// Resolve the destructive-action target from the live filtered+sorted
|
|
21430
22180
|
// list the user is looking at, run the action against it, surface the
|
|
21431
22181
|
// result on the status line, and silently refresh so the deleted item
|
|
21432
22182
|
// disappears. Called from the y-confirm path for delete-branch / delete-
|
|
21433
22183
|
// tag / drop-stash / remove-worktree / abort-operation.
|
|
21434
|
-
const runWorkflowAction = React.useCallback(async (id) => {
|
|
22184
|
+
const runWorkflowAction = React.useCallback(async (id, payload) => {
|
|
21435
22185
|
const handlers = {
|
|
22186
|
+
'create-branch': async () => {
|
|
22187
|
+
const name = payload?.trim();
|
|
22188
|
+
if (!name)
|
|
22189
|
+
return { ok: false, message: 'Branch name required' };
|
|
22190
|
+
const startPoint = context.branches?.currentBranch || 'HEAD';
|
|
22191
|
+
return createBranch(git, name, startPoint);
|
|
22192
|
+
},
|
|
22193
|
+
'create-tag': async () => {
|
|
22194
|
+
const name = payload?.trim();
|
|
22195
|
+
if (!name)
|
|
22196
|
+
return { ok: false, message: 'Tag name required' };
|
|
22197
|
+
return createLightweightTag(git, name, 'HEAD');
|
|
22198
|
+
},
|
|
22199
|
+
'checkout-branch': async () => {
|
|
22200
|
+
const all = sortBranches(context.branches?.localBranches || [], state.branchSort);
|
|
22201
|
+
const visible = state.filter
|
|
22202
|
+
? all.filter((b) => matchesPromotedFilter([b.shortName, b.upstream || ''], state.filter))
|
|
22203
|
+
: all;
|
|
22204
|
+
const branch = visible[Math.min(state.selectedBranchIndex, visible.length - 1)];
|
|
22205
|
+
if (!branch)
|
|
22206
|
+
return { ok: false, message: 'No branch selected' };
|
|
22207
|
+
if (branch.current)
|
|
22208
|
+
return { ok: true, message: `Already on ${branch.shortName}` };
|
|
22209
|
+
return checkoutBranch(git, branch);
|
|
22210
|
+
},
|
|
21436
22211
|
'delete-branch': async () => {
|
|
21437
22212
|
const all = sortBranches(context.branches?.localBranches || [], state.branchSort);
|
|
21438
22213
|
const visible = state.filter
|
|
@@ -21453,6 +22228,16 @@ function LogInkApp(deps) {
|
|
|
21453
22228
|
return { ok: false, message: 'No tag selected' };
|
|
21454
22229
|
return deleteLocalTag(git, tag.name);
|
|
21455
22230
|
},
|
|
22231
|
+
'push-tag': async () => {
|
|
22232
|
+
const all = sortTags(context.tags?.tags || [], state.tagSort);
|
|
22233
|
+
const visible = state.filter
|
|
22234
|
+
? all.filter((t) => matchesPromotedFilter([t.name, t.subject], state.filter))
|
|
22235
|
+
: all;
|
|
22236
|
+
const tag = visible[Math.min(state.selectedTagIndex, visible.length - 1)];
|
|
22237
|
+
if (!tag)
|
|
22238
|
+
return { ok: false, message: 'No tag selected' };
|
|
22239
|
+
return pushTag(git, tag.name);
|
|
22240
|
+
},
|
|
21456
22241
|
'drop-stash': async () => {
|
|
21457
22242
|
const all = context.stashes?.stashes || [];
|
|
21458
22243
|
const visible = state.filter
|
|
@@ -21463,14 +22248,82 @@ function LogInkApp(deps) {
|
|
|
21463
22248
|
return { ok: false, message: 'No stash selected' };
|
|
21464
22249
|
return dropStash(git, stash);
|
|
21465
22250
|
},
|
|
22251
|
+
'apply-stash': async () => {
|
|
22252
|
+
const all = context.stashes?.stashes || [];
|
|
22253
|
+
const visible = state.filter
|
|
22254
|
+
? all.filter((s) => matchesPromotedFilter([s.ref, s.message], state.filter))
|
|
22255
|
+
: all;
|
|
22256
|
+
const stash = visible[Math.min(state.selectedStashIndex, visible.length - 1)];
|
|
22257
|
+
if (!stash)
|
|
22258
|
+
return { ok: false, message: 'No stash selected' };
|
|
22259
|
+
return applyStash(git, stash);
|
|
22260
|
+
},
|
|
22261
|
+
'pop-stash': async () => {
|
|
22262
|
+
const all = context.stashes?.stashes || [];
|
|
22263
|
+
const visible = state.filter
|
|
22264
|
+
? all.filter((s) => matchesPromotedFilter([s.ref, s.message], state.filter))
|
|
22265
|
+
: all;
|
|
22266
|
+
const stash = visible[Math.min(state.selectedStashIndex, visible.length - 1)];
|
|
22267
|
+
if (!stash)
|
|
22268
|
+
return { ok: false, message: 'No stash selected' };
|
|
22269
|
+
return popStash(git, stash);
|
|
22270
|
+
},
|
|
22271
|
+
'checkout-file-from-stash': async () => {
|
|
22272
|
+
const path = payload?.trim();
|
|
22273
|
+
const ref = state.stashDiffRef;
|
|
22274
|
+
if (!path)
|
|
22275
|
+
return { ok: false, message: 'No stash file under cursor' };
|
|
22276
|
+
if (!ref)
|
|
22277
|
+
return { ok: false, message: 'No stash ref active' };
|
|
22278
|
+
return checkoutFileFromStash(git, ref, path);
|
|
22279
|
+
},
|
|
22280
|
+
'cherry-pick-commit': async () => {
|
|
22281
|
+
const commit = getSelectedInkCommit(state);
|
|
22282
|
+
if (!commit)
|
|
22283
|
+
return { ok: false, message: 'No commit selected' };
|
|
22284
|
+
return cherryPickCommit(git, {
|
|
22285
|
+
hash: commit.hash,
|
|
22286
|
+
shortHash: commit.shortHash,
|
|
22287
|
+
message: commit.message,
|
|
22288
|
+
});
|
|
22289
|
+
},
|
|
22290
|
+
'checkout-file-from-commit': async () => {
|
|
22291
|
+
// payload is "<sha> <path>" so we pass both through a single
|
|
22292
|
+
// string field on the action.
|
|
22293
|
+
const trimmed = payload?.trim();
|
|
22294
|
+
if (!trimmed)
|
|
22295
|
+
return { ok: false, message: 'No commit file under cursor' };
|
|
22296
|
+
const spaceIndex = trimmed.indexOf(' ');
|
|
22297
|
+
if (spaceIndex < 0)
|
|
22298
|
+
return { ok: false, message: 'Malformed commit file payload' };
|
|
22299
|
+
const sha = trimmed.slice(0, spaceIndex);
|
|
22300
|
+
const path = trimmed.slice(spaceIndex + 1);
|
|
22301
|
+
if (!sha || !path)
|
|
22302
|
+
return { ok: false, message: 'No commit file under cursor' };
|
|
22303
|
+
return checkoutFileFromCommit(git, sha, path);
|
|
22304
|
+
},
|
|
21466
22305
|
'remove-worktree': async () => {
|
|
21467
22306
|
const all = context.worktreeList?.worktrees || [];
|
|
21468
|
-
//
|
|
21469
|
-
//
|
|
21470
|
-
|
|
21471
|
-
|
|
21472
|
-
|
|
21473
|
-
|
|
22307
|
+
// Resolve the target from the visible (filtered) list so a
|
|
22308
|
+
// hidden filtered-out worktree can never be the action target.
|
|
22309
|
+
// Falls back to the cursor against the unfiltered list when the
|
|
22310
|
+
// action is invoked from the palette without ever visiting the
|
|
22311
|
+
// worktrees view.
|
|
22312
|
+
const visible = state.filter
|
|
22313
|
+
? all.filter((w) => matchesPromotedFilter([w.path, w.branch || ''], state.filter))
|
|
22314
|
+
: all;
|
|
22315
|
+
const cursorTarget = visible.length
|
|
22316
|
+
? visible[Math.min(state.selectedWorktreeListIndex, visible.length - 1)]
|
|
22317
|
+
: all[Math.min(state.selectedWorktreeListIndex, Math.max(0, all.length - 1))];
|
|
22318
|
+
if (!cursorTarget)
|
|
22319
|
+
return { ok: false, message: 'No worktree selected' };
|
|
22320
|
+
if (cursorTarget.current) {
|
|
22321
|
+
return {
|
|
22322
|
+
ok: false,
|
|
22323
|
+
message: 'Cannot remove the current worktree — switch to another worktree first.',
|
|
22324
|
+
};
|
|
22325
|
+
}
|
|
22326
|
+
return removeWorktree(git, cursorTarget);
|
|
21474
22327
|
},
|
|
21475
22328
|
'abort-operation': async () => {
|
|
21476
22329
|
const operation = context.operation?.operation;
|
|
@@ -21479,6 +22332,64 @@ function LogInkApp(deps) {
|
|
|
21479
22332
|
}
|
|
21480
22333
|
return abortOperation(git, operation);
|
|
21481
22334
|
},
|
|
22335
|
+
'open-pr': async () => {
|
|
22336
|
+
const repo = context.provider?.repository;
|
|
22337
|
+
if (!repo || repo.provider !== 'github' || !repo.owner || !repo.name) {
|
|
22338
|
+
return { ok: false, message: 'No GitHub remote detected for this repo' };
|
|
22339
|
+
}
|
|
22340
|
+
const pr = context.provider?.currentPullRequest || context.pullRequest?.currentPullRequest;
|
|
22341
|
+
if (pr) {
|
|
22342
|
+
return openProviderUrl(repo, { type: 'pull-request', number: pr.number });
|
|
22343
|
+
}
|
|
22344
|
+
// No PR — fall back to opening the repo page so the user can
|
|
22345
|
+
// create one or browse from there.
|
|
22346
|
+
return openProviderUrl(repo, { type: 'repo' });
|
|
22347
|
+
},
|
|
22348
|
+
'fetch-remotes': async () => fetchRemotes(git),
|
|
22349
|
+
'pull-current-branch': async () => pullCurrentBranch(git),
|
|
22350
|
+
'push-current-branch': async () => pushCurrentBranch(git),
|
|
22351
|
+
'rename-branch': async () => {
|
|
22352
|
+
const newName = payload?.trim();
|
|
22353
|
+
if (!newName)
|
|
22354
|
+
return { ok: false, message: 'New branch name required' };
|
|
22355
|
+
const all = sortBranches(context.branches?.localBranches || [], state.branchSort);
|
|
22356
|
+
const visible = state.filter
|
|
22357
|
+
? all.filter((b) => matchesPromotedFilter([b.shortName, b.upstream || ''], state.filter))
|
|
22358
|
+
: all;
|
|
22359
|
+
const branch = visible[Math.min(state.selectedBranchIndex, visible.length - 1)];
|
|
22360
|
+
if (!branch)
|
|
22361
|
+
return { ok: false, message: 'No branch selected' };
|
|
22362
|
+
return renameBranch(git, branch.shortName, newName);
|
|
22363
|
+
},
|
|
22364
|
+
'set-upstream': async () => {
|
|
22365
|
+
const upstream = payload?.trim();
|
|
22366
|
+
if (!upstream)
|
|
22367
|
+
return { ok: false, message: 'Upstream ref required' };
|
|
22368
|
+
const all = sortBranches(context.branches?.localBranches || [], state.branchSort);
|
|
22369
|
+
const visible = state.filter
|
|
22370
|
+
? all.filter((b) => matchesPromotedFilter([b.shortName, b.upstream || ''], state.filter))
|
|
22371
|
+
: all;
|
|
22372
|
+
const branch = visible[Math.min(state.selectedBranchIndex, visible.length - 1)];
|
|
22373
|
+
if (!branch)
|
|
22374
|
+
return { ok: false, message: 'No branch selected' };
|
|
22375
|
+
return setUpstream(git, branch.shortName, upstream);
|
|
22376
|
+
},
|
|
22377
|
+
'delete-remote-tag': async () => {
|
|
22378
|
+
const all = sortTags(context.tags?.tags || [], state.tagSort);
|
|
22379
|
+
const visible = state.filter
|
|
22380
|
+
? all.filter((t) => matchesPromotedFilter([t.name, t.subject], state.filter))
|
|
22381
|
+
: all;
|
|
22382
|
+
const tag = visible[Math.min(state.selectedTagIndex, visible.length - 1)];
|
|
22383
|
+
if (!tag)
|
|
22384
|
+
return { ok: false, message: 'No tag selected' };
|
|
22385
|
+
return deleteRemoteTag(git, tag.name);
|
|
22386
|
+
},
|
|
22387
|
+
'create-stash': async () => {
|
|
22388
|
+
const message = payload?.trim();
|
|
22389
|
+
if (!message)
|
|
22390
|
+
return { ok: false, message: 'Stash message required' };
|
|
22391
|
+
return createStash(git, message);
|
|
22392
|
+
},
|
|
21482
22393
|
};
|
|
21483
22394
|
const handler = handlers[id];
|
|
21484
22395
|
if (!handler) {
|
|
@@ -21491,7 +22402,8 @@ function LogInkApp(deps) {
|
|
|
21491
22402
|
// flickering the surfaces through a 'loading' phase.
|
|
21492
22403
|
await refreshContext({ silent: true });
|
|
21493
22404
|
}, [context, dispatch, git, refreshContext, state.branchSort, state.filter, state.selectedBranchIndex,
|
|
21494
|
-
state.selectedStashIndex, state.selectedTagIndex, state.
|
|
22405
|
+
state.selectedStashIndex, state.selectedTagIndex, state.selectedWorktreeListIndex, state.stashDiffRef,
|
|
22406
|
+
state.tagSort]);
|
|
21495
22407
|
React.useEffect(() => {
|
|
21496
22408
|
let active = true;
|
|
21497
22409
|
async function loadPreview() {
|
|
@@ -21597,14 +22509,53 @@ function LogInkApp(deps) {
|
|
|
21597
22509
|
.filter((tag) => matchesPromotedFilter([tag.name, tag.subject], state.filter))
|
|
21598
22510
|
.length
|
|
21599
22511
|
: context.tags?.tags.length;
|
|
21600
|
-
const
|
|
22512
|
+
const visibleStashes = state.filter
|
|
21601
22513
|
? (context.stashes?.stashes || [])
|
|
21602
22514
|
.filter((stash) => matchesPromotedFilter([stash.ref, stash.message], state.filter))
|
|
22515
|
+
: (context.stashes?.stashes || []);
|
|
22516
|
+
const stashVisibleCount = visibleStashes.length;
|
|
22517
|
+
const stashSelectedRef = visibleStashes[Math.min(state.selectedStashIndex, Math.max(0, visibleStashes.length - 1))]?.ref;
|
|
22518
|
+
// The worktrees promoted view is filterable; mirror the branches /
|
|
22519
|
+
// tags / stash pattern and feed the filtered count into the input
|
|
22520
|
+
// dispatcher so ↑/↓ stay synchronized with the visible rows.
|
|
22521
|
+
const worktreeVisibleCount = state.filter
|
|
22522
|
+
? (context.worktreeList?.worktrees || [])
|
|
22523
|
+
.filter((w) => matchesPromotedFilter([w.path, w.branch || ''], state.filter))
|
|
21603
22524
|
.length
|
|
21604
|
-
: context.
|
|
22525
|
+
: context.worktreeList?.worktrees.length;
|
|
22526
|
+
// When the diff view is showing a stash patch, swap the previewLineCount
|
|
22527
|
+
// to the stash diff length so the existing pageDetailPreview path
|
|
22528
|
+
// (j/k, PgUp/PgDn) scrolls through it without a parallel pipeline.
|
|
22529
|
+
const diffPreviewLineCount = state.diffSource === 'stash'
|
|
22530
|
+
? stashDiffLines?.length
|
|
22531
|
+
: filePreview?.hunks.length;
|
|
22532
|
+
// Parse the active stash diff into per-file sections so `]`/`[` can
|
|
22533
|
+
// jump between files and `c` knows which path the cursor is on for
|
|
22534
|
+
// a file-level cherry-pick.
|
|
22535
|
+
const stashDiffFiles = state.diffSource === 'stash' && stashDiffLines
|
|
22536
|
+
? parseStashDiffFiles(stashDiffLines)
|
|
22537
|
+
: [];
|
|
22538
|
+
const stashDiffFileOffsets = stashDiffFiles.map((file) => file.startLine);
|
|
22539
|
+
const stashDiffSelectedPath = (() => {
|
|
22540
|
+
if (state.diffSource !== 'stash' || stashDiffFiles.length === 0)
|
|
22541
|
+
return undefined;
|
|
22542
|
+
const offset = state.diffPreviewOffset;
|
|
22543
|
+
// Walk backwards to the most recent file header at or before the
|
|
22544
|
+
// current cursor offset.
|
|
22545
|
+
let current = stashDiffFiles[0];
|
|
22546
|
+
for (const file of stashDiffFiles) {
|
|
22547
|
+
if (file.startLine <= offset) {
|
|
22548
|
+
current = file;
|
|
22549
|
+
}
|
|
22550
|
+
else {
|
|
22551
|
+
break;
|
|
22552
|
+
}
|
|
22553
|
+
}
|
|
22554
|
+
return current.path;
|
|
22555
|
+
})();
|
|
21605
22556
|
getLogInkInputEvents(state, inputValue, key, {
|
|
21606
22557
|
detailFileCount: detail?.files.length,
|
|
21607
|
-
previewLineCount:
|
|
22558
|
+
previewLineCount: diffPreviewLineCount,
|
|
21608
22559
|
worktreeDiffLineCount: worktreeDiff?.lines.length,
|
|
21609
22560
|
worktreeFileCount: context.worktree?.files.length,
|
|
21610
22561
|
worktreeHunkOffsets: worktreeDiff?.hunkOffsets,
|
|
@@ -21612,6 +22563,17 @@ function LogInkApp(deps) {
|
|
|
21612
22563
|
branchCount: branchVisibleCount,
|
|
21613
22564
|
tagCount: tagVisibleCount,
|
|
21614
22565
|
stashCount: stashVisibleCount,
|
|
22566
|
+
stashSelectedRef,
|
|
22567
|
+
stashDiffFileOffsets: stashDiffFileOffsets.length ? stashDiffFileOffsets : undefined,
|
|
22568
|
+
stashDiffSelectedPath,
|
|
22569
|
+
worktreeListCount: worktreeVisibleCount,
|
|
22570
|
+
worktreeSelectedPath: context.worktree?.files[state.selectedWorktreeFileIndex]?.path,
|
|
22571
|
+
commitDiffSelectedPath: state.diffSource === 'commit'
|
|
22572
|
+
? selectedDetailFile?.path
|
|
22573
|
+
: undefined,
|
|
22574
|
+
commitDiffSelectedSha: state.diffSource === 'commit'
|
|
22575
|
+
? selected?.hash
|
|
22576
|
+
: undefined,
|
|
21615
22577
|
worktreeDirty,
|
|
21616
22578
|
}).forEach((event) => {
|
|
21617
22579
|
if (event.type === 'exit') {
|
|
@@ -21639,7 +22601,10 @@ function LogInkApp(deps) {
|
|
|
21639
22601
|
void runAiCommitDraft();
|
|
21640
22602
|
}
|
|
21641
22603
|
else if (event.type === 'runWorkflowAction') {
|
|
21642
|
-
void runWorkflowAction(event.id);
|
|
22604
|
+
void runWorkflowAction(event.id, event.payload);
|
|
22605
|
+
}
|
|
22606
|
+
else if (event.type === 'openFileInEditor') {
|
|
22607
|
+
openInEditor(event.path);
|
|
21643
22608
|
}
|
|
21644
22609
|
else {
|
|
21645
22610
|
// P4.5: enrich filter-mutating actions with a precomputed
|
|
@@ -21653,6 +22618,13 @@ function LogInkApp(deps) {
|
|
|
21653
22618
|
}
|
|
21654
22619
|
});
|
|
21655
22620
|
});
|
|
22621
|
+
// Layout depends on focus (sidebar grows when focused), so it's
|
|
22622
|
+
// computed here — after state is in scope but before the render path.
|
|
22623
|
+
const layout = getLogInkLayout({
|
|
22624
|
+
columns: windowSize.columns || process.stdout.columns || LOG_INK_DEFAULT_COLUMNS,
|
|
22625
|
+
rows: windowSize.rows || process.stdout.rows || LOG_INK_DEFAULT_ROWS,
|
|
22626
|
+
sidebarFocused: state.focus === 'sidebar',
|
|
22627
|
+
});
|
|
21656
22628
|
if (layout.tooSmall) {
|
|
21657
22629
|
return h(Box, {
|
|
21658
22630
|
flexDirection: 'column',
|
|
@@ -21666,7 +22638,7 @@ function LogInkApp(deps) {
|
|
|
21666
22638
|
if (showOnboarding) {
|
|
21667
22639
|
return renderOnboardingOverlay(h, { Box, Text }, layout.rows, layout.columns, theme, appLabel);
|
|
21668
22640
|
}
|
|
21669
|
-
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));
|
|
22641
|
+
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));
|
|
21670
22642
|
}
|
|
21671
22643
|
function renderHeader(h, components, state, context, contextStatus, columns, theme, appLabel) {
|
|
21672
22644
|
const { Box, Text } = components;
|
|
@@ -21722,28 +22694,96 @@ function renderHeader(h, components, state, context, contextStatus, columns, the
|
|
|
21722
22694
|
function renderSidebar(h, components, state, context, contextStatus, width, theme) {
|
|
21723
22695
|
const { Box, Text } = components;
|
|
21724
22696
|
const focused = state.focus === 'sidebar';
|
|
21725
|
-
const lines = sidebarLines(context, contextStatus, state.sidebarTab, width - 4, state, theme);
|
|
21726
22697
|
const tabs = getLogInkSidebarTabs();
|
|
22698
|
+
// Accordion layout — every tab's title is visible on its own line, but
|
|
22699
|
+
// only the active tab expands its content underneath. Switching tabs
|
|
22700
|
+
// (1-5 / [/]) collapses the previous and expands the next.
|
|
22701
|
+
const tabBlocks = tabs.flatMap((tab, tabIndex) => {
|
|
22702
|
+
const isActive = tab === state.sidebarTab;
|
|
22703
|
+
const count = sidebarTabCount(tab, context);
|
|
22704
|
+
const labelWithCount = count !== undefined
|
|
22705
|
+
? `${sidebarTabLabel(tab)} (${count})`
|
|
22706
|
+
: sidebarTabLabel(tab);
|
|
22707
|
+
const headerText = isActive ? `[${labelWithCount}]` : labelWithCount;
|
|
22708
|
+
const blocks = [];
|
|
22709
|
+
if (tabIndex > 0) {
|
|
22710
|
+
blocks.push(h(Text, { key: `tab-spacer-${tab}` }, ''));
|
|
22711
|
+
}
|
|
22712
|
+
blocks.push(h(Text, {
|
|
22713
|
+
key: `tab-header-${tab}`,
|
|
22714
|
+
bold: isActive,
|
|
22715
|
+
dimColor: !isActive,
|
|
22716
|
+
}, headerText));
|
|
22717
|
+
if (isActive) {
|
|
22718
|
+
blocks.push(...renderActiveSidebarContent(h, Text, tab, state, context, contextStatus, width, theme));
|
|
22719
|
+
}
|
|
22720
|
+
return blocks;
|
|
22721
|
+
});
|
|
21727
22722
|
return h(Box, {
|
|
21728
22723
|
borderColor: focusBorderColor(theme, focused),
|
|
21729
22724
|
borderStyle: theme.borderStyle,
|
|
21730
22725
|
flexDirection: 'column',
|
|
21731
22726
|
width,
|
|
21732
22727
|
paddingX: 1,
|
|
21733
|
-
}, h(Text, { bold: true }, panelTitle('Repository', focused)), h(Text,
|
|
21734
|
-
|
|
21735
|
-
|
|
21736
|
-
|
|
21737
|
-
|
|
21738
|
-
|
|
21739
|
-
|
|
22728
|
+
}, h(Text, { bold: true }, panelTitle('Repository', focused)), h(Text, undefined, ''), ...tabBlocks);
|
|
22729
|
+
}
|
|
22730
|
+
/**
|
|
22731
|
+
* Render the indented body of the active sidebar tab. The status tab
|
|
22732
|
+
* colours its summary counts (warning / danger / muted) and per-file
|
|
22733
|
+
* rows so they read as the same severity scale used in the main status
|
|
22734
|
+
* surface; every other tab falls through to `sidebarLines` for its
|
|
22735
|
+
* string-based summary.
|
|
22736
|
+
*/
|
|
22737
|
+
function renderActiveSidebarContent(h, Text, tab, state, context, contextStatus, width, theme) {
|
|
22738
|
+
if (tab === 'status') {
|
|
22739
|
+
return renderActiveStatusTabContent(h, Text, context, contextStatus, width, theme);
|
|
22740
|
+
}
|
|
22741
|
+
const lines = sidebarLines(context, contextStatus, tab, width - 6, state, theme);
|
|
22742
|
+
return lines.map((line, index) => h(Text, {
|
|
22743
|
+
key: `tab-content-${tab}-${index}`,
|
|
22744
|
+
dimColor: !line.trim(),
|
|
22745
|
+
}, truncate$1(` ${line}`, width - 4)));
|
|
22746
|
+
}
|
|
22747
|
+
function renderActiveStatusTabContent(h, Text, context, contextStatus, width, theme) {
|
|
22748
|
+
if (isLogInkContextKeyLoading(contextStatus, 'worktree')) {
|
|
22749
|
+
return [h(Text, { key: 'tab-status-loading', dimColor: true }, ' Loading status…')];
|
|
22750
|
+
}
|
|
22751
|
+
const worktree = context.worktree;
|
|
22752
|
+
if (!worktree) {
|
|
22753
|
+
return [h(Text, { key: 'tab-status-empty', dimColor: true }, ' Status unavailable')];
|
|
22754
|
+
}
|
|
22755
|
+
const colorOf = (state) => {
|
|
22756
|
+
if (theme.noColor)
|
|
22757
|
+
return undefined;
|
|
22758
|
+
if (state === 'staged')
|
|
22759
|
+
return theme.colors.warning;
|
|
22760
|
+
if (state === 'unstaged')
|
|
22761
|
+
return theme.colors.danger;
|
|
22762
|
+
return theme.colors.muted;
|
|
22763
|
+
};
|
|
22764
|
+
const summaryRow = (count, label, key, kind) => h(Text, { key }, ' ', h(Text, { color: colorOf(kind), bold: count > 0 }, `${count} ${label}`));
|
|
22765
|
+
const fileRows = worktree.files.slice(0, 12).map((file, index) => {
|
|
22766
|
+
const codes = `${file.indexStatus}${file.worktreeStatus}`;
|
|
22767
|
+
return h(Text, {
|
|
22768
|
+
key: `tab-status-file-${index}`,
|
|
22769
|
+
color: colorOf(file.state),
|
|
22770
|
+
}, truncate$1(` ${codes} ${file.path}`, width - 4));
|
|
22771
|
+
});
|
|
22772
|
+
return [
|
|
22773
|
+
summaryRow(worktree.stagedCount, 'staged', 'tab-status-staged', 'staged'),
|
|
22774
|
+
summaryRow(worktree.unstagedCount, 'unstaged', 'tab-status-unstaged', 'unstaged'),
|
|
22775
|
+
summaryRow(worktree.untrackedCount, 'untracked', 'tab-status-untracked', 'untracked'),
|
|
22776
|
+
...(fileRows.length
|
|
22777
|
+
? [h(Text, { key: 'tab-status-spacer' }, ''), ...fileRows]
|
|
22778
|
+
: []),
|
|
22779
|
+
];
|
|
21740
22780
|
}
|
|
21741
|
-
function renderMainPanel(h, components, state, context, contextStatus, worktreeDiff, worktreeDiffLoading, worktreeHunks, worktreeHunksLoading, filePreview, filePreviewLoading, commitDiffHunkOffsets, selectedDetailFile, bodyRows, width, theme, hasMoreCommits, loadingMoreCommits) {
|
|
22781
|
+
function renderMainPanel(h, components, state, context, contextStatus, worktreeDiff, worktreeDiffLoading, worktreeHunks, worktreeHunksLoading, filePreview, filePreviewLoading, commitDiffHunkOffsets, selectedDetailFile, stashDiffLines, stashDiffLoading, bodyRows, width, theme, hasMoreCommits, loadingMoreCommits) {
|
|
21742
22782
|
if (state.activeView === 'status') {
|
|
21743
22783
|
return renderStatusSurface(h, components, state, context, contextStatus, bodyRows, width, theme);
|
|
21744
22784
|
}
|
|
21745
22785
|
if (state.activeView === 'diff') {
|
|
21746
|
-
return renderDiffSurface(h, components, state, context, contextStatus, worktreeDiff, worktreeDiffLoading, worktreeHunks, worktreeHunksLoading, filePreview, filePreviewLoading, commitDiffHunkOffsets, selectedDetailFile, bodyRows, width, theme);
|
|
22786
|
+
return renderDiffSurface(h, components, state, context, contextStatus, worktreeDiff, worktreeDiffLoading, worktreeHunks, worktreeHunksLoading, filePreview, filePreviewLoading, commitDiffHunkOffsets, selectedDetailFile, stashDiffLines, stashDiffLoading, bodyRows, width, theme);
|
|
21747
22787
|
}
|
|
21748
22788
|
if (state.activeView === 'compose') {
|
|
21749
22789
|
return renderComposeSurface(h, components, state, context, contextStatus, bodyRows, width, theme);
|
|
@@ -21757,6 +22797,9 @@ function renderMainPanel(h, components, state, context, contextStatus, worktreeD
|
|
|
21757
22797
|
if (state.activeView === 'stash') {
|
|
21758
22798
|
return renderStashSurface(h, components, state, context, contextStatus, bodyRows, width, theme);
|
|
21759
22799
|
}
|
|
22800
|
+
if (state.activeView === 'worktrees') {
|
|
22801
|
+
return renderWorktreesSurface(h, components, state, context, contextStatus, bodyRows, width, theme);
|
|
22802
|
+
}
|
|
21760
22803
|
return renderHistoryPanel(h, components, state, context, bodyRows, width, theme, hasMoreCommits, loadingMoreCommits);
|
|
21761
22804
|
}
|
|
21762
22805
|
function renderHistoryPanel(h, components, state, context, bodyRows, width, theme, hasMoreCommits, loadingMoreCommits) {
|
|
@@ -21966,15 +23009,25 @@ function renderComposeSurface(h, components, state, context, contextStatus, body
|
|
|
21966
23009
|
key: `compose-body-${index}`,
|
|
21967
23010
|
dimColor: line === '<empty>',
|
|
21968
23011
|
}, ` ${line}${bodyCursor && isLast ? bodyCursor : ''}`);
|
|
21969
|
-
}),
|
|
21970
|
-
|
|
23012
|
+
}),
|
|
23013
|
+
// Loading indicator + post-action message belong inline with the draft
|
|
23014
|
+
// (they describe what just happened to the fields above). The state-
|
|
23015
|
+
// line ("Editing — Enter switches summary↔body…" / "Press e to edit
|
|
23016
|
+
// …") is footer-style guidance and now sits at the very bottom of the
|
|
23017
|
+
// pane so it doesn't visually separate the body from any
|
|
23018
|
+
// result/details.
|
|
23019
|
+
...(compose.loading
|
|
23020
|
+
? [
|
|
23021
|
+
h(Text, undefined, ''),
|
|
23022
|
+
h(Text, {
|
|
21971
23023
|
key: 'compose-loading',
|
|
21972
23024
|
bold: true,
|
|
21973
23025
|
color: theme.noColor ? undefined : theme.colors.accent,
|
|
21974
23026
|
}, theme.ascii
|
|
21975
23027
|
? '[...] Generating AI commit draft (this can take a moment)'
|
|
21976
|
-
: '⏳ Generating AI commit draft… (this can take a moment)')
|
|
21977
|
-
|
|
23028
|
+
: '⏳ Generating AI commit draft… (this can take a moment)'),
|
|
23029
|
+
]
|
|
23030
|
+
: []), ...(compose.message ? [h(Text, undefined, ''), h(Text, { key: 'compose-msg' }, truncate$1(compose.message, 140))] : []), ...(compose.details || []).map((line, index) => h(Text, {
|
|
21978
23031
|
key: `compose-detail-${index}`,
|
|
21979
23032
|
dimColor: true,
|
|
21980
23033
|
}, truncate$1(` ${line}`, 140))), ...(!hasStagedFiles && noStagedHint
|
|
@@ -21982,7 +23035,7 @@ function renderComposeSurface(h, components, state, context, contextStatus, body
|
|
|
21982
23035
|
h(Text, { key: 'compose-no-staged-spacer' }, ''),
|
|
21983
23036
|
h(Text, { key: 'compose-no-staged', dimColor: true }, truncate$1(noStagedHint, 140)),
|
|
21984
23037
|
]
|
|
21985
|
-
: []));
|
|
23038
|
+
: []), h(Box, { flexGrow: 1 }), h(Text, { key: 'compose-stateline', dimColor: true }, truncate$1(stateLine, width - 4)));
|
|
21986
23039
|
}
|
|
21987
23040
|
function matchesPromotedFilter(haystacks, filter) {
|
|
21988
23041
|
if (!filter.trim()) {
|
|
@@ -22136,6 +23189,48 @@ function renderStashSurface(h, components, state, context, contextStatus, bodyRo
|
|
|
22136
23189
|
width,
|
|
22137
23190
|
}, h(Box, { justifyContent: 'space-between' }, h(Text, { bold: true }, panelTitle('Stash', focused)), h(Text, { dimColor: true }, headerRight)), ...renderPromotedFilterAffordance(h, Text, state, theme), ...lines);
|
|
22138
23191
|
}
|
|
23192
|
+
function renderWorktreesSurface(h, components, state, context, contextStatus, bodyRows, width, theme) {
|
|
23193
|
+
const { Box, Text } = components;
|
|
23194
|
+
const focused = state.focus === 'commits';
|
|
23195
|
+
const loading = isLogInkContextKeyLoading(contextStatus, 'worktreeList');
|
|
23196
|
+
const allWorktrees = context.worktreeList?.worktrees || [];
|
|
23197
|
+
const worktrees = state.filter
|
|
23198
|
+
? allWorktrees.filter((entry) => matchesPromotedFilter([entry.path, entry.branch || '', entry.head || ''], state.filter))
|
|
23199
|
+
: allWorktrees;
|
|
23200
|
+
const selected = Math.max(0, Math.min(state.selectedWorktreeListIndex, Math.max(0, worktrees.length - 1)));
|
|
23201
|
+
const listRows = Math.max(4, bodyRows - 4);
|
|
23202
|
+
const startIndex = Math.max(0, selected - Math.floor(listRows / 2));
|
|
23203
|
+
const visible = worktrees.slice(startIndex, startIndex + listRows);
|
|
23204
|
+
const filterLabel = state.filter ? ` | filter: ${state.filter}` : '';
|
|
23205
|
+
const headerRight = loading
|
|
23206
|
+
? 'loading worktrees'
|
|
23207
|
+
: `${worktrees.length}/${allWorktrees.length} worktrees${filterLabel}`;
|
|
23208
|
+
const lines = loading
|
|
23209
|
+
? [h(Text, { key: 'worktrees-loading', dimColor: true }, formatLogInkLoading({ resource: 'worktrees' }))]
|
|
23210
|
+
: worktrees.length === 0
|
|
23211
|
+
? [h(Text, { key: 'worktrees-empty', dimColor: true }, 'No linked worktrees.')]
|
|
23212
|
+
: visible.map((entry, offset) => {
|
|
23213
|
+
const index = startIndex + offset;
|
|
23214
|
+
const isSelected = index === selected;
|
|
23215
|
+
const cursor = isSelected ? '>' : ' ';
|
|
23216
|
+
const marker = entry.current ? '*' : ' ';
|
|
23217
|
+
const branchLabel = entry.branch ? entry.branch : entry.head || '<detached>';
|
|
23218
|
+
const stateLabel = entry.dirty ? 'dirty' : 'clean';
|
|
23219
|
+
return h(Text, {
|
|
23220
|
+
key: `worktree-${index}`,
|
|
23221
|
+
bold: isSelected,
|
|
23222
|
+
dimColor: !isSelected && !entry.current,
|
|
23223
|
+
}, truncate$1(`${cursor} ${marker} ${branchLabel.padEnd(28)} ${stateLabel.padEnd(6)} ${entry.path}`, width - 4));
|
|
23224
|
+
});
|
|
23225
|
+
return h(Box, {
|
|
23226
|
+
borderColor: focusBorderColor(theme, focused),
|
|
23227
|
+
borderStyle: theme.borderStyle,
|
|
23228
|
+
flexDirection: 'column',
|
|
23229
|
+
flexShrink: 0,
|
|
23230
|
+
paddingX: 1,
|
|
23231
|
+
width,
|
|
23232
|
+
}, h(Box, { justifyContent: 'space-between' }, h(Text, { bold: true }, panelTitle('Worktrees', focused)), h(Text, { dimColor: true }, headerRight)), ...renderPromotedFilterAffordance(h, Text, state, theme), ...lines);
|
|
23233
|
+
}
|
|
22139
23234
|
/**
|
|
22140
23235
|
* Filter input cursor for the promoted views (branches/tags/stash).
|
|
22141
23236
|
* History already shows the same `filter: foo_` affordance in its header
|
|
@@ -22154,12 +23249,67 @@ function renderPromotedFilterAffordance(h, Text, state, theme) {
|
|
|
22154
23249
|
h(Text, { key: 'promoted-filter-input', color: accent }, `filter: ${state.filter}_`),
|
|
22155
23250
|
];
|
|
22156
23251
|
}
|
|
22157
|
-
function renderDiffSurface(h, components, state, context, contextStatus, worktreeDiff, worktreeDiffLoading, worktreeHunks, worktreeHunksLoading, filePreview, filePreviewLoading, commitDiffHunkOffsets, selectedDetailFile, bodyRows, width, theme) {
|
|
23252
|
+
function renderDiffSurface(h, components, state, context, contextStatus, worktreeDiff, worktreeDiffLoading, worktreeHunks, worktreeHunksLoading, filePreview, filePreviewLoading, commitDiffHunkOffsets, selectedDetailFile, stashDiffLines, stashDiffLoading, bodyRows, width, theme) {
|
|
22158
23253
|
const { Box, Text } = components;
|
|
22159
23254
|
const focused = state.focus === 'commits';
|
|
22160
23255
|
const worktree = context.worktree;
|
|
22161
23256
|
const worktreeFile = worktree?.files[state.selectedWorktreeFileIndex];
|
|
22162
23257
|
const visibleRows = Math.max(4, bodyRows - 4);
|
|
23258
|
+
// Stash diff branch: when the user opened the diff via Enter on a stash
|
|
23259
|
+
// row, render the stash patch text directly. The patch is parsed into
|
|
23260
|
+
// per-file sections so `]` / `[` jumps between files and `c`
|
|
23261
|
+
// cherry-picks the file at the cursor.
|
|
23262
|
+
if (state.diffSource === 'stash') {
|
|
23263
|
+
const lines = stashDiffLines || [];
|
|
23264
|
+
const visibleLines = lines.slice(state.diffPreviewOffset, state.diffPreviewOffset + visibleRows);
|
|
23265
|
+
const stashFiles = parseStashDiffFiles(lines);
|
|
23266
|
+
const fileCount = stashFiles.length;
|
|
23267
|
+
const currentFile = (() => {
|
|
23268
|
+
if (fileCount === 0)
|
|
23269
|
+
return undefined;
|
|
23270
|
+
let current = stashFiles[0];
|
|
23271
|
+
for (const file of stashFiles) {
|
|
23272
|
+
if (file.startLine <= state.diffPreviewOffset) {
|
|
23273
|
+
current = file;
|
|
23274
|
+
}
|
|
23275
|
+
else {
|
|
23276
|
+
break;
|
|
23277
|
+
}
|
|
23278
|
+
}
|
|
23279
|
+
return current;
|
|
23280
|
+
})();
|
|
23281
|
+
const currentFileIndex = currentFile
|
|
23282
|
+
? Math.max(0, stashFiles.findIndex((file) => file.startLine === currentFile.startLine))
|
|
23283
|
+
: -1;
|
|
23284
|
+
const headerLines = stashDiffLoading
|
|
23285
|
+
? [`Loading diff for ${state.stashDiffRef || 'stash'}...`]
|
|
23286
|
+
: lines.length
|
|
23287
|
+
? [
|
|
23288
|
+
`Stash: ${state.stashDiffRef || ''}`,
|
|
23289
|
+
fileCount > 0 && currentFile
|
|
23290
|
+
? `File ${currentFileIndex + 1}/${fileCount}: ${currentFile.path}`
|
|
23291
|
+
: 'No files in this stash.',
|
|
23292
|
+
`Lines ${Math.min(state.diffPreviewOffset + 1, lines.length)}-${Math.min(state.diffPreviewOffset + visibleLines.length, lines.length)}/${lines.length}`,
|
|
23293
|
+
'',
|
|
23294
|
+
]
|
|
23295
|
+
: ['No diff to display for this stash.'];
|
|
23296
|
+
return h(Box, {
|
|
23297
|
+
borderColor: focusBorderColor(theme, focused),
|
|
23298
|
+
borderStyle: theme.borderStyle,
|
|
23299
|
+
flexDirection: 'column',
|
|
23300
|
+
flexShrink: 0,
|
|
23301
|
+
paddingX: 1,
|
|
23302
|
+
width,
|
|
23303
|
+
}, 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, {
|
|
23304
|
+
key: `stash-diff-header-${index}`,
|
|
23305
|
+
dimColor: index > 0,
|
|
23306
|
+
}, truncate$1(line, width - 4))), ...(stashDiffLoading || !lines.length
|
|
23307
|
+
? []
|
|
23308
|
+
: visibleLines.map((line, index) => h(Text, {
|
|
23309
|
+
key: `stash-diff-line-${state.diffPreviewOffset + index}`,
|
|
23310
|
+
...diffLineProps(line, theme),
|
|
23311
|
+
}, truncate$1(line, width - 4)))));
|
|
23312
|
+
}
|
|
22163
23313
|
// diffSource disambiguates: 'commit' was set when the user opened the
|
|
22164
23314
|
// diff via history → Enter (read-only commit-diff explore), 'worktree'
|
|
22165
23315
|
// was set when they came from status → Enter (stage / hunk / revert).
|
|
@@ -22253,6 +23403,9 @@ function renderDetailPanel(h, components, state, context, contextStatus, detail,
|
|
|
22253
23403
|
if (state.showCommandPalette) {
|
|
22254
23404
|
return renderCommandPalette(h, components, state, width, theme, focused);
|
|
22255
23405
|
}
|
|
23406
|
+
if (state.inputPrompt) {
|
|
23407
|
+
return renderInputPromptPanel(h, components, state, width, theme, focused);
|
|
23408
|
+
}
|
|
22256
23409
|
if (state.pendingConfirmationId || state.pendingMutationConfirmation) {
|
|
22257
23410
|
return renderConfirmationPanel(h, components, state, width, theme, focused);
|
|
22258
23411
|
}
|
|
@@ -22652,16 +23805,40 @@ function renderCommitPanel(h, components, state, context, contextStatus, width,
|
|
|
22652
23805
|
}, h(Text, { bold: true }, panelTitle('Commit', focused)), ...headerLines.map((line, index) => h(Text, {
|
|
22653
23806
|
key: `commit-header-${index}`,
|
|
22654
23807
|
dimColor: index < 2 || line.startsWith(' ') || line === '<empty>',
|
|
22655
|
-
}, truncate$1(line, width - 4))),
|
|
22656
|
-
|
|
22657
|
-
|
|
22658
|
-
|
|
22659
|
-
|
|
22660
|
-
|
|
22661
|
-
|
|
23808
|
+
}, truncate$1(line, width - 4))),
|
|
23809
|
+
// Loading indicator + commit result/details stay inline with the body
|
|
23810
|
+
// (they describe what just happened to the fields above). The action
|
|
23811
|
+
// hint ("e edit | c commit | I AI draft") moves to the bottom of the
|
|
23812
|
+
// pane to read as footer guidance, matching the compose surface.
|
|
23813
|
+
...(loading
|
|
23814
|
+
? [h(Text, {
|
|
23815
|
+
key: 'commit-loading',
|
|
23816
|
+
bold: true,
|
|
23817
|
+
color: theme.noColor ? undefined : theme.colors.accent,
|
|
23818
|
+
}, truncate$1(theme.ascii ? '[...] Generating AI draft' : '⏳ Generating AI draft…', width - 4))]
|
|
23819
|
+
: []), ...trailerLines.map((line, index) => h(Text, {
|
|
22662
23820
|
key: `commit-trailer-${index}`,
|
|
22663
23821
|
dimColor: line.startsWith(' '),
|
|
22664
|
-
}, truncate$1(line, width - 4))))
|
|
23822
|
+
}, truncate$1(line, width - 4))), h(Box, { flexGrow: 1 }), loading
|
|
23823
|
+
? null
|
|
23824
|
+
: h(Text, { key: 'commit-state', dimColor: true }, truncate$1(stateLine, width - 4)));
|
|
23825
|
+
}
|
|
23826
|
+
function renderInputPromptPanel(h, components, state, width, theme, focused) {
|
|
23827
|
+
const { Box, Text } = components;
|
|
23828
|
+
const prompt = state.inputPrompt;
|
|
23829
|
+
if (!prompt) {
|
|
23830
|
+
return h(Box, { width });
|
|
23831
|
+
}
|
|
23832
|
+
return h(Box, {
|
|
23833
|
+
borderColor: focusBorderColor(theme, focused),
|
|
23834
|
+
borderStyle: theme.borderStyle,
|
|
23835
|
+
flexDirection: 'column',
|
|
23836
|
+
width,
|
|
23837
|
+
paddingX: 1,
|
|
23838
|
+
}, h(Text, { bold: true }, panelTitle('Prompt', focused)), h(Text, { dimColor: true }, truncate$1(prompt.label, width - 4)), h(Text, undefined, ''), h(Text, {
|
|
23839
|
+
bold: true,
|
|
23840
|
+
color: theme.noColor ? undefined : theme.colors.accent,
|
|
23841
|
+
}, truncate$1(`${prompt.value}_`, width - 4)), h(Text, undefined, ''), h(Text, { dimColor: true }, 'Enter to submit · Esc to cancel · Ctrl+u to clear'));
|
|
22665
23842
|
}
|
|
22666
23843
|
function renderConfirmationPanel(h, components, state, width, theme, focused) {
|
|
22667
23844
|
const { Box, Text } = components;
|
|
@@ -22839,6 +24016,7 @@ function renderFooter(h, components, state, theme, idleTip) {
|
|
|
22839
24016
|
const { Box, Text } = components;
|
|
22840
24017
|
const hints = getLogInkFooterHints({
|
|
22841
24018
|
activeView: state.activeView,
|
|
24019
|
+
diffSource: state.diffSource,
|
|
22842
24020
|
filterMode: state.filterMode,
|
|
22843
24021
|
focus: state.focus,
|
|
22844
24022
|
pendingKey: state.pendingKey,
|