git-coco 0.56.0 → 0.58.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/README.md +9 -7
- package/dist/index.d.ts +14 -4
- package/dist/index.esm.mjs +2654 -1610
- package/dist/index.js +2653 -1609
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -78,7 +78,7 @@ var readline__namespace = /*#__PURE__*/_interopNamespaceDefault(readline);
|
|
|
78
78
|
/**
|
|
79
79
|
* Current build version from package.json
|
|
80
80
|
*/
|
|
81
|
-
const BUILD_VERSION = "0.
|
|
81
|
+
const BUILD_VERSION = "0.58.0";
|
|
82
82
|
|
|
83
83
|
const isInteractive = (config) => {
|
|
84
84
|
return config?.mode === 'interactive' || !!config?.interactive;
|
|
@@ -1177,10 +1177,7 @@ const schema$1 = {
|
|
|
1177
1177
|
"items": {
|
|
1178
1178
|
"type": "string"
|
|
1179
1179
|
},
|
|
1180
|
-
"description": "Directories to scan for git repositories. Each entry may use a `~` prefix; resolved against the user's home directory.
|
|
1181
|
-
"default": [
|
|
1182
|
-
"~/code"
|
|
1183
|
-
]
|
|
1180
|
+
"description": "Directories to scan for git repositories. Each entry may use a `~` prefix; resolved against the user's home directory. When omitted (and no `--root` flag is passed), the workspace scans the current working directory — so a bare `coco` / `coco ws` discovers repos wherever you launched it. Set this to pin a fixed tree (e.g. `[\"~/code\"]`) regardless of where you run from.\n\n(No static `@default` — the effective default is the runtime cwd.)"
|
|
1184
1181
|
},
|
|
1185
1182
|
"knownRepos": {
|
|
1186
1183
|
"type": "array",
|
|
@@ -2156,6 +2153,10 @@ const schema$1 = {
|
|
|
2156
2153
|
"selection": {
|
|
2157
2154
|
"type": "string"
|
|
2158
2155
|
},
|
|
2156
|
+
"selectionForeground": {
|
|
2157
|
+
"type": "string",
|
|
2158
|
+
"description": "Foreground for text sitting on the `selection` background. Derived automatically from `selection` (black on light, white on dark) so the selected row stays readable regardless of the user's terminal default foreground — but can be overridden per theme via `options.colors`."
|
|
2159
|
+
},
|
|
2159
2160
|
"success": {
|
|
2160
2161
|
"type": "string"
|
|
2161
2162
|
},
|
|
@@ -2199,7 +2200,25 @@ const schema$1 = {
|
|
|
2199
2200
|
"vitesse-dark",
|
|
2200
2201
|
"vesper",
|
|
2201
2202
|
"flexoki",
|
|
2202
|
-
"mellow"
|
|
2203
|
+
"mellow",
|
|
2204
|
+
"night-owl",
|
|
2205
|
+
"cobalt2",
|
|
2206
|
+
"oceanic-next",
|
|
2207
|
+
"catppuccin-macchiato",
|
|
2208
|
+
"gruvbox-light",
|
|
2209
|
+
"tokyo-night-day",
|
|
2210
|
+
"one-light",
|
|
2211
|
+
"ayu-light",
|
|
2212
|
+
"rose-pine-dawn",
|
|
2213
|
+
"everforest-light",
|
|
2214
|
+
"vitesse-light",
|
|
2215
|
+
"dayfox",
|
|
2216
|
+
"night-owl-light",
|
|
2217
|
+
"flexoki-light",
|
|
2218
|
+
"material-lighter",
|
|
2219
|
+
"papercolor-light",
|
|
2220
|
+
"modus-operandi",
|
|
2221
|
+
"quiet-light"
|
|
2203
2222
|
]
|
|
2204
2223
|
}
|
|
2205
2224
|
}
|
|
@@ -15229,10 +15248,19 @@ const CommitSplitPlanSchema = objectType({
|
|
|
15229
15248
|
title: stringType().min(1),
|
|
15230
15249
|
body: stringType().optional(),
|
|
15231
15250
|
rationale: stringType().optional(),
|
|
15232
|
-
|
|
15233
|
-
hunks
|
|
15251
|
+
// Both optional: the model legitimately emits a group with *either*
|
|
15252
|
+
// `files` or `hunks` (a file-level vs hunk-level grouping), not always
|
|
15253
|
+
// both. Requiring both made Zod throw "Required" and the whole split
|
|
15254
|
+
// chain failed to parse before the refine could run. The refine below
|
|
15255
|
+
// still enforces "at least one", and every downstream consumer already
|
|
15256
|
+
// reads these as `group.files || []`. (Kept `.optional()` rather than
|
|
15257
|
+
// `.default([])` so the schema's input and output types stay identical
|
|
15258
|
+
// — `executeChainWithSchema` takes a `z.ZodSchema<T>`, which requires
|
|
15259
|
+
// that.)
|
|
15260
|
+
files: arrayType(stringType()).optional(),
|
|
15261
|
+
hunks: arrayType(stringType()).optional(),
|
|
15234
15262
|
})
|
|
15235
|
-
.refine((group) => group.files
|
|
15263
|
+
.refine((group) => (group.files?.length ?? 0) > 0 || (group.hunks?.length ?? 0) > 0, {
|
|
15236
15264
|
message: 'Each group must include at least one file or hunk',
|
|
15237
15265
|
}))
|
|
15238
15266
|
.min(1),
|
|
@@ -22830,6 +22858,13 @@ const LOG_INK_KEY_BINDINGS = [
|
|
|
22830
22858
|
description: 'Create a lightweight tag at the cursored commit.',
|
|
22831
22859
|
contexts: ['history'],
|
|
22832
22860
|
},
|
|
22861
|
+
{
|
|
22862
|
+
id: 'themePicker',
|
|
22863
|
+
keys: ['gC'],
|
|
22864
|
+
label: 'theme picker',
|
|
22865
|
+
description: 'Browse, live-preview, and apply a color theme.',
|
|
22866
|
+
contexts: ['normal'],
|
|
22867
|
+
},
|
|
22833
22868
|
{
|
|
22834
22869
|
id: 'viewChangelog',
|
|
22835
22870
|
keys: ['L'],
|
|
@@ -22919,6 +22954,7 @@ const BINDING_CATEGORY_BY_ID = {
|
|
|
22919
22954
|
// them above everything else.
|
|
22920
22955
|
help: 'essentials',
|
|
22921
22956
|
commandPalette: 'essentials',
|
|
22957
|
+
themePicker: 'view',
|
|
22922
22958
|
quit: 'essentials',
|
|
22923
22959
|
refresh: 'essentials',
|
|
22924
22960
|
navigateBack: 'essentials',
|
|
@@ -23554,635 +23590,1496 @@ function filterLogInkPaletteCommands(commands, filter, recent) {
|
|
|
23554
23590
|
}
|
|
23555
23591
|
|
|
23556
23592
|
/**
|
|
23557
|
-
*
|
|
23558
|
-
* (#882 phase 6). Each preset compiles to the same shape the
|
|
23559
|
-
* underlying list fetchers (`getIssueList` / `getPullRequestList`)
|
|
23560
|
-
* already accept — there's no new `gh` surface area, just a
|
|
23561
|
-
* curated set of common triage angles surfaced as a single
|
|
23562
|
-
* keystroke (`f` cycles).
|
|
23593
|
+
* Explicit color-level detection for the Ink TUI (P5.2).
|
|
23563
23594
|
*
|
|
23564
|
-
*
|
|
23565
|
-
*
|
|
23595
|
+
* Chalk already approximates hex colors when the terminal can't render
|
|
23596
|
+
* truecolor — but we want an explicit signal so the catppuccin / gruvbox
|
|
23597
|
+
* presets (which use hex) can fall back to the ANSI-named `default` preset
|
|
23598
|
+
* cleanly on minimal SSH sessions, instead of relying on chalk's
|
|
23599
|
+
* heuristics. Users who set `NO_COLOR` or pick the `monochrome` preset
|
|
23600
|
+
* still get the manual override.
|
|
23566
23601
|
*
|
|
23567
|
-
*
|
|
23568
|
-
*
|
|
23569
|
-
* -
|
|
23570
|
-
*
|
|
23571
|
-
* -
|
|
23572
|
-
* mean "I'm the assignee" (issues are tasks people pick up);
|
|
23573
|
-
* for PRs it means "I'm the author" (PRs are work people
|
|
23574
|
-
* post). The presets bake those in so the user doesn't have
|
|
23575
|
-
* to think about it.
|
|
23576
|
-
*/
|
|
23577
|
-
/** Cycle order — must match the keystroke walk on `f`. */
|
|
23578
|
-
const ISSUE_FILTER_PRESETS = [
|
|
23579
|
-
'open',
|
|
23580
|
-
'closed',
|
|
23581
|
-
'mine',
|
|
23582
|
-
'assigned',
|
|
23583
|
-
];
|
|
23584
|
-
const PULL_REQUEST_FILTER_PRESETS = [
|
|
23585
|
-
'open',
|
|
23586
|
-
'draft',
|
|
23587
|
-
'mine',
|
|
23588
|
-
'assigned',
|
|
23589
|
-
'closed',
|
|
23590
|
-
'merged',
|
|
23591
|
-
];
|
|
23592
|
-
const ISSUE_FILTER_LABELS = {
|
|
23593
|
-
open: 'open',
|
|
23594
|
-
closed: 'closed',
|
|
23595
|
-
mine: 'mine (assigned)',
|
|
23596
|
-
assigned: 'assigned to me',
|
|
23597
|
-
};
|
|
23598
|
-
const PULL_REQUEST_FILTER_LABELS = {
|
|
23599
|
-
open: 'open',
|
|
23600
|
-
draft: 'draft',
|
|
23601
|
-
mine: 'mine (authored)',
|
|
23602
|
-
assigned: 'assigned to me',
|
|
23603
|
-
closed: 'closed',
|
|
23604
|
-
merged: 'merged',
|
|
23605
|
-
};
|
|
23606
|
-
/**
|
|
23607
|
-
* Resolve a preset to the filter object the data fetcher accepts.
|
|
23608
|
-
* Pure mapping — no `gh` calls. Kept separate from `getIssueList` /
|
|
23609
|
-
* `getPullRequestList` so unit tests can assert the mapping
|
|
23610
|
-
* independently from the fetch pipeline.
|
|
23602
|
+
* Levels (matching the chalk taxonomy):
|
|
23603
|
+
* - 'mono' → no ANSI escapes at all (NO_COLOR / TERM=dumb)
|
|
23604
|
+
* - '16' → standard 16-color ANSI palette
|
|
23605
|
+
* - '256' → xterm-256color
|
|
23606
|
+
* - 'truecolor' → 24-bit RGB (COLORTERM=truecolor or known terminals)
|
|
23611
23607
|
*/
|
|
23612
|
-
function
|
|
23613
|
-
|
|
23614
|
-
|
|
23615
|
-
|
|
23616
|
-
case '
|
|
23617
|
-
return
|
|
23618
|
-
case '
|
|
23619
|
-
|
|
23620
|
-
|
|
23621
|
-
|
|
23622
|
-
|
|
23623
|
-
return
|
|
23624
|
-
case 'assigned':
|
|
23625
|
-
return { assignee: '@me' };
|
|
23608
|
+
function getColorLevel(env = process.env) {
|
|
23609
|
+
if (env.NO_COLOR)
|
|
23610
|
+
return 'mono';
|
|
23611
|
+
switch (env.FORCE_COLOR) {
|
|
23612
|
+
case '0':
|
|
23613
|
+
return 'mono';
|
|
23614
|
+
case '1':
|
|
23615
|
+
return '16';
|
|
23616
|
+
case '2':
|
|
23617
|
+
return '256';
|
|
23618
|
+
case '3':
|
|
23619
|
+
return 'truecolor';
|
|
23626
23620
|
}
|
|
23627
|
-
|
|
23628
|
-
|
|
23629
|
-
|
|
23630
|
-
case 'open':
|
|
23631
|
-
return { state: 'open' };
|
|
23632
|
-
case 'draft':
|
|
23633
|
-
// gh's `--draft` flag implies `--state open`; surface that
|
|
23634
|
-
// explicitly so the canonicalize step doesn't elide it.
|
|
23635
|
-
return { state: 'open', draft: true };
|
|
23636
|
-
case 'mine':
|
|
23637
|
-
// PRs are work — "mine" is what *I authored*. Most useful
|
|
23638
|
-
// when looking at one's own backlog of in-flight PRs.
|
|
23639
|
-
return { state: 'open', author: '@me' };
|
|
23640
|
-
case 'assigned':
|
|
23641
|
-
return { assignee: '@me' };
|
|
23642
|
-
case 'closed':
|
|
23643
|
-
return { state: 'closed' };
|
|
23644
|
-
case 'merged':
|
|
23645
|
-
return { state: 'merged' };
|
|
23621
|
+
const colorterm = env.COLORTERM?.toLowerCase();
|
|
23622
|
+
if (colorterm === 'truecolor' || colorterm === '24bit') {
|
|
23623
|
+
return 'truecolor';
|
|
23646
23624
|
}
|
|
23647
|
-
|
|
23648
|
-
|
|
23649
|
-
|
|
23650
|
-
const next = (index + 1) % ISSUE_FILTER_PRESETS.length;
|
|
23651
|
-
return ISSUE_FILTER_PRESETS[next];
|
|
23652
|
-
}
|
|
23653
|
-
function cyclePullRequestFilterPreset(current) {
|
|
23654
|
-
const index = PULL_REQUEST_FILTER_PRESETS.indexOf(current);
|
|
23655
|
-
const next = (index + 1) % PULL_REQUEST_FILTER_PRESETS.length;
|
|
23656
|
-
return PULL_REQUEST_FILTER_PRESETS[next];
|
|
23657
|
-
}
|
|
23658
|
-
|
|
23659
|
-
/**
|
|
23660
|
-
* Sort modes for the promoted views (P4.2).
|
|
23661
|
-
*
|
|
23662
|
-
* Pure: takes existing context entries + the active mode, returns a sorted
|
|
23663
|
-
* copy. Tested in isolation; the runtime just calls these helpers.
|
|
23664
|
-
*
|
|
23665
|
-
* Display label uses `▼` (U+25BC) under truecolor / UTF-8 and falls back to
|
|
23666
|
-
* `v` under ASCII. Letters (`recent` / `name` / `ahead`) carry meaning;
|
|
23667
|
-
* shape enhances.
|
|
23668
|
-
*/
|
|
23669
|
-
const BRANCH_SORT_MODES = ['name', 'recent', 'ahead'];
|
|
23670
|
-
const DEFAULT_BRANCH_SORT_MODE = 'name';
|
|
23671
|
-
function cycleBranchSort(mode) {
|
|
23672
|
-
const index = BRANCH_SORT_MODES.indexOf(mode);
|
|
23673
|
-
if (index < 0)
|
|
23674
|
-
return BRANCH_SORT_MODES[0];
|
|
23675
|
-
return BRANCH_SORT_MODES[(index + 1) % BRANCH_SORT_MODES.length];
|
|
23676
|
-
}
|
|
23677
|
-
function sortBranches(branches, mode) {
|
|
23678
|
-
// Pin the current branch at index 0 regardless of sort mode (#806
|
|
23679
|
-
// follow-up). Lands the user's cursor on the active branch by
|
|
23680
|
-
// default and keeps the most-relevant row glued to the top of the
|
|
23681
|
-
// list as they cycle sorts.
|
|
23682
|
-
const current = branches.find((entry) => entry.current);
|
|
23683
|
-
const rest = branches.filter((entry) => !entry.current);
|
|
23684
|
-
const sortedRest = rest.slice();
|
|
23685
|
-
switch (mode) {
|
|
23686
|
-
case 'name':
|
|
23687
|
-
sortedRest.sort((a, b) => a.shortName.localeCompare(b.shortName));
|
|
23688
|
-
break;
|
|
23689
|
-
case 'recent':
|
|
23690
|
-
// ISO-shaped dates compare byte-for-byte; descending so the freshest
|
|
23691
|
-
// branch sits at the top.
|
|
23692
|
-
sortedRest.sort((a, b) => (b.date || '').localeCompare(a.date || '') ||
|
|
23693
|
-
a.shortName.localeCompare(b.shortName));
|
|
23694
|
-
break;
|
|
23695
|
-
case 'ahead':
|
|
23696
|
-
// ahead-first; ties broken by behind, then by name. Keeps "this branch
|
|
23697
|
-
// has unmerged work" in the user's first scroll.
|
|
23698
|
-
sortedRest.sort((a, b) => b.ahead - a.ahead || b.behind - a.behind ||
|
|
23699
|
-
a.shortName.localeCompare(b.shortName));
|
|
23700
|
-
break;
|
|
23625
|
+
// Modern terminal emulators that publicly advertise truecolor support.
|
|
23626
|
+
if (env.KITTY_WINDOW_ID || env.TERM === 'xterm-kitty') {
|
|
23627
|
+
return 'truecolor';
|
|
23701
23628
|
}
|
|
23702
|
-
|
|
23703
|
-
|
|
23704
|
-
const TAG_SORT_MODES = ['recent', 'name'];
|
|
23705
|
-
const DEFAULT_TAG_SORT_MODE = 'recent';
|
|
23706
|
-
function cycleTagSort(mode) {
|
|
23707
|
-
const index = TAG_SORT_MODES.indexOf(mode);
|
|
23708
|
-
if (index < 0)
|
|
23709
|
-
return TAG_SORT_MODES[0];
|
|
23710
|
-
return TAG_SORT_MODES[(index + 1) % TAG_SORT_MODES.length];
|
|
23711
|
-
}
|
|
23712
|
-
function sortTags(tags, mode) {
|
|
23713
|
-
const copy = tags.slice();
|
|
23714
|
-
switch (mode) {
|
|
23715
|
-
case 'name':
|
|
23716
|
-
return copy.sort((a, b) => a.name.localeCompare(b.name));
|
|
23717
|
-
case 'recent':
|
|
23718
|
-
return copy.sort((a, b) => (b.date || '').localeCompare(a.date || '') ||
|
|
23719
|
-
a.name.localeCompare(b.name));
|
|
23720
|
-
default:
|
|
23721
|
-
return copy;
|
|
23629
|
+
if (env.WT_SESSION) {
|
|
23630
|
+
return 'truecolor';
|
|
23722
23631
|
}
|
|
23632
|
+
switch (env.TERM_PROGRAM) {
|
|
23633
|
+
case 'iTerm.app':
|
|
23634
|
+
case 'WezTerm':
|
|
23635
|
+
case 'vscode':
|
|
23636
|
+
case 'ghostty':
|
|
23637
|
+
case 'Hyper':
|
|
23638
|
+
return 'truecolor';
|
|
23639
|
+
}
|
|
23640
|
+
if (env.TERM === 'dumb')
|
|
23641
|
+
return 'mono';
|
|
23642
|
+
if (env.TERM?.includes('256color'))
|
|
23643
|
+
return '256';
|
|
23644
|
+
return '16';
|
|
23723
23645
|
}
|
|
23724
|
-
|
|
23725
|
-
function formatSortIndicator(mode, options = {}) {
|
|
23726
|
-
return `${options.ascii ? 'v' : '▼'} ${mode}`;
|
|
23727
|
-
}
|
|
23728
|
-
|
|
23729
|
-
const DEFAULT_CHANGELOG_VIEW_STATE = {
|
|
23730
|
-
status: 'idle',
|
|
23731
|
-
scrollOffset: 0,
|
|
23732
|
-
};
|
|
23733
|
-
const DEFAULT_LOG_INK_STATUS_FILTER_MASK = {
|
|
23734
|
-
staged: true,
|
|
23735
|
-
unstaged: true,
|
|
23736
|
-
untracked: true,
|
|
23737
|
-
};
|
|
23646
|
+
const TRUECOLOR_PRESETS = new Set(['catppuccin', 'gruvbox', 'dracula', 'nord', 'solarized-dark', 'tokyo-night', 'one-dark', 'rose-pine', 'kanagawa', 'everforest', 'monokai', 'synthwave', 'ayu-dark', 'palenight', 'github-dark', 'horizon', 'nightfox', 'carbonfox', 'tokyonight-storm', 'catppuccin-latte', 'solarized-light', 'github-light', 'iceberg', 'material-ocean', 'moonlight', 'poimandres', 'vitesse-dark', 'vesper', 'flexoki', 'mellow', 'night-owl', 'cobalt2', 'oceanic-next', 'catppuccin-macchiato', 'gruvbox-light', 'tokyo-night-day', 'one-light', 'ayu-light', 'rose-pine-dawn', 'everforest-light', 'vitesse-light', 'dayfox', 'night-owl-light', 'flexoki-light', 'material-lighter', 'papercolor-light', 'modus-operandi', 'quiet-light']);
|
|
23738
23647
|
/**
|
|
23739
|
-
*
|
|
23740
|
-
* `
|
|
23741
|
-
*
|
|
23742
|
-
* remainder of the string (post-prefix) becomes the value — paths and
|
|
23743
|
-
* author names commonly contain spaces, and we don't try to parse
|
|
23744
|
-
* shell-like syntax.
|
|
23648
|
+
* `true` when the named preset relies on hex colors that look best under
|
|
23649
|
+
* 24-bit RGB. Used by `createLogInkTheme` to decide whether to downgrade
|
|
23650
|
+
* to the ANSI-named `default` palette on lower-capability terminals.
|
|
23745
23651
|
*/
|
|
23746
|
-
function
|
|
23747
|
-
|
|
23748
|
-
if (trimmed.startsWith('path:')) {
|
|
23749
|
-
const value = trimmed.slice('path:'.length).trim();
|
|
23750
|
-
return value ? { path: value } : undefined;
|
|
23751
|
-
}
|
|
23752
|
-
if (trimmed.startsWith('author:')) {
|
|
23753
|
-
const value = trimmed.slice('author:'.length).trim();
|
|
23754
|
-
return value ? { author: value } : undefined;
|
|
23755
|
-
}
|
|
23756
|
-
return undefined;
|
|
23757
|
-
}
|
|
23758
|
-
const FOCUS_ORDER = ['sidebar', 'commits', 'detail'];
|
|
23759
|
-
const SIDEBAR_TABS = ['status', 'branches', 'tags', 'stashes', 'worktrees'];
|
|
23760
|
-
function searchableFields(commit) {
|
|
23761
|
-
return [
|
|
23762
|
-
commit.shortHash,
|
|
23763
|
-
commit.hash,
|
|
23764
|
-
commit.date,
|
|
23765
|
-
commit.author,
|
|
23766
|
-
commit.message,
|
|
23767
|
-
...commit.refs,
|
|
23768
|
-
];
|
|
23769
|
-
}
|
|
23770
|
-
function scoreField(field, term) {
|
|
23771
|
-
const value = field.toLowerCase();
|
|
23772
|
-
const normalized = term.toLowerCase();
|
|
23773
|
-
if (!normalized) {
|
|
23774
|
-
return 0;
|
|
23775
|
-
}
|
|
23776
|
-
if (value === normalized) {
|
|
23777
|
-
return 1000;
|
|
23778
|
-
}
|
|
23779
|
-
if (value.startsWith(normalized)) {
|
|
23780
|
-
return 800 - Math.min(value.length - normalized.length, 200);
|
|
23781
|
-
}
|
|
23782
|
-
const substringIndex = value.indexOf(normalized);
|
|
23783
|
-
if (substringIndex >= 0) {
|
|
23784
|
-
return 600 - Math.min(substringIndex, 200);
|
|
23785
|
-
}
|
|
23786
|
-
let searchIndex = 0;
|
|
23787
|
-
let distance = 0;
|
|
23788
|
-
for (const character of normalized) {
|
|
23789
|
-
const nextIndex = value.indexOf(character, searchIndex);
|
|
23790
|
-
if (nextIndex < 0) {
|
|
23791
|
-
return undefined;
|
|
23792
|
-
}
|
|
23793
|
-
distance += nextIndex - searchIndex;
|
|
23794
|
-
searchIndex = nextIndex + 1;
|
|
23795
|
-
}
|
|
23796
|
-
return 300 - Math.min(distance, 200);
|
|
23797
|
-
}
|
|
23798
|
-
function scoreLogInkCommitFilter(commit, filter) {
|
|
23799
|
-
const terms = filter.trim().split(/\s+/).filter(Boolean);
|
|
23800
|
-
if (terms.length === 0) {
|
|
23801
|
-
return 0;
|
|
23802
|
-
}
|
|
23803
|
-
const fields = searchableFields(commit);
|
|
23804
|
-
let score = 0;
|
|
23805
|
-
for (const term of terms) {
|
|
23806
|
-
const bestFieldScore = fields.reduce((best, field) => {
|
|
23807
|
-
const fieldScore = scoreField(field, term);
|
|
23808
|
-
if (fieldScore === undefined) {
|
|
23809
|
-
return best;
|
|
23810
|
-
}
|
|
23811
|
-
return best === undefined ? fieldScore : Math.max(best, fieldScore);
|
|
23812
|
-
}, undefined);
|
|
23813
|
-
if (bestFieldScore === undefined) {
|
|
23814
|
-
return undefined;
|
|
23815
|
-
}
|
|
23816
|
-
score += bestFieldScore;
|
|
23817
|
-
}
|
|
23818
|
-
return score;
|
|
23819
|
-
}
|
|
23820
|
-
function filterCommits(commits, filter) {
|
|
23821
|
-
return commits
|
|
23822
|
-
.map((commit, index) => ({
|
|
23823
|
-
commit,
|
|
23824
|
-
index,
|
|
23825
|
-
score: scoreLogInkCommitFilter(commit, filter),
|
|
23826
|
-
}))
|
|
23827
|
-
.filter((entry) => entry.score !== undefined)
|
|
23828
|
-
.sort((a, b) => b.score - a.score || a.index - b.index)
|
|
23829
|
-
.map((entry) => entry.commit);
|
|
23830
|
-
}
|
|
23831
|
-
function clampIndex(index, length) {
|
|
23832
|
-
if (length === 0) {
|
|
23833
|
-
return 0;
|
|
23834
|
-
}
|
|
23835
|
-
return Math.max(0, Math.min(index, length - 1));
|
|
23836
|
-
}
|
|
23837
|
-
function cycleValue(values, current, delta) {
|
|
23838
|
-
const currentIndex = Math.max(0, values.indexOf(current));
|
|
23839
|
-
const nextIndex = (currentIndex + delta + values.length) % values.length;
|
|
23840
|
-
return values[nextIndex];
|
|
23841
|
-
}
|
|
23842
|
-
const HOME_VIEW = 'history';
|
|
23843
|
-
function topOfStack(stack) {
|
|
23844
|
-
return stack[stack.length - 1];
|
|
23845
|
-
}
|
|
23846
|
-
function withPushedView(state, value) {
|
|
23847
|
-
if (topOfStack(state.viewStack) === value) {
|
|
23848
|
-
return { ...state, pendingKey: undefined };
|
|
23849
|
-
}
|
|
23850
|
-
const viewStack = [...state.viewStack, value];
|
|
23851
|
-
return {
|
|
23852
|
-
...state,
|
|
23853
|
-
activeView: value,
|
|
23854
|
-
viewStack,
|
|
23855
|
-
// The compose + status views' right detail panels already show
|
|
23856
|
-
// worktree info, so keeping the left sidebar on the Status tab
|
|
23857
|
-
// duplicates that information. Auto-switch to Branches when entering
|
|
23858
|
-
// either view; the user can swap back with [/] if they want.
|
|
23859
|
-
//
|
|
23860
|
-
// We update only the rendered `sidebarTab` here, never
|
|
23861
|
-
// `userSidebarTab`, so this auto-switch is invisible to per-repo
|
|
23862
|
-
// persistence and pop-view restores the previous tab.
|
|
23863
|
-
sidebarTab: value === 'compose' || value === 'status' ? 'branches' : state.sidebarTab,
|
|
23864
|
-
worktreeDiffOffset: value === 'diff' ? state.worktreeDiffOffset : 0,
|
|
23865
|
-
selectedWorktreeHunkIndex: value === 'diff' ? state.selectedWorktreeHunkIndex : 0,
|
|
23866
|
-
diffSource: value === 'diff' ? state.diffSource : undefined,
|
|
23867
|
-
stashDiffRef: value === 'diff' ? state.stashDiffRef : undefined,
|
|
23868
|
-
compareHead: value === 'diff' ? state.compareHead : undefined,
|
|
23869
|
-
pendingCommitFocused: value === 'history' ? state.pendingCommitFocused : false,
|
|
23870
|
-
statusGroupHeaderFocused: value === 'status' ? state.statusGroupHeaderFocused : false,
|
|
23871
|
-
pendingKey: undefined,
|
|
23872
|
-
};
|
|
23873
|
-
}
|
|
23874
|
-
function withPoppedView(state) {
|
|
23875
|
-
if (state.viewStack.length <= 1) {
|
|
23876
|
-
return { ...state, pendingKey: undefined };
|
|
23877
|
-
}
|
|
23878
|
-
const viewStack = state.viewStack.slice(0, -1);
|
|
23879
|
-
const next = topOfStack(viewStack);
|
|
23880
|
-
// #779 — compareBase is "cleared when the diff view is popped." We
|
|
23881
|
-
// detect that case by checking if the *previous* top was 'diff'.
|
|
23882
|
-
// The compare workflow ends when the user backs out of the compare
|
|
23883
|
-
// diff; on the next mark they re-set the base. Other view pops
|
|
23884
|
-
// preserve compareBase so the user can move between branches / tags /
|
|
23885
|
-
// history while hunting for a head ref.
|
|
23886
|
-
const wasOnDiff = state.activeView === 'diff';
|
|
23887
|
-
return {
|
|
23888
|
-
...state,
|
|
23889
|
-
activeView: next,
|
|
23890
|
-
viewStack,
|
|
23891
|
-
// Restore the user's last explicit tab choice so popping out of
|
|
23892
|
-
// compose / status (which auto-switch the sidebar to Branches)
|
|
23893
|
-
// returns the user to whatever they actually had open before.
|
|
23894
|
-
sidebarTab: state.userSidebarTab,
|
|
23895
|
-
worktreeDiffOffset: next === 'diff' ? state.worktreeDiffOffset : 0,
|
|
23896
|
-
selectedWorktreeHunkIndex: next === 'diff' ? state.selectedWorktreeHunkIndex : 0,
|
|
23897
|
-
diffSource: next === 'diff' ? state.diffSource : undefined,
|
|
23898
|
-
stashDiffRef: next === 'diff' ? state.stashDiffRef : undefined,
|
|
23899
|
-
compareBase: wasOnDiff ? undefined : state.compareBase,
|
|
23900
|
-
compareHead: next === 'diff' ? state.compareHead : undefined,
|
|
23901
|
-
pendingCommitFocused: next === 'history' ? state.pendingCommitFocused : false,
|
|
23902
|
-
statusGroupHeaderFocused: next === 'status' ? state.statusGroupHeaderFocused : false,
|
|
23903
|
-
pendingKey: undefined,
|
|
23904
|
-
};
|
|
23652
|
+
function presetUsesTrueColor(preset) {
|
|
23653
|
+
return preset !== undefined && TRUECOLOR_PRESETS.has(preset);
|
|
23905
23654
|
}
|
|
23906
23655
|
/**
|
|
23907
|
-
*
|
|
23908
|
-
*
|
|
23909
|
-
*
|
|
23910
|
-
* resets the per-frame navigation state (active view, view stack,
|
|
23911
|
-
* row / file / submodule cursors, filter) so the nested frame opens
|
|
23912
|
-
* in a clean slate — the mental equivalent of a fresh `coco ui`
|
|
23913
|
-
* launched against the submodule's working dir.
|
|
23914
|
-
*
|
|
23915
|
-
* Sidebar tab + branch / tag sort are also captured into the return
|
|
23916
|
-
* snapshot (#995) so popping back restores the parent's choices
|
|
23917
|
-
* instead of letting the submodule's tab/sort bleed across the
|
|
23918
|
-
* boundary. The values on the *new* frame are left as-is (carried
|
|
23919
|
-
* over from the parent) — the load effect in app.ts re-reads
|
|
23920
|
-
* persistence keyed on the submodule's workdir and dispatches a
|
|
23921
|
-
* restore if the user has a submodule-specific saved preference.
|
|
23922
|
-
*
|
|
23923
|
-
* Other preferences (palette recents, inspector tab, diff view mode)
|
|
23924
|
-
* stay global by design — the user's preference shouldn't reset when
|
|
23925
|
-
* they cross a submodule boundary.
|
|
23926
|
-
*
|
|
23927
|
-
* Live runtime objects (`SimpleGit`, loaded `LogInkContext`) live
|
|
23928
|
-
* outside the reducer in `app.ts`'s parallel ref structure — this
|
|
23929
|
-
* helper only manages the pure view-model side of the push.
|
|
23656
|
+
* WCAG 2.x relative luminance of a `#rrggbb` color, 0 (black) … 1 (white).
|
|
23657
|
+
* Returns `null` for anything that isn't a 6-digit hex (e.g. ANSI-named
|
|
23658
|
+
* colors), so callers can fall back rather than guess.
|
|
23930
23659
|
*/
|
|
23931
|
-
function
|
|
23932
|
-
const
|
|
23933
|
-
|
|
23934
|
-
|
|
23935
|
-
|
|
23936
|
-
|
|
23937
|
-
|
|
23938
|
-
|
|
23939
|
-
selectedFileIndex: state.selectedFileIndex,
|
|
23940
|
-
selectedSubmoduleIndex: state.selectedSubmoduleIndex,
|
|
23941
|
-
filter: state.filter,
|
|
23942
|
-
sidebarTab: state.sidebarTab,
|
|
23943
|
-
userSidebarTab: state.userSidebarTab,
|
|
23944
|
-
branchSort: state.branchSort,
|
|
23945
|
-
tagSort: state.tagSort,
|
|
23946
|
-
},
|
|
23947
|
-
};
|
|
23948
|
-
return {
|
|
23949
|
-
...state,
|
|
23950
|
-
repoStack: [...state.repoStack, newFrame],
|
|
23951
|
-
activeView: 'history',
|
|
23952
|
-
viewStack: ['history'],
|
|
23953
|
-
selectedIndex: 0,
|
|
23954
|
-
selectedFileIndex: 0,
|
|
23955
|
-
selectedSubmoduleIndex: 0,
|
|
23956
|
-
filter: '',
|
|
23957
|
-
filterMode: false,
|
|
23958
|
-
pendingCommitFocused: false,
|
|
23959
|
-
pendingKey: undefined,
|
|
23960
|
-
pendingConfirmationId: undefined,
|
|
23961
|
-
pendingConfirmationPayload: undefined,
|
|
23962
|
-
pendingMutationConfirmation: undefined,
|
|
23660
|
+
function relativeLuminance(hex) {
|
|
23661
|
+
const match = /^#?([0-9a-f]{6})$/i.exec(hex.trim());
|
|
23662
|
+
if (!match)
|
|
23663
|
+
return null;
|
|
23664
|
+
const int = parseInt(match[1], 16);
|
|
23665
|
+
const channel = (c) => {
|
|
23666
|
+
const x = c / 255;
|
|
23667
|
+
return x <= 0.03928 ? x / 12.92 : ((x + 0.055) / 1.055) ** 2.4;
|
|
23963
23668
|
};
|
|
23669
|
+
const r = channel((int >> 16) & 0xff);
|
|
23670
|
+
const g = channel((int >> 8) & 0xff);
|
|
23671
|
+
const b = channel(int & 0xff);
|
|
23672
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
23964
23673
|
}
|
|
23965
23674
|
/**
|
|
23966
|
-
*
|
|
23967
|
-
*
|
|
23968
|
-
*
|
|
23969
|
-
*
|
|
23970
|
-
* Esc auto-pop wiring that lands in a follow-up PR).
|
|
23675
|
+
* Pick a foreground guaranteed to stay readable on `bg` — black for light
|
|
23676
|
+
* backgrounds, white for dark ones. The 0.179 threshold is the luminance
|
|
23677
|
+
* crossover where black and white yield identical contrast, so the choice
|
|
23678
|
+
* always maximizes it; every background clears WCAG AA (≥ 4.5:1).
|
|
23971
23679
|
*
|
|
23972
|
-
*
|
|
23973
|
-
*
|
|
23974
|
-
*
|
|
23975
|
-
*
|
|
23680
|
+
* This is how the selected-row text stays legible across every theme:
|
|
23681
|
+
* coco controls the selection *background* but not the user's terminal
|
|
23682
|
+
* default foreground, so it must supply its own contrasting foreground
|
|
23683
|
+
* instead of hoping the terminal's happens to contrast. Returns
|
|
23684
|
+
* `undefined` for non-hex backgrounds (let the caller leave color alone).
|
|
23976
23685
|
*/
|
|
23977
|
-
function
|
|
23978
|
-
if (
|
|
23979
|
-
return
|
|
23980
|
-
|
|
23981
|
-
|
|
23982
|
-
|
|
23983
|
-
|
|
23984
|
-
if (!ret) {
|
|
23985
|
-
return { ...state, repoStack, pendingKey: undefined };
|
|
23986
|
-
}
|
|
23987
|
-
return {
|
|
23988
|
-
...state,
|
|
23989
|
-
repoStack,
|
|
23990
|
-
activeView: ret.activeView,
|
|
23991
|
-
viewStack: [ret.activeView],
|
|
23992
|
-
selectedIndex: ret.selectedIndex,
|
|
23993
|
-
selectedFileIndex: ret.selectedFileIndex,
|
|
23994
|
-
selectedSubmoduleIndex: ret.selectedSubmoduleIndex,
|
|
23995
|
-
filter: ret.filter,
|
|
23996
|
-
filterMode: false,
|
|
23997
|
-
pendingCommitFocused: false,
|
|
23998
|
-
// #995 — restore sidebar tab + sort preferences from the captured
|
|
23999
|
-
// parentReturn. Without this, the submodule's tab / sort choice
|
|
24000
|
-
// bleeds back into the parent after pop: the user picks 'tags' in
|
|
24001
|
-
// a vendored submodule, pops back to the parent, and finds the
|
|
24002
|
-
// parent's previously-selected 'branches' tab quietly replaced.
|
|
24003
|
-
sidebarTab: ret.sidebarTab,
|
|
24004
|
-
userSidebarTab: ret.userSidebarTab,
|
|
24005
|
-
branchSort: ret.branchSort,
|
|
24006
|
-
tagSort: ret.tagSort,
|
|
24007
|
-
pendingKey: undefined,
|
|
24008
|
-
pendingConfirmationId: undefined,
|
|
24009
|
-
pendingConfirmationPayload: undefined,
|
|
24010
|
-
pendingMutationConfirmation: undefined,
|
|
24011
|
-
};
|
|
23686
|
+
function readableForegroundFor(bg) {
|
|
23687
|
+
if (!bg)
|
|
23688
|
+
return undefined;
|
|
23689
|
+
const luminance = relativeLuminance(bg);
|
|
23690
|
+
if (luminance === null)
|
|
23691
|
+
return undefined;
|
|
23692
|
+
return luminance > 0.179 ? '#000000' : '#ffffff';
|
|
24012
23693
|
}
|
|
24013
|
-
|
|
24014
|
-
|
|
24015
|
-
|
|
24016
|
-
|
|
24017
|
-
|
|
24018
|
-
|
|
24019
|
-
|
|
24020
|
-
|
|
24021
|
-
|
|
24022
|
-
|
|
24023
|
-
|
|
24024
|
-
|
|
24025
|
-
|
|
24026
|
-
|
|
24027
|
-
|
|
24028
|
-
|
|
24029
|
-
|
|
24030
|
-
|
|
23694
|
+
|
|
23695
|
+
const THEME_PRESET_COLORS = {
|
|
23696
|
+
default: {
|
|
23697
|
+
accent: 'cyan',
|
|
23698
|
+
border: 'gray',
|
|
23699
|
+
danger: 'red',
|
|
23700
|
+
focusBorder: 'cyan',
|
|
23701
|
+
gitAdded: 'green',
|
|
23702
|
+
gitDeleted: 'red',
|
|
23703
|
+
gitModified: 'yellow',
|
|
23704
|
+
info: 'blue',
|
|
23705
|
+
muted: 'gray',
|
|
23706
|
+
selection: '#1a3a4a',
|
|
23707
|
+
success: 'green',
|
|
23708
|
+
warning: 'yellow',
|
|
23709
|
+
},
|
|
23710
|
+
catppuccin: {
|
|
23711
|
+
accent: '#89b4fa',
|
|
23712
|
+
border: '#585b70',
|
|
23713
|
+
danger: '#f38ba8',
|
|
23714
|
+
focusBorder: '#89dceb',
|
|
23715
|
+
gitAdded: '#a6e3a1',
|
|
23716
|
+
gitDeleted: '#f38ba8',
|
|
23717
|
+
gitModified: '#f9e2af',
|
|
23718
|
+
info: '#89b4fa',
|
|
23719
|
+
muted: '#6c7086',
|
|
23720
|
+
selection: '#45475a',
|
|
23721
|
+
success: '#a6e3a1',
|
|
23722
|
+
warning: '#f9e2af',
|
|
23723
|
+
},
|
|
23724
|
+
gruvbox: {
|
|
23725
|
+
accent: '#83a598',
|
|
23726
|
+
border: '#665c54',
|
|
23727
|
+
danger: '#fb4934',
|
|
23728
|
+
focusBorder: '#8ec07c',
|
|
23729
|
+
gitAdded: '#b8bb26',
|
|
23730
|
+
gitDeleted: '#fb4934',
|
|
23731
|
+
gitModified: '#fabd2f',
|
|
23732
|
+
info: '#83a598',
|
|
23733
|
+
muted: '#928374',
|
|
23734
|
+
selection: '#504945',
|
|
23735
|
+
success: '#b8bb26',
|
|
23736
|
+
warning: '#fabd2f',
|
|
23737
|
+
},
|
|
23738
|
+
dracula: {
|
|
23739
|
+
accent: '#bd93f9',
|
|
23740
|
+
border: '#44475a',
|
|
23741
|
+
danger: '#ff5555',
|
|
23742
|
+
focusBorder: '#ff79c6',
|
|
23743
|
+
gitAdded: '#50fa7b',
|
|
23744
|
+
gitDeleted: '#ff5555',
|
|
23745
|
+
gitModified: '#f1fa8c',
|
|
23746
|
+
info: '#8be9fd',
|
|
23747
|
+
muted: '#6272a4',
|
|
23748
|
+
selection: '#44475a',
|
|
23749
|
+
success: '#50fa7b',
|
|
23750
|
+
warning: '#f1fa8c',
|
|
23751
|
+
},
|
|
23752
|
+
nord: {
|
|
23753
|
+
accent: '#88c0d0',
|
|
23754
|
+
border: '#3b4252',
|
|
23755
|
+
danger: '#bf616a',
|
|
23756
|
+
focusBorder: '#81a1c1',
|
|
23757
|
+
gitAdded: '#a3be8c',
|
|
23758
|
+
gitDeleted: '#bf616a',
|
|
23759
|
+
gitModified: '#ebcb8b',
|
|
23760
|
+
info: '#81a1c1',
|
|
23761
|
+
muted: '#4c566a',
|
|
23762
|
+
selection: '#3b4252',
|
|
23763
|
+
success: '#a3be8c',
|
|
23764
|
+
warning: '#ebcb8b',
|
|
23765
|
+
},
|
|
23766
|
+
'solarized-dark': {
|
|
23767
|
+
accent: '#268bd2',
|
|
23768
|
+
border: '#073642',
|
|
23769
|
+
danger: '#dc322f',
|
|
23770
|
+
focusBorder: '#2aa198',
|
|
23771
|
+
gitAdded: '#859900',
|
|
23772
|
+
gitDeleted: '#dc322f',
|
|
23773
|
+
gitModified: '#b58900',
|
|
23774
|
+
info: '#268bd2',
|
|
23775
|
+
muted: '#586e75',
|
|
23776
|
+
selection: '#073642',
|
|
23777
|
+
success: '#859900',
|
|
23778
|
+
warning: '#b58900',
|
|
23779
|
+
},
|
|
23780
|
+
'tokyo-night': {
|
|
23781
|
+
accent: '#7aa2f7',
|
|
23782
|
+
border: '#3b4261',
|
|
23783
|
+
danger: '#f7768e',
|
|
23784
|
+
focusBorder: '#7dcfff',
|
|
23785
|
+
gitAdded: '#9ece6a',
|
|
23786
|
+
gitDeleted: '#f7768e',
|
|
23787
|
+
gitModified: '#e0af68',
|
|
23788
|
+
info: '#7aa2f7',
|
|
23789
|
+
muted: '#565f89',
|
|
23790
|
+
selection: '#33467c',
|
|
23791
|
+
success: '#9ece6a',
|
|
23792
|
+
warning: '#e0af68',
|
|
23793
|
+
},
|
|
23794
|
+
'one-dark': {
|
|
23795
|
+
accent: '#61afef',
|
|
23796
|
+
border: '#3e4452',
|
|
23797
|
+
danger: '#e06c75',
|
|
23798
|
+
focusBorder: '#56b6c2',
|
|
23799
|
+
gitAdded: '#98c379',
|
|
23800
|
+
gitDeleted: '#e06c75',
|
|
23801
|
+
gitModified: '#e5c07b',
|
|
23802
|
+
info: '#61afef',
|
|
23803
|
+
muted: '#5c6370',
|
|
23804
|
+
selection: '#3e4452',
|
|
23805
|
+
success: '#98c379',
|
|
23806
|
+
warning: '#e5c07b',
|
|
23807
|
+
},
|
|
23808
|
+
'rose-pine': {
|
|
23809
|
+
accent: '#c4a7e7',
|
|
23810
|
+
border: '#26233a',
|
|
23811
|
+
danger: '#eb6f92',
|
|
23812
|
+
focusBorder: '#9ccfd8',
|
|
23813
|
+
gitAdded: '#31748f',
|
|
23814
|
+
gitDeleted: '#eb6f92',
|
|
23815
|
+
gitModified: '#f6c177',
|
|
23816
|
+
info: '#9ccfd8',
|
|
23817
|
+
muted: '#6e6a86',
|
|
23818
|
+
selection: '#2a273f',
|
|
23819
|
+
success: '#31748f',
|
|
23820
|
+
warning: '#f6c177',
|
|
23821
|
+
},
|
|
23822
|
+
kanagawa: {
|
|
23823
|
+
accent: '#7e9cd8',
|
|
23824
|
+
border: '#2a2a37',
|
|
23825
|
+
danger: '#e82424',
|
|
23826
|
+
focusBorder: '#7fb4ca',
|
|
23827
|
+
gitAdded: '#76946a',
|
|
23828
|
+
gitDeleted: '#e82424',
|
|
23829
|
+
gitModified: '#dca561',
|
|
23830
|
+
info: '#7e9cd8',
|
|
23831
|
+
muted: '#727169',
|
|
23832
|
+
selection: '#2d4f67',
|
|
23833
|
+
success: '#76946a',
|
|
23834
|
+
warning: '#dca561',
|
|
23835
|
+
},
|
|
23836
|
+
everforest: {
|
|
23837
|
+
accent: '#a7c080',
|
|
23838
|
+
border: '#374145',
|
|
23839
|
+
danger: '#e67e80',
|
|
23840
|
+
focusBorder: '#83c092',
|
|
23841
|
+
gitAdded: '#a7c080',
|
|
23842
|
+
gitDeleted: '#e67e80',
|
|
23843
|
+
gitModified: '#dbbc7f',
|
|
23844
|
+
info: '#7fbbb3',
|
|
23845
|
+
muted: '#859289',
|
|
23846
|
+
selection: '#374145',
|
|
23847
|
+
success: '#a7c080',
|
|
23848
|
+
warning: '#dbbc7f',
|
|
23849
|
+
},
|
|
23850
|
+
monokai: {
|
|
23851
|
+
accent: '#66d9ef',
|
|
23852
|
+
border: '#49483e',
|
|
23853
|
+
danger: '#f92672',
|
|
23854
|
+
focusBorder: '#a6e22e',
|
|
23855
|
+
gitAdded: '#a6e22e',
|
|
23856
|
+
gitDeleted: '#f92672',
|
|
23857
|
+
gitModified: '#e6db74',
|
|
23858
|
+
info: '#66d9ef',
|
|
23859
|
+
muted: '#75715e',
|
|
23860
|
+
selection: '#49483e',
|
|
23861
|
+
success: '#a6e22e',
|
|
23862
|
+
warning: '#e6db74',
|
|
23863
|
+
},
|
|
23864
|
+
synthwave: {
|
|
23865
|
+
accent: '#f97e72',
|
|
23866
|
+
border: '#34294f',
|
|
23867
|
+
danger: '#fe4450',
|
|
23868
|
+
focusBorder: '#36f9f6',
|
|
23869
|
+
gitAdded: '#72f1b8',
|
|
23870
|
+
gitDeleted: '#fe4450',
|
|
23871
|
+
gitModified: '#fede5d',
|
|
23872
|
+
info: '#36f9f6',
|
|
23873
|
+
muted: '#848bbd',
|
|
23874
|
+
selection: '#34294f',
|
|
23875
|
+
success: '#72f1b8',
|
|
23876
|
+
warning: '#fede5d',
|
|
23877
|
+
},
|
|
23878
|
+
'ayu-dark': {
|
|
23879
|
+
accent: '#e6b450',
|
|
23880
|
+
border: '#11151c',
|
|
23881
|
+
danger: '#f07178',
|
|
23882
|
+
focusBorder: '#39bae6',
|
|
23883
|
+
gitAdded: '#7fd962',
|
|
23884
|
+
gitDeleted: '#f07178',
|
|
23885
|
+
gitModified: '#e6b450',
|
|
23886
|
+
info: '#39bae6',
|
|
23887
|
+
muted: '#565b66',
|
|
23888
|
+
selection: '#1a1f29',
|
|
23889
|
+
success: '#7fd962',
|
|
23890
|
+
warning: '#e6b450',
|
|
23891
|
+
},
|
|
23892
|
+
palenight: {
|
|
23893
|
+
accent: '#82aaff',
|
|
23894
|
+
border: '#3a3f58',
|
|
23895
|
+
danger: '#ff5370',
|
|
23896
|
+
focusBorder: '#89ddff',
|
|
23897
|
+
gitAdded: '#c3e88d',
|
|
23898
|
+
gitDeleted: '#ff5370',
|
|
23899
|
+
gitModified: '#ffcb6b',
|
|
23900
|
+
info: '#82aaff',
|
|
23901
|
+
muted: '#676e95',
|
|
23902
|
+
selection: '#3a3f58',
|
|
23903
|
+
success: '#c3e88d',
|
|
23904
|
+
warning: '#ffcb6b',
|
|
23905
|
+
},
|
|
23906
|
+
'github-dark': {
|
|
23907
|
+
accent: '#58a6ff',
|
|
23908
|
+
border: '#30363d',
|
|
23909
|
+
danger: '#f85149',
|
|
23910
|
+
focusBorder: '#58a6ff',
|
|
23911
|
+
gitAdded: '#3fb950',
|
|
23912
|
+
gitDeleted: '#f85149',
|
|
23913
|
+
gitModified: '#d29922',
|
|
23914
|
+
info: '#58a6ff',
|
|
23915
|
+
muted: '#8b949e',
|
|
23916
|
+
selection: '#264f78',
|
|
23917
|
+
success: '#3fb950',
|
|
23918
|
+
warning: '#d29922',
|
|
23919
|
+
},
|
|
23920
|
+
horizon: {
|
|
23921
|
+
accent: '#e95678',
|
|
23922
|
+
border: '#2e303e',
|
|
23923
|
+
danger: '#e95678',
|
|
23924
|
+
focusBorder: '#25b0bc',
|
|
23925
|
+
gitAdded: '#09f7a0',
|
|
23926
|
+
gitDeleted: '#e95678',
|
|
23927
|
+
gitModified: '#fab795',
|
|
23928
|
+
info: '#25b0bc',
|
|
23929
|
+
muted: '#6c6f93',
|
|
23930
|
+
selection: '#2e303e',
|
|
23931
|
+
success: '#09f7a0',
|
|
23932
|
+
warning: '#fab795',
|
|
23933
|
+
},
|
|
23934
|
+
nightfox: {
|
|
23935
|
+
accent: '#719cd6',
|
|
23936
|
+
border: '#2b3b51',
|
|
23937
|
+
danger: '#c94f6d',
|
|
23938
|
+
focusBorder: '#63cdcf',
|
|
23939
|
+
gitAdded: '#81b29a',
|
|
23940
|
+
gitDeleted: '#c94f6d',
|
|
23941
|
+
gitModified: '#dbc074',
|
|
23942
|
+
info: '#719cd6',
|
|
23943
|
+
muted: '#738091',
|
|
23944
|
+
selection: '#2b3b51',
|
|
23945
|
+
success: '#81b29a',
|
|
23946
|
+
warning: '#dbc074',
|
|
23947
|
+
},
|
|
23948
|
+
carbonfox: {
|
|
23949
|
+
accent: '#78a9ff',
|
|
23950
|
+
border: '#353535',
|
|
23951
|
+
danger: '#ee5396',
|
|
23952
|
+
focusBorder: '#33b1ff',
|
|
23953
|
+
gitAdded: '#42be65',
|
|
23954
|
+
gitDeleted: '#ee5396',
|
|
23955
|
+
gitModified: '#ffe97b',
|
|
23956
|
+
info: '#78a9ff',
|
|
23957
|
+
muted: '#7b7c7e',
|
|
23958
|
+
selection: '#353535',
|
|
23959
|
+
success: '#42be65',
|
|
23960
|
+
warning: '#ffe97b',
|
|
23961
|
+
},
|
|
23962
|
+
'tokyonight-storm': {
|
|
23963
|
+
accent: '#7aa2f7',
|
|
23964
|
+
border: '#2f334d',
|
|
23965
|
+
danger: '#f7768e',
|
|
23966
|
+
focusBorder: '#2ac3de',
|
|
23967
|
+
gitAdded: '#9ece6a',
|
|
23968
|
+
gitDeleted: '#f7768e',
|
|
23969
|
+
gitModified: '#e0af68',
|
|
23970
|
+
info: '#2ac3de',
|
|
23971
|
+
muted: '#545c7e',
|
|
23972
|
+
selection: '#2f334d',
|
|
23973
|
+
success: '#9ece6a',
|
|
23974
|
+
warning: '#e0af68',
|
|
23975
|
+
},
|
|
23976
|
+
'catppuccin-latte': {
|
|
23977
|
+
accent: '#1e66f5',
|
|
23978
|
+
border: '#ccd0da',
|
|
23979
|
+
danger: '#d20f39',
|
|
23980
|
+
focusBorder: '#179299',
|
|
23981
|
+
gitAdded: '#40a02b',
|
|
23982
|
+
gitDeleted: '#d20f39',
|
|
23983
|
+
gitModified: '#df8e1d',
|
|
23984
|
+
info: '#1e66f5',
|
|
23985
|
+
muted: '#9ca0b0',
|
|
23986
|
+
selection: '#ccd0da',
|
|
23987
|
+
success: '#40a02b',
|
|
23988
|
+
warning: '#df8e1d',
|
|
23989
|
+
},
|
|
23990
|
+
'solarized-light': {
|
|
23991
|
+
accent: '#268bd2',
|
|
23992
|
+
border: '#eee8d5',
|
|
23993
|
+
danger: '#dc322f',
|
|
23994
|
+
focusBorder: '#2aa198',
|
|
23995
|
+
gitAdded: '#859900',
|
|
23996
|
+
gitDeleted: '#dc322f',
|
|
23997
|
+
gitModified: '#b58900',
|
|
23998
|
+
info: '#268bd2',
|
|
23999
|
+
muted: '#93a1a1',
|
|
24000
|
+
selection: '#eee8d5',
|
|
24001
|
+
success: '#859900',
|
|
24002
|
+
warning: '#b58900',
|
|
24003
|
+
},
|
|
24004
|
+
'github-light': {
|
|
24005
|
+
accent: '#0969da',
|
|
24006
|
+
border: '#d0d7de',
|
|
24007
|
+
danger: '#cf222e',
|
|
24008
|
+
focusBorder: '#0969da',
|
|
24009
|
+
gitAdded: '#1a7f37',
|
|
24010
|
+
gitDeleted: '#cf222e',
|
|
24011
|
+
gitModified: '#9a6700',
|
|
24012
|
+
info: '#0969da',
|
|
24013
|
+
muted: '#656d76',
|
|
24014
|
+
selection: '#ddf4ff',
|
|
24015
|
+
success: '#1a7f37',
|
|
24016
|
+
warning: '#9a6700',
|
|
24017
|
+
},
|
|
24018
|
+
iceberg: {
|
|
24019
|
+
accent: '#84a0c6',
|
|
24020
|
+
border: '#1e2132',
|
|
24021
|
+
danger: '#e27878',
|
|
24022
|
+
focusBorder: '#89b8c2',
|
|
24023
|
+
gitAdded: '#b4be82',
|
|
24024
|
+
gitDeleted: '#e27878',
|
|
24025
|
+
gitModified: '#e2a478',
|
|
24026
|
+
info: '#84a0c6',
|
|
24027
|
+
muted: '#6b7089',
|
|
24028
|
+
selection: '#1e2132',
|
|
24029
|
+
success: '#b4be82',
|
|
24030
|
+
warning: '#e2a478',
|
|
24031
|
+
},
|
|
24032
|
+
'material-ocean': {
|
|
24033
|
+
accent: '#82aaff',
|
|
24034
|
+
border: '#2b2f3a',
|
|
24035
|
+
danger: '#f07178',
|
|
24036
|
+
focusBorder: '#89ddff',
|
|
24037
|
+
gitAdded: '#c3e88d',
|
|
24038
|
+
gitDeleted: '#f07178',
|
|
24039
|
+
gitModified: '#ffcb6b',
|
|
24040
|
+
info: '#82aaff',
|
|
24041
|
+
muted: '#464b5d',
|
|
24042
|
+
selection: '#2b2f3a',
|
|
24043
|
+
success: '#c3e88d',
|
|
24044
|
+
warning: '#ffcb6b',
|
|
24045
|
+
},
|
|
24046
|
+
moonlight: {
|
|
24047
|
+
accent: '#82aaff',
|
|
24048
|
+
border: '#2f334d',
|
|
24049
|
+
danger: '#ff757f',
|
|
24050
|
+
focusBorder: '#86e1fc',
|
|
24051
|
+
gitAdded: '#c3e88d',
|
|
24052
|
+
gitDeleted: '#ff757f',
|
|
24053
|
+
gitModified: '#ffc777',
|
|
24054
|
+
info: '#82aaff',
|
|
24055
|
+
muted: '#636da6',
|
|
24056
|
+
selection: '#2f334d',
|
|
24057
|
+
success: '#c3e88d',
|
|
24058
|
+
warning: '#ffc777',
|
|
24059
|
+
},
|
|
24060
|
+
poimandres: {
|
|
24061
|
+
accent: '#add7ff',
|
|
24062
|
+
border: '#1b1e28',
|
|
24063
|
+
danger: '#d0679d',
|
|
24064
|
+
focusBorder: '#5de4c7',
|
|
24065
|
+
gitAdded: '#5de4c7',
|
|
24066
|
+
gitDeleted: '#d0679d',
|
|
24067
|
+
gitModified: '#fffac2',
|
|
24068
|
+
info: '#add7ff',
|
|
24069
|
+
muted: '#506477',
|
|
24070
|
+
selection: '#1b1e28',
|
|
24071
|
+
success: '#5de4c7',
|
|
24072
|
+
warning: '#fffac2',
|
|
24073
|
+
},
|
|
24074
|
+
'vitesse-dark': {
|
|
24075
|
+
accent: '#4d9375',
|
|
24076
|
+
border: '#282828',
|
|
24077
|
+
danger: '#cb7676',
|
|
24078
|
+
focusBorder: '#4d9375',
|
|
24079
|
+
gitAdded: '#4d9375',
|
|
24080
|
+
gitDeleted: '#cb7676',
|
|
24081
|
+
gitModified: '#e6cc77',
|
|
24082
|
+
info: '#6394bf',
|
|
24083
|
+
muted: '#758575',
|
|
24084
|
+
selection: '#282828',
|
|
24085
|
+
success: '#4d9375',
|
|
24086
|
+
warning: '#e6cc77',
|
|
24087
|
+
},
|
|
24088
|
+
vesper: {
|
|
24089
|
+
accent: '#ffc799',
|
|
24090
|
+
border: '#232323',
|
|
24091
|
+
danger: '#f5a191',
|
|
24092
|
+
focusBorder: '#99ffe4',
|
|
24093
|
+
gitAdded: '#99ffe4',
|
|
24094
|
+
gitDeleted: '#f5a191',
|
|
24095
|
+
gitModified: '#ffc799',
|
|
24096
|
+
info: '#a0c4ff',
|
|
24097
|
+
muted: '#575757',
|
|
24098
|
+
selection: '#232323',
|
|
24099
|
+
success: '#99ffe4',
|
|
24100
|
+
warning: '#ffc799',
|
|
24101
|
+
},
|
|
24102
|
+
flexoki: {
|
|
24103
|
+
accent: '#205ea6',
|
|
24104
|
+
border: '#343331',
|
|
24105
|
+
danger: '#af3029',
|
|
24106
|
+
focusBorder: '#24837b',
|
|
24107
|
+
gitAdded: '#66800b',
|
|
24108
|
+
gitDeleted: '#af3029',
|
|
24109
|
+
gitModified: '#ad8301',
|
|
24110
|
+
info: '#205ea6',
|
|
24111
|
+
muted: '#878580',
|
|
24112
|
+
selection: '#343331',
|
|
24113
|
+
success: '#66800b',
|
|
24114
|
+
warning: '#ad8301',
|
|
24115
|
+
},
|
|
24116
|
+
mellow: {
|
|
24117
|
+
accent: '#7eb8da',
|
|
24118
|
+
border: '#2a2a2a',
|
|
24119
|
+
danger: '#f5a191',
|
|
24120
|
+
focusBorder: '#a3d4a0',
|
|
24121
|
+
gitAdded: '#a3d4a0',
|
|
24122
|
+
gitDeleted: '#f5a191',
|
|
24123
|
+
gitModified: '#f0c674',
|
|
24124
|
+
info: '#7eb8da',
|
|
24125
|
+
muted: '#6b6b6b',
|
|
24126
|
+
selection: '#2a2a2a',
|
|
24127
|
+
success: '#a3d4a0',
|
|
24128
|
+
warning: '#f0c674',
|
|
24129
|
+
},
|
|
24130
|
+
'night-owl': {
|
|
24131
|
+
accent: '#82aaff',
|
|
24132
|
+
border: '#1d3b53',
|
|
24133
|
+
danger: '#ef5350',
|
|
24134
|
+
focusBorder: '#7fdbca',
|
|
24135
|
+
gitAdded: '#addb67',
|
|
24136
|
+
gitDeleted: '#ef5350',
|
|
24137
|
+
gitModified: '#ecc48d',
|
|
24138
|
+
info: '#82aaff',
|
|
24139
|
+
muted: '#637777',
|
|
24140
|
+
selection: '#1d3b53',
|
|
24141
|
+
success: '#addb67',
|
|
24142
|
+
warning: '#ecc48d',
|
|
24143
|
+
},
|
|
24144
|
+
cobalt2: {
|
|
24145
|
+
accent: '#ffc600',
|
|
24146
|
+
border: '#234e6d',
|
|
24147
|
+
danger: '#ff628c',
|
|
24148
|
+
focusBorder: '#9effff',
|
|
24149
|
+
gitAdded: '#3ad900',
|
|
24150
|
+
gitDeleted: '#ff628c',
|
|
24151
|
+
gitModified: '#ffc600',
|
|
24152
|
+
info: '#9effff',
|
|
24153
|
+
muted: '#627e99',
|
|
24154
|
+
selection: '#0d3a58',
|
|
24155
|
+
success: '#3ad900',
|
|
24156
|
+
warning: '#ffc600',
|
|
24157
|
+
},
|
|
24158
|
+
'oceanic-next': {
|
|
24159
|
+
accent: '#6699cc',
|
|
24160
|
+
border: '#343d46',
|
|
24161
|
+
danger: '#ec5f67',
|
|
24162
|
+
focusBorder: '#5fb3b3',
|
|
24163
|
+
gitAdded: '#99c794',
|
|
24164
|
+
gitDeleted: '#ec5f67',
|
|
24165
|
+
gitModified: '#fac863',
|
|
24166
|
+
info: '#6699cc',
|
|
24167
|
+
muted: '#65737e',
|
|
24168
|
+
selection: '#4f5b66',
|
|
24169
|
+
success: '#99c794',
|
|
24170
|
+
warning: '#fac863',
|
|
24171
|
+
},
|
|
24172
|
+
'catppuccin-macchiato': {
|
|
24173
|
+
accent: '#8aadf4',
|
|
24174
|
+
border: '#494d64',
|
|
24175
|
+
danger: '#ed8796',
|
|
24176
|
+
focusBorder: '#91d7e3',
|
|
24177
|
+
gitAdded: '#a6da95',
|
|
24178
|
+
gitDeleted: '#ed8796',
|
|
24179
|
+
gitModified: '#eed49f',
|
|
24180
|
+
info: '#8aadf4',
|
|
24181
|
+
muted: '#6e738d',
|
|
24182
|
+
selection: '#363a4f',
|
|
24183
|
+
success: '#a6da95',
|
|
24184
|
+
warning: '#eed49f',
|
|
24185
|
+
},
|
|
24186
|
+
'gruvbox-light': {
|
|
24187
|
+
accent: '#076678',
|
|
24188
|
+
border: '#bdae93',
|
|
24189
|
+
danger: '#9d0006',
|
|
24190
|
+
focusBorder: '#427b58',
|
|
24191
|
+
gitAdded: '#79740e',
|
|
24192
|
+
gitDeleted: '#9d0006',
|
|
24193
|
+
gitModified: '#b57614',
|
|
24194
|
+
info: '#076678',
|
|
24195
|
+
muted: '#7c6f64',
|
|
24196
|
+
selection: '#ebdbb2',
|
|
24197
|
+
success: '#79740e',
|
|
24198
|
+
warning: '#b57614',
|
|
24199
|
+
},
|
|
24200
|
+
'tokyo-night-day': {
|
|
24201
|
+
accent: '#2e7de9',
|
|
24202
|
+
border: '#b7c1e3',
|
|
24203
|
+
danger: '#f52a65',
|
|
24204
|
+
focusBorder: '#007197',
|
|
24205
|
+
gitAdded: '#587539',
|
|
24206
|
+
gitDeleted: '#f52a65',
|
|
24207
|
+
gitModified: '#8c6c3e',
|
|
24208
|
+
info: '#2e7de9',
|
|
24209
|
+
muted: '#848cb5',
|
|
24210
|
+
selection: '#b7c1e3',
|
|
24211
|
+
success: '#587539',
|
|
24212
|
+
warning: '#8c6c3e',
|
|
24213
|
+
},
|
|
24214
|
+
'one-light': {
|
|
24215
|
+
accent: '#4078f2',
|
|
24216
|
+
border: '#d4d4d4',
|
|
24217
|
+
danger: '#e45649',
|
|
24218
|
+
focusBorder: '#0184bc',
|
|
24219
|
+
gitAdded: '#50a14f',
|
|
24220
|
+
gitDeleted: '#e45649',
|
|
24221
|
+
gitModified: '#c18401',
|
|
24222
|
+
info: '#4078f2',
|
|
24223
|
+
muted: '#a0a1a7',
|
|
24224
|
+
selection: '#e5e5e6',
|
|
24225
|
+
success: '#50a14f',
|
|
24226
|
+
warning: '#c18401',
|
|
24227
|
+
},
|
|
24228
|
+
'ayu-light': {
|
|
24229
|
+
accent: '#fa8d3e',
|
|
24230
|
+
border: '#e6e6e6',
|
|
24231
|
+
danger: '#e65050',
|
|
24232
|
+
focusBorder: '#4cbf99',
|
|
24233
|
+
gitAdded: '#6cbf43',
|
|
24234
|
+
gitDeleted: '#e65050',
|
|
24235
|
+
gitModified: '#f2ae49',
|
|
24236
|
+
info: '#399ee6',
|
|
24237
|
+
muted: '#abb0b6',
|
|
24238
|
+
selection: '#d1e4f4',
|
|
24239
|
+
success: '#6cbf43',
|
|
24240
|
+
warning: '#f2ae49',
|
|
24241
|
+
},
|
|
24242
|
+
'rose-pine-dawn': {
|
|
24243
|
+
accent: '#907aa9',
|
|
24244
|
+
border: '#dfdad9',
|
|
24245
|
+
danger: '#b4637a',
|
|
24246
|
+
focusBorder: '#56949f',
|
|
24247
|
+
gitAdded: '#286983',
|
|
24248
|
+
gitDeleted: '#b4637a',
|
|
24249
|
+
gitModified: '#ea9d34',
|
|
24250
|
+
info: '#56949f',
|
|
24251
|
+
muted: '#9893a5',
|
|
24252
|
+
selection: '#dfdad9',
|
|
24253
|
+
success: '#286983',
|
|
24254
|
+
warning: '#ea9d34',
|
|
24255
|
+
},
|
|
24256
|
+
'everforest-light': {
|
|
24257
|
+
accent: '#8da101',
|
|
24258
|
+
border: '#ddd8be',
|
|
24259
|
+
danger: '#f85552',
|
|
24260
|
+
focusBorder: '#35a77c',
|
|
24261
|
+
gitAdded: '#8da101',
|
|
24262
|
+
gitDeleted: '#f85552',
|
|
24263
|
+
gitModified: '#dfa000',
|
|
24264
|
+
info: '#3a94c5',
|
|
24265
|
+
muted: '#939f91',
|
|
24266
|
+
selection: '#edeada',
|
|
24267
|
+
success: '#8da101',
|
|
24268
|
+
warning: '#dfa000',
|
|
24269
|
+
},
|
|
24270
|
+
'vitesse-light': {
|
|
24271
|
+
accent: '#1e754f',
|
|
24272
|
+
border: '#e0e0e0',
|
|
24273
|
+
danger: '#ab5959',
|
|
24274
|
+
focusBorder: '#2993a3',
|
|
24275
|
+
gitAdded: '#1e754f',
|
|
24276
|
+
gitDeleted: '#ab5959',
|
|
24277
|
+
gitModified: '#b07d48',
|
|
24278
|
+
info: '#296aa3',
|
|
24279
|
+
muted: '#999fa6',
|
|
24280
|
+
selection: '#eaeaeb',
|
|
24281
|
+
success: '#1e754f',
|
|
24282
|
+
warning: '#b07d48',
|
|
24283
|
+
},
|
|
24284
|
+
dayfox: {
|
|
24285
|
+
accent: '#2848a9',
|
|
24286
|
+
border: '#e4dcd4',
|
|
24287
|
+
danger: '#a5222f',
|
|
24288
|
+
focusBorder: '#287980',
|
|
24289
|
+
gitAdded: '#396847',
|
|
24290
|
+
gitDeleted: '#a5222f',
|
|
24291
|
+
gitModified: '#ac5402',
|
|
24292
|
+
info: '#2848a9',
|
|
24293
|
+
muted: '#908479',
|
|
24294
|
+
selection: '#e7d2be',
|
|
24295
|
+
success: '#396847',
|
|
24296
|
+
warning: '#ac5402',
|
|
24297
|
+
},
|
|
24298
|
+
'night-owl-light': {
|
|
24299
|
+
accent: '#288ed7',
|
|
24300
|
+
border: '#d9d9d9',
|
|
24301
|
+
danger: '#d3423e',
|
|
24302
|
+
focusBorder: '#2aa298',
|
|
24303
|
+
gitAdded: '#08916a',
|
|
24304
|
+
gitDeleted: '#d3423e',
|
|
24305
|
+
gitModified: '#daaa01',
|
|
24306
|
+
info: '#288ed7',
|
|
24307
|
+
muted: '#989fb1',
|
|
24308
|
+
selection: '#e4e8f0',
|
|
24309
|
+
success: '#08916a',
|
|
24310
|
+
warning: '#daaa01',
|
|
24311
|
+
},
|
|
24312
|
+
'flexoki-light': {
|
|
24313
|
+
accent: '#205ea6',
|
|
24314
|
+
border: '#cecdc3',
|
|
24315
|
+
danger: '#af3029',
|
|
24316
|
+
focusBorder: '#24837b',
|
|
24317
|
+
gitAdded: '#66800b',
|
|
24318
|
+
gitDeleted: '#af3029',
|
|
24319
|
+
gitModified: '#ad8301',
|
|
24320
|
+
info: '#205ea6',
|
|
24321
|
+
muted: '#6f6e69',
|
|
24322
|
+
selection: '#e6e4d9',
|
|
24323
|
+
success: '#66800b',
|
|
24324
|
+
warning: '#ad8301',
|
|
24325
|
+
},
|
|
24326
|
+
'material-lighter': {
|
|
24327
|
+
accent: '#39adb5',
|
|
24328
|
+
border: '#e7eaec',
|
|
24329
|
+
danger: '#e53935',
|
|
24330
|
+
focusBorder: '#39adb5',
|
|
24331
|
+
gitAdded: '#91b859',
|
|
24332
|
+
gitDeleted: '#e53935',
|
|
24333
|
+
gitModified: '#f6a434',
|
|
24334
|
+
info: '#6182b8',
|
|
24335
|
+
muted: '#90a4ae',
|
|
24336
|
+
selection: '#d3e1e8',
|
|
24337
|
+
success: '#91b859',
|
|
24338
|
+
warning: '#f6a434',
|
|
24339
|
+
},
|
|
24340
|
+
'papercolor-light': {
|
|
24341
|
+
accent: '#0087af',
|
|
24342
|
+
border: '#d7d7d7',
|
|
24343
|
+
danger: '#af0000',
|
|
24344
|
+
focusBorder: '#005f87',
|
|
24345
|
+
gitAdded: '#008700',
|
|
24346
|
+
gitDeleted: '#af0000',
|
|
24347
|
+
gitModified: '#d75f00',
|
|
24348
|
+
info: '#0087af',
|
|
24349
|
+
muted: '#878787',
|
|
24350
|
+
selection: '#d0d0d0',
|
|
24351
|
+
success: '#008700',
|
|
24352
|
+
warning: '#d75f00',
|
|
24353
|
+
},
|
|
24354
|
+
'modus-operandi': {
|
|
24355
|
+
accent: '#0031a9',
|
|
24356
|
+
border: '#d7d7d7',
|
|
24357
|
+
danger: '#a60000',
|
|
24358
|
+
focusBorder: '#005e8b',
|
|
24359
|
+
gitAdded: '#006800',
|
|
24360
|
+
gitDeleted: '#a60000',
|
|
24361
|
+
gitModified: '#6f5500',
|
|
24362
|
+
info: '#0031a9',
|
|
24363
|
+
muted: '#595959',
|
|
24364
|
+
selection: '#c0deff',
|
|
24365
|
+
success: '#006800',
|
|
24366
|
+
warning: '#6f5500',
|
|
24367
|
+
},
|
|
24368
|
+
'quiet-light': {
|
|
24369
|
+
accent: '#4b83cd',
|
|
24370
|
+
border: '#e0e0e0',
|
|
24371
|
+
danger: '#aa3731',
|
|
24372
|
+
focusBorder: '#4b83cd',
|
|
24373
|
+
gitAdded: '#448c27',
|
|
24374
|
+
gitDeleted: '#aa3731',
|
|
24375
|
+
gitModified: '#a67d00',
|
|
24376
|
+
info: '#4b83cd',
|
|
24377
|
+
muted: '#a3a6ad',
|
|
24378
|
+
selection: '#c9d0d9',
|
|
24379
|
+
success: '#448c27',
|
|
24380
|
+
warning: '#a67d00',
|
|
24381
|
+
},
|
|
24382
|
+
};
|
|
24383
|
+
/**
|
|
24384
|
+
* Ordered list of every selectable theme preset, for the `coco ui` theme
|
|
24385
|
+
* picker and any UI that enumerates themes. `monochrome` isn't a key in
|
|
24386
|
+
* `THEME_PRESET_COLORS` (it's handled via `noColor`), so it's spliced in
|
|
24387
|
+
* right after `default` — the two non-color baselines sit together at the
|
|
24388
|
+
* top, followed by the color themes in catalog order.
|
|
24389
|
+
*/
|
|
24390
|
+
function getLogInkThemePresets() {
|
|
24391
|
+
const keys = Object.keys(THEME_PRESET_COLORS);
|
|
24392
|
+
const [first, ...rest] = keys;
|
|
24393
|
+
return first === 'default'
|
|
24394
|
+
? ['default', 'monochrome', ...rest]
|
|
24395
|
+
: ['monochrome', ...keys];
|
|
24031
24396
|
}
|
|
24032
|
-
function
|
|
24033
|
-
|
|
24034
|
-
|
|
24035
|
-
|
|
24036
|
-
|
|
24037
|
-
// when the previously-selected item dropped out. Falls back to the older
|
|
24038
|
-
// "snap to 0" behavior when no snapshot was provided (test paths,
|
|
24039
|
-
// dispatchers without context).
|
|
24040
|
-
const filterChanged = state.filter !== filter;
|
|
24041
|
-
const branchIndex = promotedSelections?.branchIndex ??
|
|
24042
|
-
(filterChanged ? 0 : state.selectedBranchIndex);
|
|
24043
|
-
const tagIndex = promotedSelections?.tagIndex ??
|
|
24044
|
-
(filterChanged ? 0 : state.selectedTagIndex);
|
|
24045
|
-
const stashIndex = promotedSelections?.stashIndex ??
|
|
24046
|
-
(filterChanged ? 0 : state.selectedStashIndex);
|
|
24047
|
-
// Reflog (#781) snaps to 0 on filter change rather than rectifying.
|
|
24048
|
-
// The list is chronological and the user is unlikely to be tracking
|
|
24049
|
-
// a specific entry through filter changes — the simpler reset
|
|
24050
|
-
// matches the "find recovery target by typing" interaction.
|
|
24051
|
-
const reflogIndex = filterChanged ? 0 : state.selectedReflogIndex;
|
|
24052
|
-
return {
|
|
24053
|
-
...state,
|
|
24054
|
-
filter,
|
|
24055
|
-
filteredCommits,
|
|
24056
|
-
selectedIndex: clampIndex(state.selectedIndex, filteredCommits.length),
|
|
24057
|
-
selectedFileIndex: 0,
|
|
24058
|
-
selectedBranchIndex: branchIndex,
|
|
24059
|
-
selectedTagIndex: tagIndex,
|
|
24060
|
-
selectedStashIndex: stashIndex,
|
|
24061
|
-
selectedReflogIndex: reflogIndex,
|
|
24062
|
-
diffPreviewOffset: 0,
|
|
24063
|
-
pendingKey: undefined,
|
|
24064
|
-
};
|
|
24397
|
+
function shouldUseAscii(term) {
|
|
24398
|
+
if (!term) {
|
|
24399
|
+
return false;
|
|
24400
|
+
}
|
|
24401
|
+
return term === 'dumb' || term.startsWith('vt100');
|
|
24065
24402
|
}
|
|
24066
|
-
function
|
|
24067
|
-
|
|
24068
|
-
|
|
24069
|
-
|
|
24070
|
-
|
|
24071
|
-
|
|
24072
|
-
|
|
24403
|
+
function createLogInkTheme(options = {}) {
|
|
24404
|
+
const noColor = (options.noColor ?? Boolean(process.env.NO_COLOR)) ||
|
|
24405
|
+
options.preset === 'monochrome';
|
|
24406
|
+
const ascii = options.ascii ?? shouldUseAscii(options.term ?? process.env.TERM);
|
|
24407
|
+
const requestedPreset = options.preset && options.preset !== 'monochrome' ? options.preset : 'default';
|
|
24408
|
+
// P5.2 — gracefully downgrade hex presets (catppuccin / gruvbox) when
|
|
24409
|
+
// the host terminal can't render truecolor. Chalk approximates hex in
|
|
24410
|
+
// those modes anyway, but the default preset's ANSI-named palette
|
|
24411
|
+
// renders far more faithfully on 16-color terminals.
|
|
24412
|
+
const colorLevel = getColorLevel(options.env ?? process.env);
|
|
24413
|
+
const preset = !noColor && presetUsesTrueColor(requestedPreset) && colorLevel !== 'truecolor'
|
|
24414
|
+
? 'default'
|
|
24415
|
+
: requestedPreset;
|
|
24416
|
+
const colors = noColor
|
|
24417
|
+
? {}
|
|
24418
|
+
: {
|
|
24419
|
+
...THEME_PRESET_COLORS[preset],
|
|
24420
|
+
// Preserve the requested theme's selection background even when the
|
|
24421
|
+
// rest of the palette downgrades to `default`. The selection is a
|
|
24422
|
+
// single background color the terminal can approximate; without this,
|
|
24423
|
+
// a light theme inherits `default`'s dark selection (#1a3a4a) and the
|
|
24424
|
+
// selected row renders as a dark bar on a light background.
|
|
24425
|
+
...(preset !== requestedPreset && THEME_PRESET_COLORS[requestedPreset]?.selection
|
|
24426
|
+
? { selection: THEME_PRESET_COLORS[requestedPreset].selection }
|
|
24427
|
+
: {}),
|
|
24428
|
+
...options.colors,
|
|
24429
|
+
};
|
|
24430
|
+
// Derive a contrasting foreground for the selected row from its own
|
|
24431
|
+
// selection background, unless the caller supplied one explicitly. coco
|
|
24432
|
+
// owns the selection background but not the terminal's default foreground,
|
|
24433
|
+
// so without this the selected row's text falls back to whatever the
|
|
24434
|
+
// user's terminal foreground is — which may not contrast with the bar at
|
|
24435
|
+
// all (the bug behind unreadable selected rows on many themes).
|
|
24436
|
+
if (!noColor && colors.selection && !colors.selectionForeground) {
|
|
24437
|
+
const selectionForeground = readableForegroundFor(colors.selection);
|
|
24438
|
+
if (selectionForeground) {
|
|
24439
|
+
colors.selectionForeground = selectionForeground;
|
|
24440
|
+
}
|
|
24441
|
+
}
|
|
24073
24442
|
return {
|
|
24074
|
-
|
|
24075
|
-
|
|
24076
|
-
|
|
24077
|
-
|
|
24078
|
-
selectedIndex: 0,
|
|
24079
|
-
selectedFileIndex: 0,
|
|
24080
|
-
pendingCommitFocused: false,
|
|
24081
|
-
pendingKey: undefined,
|
|
24082
|
-
// Rows just landed — clear the boot-loading flag so the history
|
|
24083
|
-
// surface drops the "Loading commits…" placeholder. Safe to clear
|
|
24084
|
-
// unconditionally because `replaceRows` only fires after a real
|
|
24085
|
-
// git log returns.
|
|
24086
|
-
bootLoading: false,
|
|
24443
|
+
noColor,
|
|
24444
|
+
ascii,
|
|
24445
|
+
borderStyle: options.borderStyle || (ascii ? 'classic' : 'round'),
|
|
24446
|
+
colors,
|
|
24087
24447
|
};
|
|
24088
24448
|
}
|
|
24089
|
-
|
|
24090
|
-
|
|
24091
|
-
|
|
24092
|
-
|
|
24093
|
-
|
|
24094
|
-
|
|
24095
|
-
|
|
24449
|
+
|
|
24450
|
+
/**
|
|
24451
|
+
* Canned filter presets for the issue / PR triage TUI views
|
|
24452
|
+
* (#882 phase 6). Each preset compiles to the same shape the
|
|
24453
|
+
* underlying list fetchers (`getIssueList` / `getPullRequestList`)
|
|
24454
|
+
* already accept — there's no new `gh` surface area, just a
|
|
24455
|
+
* curated set of common triage angles surfaced as a single
|
|
24456
|
+
* keystroke (`f` cycles).
|
|
24457
|
+
*
|
|
24458
|
+
* The presets are deliberately *not* a 1:1 mirror across the two
|
|
24459
|
+
* surfaces:
|
|
24460
|
+
*
|
|
24461
|
+
* - Issues have no draft / mergeable concept, so `draft` /
|
|
24462
|
+
* `mergeable` are skipped on the issue list.
|
|
24463
|
+
* - PRs have a `merged` state distinct from `closed`; issues
|
|
24464
|
+
* don't.
|
|
24465
|
+
* - `mine` semantics differ subtly: for issues it tends to
|
|
24466
|
+
* mean "I'm the assignee" (issues are tasks people pick up);
|
|
24467
|
+
* for PRs it means "I'm the author" (PRs are work people
|
|
24468
|
+
* post). The presets bake those in so the user doesn't have
|
|
24469
|
+
* to think about it.
|
|
24470
|
+
*/
|
|
24471
|
+
/** Cycle order — must match the keystroke walk on `f`. */
|
|
24472
|
+
const ISSUE_FILTER_PRESETS = [
|
|
24473
|
+
'open',
|
|
24474
|
+
'closed',
|
|
24475
|
+
'mine',
|
|
24476
|
+
'assigned',
|
|
24477
|
+
];
|
|
24478
|
+
const PULL_REQUEST_FILTER_PRESETS = [
|
|
24479
|
+
'open',
|
|
24480
|
+
'draft',
|
|
24481
|
+
'mine',
|
|
24482
|
+
'assigned',
|
|
24483
|
+
'closed',
|
|
24484
|
+
'merged',
|
|
24485
|
+
];
|
|
24486
|
+
const ISSUE_FILTER_LABELS = {
|
|
24487
|
+
open: 'open',
|
|
24488
|
+
closed: 'closed',
|
|
24489
|
+
mine: 'mine (assigned)',
|
|
24490
|
+
assigned: 'assigned to me',
|
|
24491
|
+
};
|
|
24492
|
+
const PULL_REQUEST_FILTER_LABELS = {
|
|
24493
|
+
open: 'open',
|
|
24494
|
+
draft: 'draft',
|
|
24495
|
+
mine: 'mine (authored)',
|
|
24496
|
+
assigned: 'assigned to me',
|
|
24497
|
+
closed: 'closed',
|
|
24498
|
+
merged: 'merged',
|
|
24499
|
+
};
|
|
24500
|
+
/**
|
|
24501
|
+
* Resolve a preset to the filter object the data fetcher accepts.
|
|
24502
|
+
* Pure mapping — no `gh` calls. Kept separate from `getIssueList` /
|
|
24503
|
+
* `getPullRequestList` so unit tests can assert the mapping
|
|
24504
|
+
* independently from the fetch pipeline.
|
|
24505
|
+
*/
|
|
24506
|
+
function issueFilterForPreset(preset) {
|
|
24507
|
+
switch (preset) {
|
|
24508
|
+
case 'open':
|
|
24509
|
+
return { state: 'open' };
|
|
24510
|
+
case 'closed':
|
|
24511
|
+
return { state: 'closed' };
|
|
24512
|
+
case 'mine':
|
|
24513
|
+
// Issues are tasks — "mine" is what *I'm working on*, i.e.
|
|
24514
|
+
// assigned to me + still open. Same as `assigned` plus the
|
|
24515
|
+
// open-state filter for ergonomic single-keystroke focus on
|
|
24516
|
+
// the active backlog.
|
|
24517
|
+
return { state: 'open', assignee: '@me' };
|
|
24518
|
+
case 'assigned':
|
|
24519
|
+
return { assignee: '@me' };
|
|
24520
|
+
}
|
|
24521
|
+
}
|
|
24522
|
+
function pullRequestFilterForPreset(preset) {
|
|
24523
|
+
switch (preset) {
|
|
24524
|
+
case 'open':
|
|
24525
|
+
return { state: 'open' };
|
|
24526
|
+
case 'draft':
|
|
24527
|
+
// gh's `--draft` flag implies `--state open`; surface that
|
|
24528
|
+
// explicitly so the canonicalize step doesn't elide it.
|
|
24529
|
+
return { state: 'open', draft: true };
|
|
24530
|
+
case 'mine':
|
|
24531
|
+
// PRs are work — "mine" is what *I authored*. Most useful
|
|
24532
|
+
// when looking at one's own backlog of in-flight PRs.
|
|
24533
|
+
return { state: 'open', author: '@me' };
|
|
24534
|
+
case 'assigned':
|
|
24535
|
+
return { assignee: '@me' };
|
|
24536
|
+
case 'closed':
|
|
24537
|
+
return { state: 'closed' };
|
|
24538
|
+
case 'merged':
|
|
24539
|
+
return { state: 'merged' };
|
|
24540
|
+
}
|
|
24541
|
+
}
|
|
24542
|
+
function cycleIssueFilterPreset(current) {
|
|
24543
|
+
const index = ISSUE_FILTER_PRESETS.indexOf(current);
|
|
24544
|
+
const next = (index + 1) % ISSUE_FILTER_PRESETS.length;
|
|
24545
|
+
return ISSUE_FILTER_PRESETS[next];
|
|
24546
|
+
}
|
|
24547
|
+
function cyclePullRequestFilterPreset(current) {
|
|
24548
|
+
const index = PULL_REQUEST_FILTER_PRESETS.indexOf(current);
|
|
24549
|
+
const next = (index + 1) % PULL_REQUEST_FILTER_PRESETS.length;
|
|
24550
|
+
return PULL_REQUEST_FILTER_PRESETS[next];
|
|
24551
|
+
}
|
|
24552
|
+
|
|
24553
|
+
/**
|
|
24554
|
+
* Sort modes for the promoted views (P4.2).
|
|
24555
|
+
*
|
|
24556
|
+
* Pure: takes existing context entries + the active mode, returns a sorted
|
|
24557
|
+
* copy. Tested in isolation; the runtime just calls these helpers.
|
|
24558
|
+
*
|
|
24559
|
+
* Display label uses `▼` (U+25BC) under truecolor / UTF-8 and falls back to
|
|
24560
|
+
* `v` under ASCII. Letters (`recent` / `name` / `ahead`) carry meaning;
|
|
24561
|
+
* shape enhances.
|
|
24562
|
+
*/
|
|
24563
|
+
const BRANCH_SORT_MODES = ['name', 'recent', 'ahead'];
|
|
24564
|
+
const DEFAULT_BRANCH_SORT_MODE = 'name';
|
|
24565
|
+
function cycleBranchSort(mode) {
|
|
24566
|
+
const index = BRANCH_SORT_MODES.indexOf(mode);
|
|
24567
|
+
if (index < 0)
|
|
24568
|
+
return BRANCH_SORT_MODES[0];
|
|
24569
|
+
return BRANCH_SORT_MODES[(index + 1) % BRANCH_SORT_MODES.length];
|
|
24570
|
+
}
|
|
24571
|
+
function sortBranches(branches, mode) {
|
|
24572
|
+
// Pin the current branch at index 0 regardless of sort mode (#806
|
|
24573
|
+
// follow-up). Lands the user's cursor on the active branch by
|
|
24574
|
+
// default and keeps the most-relevant row glued to the top of the
|
|
24575
|
+
// list as they cycle sorts.
|
|
24576
|
+
const current = branches.find((entry) => entry.current);
|
|
24577
|
+
const rest = branches.filter((entry) => !entry.current);
|
|
24578
|
+
const sortedRest = rest.slice();
|
|
24579
|
+
switch (mode) {
|
|
24580
|
+
case 'name':
|
|
24581
|
+
sortedRest.sort((a, b) => a.shortName.localeCompare(b.shortName));
|
|
24582
|
+
break;
|
|
24583
|
+
case 'recent':
|
|
24584
|
+
// ISO-shaped dates compare byte-for-byte; descending so the freshest
|
|
24585
|
+
// branch sits at the top.
|
|
24586
|
+
sortedRest.sort((a, b) => (b.date || '').localeCompare(a.date || '') ||
|
|
24587
|
+
a.shortName.localeCompare(b.shortName));
|
|
24588
|
+
break;
|
|
24589
|
+
case 'ahead':
|
|
24590
|
+
// ahead-first; ties broken by behind, then by name. Keeps "this branch
|
|
24591
|
+
// has unmerged work" in the user's first scroll.
|
|
24592
|
+
sortedRest.sort((a, b) => b.ahead - a.ahead || b.behind - a.behind ||
|
|
24593
|
+
a.shortName.localeCompare(b.shortName));
|
|
24594
|
+
break;
|
|
24595
|
+
}
|
|
24596
|
+
return current ? [current, ...sortedRest] : sortedRest;
|
|
24597
|
+
}
|
|
24598
|
+
const TAG_SORT_MODES = ['recent', 'name'];
|
|
24599
|
+
const DEFAULT_TAG_SORT_MODE = 'recent';
|
|
24600
|
+
function cycleTagSort(mode) {
|
|
24601
|
+
const index = TAG_SORT_MODES.indexOf(mode);
|
|
24602
|
+
if (index < 0)
|
|
24603
|
+
return TAG_SORT_MODES[0];
|
|
24604
|
+
return TAG_SORT_MODES[(index + 1) % TAG_SORT_MODES.length];
|
|
24605
|
+
}
|
|
24606
|
+
function sortTags(tags, mode) {
|
|
24607
|
+
const copy = tags.slice();
|
|
24608
|
+
switch (mode) {
|
|
24609
|
+
case 'name':
|
|
24610
|
+
return copy.sort((a, b) => a.name.localeCompare(b.name));
|
|
24611
|
+
case 'recent':
|
|
24612
|
+
return copy.sort((a, b) => (b.date || '').localeCompare(a.date || '') ||
|
|
24613
|
+
a.name.localeCompare(b.name));
|
|
24614
|
+
default:
|
|
24615
|
+
return copy;
|
|
24616
|
+
}
|
|
24617
|
+
}
|
|
24618
|
+
/* ---------------------------- header indicator -------------------------- */
|
|
24619
|
+
function formatSortIndicator(mode, options = {}) {
|
|
24620
|
+
return `${options.ascii ? 'v' : '▼'} ${mode}`;
|
|
24621
|
+
}
|
|
24622
|
+
|
|
24623
|
+
const DEFAULT_CHANGELOG_VIEW_STATE = {
|
|
24624
|
+
status: 'idle',
|
|
24625
|
+
scrollOffset: 0,
|
|
24626
|
+
};
|
|
24627
|
+
const DEFAULT_LOG_INK_STATUS_FILTER_MASK = {
|
|
24628
|
+
staged: true,
|
|
24629
|
+
unstaged: true,
|
|
24630
|
+
untracked: true,
|
|
24631
|
+
};
|
|
24632
|
+
/**
|
|
24633
|
+
* Detect a history server-side filter prefix (#776). Returns the parsed
|
|
24634
|
+
* `LogInkHistoryFetchArgs` for `path:<value>` and `author:<value>`
|
|
24635
|
+
* prefixes, or `undefined` for a plain (client-side) filter. The whole
|
|
24636
|
+
* remainder of the string (post-prefix) becomes the value — paths and
|
|
24637
|
+
* author names commonly contain spaces, and we don't try to parse
|
|
24638
|
+
* shell-like syntax.
|
|
24639
|
+
*/
|
|
24640
|
+
function parseLogInkHistoryFetchPrefix(filter) {
|
|
24641
|
+
const trimmed = filter.trim();
|
|
24642
|
+
if (trimmed.startsWith('path:')) {
|
|
24643
|
+
const value = trimmed.slice('path:'.length).trim();
|
|
24644
|
+
return value ? { path: value } : undefined;
|
|
24645
|
+
}
|
|
24646
|
+
if (trimmed.startsWith('author:')) {
|
|
24647
|
+
const value = trimmed.slice('author:'.length).trim();
|
|
24648
|
+
return value ? { author: value } : undefined;
|
|
24649
|
+
}
|
|
24650
|
+
return undefined;
|
|
24651
|
+
}
|
|
24652
|
+
const FOCUS_ORDER = ['sidebar', 'commits', 'detail'];
|
|
24653
|
+
const SIDEBAR_TABS = ['status', 'branches', 'tags', 'stashes', 'worktrees'];
|
|
24654
|
+
function searchableFields(commit) {
|
|
24655
|
+
return [
|
|
24656
|
+
commit.shortHash,
|
|
24657
|
+
commit.hash,
|
|
24658
|
+
commit.date,
|
|
24659
|
+
commit.author,
|
|
24660
|
+
commit.message,
|
|
24661
|
+
...commit.refs,
|
|
24662
|
+
];
|
|
24663
|
+
}
|
|
24664
|
+
function scoreField(field, term) {
|
|
24665
|
+
const value = field.toLowerCase();
|
|
24666
|
+
const normalized = term.toLowerCase();
|
|
24667
|
+
if (!normalized) {
|
|
24668
|
+
return 0;
|
|
24669
|
+
}
|
|
24670
|
+
if (value === normalized) {
|
|
24671
|
+
return 1000;
|
|
24672
|
+
}
|
|
24673
|
+
if (value.startsWith(normalized)) {
|
|
24674
|
+
return 800 - Math.min(value.length - normalized.length, 200);
|
|
24675
|
+
}
|
|
24676
|
+
const substringIndex = value.indexOf(normalized);
|
|
24677
|
+
if (substringIndex >= 0) {
|
|
24678
|
+
return 600 - Math.min(substringIndex, 200);
|
|
24679
|
+
}
|
|
24680
|
+
let searchIndex = 0;
|
|
24681
|
+
let distance = 0;
|
|
24682
|
+
for (const character of normalized) {
|
|
24683
|
+
const nextIndex = value.indexOf(character, searchIndex);
|
|
24684
|
+
if (nextIndex < 0) {
|
|
24685
|
+
return undefined;
|
|
24096
24686
|
}
|
|
24097
|
-
|
|
24098
|
-
|
|
24099
|
-
}
|
|
24100
|
-
|
|
24101
|
-
|
|
24102
|
-
|
|
24103
|
-
|
|
24687
|
+
distance += nextIndex - searchIndex;
|
|
24688
|
+
searchIndex = nextIndex + 1;
|
|
24689
|
+
}
|
|
24690
|
+
return 300 - Math.min(distance, 200);
|
|
24691
|
+
}
|
|
24692
|
+
function scoreLogInkCommitFilter(commit, filter) {
|
|
24693
|
+
const terms = filter.trim().split(/\s+/).filter(Boolean);
|
|
24694
|
+
if (terms.length === 0) {
|
|
24695
|
+
return 0;
|
|
24696
|
+
}
|
|
24697
|
+
const fields = searchableFields(commit);
|
|
24698
|
+
let score = 0;
|
|
24699
|
+
for (const term of terms) {
|
|
24700
|
+
const bestFieldScore = fields.reduce((best, field) => {
|
|
24701
|
+
const fieldScore = scoreField(field, term);
|
|
24702
|
+
if (fieldScore === undefined) {
|
|
24703
|
+
return best;
|
|
24704
|
+
}
|
|
24705
|
+
return best === undefined ? fieldScore : Math.max(best, fieldScore);
|
|
24706
|
+
}, undefined);
|
|
24707
|
+
if (bestFieldScore === undefined) {
|
|
24708
|
+
return undefined;
|
|
24709
|
+
}
|
|
24710
|
+
score += bestFieldScore;
|
|
24711
|
+
}
|
|
24712
|
+
return score;
|
|
24713
|
+
}
|
|
24714
|
+
function filterCommits(commits, filter) {
|
|
24715
|
+
return commits
|
|
24716
|
+
.map((commit, index) => ({
|
|
24717
|
+
commit,
|
|
24718
|
+
index,
|
|
24719
|
+
score: scoreLogInkCommitFilter(commit, filter),
|
|
24720
|
+
}))
|
|
24721
|
+
.filter((entry) => entry.score !== undefined)
|
|
24722
|
+
.sort((a, b) => b.score - a.score || a.index - b.index)
|
|
24723
|
+
.map((entry) => entry.commit);
|
|
24724
|
+
}
|
|
24725
|
+
function clampIndex(index, length) {
|
|
24726
|
+
if (length === 0) {
|
|
24727
|
+
return 0;
|
|
24728
|
+
}
|
|
24729
|
+
return Math.max(0, Math.min(index, length - 1));
|
|
24730
|
+
}
|
|
24731
|
+
function cycleValue(values, current, delta) {
|
|
24732
|
+
const currentIndex = Math.max(0, values.indexOf(current));
|
|
24733
|
+
const nextIndex = (currentIndex + delta + values.length) % values.length;
|
|
24734
|
+
return values[nextIndex];
|
|
24735
|
+
}
|
|
24736
|
+
const HOME_VIEW = 'history';
|
|
24737
|
+
function topOfStack(stack) {
|
|
24738
|
+
return stack[stack.length - 1];
|
|
24739
|
+
}
|
|
24740
|
+
function withPushedView(state, value) {
|
|
24741
|
+
if (topOfStack(state.viewStack) === value) {
|
|
24742
|
+
return { ...state, pendingKey: undefined };
|
|
24743
|
+
}
|
|
24744
|
+
const viewStack = [...state.viewStack, value];
|
|
24104
24745
|
return {
|
|
24105
24746
|
...state,
|
|
24106
|
-
|
|
24107
|
-
|
|
24108
|
-
|
|
24109
|
-
|
|
24110
|
-
|
|
24111
|
-
|
|
24747
|
+
activeView: value,
|
|
24748
|
+
viewStack,
|
|
24749
|
+
// The compose + status views' right detail panels already show
|
|
24750
|
+
// worktree info, so keeping the left sidebar on the Status tab
|
|
24751
|
+
// duplicates that information. Auto-switch to Branches when entering
|
|
24752
|
+
// either view; the user can swap back with [/] if they want.
|
|
24753
|
+
//
|
|
24754
|
+
// We update only the rendered `sidebarTab` here, never
|
|
24755
|
+
// `userSidebarTab`, so this auto-switch is invisible to per-repo
|
|
24756
|
+
// persistence and pop-view restores the previous tab.
|
|
24757
|
+
sidebarTab: value === 'compose' || value === 'status' ? 'branches' : state.sidebarTab,
|
|
24758
|
+
worktreeDiffOffset: value === 'diff' ? state.worktreeDiffOffset : 0,
|
|
24759
|
+
selectedWorktreeHunkIndex: value === 'diff' ? state.selectedWorktreeHunkIndex : 0,
|
|
24760
|
+
diffSource: value === 'diff' ? state.diffSource : undefined,
|
|
24761
|
+
stashDiffRef: value === 'diff' ? state.stashDiffRef : undefined,
|
|
24762
|
+
compareHead: value === 'diff' ? state.compareHead : undefined,
|
|
24763
|
+
pendingCommitFocused: value === 'history' ? state.pendingCommitFocused : false,
|
|
24764
|
+
statusGroupHeaderFocused: value === 'status' ? state.statusGroupHeaderFocused : false,
|
|
24112
24765
|
pendingKey: undefined,
|
|
24113
24766
|
};
|
|
24114
24767
|
}
|
|
24115
|
-
function
|
|
24116
|
-
if (
|
|
24117
|
-
return
|
|
24118
|
-
}
|
|
24119
|
-
if (delta > 0) {
|
|
24120
|
-
const nextOffset = hunkOffsets.find((offset) => offset > currentOffset);
|
|
24121
|
-
return nextOffset === undefined ? currentOffset : nextOffset;
|
|
24768
|
+
function withPoppedView(state) {
|
|
24769
|
+
if (state.viewStack.length <= 1) {
|
|
24770
|
+
return { ...state, pendingKey: undefined };
|
|
24122
24771
|
}
|
|
24123
|
-
const
|
|
24124
|
-
|
|
24125
|
-
|
|
24126
|
-
|
|
24127
|
-
|
|
24128
|
-
|
|
24129
|
-
|
|
24130
|
-
|
|
24131
|
-
|
|
24772
|
+
const viewStack = state.viewStack.slice(0, -1);
|
|
24773
|
+
const next = topOfStack(viewStack);
|
|
24774
|
+
// #779 — compareBase is "cleared when the diff view is popped." We
|
|
24775
|
+
// detect that case by checking if the *previous* top was 'diff'.
|
|
24776
|
+
// The compare workflow ends when the user backs out of the compare
|
|
24777
|
+
// diff; on the next mark they re-set the base. Other view pops
|
|
24778
|
+
// preserve compareBase so the user can move between branches / tags /
|
|
24779
|
+
// history while hunting for a head ref.
|
|
24780
|
+
const wasOnDiff = state.activeView === 'diff';
|
|
24781
|
+
return {
|
|
24782
|
+
...state,
|
|
24783
|
+
activeView: next,
|
|
24784
|
+
viewStack,
|
|
24785
|
+
// Restore the user's last explicit tab choice so popping out of
|
|
24786
|
+
// compose / status (which auto-switch the sidebar to Branches)
|
|
24787
|
+
// returns the user to whatever they actually had open before.
|
|
24788
|
+
sidebarTab: state.userSidebarTab,
|
|
24789
|
+
worktreeDiffOffset: next === 'diff' ? state.worktreeDiffOffset : 0,
|
|
24790
|
+
selectedWorktreeHunkIndex: next === 'diff' ? state.selectedWorktreeHunkIndex : 0,
|
|
24791
|
+
diffSource: next === 'diff' ? state.diffSource : undefined,
|
|
24792
|
+
stashDiffRef: next === 'diff' ? state.stashDiffRef : undefined,
|
|
24793
|
+
compareBase: wasOnDiff ? undefined : state.compareBase,
|
|
24794
|
+
compareHead: next === 'diff' ? state.compareHead : undefined,
|
|
24795
|
+
pendingCommitFocused: next === 'history' ? state.pendingCommitFocused : false,
|
|
24796
|
+
statusGroupHeaderFocused: next === 'status' ? state.statusGroupHeaderFocused : false,
|
|
24797
|
+
pendingKey: undefined,
|
|
24798
|
+
};
|
|
24132
24799
|
}
|
|
24133
|
-
|
|
24134
|
-
|
|
24135
|
-
|
|
24800
|
+
/**
|
|
24801
|
+
* Push a nested-repo frame onto `state.repoStack` (#931). Snapshots
|
|
24802
|
+
* the active view position into the new frame's `parentReturn` so a
|
|
24803
|
+
* subsequent pop lands the user back where they came from, then
|
|
24804
|
+
* resets the per-frame navigation state (active view, view stack,
|
|
24805
|
+
* row / file / submodule cursors, filter) so the nested frame opens
|
|
24806
|
+
* in a clean slate — the mental equivalent of a fresh `coco ui`
|
|
24807
|
+
* launched against the submodule's working dir.
|
|
24808
|
+
*
|
|
24809
|
+
* Sidebar tab + branch / tag sort are also captured into the return
|
|
24810
|
+
* snapshot (#995) so popping back restores the parent's choices
|
|
24811
|
+
* instead of letting the submodule's tab/sort bleed across the
|
|
24812
|
+
* boundary. The values on the *new* frame are left as-is (carried
|
|
24813
|
+
* over from the parent) — the load effect in app.ts re-reads
|
|
24814
|
+
* persistence keyed on the submodule's workdir and dispatches a
|
|
24815
|
+
* restore if the user has a submodule-specific saved preference.
|
|
24816
|
+
*
|
|
24817
|
+
* Other preferences (palette recents, inspector tab, diff view mode)
|
|
24818
|
+
* stay global by design — the user's preference shouldn't reset when
|
|
24819
|
+
* they cross a submodule boundary.
|
|
24820
|
+
*
|
|
24821
|
+
* Live runtime objects (`SimpleGit`, loaded `LogInkContext`) live
|
|
24822
|
+
* outside the reducer in `app.ts`'s parallel ref structure — this
|
|
24823
|
+
* helper only manages the pure view-model side of the push.
|
|
24824
|
+
*/
|
|
24825
|
+
function withPushedRepoFrame(state, payload) {
|
|
24826
|
+
const newFrame = {
|
|
24827
|
+
label: payload.label,
|
|
24828
|
+
workdir: payload.workdir,
|
|
24829
|
+
entryRange: payload.entryRange,
|
|
24830
|
+
parentReturn: {
|
|
24831
|
+
activeView: state.activeView,
|
|
24832
|
+
selectedIndex: state.selectedIndex,
|
|
24833
|
+
selectedFileIndex: state.selectedFileIndex,
|
|
24834
|
+
selectedSubmoduleIndex: state.selectedSubmoduleIndex,
|
|
24835
|
+
filter: state.filter,
|
|
24836
|
+
sidebarTab: state.sidebarTab,
|
|
24837
|
+
userSidebarTab: state.userSidebarTab,
|
|
24838
|
+
branchSort: state.branchSort,
|
|
24839
|
+
tagSort: state.tagSort,
|
|
24840
|
+
},
|
|
24841
|
+
};
|
|
24136
24842
|
return {
|
|
24137
|
-
|
|
24138
|
-
|
|
24139
|
-
|
|
24140
|
-
|
|
24141
|
-
filteredCommits: commits,
|
|
24843
|
+
...state,
|
|
24844
|
+
repoStack: [...state.repoStack, newFrame],
|
|
24845
|
+
activeView: 'history',
|
|
24846
|
+
viewStack: ['history'],
|
|
24142
24847
|
selectedIndex: 0,
|
|
24143
24848
|
selectedFileIndex: 0,
|
|
24144
|
-
selectedWorktreeFileIndex: 0,
|
|
24145
|
-
selectedWorktreeHunkIndex: 0,
|
|
24146
|
-
selectedBranchIndex: 0,
|
|
24147
|
-
selectedTagIndex: 0,
|
|
24148
|
-
selectedStashIndex: 0,
|
|
24149
|
-
selectedWorktreeListIndex: 0,
|
|
24150
|
-
selectedConflictFileIndex: 0,
|
|
24151
|
-
selectedReflogIndex: 0,
|
|
24152
24849
|
selectedSubmoduleIndex: 0,
|
|
24153
|
-
selectedIssueIndex: 0,
|
|
24154
|
-
selectedPullRequestTriageIndex: 0,
|
|
24155
|
-
selectedIssueFilter: 'open',
|
|
24156
|
-
selectedPullRequestFilter: 'open',
|
|
24157
|
-
repoStack: [{ label: options.repoLabel || 'root', workdir: options.repoWorkdir }],
|
|
24158
|
-
branchSort: DEFAULT_BRANCH_SORT_MODE,
|
|
24159
|
-
tagSort: DEFAULT_TAG_SORT_MODE,
|
|
24160
|
-
paletteFilter: '',
|
|
24161
|
-
paletteSelectedIndex: 0,
|
|
24162
|
-
paletteRecent: [],
|
|
24163
|
-
commitCompose: createCommitComposeState(),
|
|
24164
|
-
diffPreviewOffset: 0,
|
|
24165
|
-
worktreeDiffOffset: 0,
|
|
24166
24850
|
filter: '',
|
|
24167
24851
|
filterMode: false,
|
|
24168
|
-
|
|
24169
|
-
|
|
24170
|
-
// out of the box. Pre-0.54.x this defaulted to false (current
|
|
24171
|
-
// branch only); user feedback consistently asked for the
|
|
24172
|
-
// GitKraken-style "see everything" view as the starting state.
|
|
24173
|
-
// The `\` toggle still flips back to compact / current-branch
|
|
24174
|
-
// mode for users who want the cleaner single-line graph. Tests
|
|
24175
|
-
// override via `options.fullGraph` when they need the compact
|
|
24176
|
-
// case explicitly.
|
|
24177
|
-
fullGraph: options.fullGraph ?? true,
|
|
24178
|
-
showHelp: false,
|
|
24179
|
-
helpScrollOffset: 0,
|
|
24180
|
-
showCommandPalette: false,
|
|
24181
|
-
workflowActionId: undefined,
|
|
24852
|
+
pendingCommitFocused: false,
|
|
24853
|
+
pendingKey: undefined,
|
|
24182
24854
|
pendingConfirmationId: undefined,
|
|
24183
24855
|
pendingConfirmationPayload: undefined,
|
|
24184
24856
|
pendingMutationConfirmation: undefined,
|
|
24185
|
-
|
|
24857
|
+
};
|
|
24858
|
+
}
|
|
24859
|
+
/**
|
|
24860
|
+
* Pop the top repo frame off `state.repoStack` (#931) and restore
|
|
24861
|
+
* the parent's view position from the captured `parentReturn`. A
|
|
24862
|
+
* no-op when the stack is already at its single root frame so this
|
|
24863
|
+
* action is safe to dispatch from generic input handlers (e.g. the
|
|
24864
|
+
* Esc auto-pop wiring that lands in a follow-up PR).
|
|
24865
|
+
*
|
|
24866
|
+
* The defensive `parentReturn` fallback handles the never-supposed-
|
|
24867
|
+
* to-happen case where a non-root frame somehow has no return state
|
|
24868
|
+
* recorded — drop the frame but leave the user's view position
|
|
24869
|
+
* alone rather than crash mid-session.
|
|
24870
|
+
*/
|
|
24871
|
+
function withPoppedRepoFrame(state) {
|
|
24872
|
+
if (state.repoStack.length <= 1) {
|
|
24873
|
+
return { ...state, pendingKey: undefined };
|
|
24874
|
+
}
|
|
24875
|
+
const topFrame = state.repoStack[state.repoStack.length - 1];
|
|
24876
|
+
const ret = topFrame.parentReturn;
|
|
24877
|
+
const repoStack = state.repoStack.slice(0, -1);
|
|
24878
|
+
if (!ret) {
|
|
24879
|
+
return { ...state, repoStack, pendingKey: undefined };
|
|
24880
|
+
}
|
|
24881
|
+
return {
|
|
24882
|
+
...state,
|
|
24883
|
+
repoStack,
|
|
24884
|
+
activeView: ret.activeView,
|
|
24885
|
+
viewStack: [ret.activeView],
|
|
24886
|
+
selectedIndex: ret.selectedIndex,
|
|
24887
|
+
selectedFileIndex: ret.selectedFileIndex,
|
|
24888
|
+
selectedSubmoduleIndex: ret.selectedSubmoduleIndex,
|
|
24889
|
+
filter: ret.filter,
|
|
24890
|
+
filterMode: false,
|
|
24891
|
+
pendingCommitFocused: false,
|
|
24892
|
+
// #995 — restore sidebar tab + sort preferences from the captured
|
|
24893
|
+
// parentReturn. Without this, the submodule's tab / sort choice
|
|
24894
|
+
// bleeds back into the parent after pop: the user picks 'tags' in
|
|
24895
|
+
// a vendored submodule, pops back to the parent, and finds the
|
|
24896
|
+
// parent's previously-selected 'branches' tab quietly replaced.
|
|
24897
|
+
sidebarTab: ret.sidebarTab,
|
|
24898
|
+
userSidebarTab: ret.userSidebarTab,
|
|
24899
|
+
branchSort: ret.branchSort,
|
|
24900
|
+
tagSort: ret.tagSort,
|
|
24901
|
+
pendingKey: undefined,
|
|
24902
|
+
pendingConfirmationId: undefined,
|
|
24903
|
+
pendingConfirmationPayload: undefined,
|
|
24904
|
+
pendingMutationConfirmation: undefined,
|
|
24905
|
+
};
|
|
24906
|
+
}
|
|
24907
|
+
function withReplacedView(state, value) {
|
|
24908
|
+
if (topOfStack(state.viewStack) === value) {
|
|
24909
|
+
return { ...state, pendingKey: undefined };
|
|
24910
|
+
}
|
|
24911
|
+
const viewStack = [...state.viewStack.slice(0, -1), value];
|
|
24912
|
+
return {
|
|
24913
|
+
...state,
|
|
24914
|
+
activeView: value,
|
|
24915
|
+
viewStack,
|
|
24916
|
+
worktreeDiffOffset: value === 'diff' ? state.worktreeDiffOffset : 0,
|
|
24917
|
+
selectedWorktreeHunkIndex: value === 'diff' ? state.selectedWorktreeHunkIndex : 0,
|
|
24918
|
+
diffSource: value === 'diff' ? state.diffSource : undefined,
|
|
24919
|
+
stashDiffRef: value === 'diff' ? state.stashDiffRef : undefined,
|
|
24920
|
+
compareHead: value === 'diff' ? state.compareHead : undefined,
|
|
24921
|
+
pendingCommitFocused: value === 'history' ? state.pendingCommitFocused : false,
|
|
24922
|
+
statusGroupHeaderFocused: value === 'status' ? state.statusGroupHeaderFocused : false,
|
|
24923
|
+
pendingKey: undefined,
|
|
24924
|
+
};
|
|
24925
|
+
}
|
|
24926
|
+
function withFilter(state, filter, promotedSelections) {
|
|
24927
|
+
const filteredCommits = filterCommits(state.commits, filter);
|
|
24928
|
+
// P4.5: rectify promoted-view selections when the filter changes. Prefer
|
|
24929
|
+
// the runtime-supplied snapshot — which preserves the cursor on the same
|
|
24930
|
+
// item when it's still in the filtered list and only snaps to result[0]
|
|
24931
|
+
// when the previously-selected item dropped out. Falls back to the older
|
|
24932
|
+
// "snap to 0" behavior when no snapshot was provided (test paths,
|
|
24933
|
+
// dispatchers without context).
|
|
24934
|
+
const filterChanged = state.filter !== filter;
|
|
24935
|
+
const branchIndex = promotedSelections?.branchIndex ??
|
|
24936
|
+
(filterChanged ? 0 : state.selectedBranchIndex);
|
|
24937
|
+
const tagIndex = promotedSelections?.tagIndex ??
|
|
24938
|
+
(filterChanged ? 0 : state.selectedTagIndex);
|
|
24939
|
+
const stashIndex = promotedSelections?.stashIndex ??
|
|
24940
|
+
(filterChanged ? 0 : state.selectedStashIndex);
|
|
24941
|
+
// Reflog (#781) snaps to 0 on filter change rather than rectifying.
|
|
24942
|
+
// The list is chronological and the user is unlikely to be tracking
|
|
24943
|
+
// a specific entry through filter changes — the simpler reset
|
|
24944
|
+
// matches the "find recovery target by typing" interaction.
|
|
24945
|
+
const reflogIndex = filterChanged ? 0 : state.selectedReflogIndex;
|
|
24946
|
+
return {
|
|
24947
|
+
...state,
|
|
24948
|
+
filter,
|
|
24949
|
+
filteredCommits,
|
|
24950
|
+
selectedIndex: clampIndex(state.selectedIndex, filteredCommits.length),
|
|
24951
|
+
selectedFileIndex: 0,
|
|
24952
|
+
selectedBranchIndex: branchIndex,
|
|
24953
|
+
selectedTagIndex: tagIndex,
|
|
24954
|
+
selectedStashIndex: stashIndex,
|
|
24955
|
+
selectedReflogIndex: reflogIndex,
|
|
24956
|
+
diffPreviewOffset: 0,
|
|
24957
|
+
pendingKey: undefined,
|
|
24958
|
+
};
|
|
24959
|
+
}
|
|
24960
|
+
function replaceRows(state, rows) {
|
|
24961
|
+
// Wholesale row replacement after a server-side re-fetch (#776).
|
|
24962
|
+
// Resets the cursor to the top because the new commit set may not
|
|
24963
|
+
// share any hashes with the old one (e.g. switching from `--all` to
|
|
24964
|
+
// `-- some/path` typically dumps the previous selection).
|
|
24965
|
+
const commits = getCommitRows(rows);
|
|
24966
|
+
const filteredCommits = filterCommits(commits, state.filter);
|
|
24967
|
+
return {
|
|
24968
|
+
...state,
|
|
24969
|
+
rows,
|
|
24970
|
+
commits,
|
|
24971
|
+
filteredCommits,
|
|
24972
|
+
selectedIndex: 0,
|
|
24973
|
+
selectedFileIndex: 0,
|
|
24974
|
+
pendingCommitFocused: false,
|
|
24975
|
+
pendingKey: undefined,
|
|
24976
|
+
// Rows just landed — clear the boot-loading flag so the history
|
|
24977
|
+
// surface drops the "Loading commits…" placeholder. Safe to clear
|
|
24978
|
+
// unconditionally because `replaceRows` only fires after a real
|
|
24979
|
+
// git log returns.
|
|
24980
|
+
bootLoading: false,
|
|
24981
|
+
};
|
|
24982
|
+
}
|
|
24983
|
+
function appendRows(state, rows) {
|
|
24984
|
+
const selected = getSelectedInkCommit(state);
|
|
24985
|
+
const nextRows = [...state.rows, ...rows];
|
|
24986
|
+
const seen = new Set();
|
|
24987
|
+
const commits = getCommitRows(nextRows).filter((commit) => {
|
|
24988
|
+
if (seen.has(commit.hash)) {
|
|
24989
|
+
return false;
|
|
24990
|
+
}
|
|
24991
|
+
seen.add(commit.hash);
|
|
24992
|
+
return true;
|
|
24993
|
+
});
|
|
24994
|
+
const filteredCommits = filterCommits(commits, state.filter);
|
|
24995
|
+
const selectedIndex = selected
|
|
24996
|
+
? filteredCommits.findIndex((commit) => commit.hash === selected.hash)
|
|
24997
|
+
: state.selectedIndex;
|
|
24998
|
+
return {
|
|
24999
|
+
...state,
|
|
25000
|
+
rows: nextRows,
|
|
25001
|
+
commits,
|
|
25002
|
+
filteredCommits,
|
|
25003
|
+
selectedIndex: selectedIndex >= 0
|
|
25004
|
+
? selectedIndex
|
|
25005
|
+
: clampIndex(state.selectedIndex, filteredCommits.length),
|
|
25006
|
+
pendingKey: undefined,
|
|
25007
|
+
};
|
|
25008
|
+
}
|
|
25009
|
+
function nextHunkOffset(currentOffset, hunkOffsets, delta) {
|
|
25010
|
+
if (hunkOffsets.length === 0) {
|
|
25011
|
+
return currentOffset;
|
|
25012
|
+
}
|
|
25013
|
+
if (delta > 0) {
|
|
25014
|
+
const nextOffset = hunkOffsets.find((offset) => offset > currentOffset);
|
|
25015
|
+
return nextOffset === undefined ? currentOffset : nextOffset;
|
|
25016
|
+
}
|
|
25017
|
+
const previousOffset = [...hunkOffsets].reverse().find((offset) => offset < currentOffset);
|
|
25018
|
+
return previousOffset === undefined ? currentOffset : previousOffset;
|
|
25019
|
+
}
|
|
25020
|
+
function nextHunkIndex(currentOffset, hunkOffsets, delta) {
|
|
25021
|
+
const offset = nextHunkOffset(currentOffset, hunkOffsets, delta);
|
|
25022
|
+
return Math.max(0, hunkOffsets.indexOf(offset));
|
|
25023
|
+
}
|
|
25024
|
+
function getLogInkSidebarTabs() {
|
|
25025
|
+
return [...SIDEBAR_TABS];
|
|
25026
|
+
}
|
|
25027
|
+
function createLogInkState(rows, options = {}) {
|
|
25028
|
+
const commits = getCommitRows(rows);
|
|
25029
|
+
const initialView = options.activeView || 'history';
|
|
25030
|
+
return {
|
|
25031
|
+
activeView: initialView,
|
|
25032
|
+
viewStack: [initialView],
|
|
25033
|
+
rows,
|
|
25034
|
+
commits,
|
|
25035
|
+
filteredCommits: commits,
|
|
25036
|
+
selectedIndex: 0,
|
|
25037
|
+
selectedFileIndex: 0,
|
|
25038
|
+
selectedWorktreeFileIndex: 0,
|
|
25039
|
+
selectedWorktreeHunkIndex: 0,
|
|
25040
|
+
selectedBranchIndex: 0,
|
|
25041
|
+
selectedTagIndex: 0,
|
|
25042
|
+
selectedStashIndex: 0,
|
|
25043
|
+
selectedWorktreeListIndex: 0,
|
|
25044
|
+
selectedConflictFileIndex: 0,
|
|
25045
|
+
selectedReflogIndex: 0,
|
|
25046
|
+
selectedSubmoduleIndex: 0,
|
|
25047
|
+
selectedIssueIndex: 0,
|
|
25048
|
+
selectedPullRequestTriageIndex: 0,
|
|
25049
|
+
selectedIssueFilter: 'open',
|
|
25050
|
+
selectedPullRequestFilter: 'open',
|
|
25051
|
+
repoStack: [{ label: options.repoLabel || 'root', workdir: options.repoWorkdir }],
|
|
25052
|
+
branchSort: DEFAULT_BRANCH_SORT_MODE,
|
|
25053
|
+
tagSort: DEFAULT_TAG_SORT_MODE,
|
|
25054
|
+
paletteFilter: '',
|
|
25055
|
+
paletteSelectedIndex: 0,
|
|
25056
|
+
paletteRecent: [],
|
|
25057
|
+
showThemePicker: false,
|
|
25058
|
+
themePickerFilter: '',
|
|
25059
|
+
themePickerIndex: 0,
|
|
25060
|
+
commitCompose: createCommitComposeState(),
|
|
25061
|
+
diffPreviewOffset: 0,
|
|
25062
|
+
worktreeDiffOffset: 0,
|
|
25063
|
+
filter: '',
|
|
25064
|
+
filterMode: false,
|
|
25065
|
+
// Default to the full multi-ref graph (`git log --all`) so users
|
|
25066
|
+
// see how branches, tags, and stashes weave through the history
|
|
25067
|
+
// out of the box. Pre-0.54.x this defaulted to false (current
|
|
25068
|
+
// branch only); user feedback consistently asked for the
|
|
25069
|
+
// GitKraken-style "see everything" view as the starting state.
|
|
25070
|
+
// The `\` toggle still flips back to compact / current-branch
|
|
25071
|
+
// mode for users who want the cleaner single-line graph. Tests
|
|
25072
|
+
// override via `options.fullGraph` when they need the compact
|
|
25073
|
+
// case explicitly.
|
|
25074
|
+
fullGraph: options.fullGraph ?? true,
|
|
25075
|
+
showHelp: false,
|
|
25076
|
+
helpScrollOffset: 0,
|
|
25077
|
+
showCommandPalette: false,
|
|
25078
|
+
workflowActionId: undefined,
|
|
25079
|
+
pendingConfirmationId: undefined,
|
|
25080
|
+
pendingConfirmationPayload: undefined,
|
|
25081
|
+
pendingMutationConfirmation: undefined,
|
|
25082
|
+
pendingKey: undefined,
|
|
24186
25083
|
focus: 'commits',
|
|
24187
25084
|
// Default first-time tab is 'branches' — it's the most useful
|
|
24188
25085
|
// landing surface in the workstation (current branch + recent
|
|
@@ -24921,6 +25818,46 @@ function applyLogInkAction(state, action) {
|
|
|
24921
25818
|
pendingKey: undefined,
|
|
24922
25819
|
};
|
|
24923
25820
|
}
|
|
25821
|
+
case 'toggleThemePicker': {
|
|
25822
|
+
const opening = !state.showThemePicker;
|
|
25823
|
+
return {
|
|
25824
|
+
...state,
|
|
25825
|
+
showThemePicker: opening,
|
|
25826
|
+
// Only one overlay at a time — close help / palette on open.
|
|
25827
|
+
showHelp: false,
|
|
25828
|
+
showCommandPalette: false,
|
|
25829
|
+
themePickerFilter: '',
|
|
25830
|
+
themePickerIndex: 0,
|
|
25831
|
+
pendingKey: undefined,
|
|
25832
|
+
};
|
|
25833
|
+
}
|
|
25834
|
+
case 'moveThemePicker':
|
|
25835
|
+
return {
|
|
25836
|
+
...state,
|
|
25837
|
+
themePickerIndex: clampIndex(state.themePickerIndex + action.delta, action.presetCount),
|
|
25838
|
+
pendingKey: undefined,
|
|
25839
|
+
};
|
|
25840
|
+
case 'appendThemePickerFilter':
|
|
25841
|
+
return {
|
|
25842
|
+
...state,
|
|
25843
|
+
themePickerFilter: `${state.themePickerFilter}${action.value}`,
|
|
25844
|
+
themePickerIndex: 0,
|
|
25845
|
+
pendingKey: undefined,
|
|
25846
|
+
};
|
|
25847
|
+
case 'backspaceThemePickerFilter':
|
|
25848
|
+
return {
|
|
25849
|
+
...state,
|
|
25850
|
+
themePickerFilter: state.themePickerFilter.slice(0, -1),
|
|
25851
|
+
themePickerIndex: 0,
|
|
25852
|
+
pendingKey: undefined,
|
|
25853
|
+
};
|
|
25854
|
+
case 'clearThemePickerFilter':
|
|
25855
|
+
return {
|
|
25856
|
+
...state,
|
|
25857
|
+
themePickerFilter: '',
|
|
25858
|
+
themePickerIndex: 0,
|
|
25859
|
+
pendingKey: undefined,
|
|
25860
|
+
};
|
|
24924
25861
|
case 'setChangelogLoading':
|
|
24925
25862
|
return {
|
|
24926
25863
|
...state,
|
|
@@ -25128,6 +26065,71 @@ function applyLogInkAction(state, action) {
|
|
|
25128
26065
|
return state;
|
|
25129
26066
|
}
|
|
25130
26067
|
}
|
|
26068
|
+
/**
|
|
26069
|
+
* Fuzzy (subsequence) score for a preset id against a lowercase query.
|
|
26070
|
+
* Returns `null` when the query chars don't appear in order; otherwise a
|
|
26071
|
+
* score where contiguous runs, a start-of-string match, and matches right
|
|
26072
|
+
* after a `-` separator are rewarded — so `gl` ranks `gruvbox-light` /
|
|
26073
|
+
* `github-light` above incidental matches, and `tn` finds `tokyo-night`.
|
|
26074
|
+
*/
|
|
26075
|
+
function fuzzyScoreThemePreset(preset, query) {
|
|
26076
|
+
const target = preset.toLowerCase();
|
|
26077
|
+
let qi = 0;
|
|
26078
|
+
let score = 0;
|
|
26079
|
+
let lastMatch = -2;
|
|
26080
|
+
for (let i = 0; i < target.length && qi < query.length; i += 1) {
|
|
26081
|
+
if (target[i] === query[qi]) {
|
|
26082
|
+
score += 1;
|
|
26083
|
+
if (i === lastMatch + 1)
|
|
26084
|
+
score += 4; // contiguous run
|
|
26085
|
+
if (i === 0)
|
|
26086
|
+
score += 8; // matches the very start
|
|
26087
|
+
else if (target[i - 1] === '-')
|
|
26088
|
+
score += 4; // start of a word segment
|
|
26089
|
+
lastMatch = i;
|
|
26090
|
+
qi += 1;
|
|
26091
|
+
}
|
|
26092
|
+
}
|
|
26093
|
+
return qi === query.length ? score : null;
|
|
26094
|
+
}
|
|
26095
|
+
/**
|
|
26096
|
+
* Filter the full preset list by a fuzzy (subsequence) query, ranked best
|
|
26097
|
+
* match first (ties broken by catalog order). An empty query returns every
|
|
26098
|
+
* preset in catalog order. Shared by the theme picker overlay renderer, the
|
|
26099
|
+
* input handler (for cursor bounds), and the live-preview selector so all
|
|
26100
|
+
* three agree on the same filtered list.
|
|
26101
|
+
*/
|
|
26102
|
+
function filterThemePresets(filter) {
|
|
26103
|
+
const query = filter.trim().toLowerCase();
|
|
26104
|
+
const all = getLogInkThemePresets();
|
|
26105
|
+
if (!query) {
|
|
26106
|
+
return all;
|
|
26107
|
+
}
|
|
26108
|
+
return all
|
|
26109
|
+
.map((preset, index) => ({ preset, index, score: fuzzyScoreThemePreset(preset, query) }))
|
|
26110
|
+
.filter((entry) => entry.score !== null)
|
|
26111
|
+
.sort((a, b) => b.score - a.score || a.index - b.index)
|
|
26112
|
+
.map((entry) => entry.preset);
|
|
26113
|
+
}
|
|
26114
|
+
/**
|
|
26115
|
+
* The preset currently under the theme-picker cursor (clamped to the
|
|
26116
|
+
* filtered list). `undefined` when the filter matches nothing.
|
|
26117
|
+
*/
|
|
26118
|
+
function getThemePickerSelection(state) {
|
|
26119
|
+
return getThemePickerSelectionFor(state.themePickerFilter, state.themePickerIndex);
|
|
26120
|
+
}
|
|
26121
|
+
/**
|
|
26122
|
+
* State-model-agnostic variant: the preset under the picker cursor for a
|
|
26123
|
+
* raw `filter` + `index`. Used by the workspace top-level surface, which
|
|
26124
|
+
* keeps its own state shape but shares the picker filtering.
|
|
26125
|
+
*/
|
|
26126
|
+
function getThemePickerSelectionFor(filter, index) {
|
|
26127
|
+
const filtered = filterThemePresets(filter);
|
|
26128
|
+
if (filtered.length === 0) {
|
|
26129
|
+
return undefined;
|
|
26130
|
+
}
|
|
26131
|
+
return filtered[clampIndex(index, filtered.length)];
|
|
26132
|
+
}
|
|
25131
26133
|
|
|
25132
26134
|
/**
|
|
25133
26135
|
* In-sidebar selection helpers (#791 follow-up — sidebar entity ops).
|
|
@@ -25589,6 +26591,10 @@ function getLogInkPaletteExecuteEvents(command, state) {
|
|
|
25589
26591
|
case 'commandPalette':
|
|
25590
26592
|
// Re-toggling closes; the dispatcher will close after execute anyway.
|
|
25591
26593
|
return [];
|
|
26594
|
+
case 'themePicker':
|
|
26595
|
+
// Palette closes on execute (toggleCommandPalette runs first), then
|
|
26596
|
+
// this opens the theme picker.
|
|
26597
|
+
return [action({ type: 'toggleThemePicker' })];
|
|
25592
26598
|
case 'workflowDeleteBranch':
|
|
25593
26599
|
case 'workflowDeleteTag':
|
|
25594
26600
|
case 'workflowDropStash':
|
|
@@ -26121,6 +27127,45 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
|
|
|
26121
27127
|
// overlay open without dispatching any state change.
|
|
26122
27128
|
return [];
|
|
26123
27129
|
}
|
|
27130
|
+
if (state.showThemePicker) {
|
|
27131
|
+
const filtered = filterThemePresets(state.themePickerFilter);
|
|
27132
|
+
if (key.escape) {
|
|
27133
|
+
// Two-stage Esc: clear a non-empty filter first, then close (and
|
|
27134
|
+
// revert the live preview to the previously-active theme).
|
|
27135
|
+
if (state.themePickerFilter.length > 0) {
|
|
27136
|
+
return [action({ type: 'clearThemePickerFilter' })];
|
|
27137
|
+
}
|
|
27138
|
+
return [action({ type: 'toggleThemePicker' })];
|
|
27139
|
+
}
|
|
27140
|
+
if (key.return) {
|
|
27141
|
+
const selected = getThemePickerSelection(state);
|
|
27142
|
+
if (!selected) {
|
|
27143
|
+
return [action({ type: 'toggleThemePicker' })];
|
|
27144
|
+
}
|
|
27145
|
+
return [
|
|
27146
|
+
action({ type: 'toggleThemePicker' }),
|
|
27147
|
+
{ type: 'applyThemePreset', preset: selected },
|
|
27148
|
+
];
|
|
27149
|
+
}
|
|
27150
|
+
if (key.upArrow || (key.ctrl && inputValue === 'p')) {
|
|
27151
|
+
return [action({ type: 'moveThemePicker', delta: -1, presetCount: filtered.length })];
|
|
27152
|
+
}
|
|
27153
|
+
if (key.downArrow || (key.ctrl && inputValue === 'n')) {
|
|
27154
|
+
return [action({ type: 'moveThemePicker', delta: 1, presetCount: filtered.length })];
|
|
27155
|
+
}
|
|
27156
|
+
if (key.backspace || key.delete) {
|
|
27157
|
+
return [action({ type: 'backspaceThemePickerFilter' })];
|
|
27158
|
+
}
|
|
27159
|
+
if (key.ctrl && inputValue === 'u') {
|
|
27160
|
+
return [action({ type: 'clearThemePickerFilter' })];
|
|
27161
|
+
}
|
|
27162
|
+
// All other printable input filters the list (so `j`/`k` type into the
|
|
27163
|
+
// filter rather than navigating — matching the command palette).
|
|
27164
|
+
if (inputValue && !key.ctrl && !key.meta) {
|
|
27165
|
+
return [action({ type: 'appendThemePickerFilter', value: inputValue })];
|
|
27166
|
+
}
|
|
27167
|
+
return [];
|
|
27168
|
+
}
|
|
26124
27169
|
if (state.showCommandPalette) {
|
|
26125
27170
|
const filtered = filterLogInkPaletteCommands(getLogInkPaletteCommands(), state.paletteFilter, state.paletteRecent);
|
|
26126
27171
|
if (key.escape) {
|
|
@@ -26391,6 +27436,13 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
|
|
|
26391
27436
|
action({ type: 'setStatus', value: 'gT creates a tag at the cursored commit on the history view', kind: 'warning' }),
|
|
26392
27437
|
];
|
|
26393
27438
|
}
|
|
27439
|
+
// gC — open the theme picker (browse + live-preview + apply a color theme).
|
|
27440
|
+
if (state.pendingKey === 'g' && inputValue === 'C') {
|
|
27441
|
+
return [
|
|
27442
|
+
action({ type: 'setPendingKey', value: undefined }),
|
|
27443
|
+
action({ type: 'toggleThemePicker' }),
|
|
27444
|
+
];
|
|
27445
|
+
}
|
|
26394
27446
|
// #784 — bisect view action keys. Scoped to `state.activeView ===
|
|
26395
27447
|
// 'bisect' && state.focus === 'commits'` so the single-letter keys
|
|
26396
27448
|
// stay free everywhere else. `g` and `b` collide with the global
|
|
@@ -27862,6 +28914,60 @@ function markOnboardingSeen() {
|
|
|
27862
28914
|
}
|
|
27863
28915
|
}
|
|
27864
28916
|
|
|
28917
|
+
/**
|
|
28918
|
+
* Persist the user's chosen `coco ui` theme preset to the global XDG
|
|
28919
|
+
* config (`$XDG_CONFIG_HOME/coco/config.json`, default `~/.config/...`),
|
|
28920
|
+
* so a theme picked in the workstation sticks across every repo and
|
|
28921
|
+
* launch. This is the same file `loadXDGConfig` reads.
|
|
28922
|
+
*
|
|
28923
|
+
* Read-modify-write that preserves every other key (we only touch
|
|
28924
|
+
* `logTui.theme.preset`), unlike the whole-object project-config writer.
|
|
28925
|
+
* Best-effort: a read-only HOME or malformed file never throws — the
|
|
28926
|
+
* picker still applies the theme for the session.
|
|
28927
|
+
*/
|
|
28928
|
+
const VALID_PRESETS = new Set(getLogInkThemePresets());
|
|
28929
|
+
function getXdgConfigPath() {
|
|
28930
|
+
const home = process.env.XDG_CONFIG_HOME || path__namespace$1.join(os__namespace$1.homedir(), '.config');
|
|
28931
|
+
return path__namespace$1.join(home, 'coco', 'config.json');
|
|
28932
|
+
}
|
|
28933
|
+
function isRecord(value) {
|
|
28934
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
28935
|
+
}
|
|
28936
|
+
/**
|
|
28937
|
+
* Write `logTui.theme.preset = <preset>` into the global config, merging
|
|
28938
|
+
* into any existing content. Returns `true` on success, `false` if the
|
|
28939
|
+
* preset is unknown or the write failed (caller treats failure as
|
|
28940
|
+
* "applied for this session only").
|
|
28941
|
+
*/
|
|
28942
|
+
function saveThemePreset(preset) {
|
|
28943
|
+
if (!VALID_PRESETS.has(preset)) {
|
|
28944
|
+
return false;
|
|
28945
|
+
}
|
|
28946
|
+
const file = getXdgConfigPath();
|
|
28947
|
+
try {
|
|
28948
|
+
let config = {};
|
|
28949
|
+
try {
|
|
28950
|
+
const parsed = JSON.parse(fs__namespace$1.readFileSync(file, 'utf8'));
|
|
28951
|
+
if (isRecord(parsed)) {
|
|
28952
|
+
config = parsed;
|
|
28953
|
+
}
|
|
28954
|
+
}
|
|
28955
|
+
catch {
|
|
28956
|
+
// No existing file (or unreadable/malformed) — start fresh.
|
|
28957
|
+
config = {};
|
|
28958
|
+
}
|
|
28959
|
+
const logTui = isRecord(config.logTui) ? config.logTui : {};
|
|
28960
|
+
const theme = isRecord(logTui.theme) ? logTui.theme : {};
|
|
28961
|
+
config.logTui = { ...logTui, theme: { ...theme, preset } };
|
|
28962
|
+
fs__namespace$1.mkdirSync(path__namespace$1.dirname(file), { recursive: true });
|
|
28963
|
+
fs__namespace$1.writeFileSync(file, `${JSON.stringify(config, null, 2)}\n`);
|
|
28964
|
+
return true;
|
|
28965
|
+
}
|
|
28966
|
+
catch {
|
|
28967
|
+
return false;
|
|
28968
|
+
}
|
|
28969
|
+
}
|
|
28970
|
+
|
|
27865
28971
|
/**
|
|
27866
28972
|
* Status-line hints for "what to do next" after a workflow that
|
|
27867
28973
|
* mutates the worktree (split-apply, etc.). Pure formatting — the
|
|
@@ -32197,6 +33303,10 @@ function renderBranchesSurface(h, components, state, context, contextStatus, bod
|
|
|
32197
33303
|
// row's dim and read as quiet chrome.
|
|
32198
33304
|
h(Text, { color: markerColor, dimColor: markerColor ? false : undefined }, marker.glyph), trailingName, h(Text, { dimColor: true }, timestampPadded), trailingDivergence);
|
|
32199
33305
|
});
|
|
33306
|
+
// Scroll indicators — same "N more above/below" pattern as the
|
|
33307
|
+
// sidebar and help overlay so the user knows the list continues.
|
|
33308
|
+
const branchesHasMoreAbove = startIndex > 0 && localBranches.length > 0;
|
|
33309
|
+
const branchesHasMoreBelow = startIndex + listRows < localBranches.length;
|
|
32200
33310
|
return h(Box, {
|
|
32201
33311
|
borderColor: focusBorderColor(theme, focused),
|
|
32202
33312
|
borderStyle: theme.borderStyle,
|
|
@@ -32204,7 +33314,11 @@ function renderBranchesSurface(h, components, state, context, contextStatus, bod
|
|
|
32204
33314
|
flexShrink: 0,
|
|
32205
33315
|
paddingX: 1,
|
|
32206
33316
|
width,
|
|
32207
|
-
}, h(Box, { justifyContent: 'space-between' }, h(Text, { bold: true }, panelTitle('Branches', focused)), h(Text, { dimColor: true }, headerRight)), ...renderPromotedFilterAffordance(h, Text, state, theme), ...
|
|
33317
|
+
}, h(Box, { justifyContent: 'space-between' }, h(Text, { bold: true }, panelTitle('Branches', focused)), h(Text, { dimColor: true }, headerRight)), ...renderPromotedFilterAffordance(h, Text, state, theme), ...(branchesHasMoreAbove
|
|
33318
|
+
? [h(Text, { key: 'branches-more-above', dimColor: true }, ` ↑ ${startIndex} more above`)]
|
|
33319
|
+
: []), ...lines, ...(branchesHasMoreBelow
|
|
33320
|
+
? [h(Text, { key: 'branches-more-below', dimColor: true }, ` ↓ ${localBranches.length - (startIndex + listRows)} more below`)]
|
|
33321
|
+
: []));
|
|
32208
33322
|
}
|
|
32209
33323
|
|
|
32210
33324
|
/**
|
|
@@ -32977,12 +34091,19 @@ function renderDiffSurface(h, components, state, context, contextStatus, worktre
|
|
|
32977
34091
|
// sees at a glance which file the cursor is inside.
|
|
32978
34092
|
const isActive = absoluteIndex === activeStartLine;
|
|
32979
34093
|
const arrow = theme.ascii ? '> ' : '▾ ';
|
|
34094
|
+
const activeHeader = isActive && focused && !theme.noColor;
|
|
32980
34095
|
return h(Text, {
|
|
32981
34096
|
key: `stash-diff-line-${absoluteIndex}`,
|
|
32982
34097
|
bold: true,
|
|
32983
|
-
|
|
32984
|
-
|
|
32985
|
-
inverse
|
|
34098
|
+
// Active header sits on the selection bar with a
|
|
34099
|
+
// contrast-guaranteed foreground (matches history/status).
|
|
34100
|
+
// The old `inverse` swap turned the accent into the bar and
|
|
34101
|
+
// left the path in the selection color — low-contrast on
|
|
34102
|
+
// light themes (e.g. accent blue bar + light-gray text).
|
|
34103
|
+
color: activeHeader
|
|
34104
|
+
? theme.colors.selectionForeground
|
|
34105
|
+
: (theme.noColor ? undefined : theme.colors.accent),
|
|
34106
|
+
backgroundColor: activeHeader ? theme.colors.selection : undefined,
|
|
32986
34107
|
}, (() => {
|
|
32987
34108
|
// Smart path truncation for the diff file header: keep
|
|
32988
34109
|
// the leading arrow glyph and elide middle path
|
|
@@ -34060,7 +35181,7 @@ function formatHistoryFetchArgs(args) {
|
|
|
34060
35181
|
* Returns the spans flat so the caller can splat them into the row's
|
|
34061
35182
|
* outer Text alongside other segments without an extra wrapper.
|
|
34062
35183
|
*/
|
|
34063
|
-
function renderTypedSubject(h, Text, text, theme, key) {
|
|
35184
|
+
function renderTypedSubject(h, Text, text, theme, key, suppressColor = false) {
|
|
34064
35185
|
const parsed = parseConventionalCommitPrefix(text);
|
|
34065
35186
|
if (!parsed) {
|
|
34066
35187
|
return [h(Text, { key: `${key}-msg` }, text)];
|
|
@@ -34068,7 +35189,9 @@ function renderTypedSubject(h, Text, text, theme, key) {
|
|
|
34068
35189
|
if (text.length < parsed.prefix.length) {
|
|
34069
35190
|
return [h(Text, { key: `${key}-msg` }, text)];
|
|
34070
35191
|
}
|
|
34071
|
-
|
|
35192
|
+
// When the row is selected (inverted), suppress the type color so
|
|
35193
|
+
// text inherits the dark inverted foreground and stays readable.
|
|
35194
|
+
const color = suppressColor ? undefined : getConventionalCommitColor(parsed, theme);
|
|
34072
35195
|
return [
|
|
34073
35196
|
h(Text, { key: `${key}-type`, color, bold: parsed.breaking }, parsed.prefix),
|
|
34074
35197
|
h(Text, { key: `${key}-rest` }, text.slice(parsed.prefix.length)),
|
|
@@ -34089,15 +35212,10 @@ function renderLaneSegmentSpans(h, Text, segments, theme, padTo, keyPrefix, opti
|
|
|
34089
35212
|
const elements = [];
|
|
34090
35213
|
let totalLen = 0;
|
|
34091
35214
|
segments.forEach((seg, idx) => {
|
|
34092
|
-
const laneColor = getLaneColor(seg.laneId, theme);
|
|
35215
|
+
const laneColor = options.suppressColor ? undefined : (getLaneColor(seg.laneId, theme) ?? muted);
|
|
34093
35216
|
elements.push(h(Text, {
|
|
34094
35217
|
key: `${keyPrefix}-${idx}`,
|
|
34095
|
-
color: laneColor
|
|
34096
|
-
// Ink does not cascade dimColor from a parent Text to children,
|
|
34097
|
-
// so the caller's "this whole row should fade" intent has to
|
|
34098
|
-
// travel here as an explicit flag (#831). Used for graph-only
|
|
34099
|
-
// lane-closure rows, where the lane colors otherwise compete
|
|
34100
|
-
// for attention with the commits they connect.
|
|
35218
|
+
color: laneColor,
|
|
34101
35219
|
dimColor: options.forceDim || (theme.noColor && seg.laneId === undefined),
|
|
34102
35220
|
}, seg.text));
|
|
34103
35221
|
totalLen += seg.text.length;
|
|
@@ -34148,18 +35266,26 @@ function renderCommitHistoryRow(h, Text, commit, graph, graphWidth, selected, th
|
|
|
34148
35266
|
const messageRoom = Math.max(8, totalWidth - fixedWidth - cellWidth(refsTrunc));
|
|
34149
35267
|
const message = truncateCells(commit.message, messageRoom);
|
|
34150
35268
|
const selectedBg = selected && !theme.noColor ? theme.colors.selection : undefined;
|
|
34151
|
-
|
|
34152
|
-
|
|
35269
|
+
// Don't use inverse — it makes child colors unreadable. Instead, set a
|
|
35270
|
+
// background on the row AND an explicit, contrast-guaranteed foreground
|
|
35271
|
+
// (`selectionForeground`, derived from the selection bg) on the outer
|
|
35272
|
+
// span. Suppressing each child's own color to `undefined` then lets it
|
|
35273
|
+
// inherit that readable foreground — so the whole selected row stays
|
|
35274
|
+
// legible regardless of the user's terminal default foreground, which
|
|
35275
|
+
// is what the old "rely on the default fg" approach got wrong.
|
|
35276
|
+
const selectedFg = selected && !theme.noColor ? theme.colors.selectionForeground : undefined;
|
|
35277
|
+
const accent = selected ? undefined : (theme.noColor ? undefined : theme.colors.accent);
|
|
35278
|
+
const muted = selected ? undefined : (theme.noColor ? undefined : theme.colors.muted);
|
|
34153
35279
|
// Lane-colored graph spans when full graph mode + non-ASCII rendering
|
|
34154
35280
|
// is in play; otherwise fall back to the legacy single-muted span so
|
|
34155
35281
|
// compact mode and legacy terminals stay visually unchanged.
|
|
34156
35282
|
const graphChildren = laneSegments && !theme.ascii
|
|
34157
|
-
? renderLaneSegmentSpans(h, Text, laneSegments, theme, graphWidth, `c${index}
|
|
34158
|
-
: [h(Text, { color: muted, dimColor: theme.noColor }, substituteGraphChars(graph.padEnd(graphWidth), { ascii: theme.ascii }))];
|
|
35283
|
+
? renderLaneSegmentSpans(h, Text, laneSegments, theme, graphWidth, `c${index}`, { suppressColor: selected })
|
|
35284
|
+
: [h(Text, { color: muted, dimColor: !selected && theme.noColor }, substituteGraphChars(graph.padEnd(graphWidth), { ascii: theme.ascii }))];
|
|
34159
35285
|
return h(Text, {
|
|
34160
35286
|
key: `${commit.hash}-${index}`,
|
|
34161
35287
|
backgroundColor: selectedBg,
|
|
34162
|
-
|
|
35288
|
+
color: selectedFg,
|
|
34163
35289
|
}, ...graphChildren, ' ',
|
|
34164
35290
|
// "Just landed" marker — a single thick vertical bar in the
|
|
34165
35291
|
// accent color before the short hash. Fades when the runtime
|
|
@@ -34181,11 +35307,11 @@ function renderCommitHistoryRow(h, Text, commit, graph, graphWidth, selected, th
|
|
|
34181
35307
|
// Date column drops out entirely at `tight` density — no spacer
|
|
34182
35308
|
// either, so the message column slides left into the freed cells.
|
|
34183
35309
|
dateText
|
|
34184
|
-
? h(Text, { key: `${commit.hash}-${index}-date`, dimColor:
|
|
35310
|
+
? h(Text, { key: `${commit.hash}-${index}-date`, dimColor: !selected }, dateText, ' ')
|
|
34185
35311
|
: null,
|
|
34186
35312
|
// Branch chip prefix (full-graph mode only) lands right before the
|
|
34187
35313
|
// message so the eye reads "branch · subject" as a unit.
|
|
34188
|
-
chip.node, ...renderTypedSubject(h, Text, message, theme, `${commit.hash}-${index}-subj
|
|
35314
|
+
chip.node, ...renderTypedSubject(h, Text, message, theme, `${commit.hash}-${index}-subj`, selected), refsTrunc ? h(Text, { color: accent }, refsTrunc) : null);
|
|
34189
35315
|
}
|
|
34190
35316
|
/**
|
|
34191
35317
|
* Stacked variant used at `rowMode='stacked'` (rail tier). Each
|
|
@@ -34200,9 +35326,13 @@ function renderCommitHistoryRow(h, Text, commit, graph, graphWidth, selected, th
|
|
|
34200
35326
|
*/
|
|
34201
35327
|
function renderStackedCommitHistoryRow(h, Text, Box, commit, graph, graphWidth, selected, theme, index, panelWidth, fullGraph, now, laneSegments, isRecent = false, remoteNames) {
|
|
34202
35328
|
const totalWidth = Math.max(20, panelWidth - 4);
|
|
34203
|
-
|
|
34204
|
-
|
|
35329
|
+
// Suppress child colors on selected rows so each span inherits the
|
|
35330
|
+
// contrast-guaranteed `selectionForeground` set on the line-1 span,
|
|
35331
|
+
// keeping the selected row readable against the selection bg.
|
|
35332
|
+
const accent = selected ? undefined : (theme.noColor ? undefined : theme.colors.accent);
|
|
35333
|
+
const muted = selected ? undefined : (theme.noColor ? undefined : theme.colors.muted);
|
|
34205
35334
|
const selectedBg = selected && !theme.noColor ? theme.colors.selection : undefined;
|
|
35335
|
+
const selectedFg = selected && !theme.noColor ? theme.colors.selectionForeground : undefined;
|
|
34206
35336
|
// Line 1 — subject row. Mostly mirrors the single-line layout but
|
|
34207
35337
|
// skips the date and refs so the message has the whole tail to
|
|
34208
35338
|
// itself. Branch chip rides between the hash and the subject the
|
|
@@ -34214,15 +35344,15 @@ function renderStackedCommitHistoryRow(h, Text, Box, commit, graph, graphWidth,
|
|
|
34214
35344
|
const lineOneFixed = graphWidth + 1 + commit.shortHash.length + 1 + recentMarkerWidth + chip.width;
|
|
34215
35345
|
const subject = truncateCells(commit.message, Math.max(8, totalWidth - lineOneFixed));
|
|
34216
35346
|
const graphChildren = laneSegments && !theme.ascii
|
|
34217
|
-
? renderLaneSegmentSpans(h, Text, laneSegments, theme, graphWidth, `cs${index}
|
|
34218
|
-
: [h(Text, { color: muted, dimColor: theme.noColor }, substituteGraphChars(graph.padEnd(graphWidth), { ascii: theme.ascii }))];
|
|
35347
|
+
? renderLaneSegmentSpans(h, Text, laneSegments, theme, graphWidth, `cs${index}`, { suppressColor: selected })
|
|
35348
|
+
: [h(Text, { color: muted, dimColor: !selected && theme.noColor }, substituteGraphChars(graph.padEnd(graphWidth), { ascii: theme.ascii }))];
|
|
34219
35349
|
const lineOne = h(Text, {
|
|
34220
35350
|
key: `${commit.hash}-${index}-l1`,
|
|
34221
35351
|
backgroundColor: selectedBg,
|
|
34222
|
-
|
|
35352
|
+
color: selectedFg,
|
|
34223
35353
|
}, ...graphChildren, ' ', isRecent
|
|
34224
35354
|
? h(Text, { color: accent, bold: true }, theme.ascii ? '* ' : '▎ ')
|
|
34225
|
-
: null, h(Text, { color: accent, bold: selected || isRecent }, commit.shortHash), ' ', chip.node, ...renderTypedSubject(h, Text, subject, theme, `${commit.hash}-${index}-stk-subj
|
|
35355
|
+
: null, h(Text, { color: accent, bold: selected || isRecent }, commit.shortHash), ' ', chip.node, ...renderTypedSubject(h, Text, subject, theme, `${commit.hash}-${index}-stk-subj`, selected));
|
|
34226
35356
|
// Line 2 — metadata row, padded to align with the start of the
|
|
34227
35357
|
// shortHash on line 1 so the eye still groups them as one commit.
|
|
34228
35358
|
// Selection background does not extend here so we don't get a thick
|
|
@@ -34275,8 +35405,11 @@ function renderPendingCommitRow(h, Text, worktree, selected, theme) {
|
|
|
34275
35405
|
return h(Text, {
|
|
34276
35406
|
key: 'pending-commit-row',
|
|
34277
35407
|
bold: true,
|
|
34278
|
-
|
|
34279
|
-
|
|
35408
|
+
// On selection, swap to the contrast-guaranteed foreground so the
|
|
35409
|
+
// accent label doesn't wash out against the selection bar.
|
|
35410
|
+
color: selected && !theme.noColor
|
|
35411
|
+
? theme.colors.selectionForeground
|
|
35412
|
+
: (theme.noColor ? undefined : theme.colors.accent),
|
|
34280
35413
|
backgroundColor: selected && !theme.noColor ? theme.colors.selection : undefined,
|
|
34281
35414
|
}, truncateCells(label, 140));
|
|
34282
35415
|
}
|
|
@@ -34690,6 +35823,10 @@ function renderCommandPalette(h, components, state, width, theme, focused) {
|
|
|
34690
35823
|
dimColor: !isSelected,
|
|
34691
35824
|
}, truncateCells(line, width - 4));
|
|
34692
35825
|
});
|
|
35826
|
+
// Scroll indicators for the palette list — same pattern as the
|
|
35827
|
+
// sidebar and help overlay so the user knows there's more content.
|
|
35828
|
+
const paletteHasMoreAbove = startIndex > 0 && filtered.length > 0;
|
|
35829
|
+
const paletteHasMoreBelow = startIndex + listRows < filtered.length;
|
|
34693
35830
|
return h(Box, {
|
|
34694
35831
|
borderColor: focusBorderColor(theme, focused),
|
|
34695
35832
|
borderStyle: theme.borderStyle,
|
|
@@ -34698,7 +35835,66 @@ function renderCommandPalette(h, components, state, width, theme, focused) {
|
|
|
34698
35835
|
paddingX: 1,
|
|
34699
35836
|
}, h(Box, { justifyContent: 'space-between' }, h(Text, { bold: true }, panelTitle('Command palette', focused)), h(Text, { dimColor: true }, matchSummary)), h(Text, { color: theme.colors.accent }, truncateCells(inputLine, width - 4)), h(Text, { dimColor: true }, truncateCells(hint, width - 4)), h(Text, undefined, ''), ...(showingRecent
|
|
34700
35837
|
? [h(Text, { key: 'palette-recent-hint', dimColor: true }, '· marks recently-used')]
|
|
34701
|
-
: []), ...
|
|
35838
|
+
: []), ...(paletteHasMoreAbove
|
|
35839
|
+
? [h(Text, { key: 'palette-more-above', dimColor: true }, ` ↑ ${startIndex} more above`)]
|
|
35840
|
+
: []), ...itemLines, ...(paletteHasMoreBelow
|
|
35841
|
+
? [h(Text, { key: 'palette-more-below', dimColor: true }, ` ↓ ${filtered.length - (startIndex + listRows)} more below`)]
|
|
35842
|
+
: []));
|
|
35843
|
+
}
|
|
35844
|
+
/**
|
|
35845
|
+
* Theme picker overlay (`gC`). Renders like the command palette so the
|
|
35846
|
+
* rest of the surface live-previews the cursored theme underneath. Type to
|
|
35847
|
+
* filter, ↑/↓ to move, Enter applies (and persists), Esc cancels. Takes the
|
|
35848
|
+
* raw `filter` + `index` rather than a `LogInkState` so it's reusable by
|
|
35849
|
+
* the workspace top-level surface, which has its own state model.
|
|
35850
|
+
*/
|
|
35851
|
+
function renderThemePickerOverlay(h, components, filter, index, width, theme, focused) {
|
|
35852
|
+
const { Box, Text } = components;
|
|
35853
|
+
const filtered = filterThemePresets(filter);
|
|
35854
|
+
const selectedIndex = filtered.length === 0
|
|
35855
|
+
? 0
|
|
35856
|
+
: Math.max(0, Math.min(index, filtered.length - 1));
|
|
35857
|
+
const listRows = 14;
|
|
35858
|
+
const startIndex = Math.max(0, selectedIndex - Math.floor(listRows / 2));
|
|
35859
|
+
const visible = filtered.slice(startIndex, startIndex + listRows);
|
|
35860
|
+
const inputLine = `> ${filter}_`;
|
|
35861
|
+
const matchSummary = filtered.length === 0
|
|
35862
|
+
? 'no matches'
|
|
35863
|
+
: `${filtered.length} ${filtered.length === 1 ? 'theme' : 'themes'}`;
|
|
35864
|
+
const hint = '↑/↓ select · type to filter · enter apply · esc close';
|
|
35865
|
+
const itemLines = filtered.length === 0
|
|
35866
|
+
? [h(Text, { key: 'theme-empty', dimColor: true }, 'No themes match the current filter.')]
|
|
35867
|
+
: visible.map((preset, offset) => {
|
|
35868
|
+
const index = startIndex + offset;
|
|
35869
|
+
const isSelected = index === selectedIndex;
|
|
35870
|
+
const cursor = isSelected ? '>' : ' ';
|
|
35871
|
+
// Accent swatch per theme (no swatch for the monochrome baseline or
|
|
35872
|
+
// when color is off). `default` is the only ANSI-named accent.
|
|
35873
|
+
const accent = preset === 'monochrome'
|
|
35874
|
+
? undefined
|
|
35875
|
+
: THEME_PRESET_COLORS[preset]?.accent;
|
|
35876
|
+
const swatch = accent && !theme.noColor
|
|
35877
|
+
? h(Text, { key: `theme-swatch-${preset}`, color: accent }, '● ')
|
|
35878
|
+
: h(Text, { key: `theme-swatch-${preset}`, dimColor: true }, '· ');
|
|
35879
|
+
return h(Text, {
|
|
35880
|
+
key: `theme-${preset}`,
|
|
35881
|
+
bold: isSelected,
|
|
35882
|
+
dimColor: !isSelected,
|
|
35883
|
+
}, `${cursor} `, swatch, truncateCells(preset, width - 8));
|
|
35884
|
+
});
|
|
35885
|
+
const hasMoreAbove = startIndex > 0 && filtered.length > 0;
|
|
35886
|
+
const hasMoreBelow = startIndex + listRows < filtered.length;
|
|
35887
|
+
return h(Box, {
|
|
35888
|
+
borderColor: focusBorderColor(theme, focused),
|
|
35889
|
+
borderStyle: theme.borderStyle,
|
|
35890
|
+
flexDirection: 'column',
|
|
35891
|
+
width,
|
|
35892
|
+
paddingX: 1,
|
|
35893
|
+
}, h(Box, { justifyContent: 'space-between' }, h(Text, { bold: true }, panelTitle('Theme picker', focused)), h(Text, { dimColor: true }, matchSummary)), h(Text, { color: theme.colors.accent }, truncateCells(inputLine, width - 4)), h(Text, { dimColor: true }, truncateCells(hint, width - 4)), h(Text, undefined, ''), ...(hasMoreAbove
|
|
35894
|
+
? [h(Text, { key: 'theme-more-above', dimColor: true }, ` ↑ ${startIndex} more above`)]
|
|
35895
|
+
: []), ...itemLines, ...(hasMoreBelow
|
|
35896
|
+
? [h(Text, { key: 'theme-more-below', dimColor: true }, ` ↓ ${filtered.length - (startIndex + listRows)} more below`)]
|
|
35897
|
+
: []));
|
|
34702
35898
|
}
|
|
34703
35899
|
/**
|
|
34704
35900
|
* Split-plan overlay (#907) — renders the proposed commit groups for
|
|
@@ -35554,6 +36750,8 @@ function renderStashSurface(h, components, state, context, contextStatus, bodyRo
|
|
|
35554
36750
|
dimColor: !isSelected,
|
|
35555
36751
|
}, truncateCells(`${cursor} ${stash.ref.padEnd(12)} ${stash.message}`, 140));
|
|
35556
36752
|
});
|
|
36753
|
+
const stashHasMoreAbove = startIndex > 0 && stashes.length > 0;
|
|
36754
|
+
const stashHasMoreBelow = startIndex + listRows < stashes.length;
|
|
35557
36755
|
return h(Box, {
|
|
35558
36756
|
borderColor: focusBorderColor(theme, focused),
|
|
35559
36757
|
borderStyle: theme.borderStyle,
|
|
@@ -35561,7 +36759,11 @@ function renderStashSurface(h, components, state, context, contextStatus, bodyRo
|
|
|
35561
36759
|
flexShrink: 0,
|
|
35562
36760
|
paddingX: 1,
|
|
35563
36761
|
width,
|
|
35564
|
-
}, h(Box, { justifyContent: 'space-between' }, h(Text, { bold: true }, panelTitle('Stash', focused)), h(Text, { dimColor: true }, headerRight)), ...renderPromotedFilterAffordance(h, Text, state, theme), ...
|
|
36762
|
+
}, h(Box, { justifyContent: 'space-between' }, h(Text, { bold: true }, panelTitle('Stash', focused)), h(Text, { dimColor: true }, headerRight)), ...renderPromotedFilterAffordance(h, Text, state, theme), ...(stashHasMoreAbove
|
|
36763
|
+
? [h(Text, { key: 'stash-more-above', dimColor: true }, ` ↑ ${startIndex} more above`)]
|
|
36764
|
+
: []), ...lines, ...(stashHasMoreBelow
|
|
36765
|
+
? [h(Text, { key: 'stash-more-below', dimColor: true }, ` ↓ ${stashes.length - (startIndex + listRows)} more below`)]
|
|
36766
|
+
: []));
|
|
35565
36767
|
}
|
|
35566
36768
|
|
|
35567
36769
|
/**
|
|
@@ -35657,7 +36859,7 @@ function renderStatusSurface(h, components, state, context, contextStatus, bodyR
|
|
|
35657
36859
|
bold: true,
|
|
35658
36860
|
dimColor: !headerSelected && rowIndex > cursorRowIndex,
|
|
35659
36861
|
backgroundColor: headerSelected && !theme.noColor ? theme.colors.selection : undefined,
|
|
35660
|
-
|
|
36862
|
+
color: headerSelected && !theme.noColor ? theme.colors.selectionForeground : undefined,
|
|
35661
36863
|
}, truncateCells(text, 140));
|
|
35662
36864
|
}
|
|
35663
36865
|
const isSelected = !headerFocused && row.flatIndex === selectedIndex;
|
|
@@ -35677,8 +36879,11 @@ function renderStatusSurface(h, components, state, context, contextStatus, bodyR
|
|
|
35677
36879
|
key: `status-file-${row.flatIndex}-${rowIndex}`,
|
|
35678
36880
|
dimColor: !isSelected && rowIndex > cursorRowIndex,
|
|
35679
36881
|
backgroundColor: isSelected && focused && !theme.noColor ? theme.colors.selection : undefined,
|
|
35680
|
-
|
|
35681
|
-
}, ` ${cursorPart}`,
|
|
36882
|
+
color: isSelected && focused && !theme.noColor ? theme.colors.selectionForeground : undefined,
|
|
36883
|
+
}, ` ${cursorPart}`,
|
|
36884
|
+
// Suppress the dot's own color on selected rows so it inherits the
|
|
36885
|
+
// contrast-guaranteed selection foreground set on the row span.
|
|
36886
|
+
...(useDot ? [h(Text, { color: (isSelected && focused) ? undefined : dotColor }, STAGE_STATUS_DOT), ' '] : []), tailTrunc);
|
|
35682
36887
|
});
|
|
35683
36888
|
// When the mask narrows the list to nothing but the underlying repo
|
|
35684
36889
|
// is non-clean, surface why the panel looks empty so the user can
|
|
@@ -35693,6 +36898,10 @@ function renderStatusSurface(h, components, state, context, contextStatus, bodyR
|
|
|
35693
36898
|
: cleanHint
|
|
35694
36899
|
? [cleanHint]
|
|
35695
36900
|
: ['Worktree clean'];
|
|
36901
|
+
// Scroll indicators for the status file list — same pattern as
|
|
36902
|
+
// branches and the sidebar so the user knows there's more content.
|
|
36903
|
+
const statusHasMoreAbove = windowStart > 0 && surfaceRows.length > 0;
|
|
36904
|
+
const statusHasMoreBelow = windowStart + listRows < surfaceRows.length;
|
|
35696
36905
|
return h(Box, {
|
|
35697
36906
|
borderColor: focusBorderColor(theme, focused),
|
|
35698
36907
|
borderStyle: theme.borderStyle,
|
|
@@ -35708,7 +36917,11 @@ function renderStatusSurface(h, components, state, context, contextStatus, bodyR
|
|
|
35708
36917
|
// never touch the filter.
|
|
35709
36918
|
...(isStatusFilterMaskActive(state.statusFilterMask)
|
|
35710
36919
|
? [h(Text, { key: 'status-mask-indicator', dimColor: true }, `filter: ${formatStatusFilterMask(state.statusFilterMask)} (1/2/3 to toggle)`)]
|
|
35711
|
-
: []), ...
|
|
36920
|
+
: []), ...(statusHasMoreAbove
|
|
36921
|
+
? [h(Text, { key: 'status-more-above', dimColor: true }, ` ↑ ${windowStart} more above`)]
|
|
36922
|
+
: []), ...renderedRows, ...(statusHasMoreBelow
|
|
36923
|
+
? [h(Text, { key: 'status-more-below', dimColor: true }, ` ↓ ${surfaceRows.length - (windowStart + listRows)} more below`)]
|
|
36924
|
+
: []), ...fallbackLines.map((line, index) => h(Text, {
|
|
35712
36925
|
key: `status-surface-fallback-${index}`,
|
|
35713
36926
|
dimColor: index > 0,
|
|
35714
36927
|
}, truncateCells(line, 140))));
|
|
@@ -35938,6 +37151,8 @@ function renderTagsSurface(h, components, state, context, contextStatus, bodyRow
|
|
|
35938
37151
|
dimColor: !isSelected,
|
|
35939
37152
|
}, before, formatHyperlink(namePadded, url), after);
|
|
35940
37153
|
});
|
|
37154
|
+
const tagsHasMoreAbove = startIndex > 0 && tags.length > 0;
|
|
37155
|
+
const tagsHasMoreBelow = startIndex + listRows < tags.length;
|
|
35941
37156
|
return h(Box, {
|
|
35942
37157
|
borderColor: focusBorderColor(theme, focused),
|
|
35943
37158
|
borderStyle: theme.borderStyle,
|
|
@@ -35945,7 +37160,11 @@ function renderTagsSurface(h, components, state, context, contextStatus, bodyRow
|
|
|
35945
37160
|
flexShrink: 0,
|
|
35946
37161
|
paddingX: 1,
|
|
35947
37162
|
width,
|
|
35948
|
-
}, h(Box, { justifyContent: 'space-between' }, h(Text, { bold: true }, panelTitle('Tags', focused)), h(Text, { dimColor: true }, headerRight)), ...renderPromotedFilterAffordance(h, Text, state, theme), ...
|
|
37163
|
+
}, h(Box, { justifyContent: 'space-between' }, h(Text, { bold: true }, panelTitle('Tags', focused)), h(Text, { dimColor: true }, headerRight)), ...renderPromotedFilterAffordance(h, Text, state, theme), ...(tagsHasMoreAbove
|
|
37164
|
+
? [h(Text, { key: 'tags-more-above', dimColor: true }, ` ↑ ${startIndex} more above`)]
|
|
37165
|
+
: []), ...lines, ...(tagsHasMoreBelow
|
|
37166
|
+
? [h(Text, { key: 'tags-more-below', dimColor: true }, ` ↓ ${tags.length - (startIndex + listRows)} more below`)]
|
|
37167
|
+
: []));
|
|
35949
37168
|
}
|
|
35950
37169
|
|
|
35951
37170
|
/**
|
|
@@ -36464,12 +37683,17 @@ function renderInspectorActionsSection(h, Text, context, width, theme, options =
|
|
|
36464
37683
|
h(Text, { key: 'actions-title' }, cursorActive ? '[Actions]' : 'Actions:'),
|
|
36465
37684
|
...actions.map((action, index) => {
|
|
36466
37685
|
const isSelected = cursorActive && index === cursorIndex;
|
|
37686
|
+
// On the selected row, swap every span to the contrast-guaranteed
|
|
37687
|
+
// selection foreground so the key glyph / destructive marker don't
|
|
37688
|
+
// wash out against the selection bar; the row is already highlighted,
|
|
37689
|
+
// and the label text still conveys which actions are destructive.
|
|
37690
|
+
const selectedFg = isSelected && !theme.noColor ? theme.colors.selectionForeground : undefined;
|
|
36467
37691
|
const keyCell = action.key.padEnd(KEY_COLUMN);
|
|
36468
37692
|
const label = truncateCells(action.label, labelBudget);
|
|
36469
37693
|
const children = [
|
|
36470
37694
|
h(Text, {
|
|
36471
37695
|
key: `actions-${index}-key`,
|
|
36472
|
-
color: action.destructive ? theme.colors.danger : theme.colors.accent,
|
|
37696
|
+
color: selectedFg ?? (action.destructive ? theme.colors.danger : theme.colors.accent),
|
|
36473
37697
|
}, keyCell),
|
|
36474
37698
|
GAP,
|
|
36475
37699
|
label,
|
|
@@ -36477,14 +37701,14 @@ function renderInspectorActionsSection(h, Text, context, width, theme, options =
|
|
|
36477
37701
|
if (action.destructive) {
|
|
36478
37702
|
children.push(h(Text, {
|
|
36479
37703
|
key: `actions-${index}-mark`,
|
|
36480
|
-
color: theme.colors.danger,
|
|
37704
|
+
color: selectedFg ?? theme.colors.danger,
|
|
36481
37705
|
dimColor: false,
|
|
36482
37706
|
}, DESTRUCTIVE_SUFFIX));
|
|
36483
37707
|
}
|
|
36484
37708
|
return h(Text, {
|
|
36485
37709
|
key: `actions-${index}`,
|
|
36486
37710
|
backgroundColor: isSelected && !theme.noColor ? theme.colors.selection : undefined,
|
|
36487
|
-
|
|
37711
|
+
color: selectedFg,
|
|
36488
37712
|
}, ...children);
|
|
36489
37713
|
}),
|
|
36490
37714
|
];
|
|
@@ -36561,7 +37785,6 @@ function renderCommitFileList(h, Text, files, selectedIndex, focused, maxRows, w
|
|
|
36561
37785
|
return h(Text, {
|
|
36562
37786
|
key: `commit-file-${index}`,
|
|
36563
37787
|
color: statusCodeColor(file.status, theme),
|
|
36564
|
-
inverse: isSelected && focused && !theme.noColor,
|
|
36565
37788
|
bold: isSelected,
|
|
36566
37789
|
}, label);
|
|
36567
37790
|
});
|
|
@@ -37109,6 +38332,9 @@ function renderDetailPanel(h, components, state, context, contextStatus, detail,
|
|
|
37109
38332
|
if (state.showCommandPalette) {
|
|
37110
38333
|
return renderCommandPalette(h, components, state, width, theme, focused);
|
|
37111
38334
|
}
|
|
38335
|
+
if (state.showThemePicker) {
|
|
38336
|
+
return renderThemePickerOverlay(h, components, state.themePickerFilter, state.themePickerIndex, width, theme, focused);
|
|
38337
|
+
}
|
|
37112
38338
|
if (state.inputPrompt) {
|
|
37113
38339
|
return renderInputPromptPanel(h, components, state, width, theme, focused);
|
|
37114
38340
|
}
|
|
@@ -37403,9 +38629,21 @@ function enrichFilterActionWithRectification(action, state, context) {
|
|
|
37403
38629
|
}
|
|
37404
38630
|
}
|
|
37405
38631
|
function LogInkApp(deps) {
|
|
37406
|
-
const { appLabel, clipboardRunner, dateBucketingEnabled, git: rootGit, idleTipsEnabled, ink, initialView, loadRows, logArgv, React, resumeRef, rows, theme } = deps;
|
|
38632
|
+
const { appLabel, clipboardRunner, dateBucketingEnabled, git: rootGit, idleTipsEnabled, ink, initialView, loadRows, logArgv, React, resumeRef, rows, theme: baseTheme, themeConfig } = deps;
|
|
37407
38633
|
const { Box, Text, useApp, useInput, useWindowSize } = ink;
|
|
37408
38634
|
const h = React.createElement;
|
|
38635
|
+
// Theme picker (gC) — live preview + apply. `themePreviewPreset` follows
|
|
38636
|
+
// the picker cursor while the overlay is open; `themeSessionPreset` is the
|
|
38637
|
+
// applied choice that survives close. The effective theme is rebuilt from
|
|
38638
|
+
// the original `themeConfig` so ascii/border/noColor + truecolor-downgrade
|
|
38639
|
+
// semantics are preserved; when neither override is set we use the static
|
|
38640
|
+
// `baseTheme` unchanged (so behavior is identical until the picker is used).
|
|
38641
|
+
const [themePreviewPreset, setThemePreviewPreset] = React.useState(undefined);
|
|
38642
|
+
const [themeSessionPreset, setThemeSessionPreset] = React.useState(undefined);
|
|
38643
|
+
const effectiveThemePreset = themePreviewPreset ?? themeSessionPreset;
|
|
38644
|
+
const theme = React.useMemo(() => effectiveThemePreset
|
|
38645
|
+
? createLogInkTheme({ ...themeConfig, preset: effectiveThemePreset })
|
|
38646
|
+
: baseTheme, [effectiveThemePreset, themeConfig, baseTheme]);
|
|
37409
38647
|
const { exit } = useApp();
|
|
37410
38648
|
const windowSize = useWindowSize();
|
|
37411
38649
|
// Bumping this on SIGCONT forces the existing tree to repaint so users
|
|
@@ -37434,6 +38672,17 @@ function LogInkApp(deps) {
|
|
|
37434
38672
|
// immediately while the chrome still flags the refresh.
|
|
37435
38673
|
bootLoading: Boolean(loadRows),
|
|
37436
38674
|
}));
|
|
38675
|
+
// Theme picker live preview: keep `themePreviewPreset` in sync with the
|
|
38676
|
+
// preset under the picker cursor while the overlay is open; clear it when
|
|
38677
|
+
// the overlay closes so the theme reverts to the applied session preset
|
|
38678
|
+
// (or the original config theme). The derived-theme `useMemo` above does
|
|
38679
|
+
// the actual re-render from this state.
|
|
38680
|
+
const themePickerSelection = state.showThemePicker
|
|
38681
|
+
? getThemePickerSelection(state)
|
|
38682
|
+
: undefined;
|
|
38683
|
+
React.useEffect(() => {
|
|
38684
|
+
setThemePreviewPreset(state.showThemePicker ? themePickerSelection : undefined);
|
|
38685
|
+
}, [state.showThemePicker, themePickerSelection]);
|
|
37437
38686
|
// Nested-repo runtime stack (#931). Each frame holds the live
|
|
37438
38687
|
// `SimpleGit`, the loaded `LogInkContext`, and the per-key load
|
|
37439
38688
|
// status the chrome reads. The active (top-of-stack) entry drives
|
|
@@ -40856,946 +42105,423 @@ function LogInkApp(deps) {
|
|
|
40856
42105
|
// (failure), surfacing the right message.
|
|
40857
42106
|
}
|
|
40858
42107
|
else {
|
|
40859
|
-
dispatch({
|
|
40860
|
-
type: 'setStatus',
|
|
40861
|
-
value: `${target.label} target commit returned no rows — orphan ref?`,
|
|
40862
|
-
kind: 'warning',
|
|
40863
|
-
});
|
|
40864
|
-
}
|
|
40865
|
-
}
|
|
40866
|
-
catch (error) {
|
|
40867
|
-
if (mountedRef.current) {
|
|
40868
|
-
dispatch({
|
|
40869
|
-
type: 'setStatus',
|
|
40870
|
-
value: `Failed to load context for ${target.label}: ${error instanceof Error ? error.message : String(error)}`,
|
|
40871
|
-
kind: 'error',
|
|
40872
|
-
});
|
|
40873
|
-
}
|
|
40874
|
-
}
|
|
40875
|
-
}, [dispatch, git]);
|
|
40876
|
-
React.useEffect(() => {
|
|
40877
|
-
loadCommitContextRef.current = loadCommitContext;
|
|
40878
|
-
}, [loadCommitContext]);
|
|
40879
|
-
// Server-side history filter (#776). When the user submits `path:foo`
|
|
40880
|
-
// or `author:foo`, the filter parser dispatches setHistoryFetchArgs;
|
|
40881
|
-
// this effect picks up the change, re-runs `getLogRows` with merged
|
|
40882
|
-
// args, and replaces the rows. Clearing the fetch args (Ctrl+U inside
|
|
40883
|
-
// filter mode) re-fetches with the original logArgv so the user gets
|
|
40884
|
-
// the live full log back, not a stale snapshot of the initial rows.
|
|
40885
|
-
const historyFetchEffectInitialized = React.useRef(false);
|
|
40886
|
-
const historyFetchRequestRef = React.useRef(0);
|
|
40887
|
-
React.useEffect(() => {
|
|
40888
|
-
if (!logArgv)
|
|
40889
|
-
return;
|
|
40890
|
-
// Skip the first run — initial rows came in via deps.rows; we only
|
|
40891
|
-
// want to fetch in response to *changes* to historyFetchArgs.
|
|
40892
|
-
if (!historyFetchEffectInitialized.current) {
|
|
40893
|
-
historyFetchEffectInitialized.current = true;
|
|
40894
|
-
return;
|
|
40895
|
-
}
|
|
40896
|
-
const requestId = historyFetchRequestRef.current + 1;
|
|
40897
|
-
historyFetchRequestRef.current = requestId;
|
|
40898
|
-
const fetchArgs = state.historyFetchArgs;
|
|
40899
|
-
const merged = {
|
|
40900
|
-
...logArgv,
|
|
40901
|
-
...(fetchArgs?.author ? { author: fetchArgs.author } : {}),
|
|
40902
|
-
...(fetchArgs?.path ? { path: fetchArgs.path } : {}),
|
|
40903
|
-
};
|
|
40904
|
-
const description = fetchArgs?.author
|
|
40905
|
-
? `author:${fetchArgs.author}`
|
|
40906
|
-
: fetchArgs?.path
|
|
40907
|
-
? `path:${fetchArgs.path}`
|
|
40908
|
-
: undefined;
|
|
40909
|
-
dispatch({
|
|
40910
|
-
type: 'setStatus',
|
|
40911
|
-
value: description ? `Refetching with ${description}` : 'Restoring full log',
|
|
40912
|
-
});
|
|
40913
|
-
void (async () => {
|
|
40914
|
-
const stashHashes = await getStashCommitHashes(git).catch(() => []);
|
|
40915
|
-
const nextRows = await safe(getLogRows(git, merged, {
|
|
40916
|
-
limit: LOG_INTERACTIVE_DEFAULT_LIMIT,
|
|
40917
|
-
extraRefs: stashHashes,
|
|
40918
|
-
}));
|
|
40919
|
-
if (!mountedRef.current || historyFetchRequestRef.current !== requestId) {
|
|
40920
|
-
return;
|
|
40921
|
-
}
|
|
40922
|
-
if (!nextRows) {
|
|
40923
|
-
dispatch({ type: 'setStatus', value: 'Failed to refetch with active filter', kind: 'error' });
|
|
40924
|
-
return;
|
|
40925
|
-
}
|
|
40926
|
-
dispatch({ type: 'replaceRows', rows: nextRows });
|
|
40927
|
-
const matched = getCommitRows(nextRows).length;
|
|
40928
|
-
setHasMoreCommits(matched >= LOG_INTERACTIVE_DEFAULT_LIMIT);
|
|
40929
|
-
dispatch({
|
|
40930
|
-
type: 'setStatus',
|
|
40931
|
-
value: description
|
|
40932
|
-
? `Showing ${matched} commits matching ${description}`
|
|
40933
|
-
: 'Showing full log',
|
|
40934
|
-
kind: 'success',
|
|
40935
|
-
});
|
|
40936
|
-
})();
|
|
40937
|
-
}, [dispatch, git, logArgv, state.historyFetchArgs]);
|
|
40938
|
-
// Graph mode toggle (`g` key, #791 follow-up). The header label flips
|
|
40939
|
-
// between "compact graph" and "full graph", but unless we re-fetch with
|
|
40940
|
-
// the right `view`, the underlying rows still come from the user's
|
|
40941
|
-
// initial argv (default `--first-parent --no-merges`) and the renderer
|
|
40942
|
-
// has no topology to draw — defeating the per-lane / junction work.
|
|
40943
|
-
// Mirrors the historyFetchArgs effect: skip first run, request-id ref
|
|
40944
|
-
// for stale-completion guard, swap rows in place via replaceRows.
|
|
40945
|
-
const toggleGraphEffectInitialized = React.useRef(false);
|
|
40946
|
-
const toggleGraphRequestRef = React.useRef(0);
|
|
40947
|
-
React.useEffect(() => {
|
|
40948
|
-
if (!logArgv)
|
|
40949
|
-
return;
|
|
40950
|
-
if (!toggleGraphEffectInitialized.current) {
|
|
40951
|
-
toggleGraphEffectInitialized.current = true;
|
|
40952
|
-
return;
|
|
40953
|
-
}
|
|
40954
|
-
const requestId = toggleGraphRequestRef.current + 1;
|
|
40955
|
-
toggleGraphRequestRef.current = requestId;
|
|
40956
|
-
const merged = buildToggleGraphArgs(logArgv, state.fullGraph);
|
|
40957
|
-
dispatch({
|
|
40958
|
-
type: 'setStatus',
|
|
40959
|
-
value: state.fullGraph
|
|
40960
|
-
? 'Loading full topology…'
|
|
40961
|
-
: 'Loading compact history…',
|
|
40962
|
-
});
|
|
40963
|
-
void (async () => {
|
|
40964
|
-
// Include stash commits as graph roots so the toggle's re-fetch
|
|
40965
|
-
// sees the same rich graph the boot loader assembles. Without
|
|
40966
|
-
// this, flipping `\` into full mode and back loses the stash
|
|
40967
|
-
// anchors that loadRowsWithStashes seeded on boot.
|
|
40968
|
-
const stashHashes = await getStashCommitHashes(git).catch(() => []);
|
|
40969
|
-
const nextRows = await safe(getLogRows(git, merged, {
|
|
40970
|
-
limit: LOG_INTERACTIVE_DEFAULT_LIMIT,
|
|
40971
|
-
extraRefs: stashHashes,
|
|
40972
|
-
}));
|
|
40973
|
-
if (!mountedRef.current || toggleGraphRequestRef.current !== requestId) {
|
|
40974
|
-
return;
|
|
40975
|
-
}
|
|
40976
|
-
if (!nextRows) {
|
|
40977
|
-
dispatch({ type: 'setStatus', value: 'Failed to refetch graph rows', kind: 'error' });
|
|
40978
|
-
return;
|
|
40979
|
-
}
|
|
40980
|
-
dispatch({ type: 'replaceRows', rows: nextRows });
|
|
40981
|
-
const matched = getCommitRows(nextRows).length;
|
|
40982
|
-
setHasMoreCommits(matched >= LOG_INTERACTIVE_DEFAULT_LIMIT);
|
|
40983
|
-
dispatch({
|
|
40984
|
-
type: 'setStatus',
|
|
40985
|
-
value: state.fullGraph
|
|
40986
|
-
? `Showing ${matched} commits across all branches`
|
|
40987
|
-
: `Showing ${matched} commits (compact)`,
|
|
40988
|
-
kind: 'success',
|
|
40989
|
-
});
|
|
40990
|
-
})();
|
|
40991
|
-
}, [dispatch, git, logArgv, state.fullGraph]);
|
|
40992
|
-
const commitDiffHunkOffsets = React.useMemo(() => (filePreview?.hunks
|
|
40993
|
-
.map((line, index) => (line.startsWith('@@') ? index : -1))
|
|
40994
|
-
.filter((index) => index >= 0)), [filePreview]);
|
|
40995
|
-
const worktreeDirty = Boolean(context.worktree &&
|
|
40996
|
-
(context.worktree.stagedCount + context.worktree.unstagedCount + context.worktree.untrackedCount) > 0);
|
|
40997
|
-
useInput((inputValue, key) => {
|
|
40998
|
-
// First-launch onboarding (P1.3): any keystroke dismisses the overlay
|
|
40999
|
-
// and writes the seen-marker. Swallow the keystroke so the same key
|
|
41000
|
-
// doesn't also trigger normal input dispatch.
|
|
41001
|
-
if (showOnboarding) {
|
|
41002
|
-
setShowOnboarding(false);
|
|
41003
|
-
markOnboardingSeen();
|
|
41004
|
-
return;
|
|
41005
|
-
}
|
|
41006
|
-
// P4.5: navigation in branches/tags/stash uses the FILTERED list
|
|
41007
|
-
// length when a filter is active so j/k stay live instead of getting
|
|
41008
|
-
// stuck against a full-list count that no longer matches what's on
|
|
41009
|
-
// screen. The filtered lists are memoized at LogInkApp scope (#808
|
|
41010
|
-
// perf pass) — reading them here is O(1) instead of O(branches +
|
|
41011
|
-
// tags + stashes + worktrees) per keystroke.
|
|
41012
|
-
const branchVisibleCount = filteredBranchList.length;
|
|
41013
|
-
const branchSelectedShortName = filteredBranchList[Math.min(state.selectedBranchIndex, Math.max(0, filteredBranchList.length - 1))]?.shortName;
|
|
41014
|
-
const tagVisibleCount = filteredTagList.length;
|
|
41015
|
-
const tagSelectedName = filteredTagList[Math.min(state.selectedTagIndex, Math.max(0, filteredTagList.length - 1))]?.name;
|
|
41016
|
-
const stashVisibleCount = filteredStashList.length;
|
|
41017
|
-
const stashSelectedRef = filteredStashList[Math.min(state.selectedStashIndex, Math.max(0, filteredStashList.length - 1))]?.ref;
|
|
41018
|
-
const reflogVisibleCount = filteredReflogList.length;
|
|
41019
|
-
const reflogSelectedHash = filteredReflogList[Math.min(state.selectedReflogIndex, Math.max(0, filteredReflogList.length - 1))]?.hash;
|
|
41020
|
-
const submoduleVisibleCount = filteredSubmoduleList.length;
|
|
41021
|
-
filteredSubmoduleList[Math.min(state.selectedSubmoduleIndex, Math.max(0, filteredSubmoduleList.length - 1))]?.path;
|
|
41022
|
-
const issueVisibleCount = filteredIssueList.length;
|
|
41023
|
-
const issueSelectedUrl = filteredIssueList[Math.min(state.selectedIssueIndex, Math.max(0, filteredIssueList.length - 1))]?.url;
|
|
41024
|
-
const pullRequestTriageVisibleCount = filteredPullRequestTriageList.length;
|
|
41025
|
-
const pullRequestTriageSelectedUrl = filteredPullRequestTriageList[Math.min(state.selectedPullRequestTriageIndex, Math.max(0, filteredPullRequestTriageList.length - 1))]?.url;
|
|
41026
|
-
const worktreeVisibleCount = filteredWorktreeList.length;
|
|
41027
|
-
// When the diff view is showing a stash patch, swap the previewLineCount
|
|
41028
|
-
// to the stash diff length so the existing pageDetailPreview path
|
|
41029
|
-
// (j/k, PgUp/PgDn) scrolls through it without a parallel pipeline.
|
|
41030
|
-
const diffPreviewLineCount = state.diffSource === 'stash'
|
|
41031
|
-
? stashDiffLines?.length
|
|
41032
|
-
: filePreview?.hunks.length;
|
|
41033
|
-
// Per-file segmentation for stash diffs reads the LogInkApp-scoped
|
|
41034
|
-
// memo so navigation keys + the input-context derivation share a
|
|
41035
|
-
// single parse pass per stash patch instead of re-walking the
|
|
41036
|
-
// entire patch text on every keystroke.
|
|
41037
|
-
const stashDiffFiles = state.diffSource === 'stash' ? stashDiffParsedFiles : [];
|
|
41038
|
-
const stashDiffFileOffsets = stashDiffFiles.map((file) => file.startLine);
|
|
41039
|
-
const stashDiffSelectedPath = state.diffSource === 'stash'
|
|
41040
|
-
? findStashFileForOffset(stashDiffFiles, state.diffPreviewOffset)?.path
|
|
41041
|
-
: undefined;
|
|
41042
|
-
getLogInkInputEvents(state, inputValue, key, {
|
|
41043
|
-
detailFileCount: detail?.files.length,
|
|
41044
|
-
previewLineCount: diffPreviewLineCount,
|
|
41045
|
-
worktreeDiffLineCount: worktreeDiff?.lines.length,
|
|
41046
|
-
worktreeFileCount: visibleWorktreeFilesGrouped.length,
|
|
41047
|
-
worktreeHunkOffsets: worktreeDiff?.hunkOffsets,
|
|
41048
|
-
commitDiffHunkOffsets,
|
|
41049
|
-
branchCount: branchVisibleCount,
|
|
41050
|
-
branchSelectedShortName,
|
|
41051
|
-
tagCount: tagVisibleCount,
|
|
41052
|
-
tagSelectedName,
|
|
41053
|
-
stashCount: stashVisibleCount,
|
|
41054
|
-
reflogCount: reflogVisibleCount,
|
|
41055
|
-
reflogSelectedHash,
|
|
41056
|
-
submoduleCount: submoduleVisibleCount,
|
|
41057
|
-
issueCount: issueVisibleCount,
|
|
41058
|
-
issueSelectedUrl,
|
|
41059
|
-
pullRequestTriageCount: pullRequestTriageVisibleCount,
|
|
41060
|
-
pullRequestTriageSelectedUrl,
|
|
41061
|
-
stashSelectedRef,
|
|
41062
|
-
stashDiffFileOffsets: stashDiffFileOffsets.length ? stashDiffFileOffsets : undefined,
|
|
41063
|
-
stashDiffSelectedPath,
|
|
41064
|
-
worktreeListCount: worktreeVisibleCount,
|
|
41065
|
-
worktreeSelectedPath: visibleWorktreeFilesGrouped[state.selectedWorktreeFileIndex]?.path,
|
|
41066
|
-
statusGroups: visibleWorktreeGroups.map((group) => ({
|
|
41067
|
-
state: group.state,
|
|
41068
|
-
count: group.files.length,
|
|
41069
|
-
startIndex: group.startIndex,
|
|
41070
|
-
})),
|
|
41071
|
-
inspectorActionCount: getInspectorActionsForState(state).length,
|
|
41072
|
-
commitDiffSelectedPath: state.diffSource === 'commit'
|
|
41073
|
-
? selectedDetailFile?.path
|
|
41074
|
-
: undefined,
|
|
41075
|
-
commitDiffSelectedSha: state.diffSource === 'commit'
|
|
41076
|
-
? selected?.hash
|
|
41077
|
-
: undefined,
|
|
41078
|
-
// #931 PR 3b — Submodule drill-in target for the cursored file
|
|
41079
|
-
// in a commit diff. Resolved per-render so the Enter handler in
|
|
41080
|
-
// `inkInput.ts` doesn't have to re-walk the submodule overview;
|
|
41081
|
-
// undefined whenever the cursored file isn't a registered
|
|
41082
|
-
// submodule (or the overview / repo root haven't loaded yet).
|
|
41083
|
-
commitDiffSubmoduleDrillIn: state.diffSource === 'commit' && selectedDetailFile
|
|
41084
|
-
? resolveCommitDiffDrillInTarget({
|
|
41085
|
-
selectedFile: {
|
|
41086
|
-
path: selectedDetailFile.path,
|
|
41087
|
-
submoduleChange: filePreview?.path === selectedDetailFile.path
|
|
41088
|
-
? filePreview.submoduleChange
|
|
41089
|
-
: undefined,
|
|
41090
|
-
},
|
|
41091
|
-
submodules: context.submodules,
|
|
41092
|
-
activeRepoRoot,
|
|
41093
|
-
})
|
|
41094
|
-
: undefined,
|
|
41095
|
-
// #931 PR 4 / #932 — Submodule drill-in target for the cursored
|
|
41096
|
-
// row in the dedicated submodules view. Resolved per-render so
|
|
41097
|
-
// the Enter handler in `inkInput.ts` doesn't have to re-walk the
|
|
41098
|
-
// submodule overview. Gated on `activeView === 'submodules'` so
|
|
41099
|
-
// a stale resolution from a different view can't accidentally
|
|
41100
|
-
// fire — the runtime only ever populates it when the user is
|
|
41101
|
-
// actually on the view.
|
|
41102
|
-
submoduleViewDrillIn: state.activeView === 'submodules'
|
|
41103
|
-
? resolveSubmoduleViewDrillInTarget({
|
|
41104
|
-
selectedIndex: state.selectedSubmoduleIndex,
|
|
41105
|
-
submodules: context.submodules,
|
|
41106
|
-
activeRepoRoot,
|
|
41107
|
-
})
|
|
41108
|
-
: undefined,
|
|
41109
|
-
worktreeDirty,
|
|
41110
|
-
conflictFileCount: context.operation?.conflictedFiles.length,
|
|
41111
|
-
conflictSelectedPath: (() => {
|
|
41112
|
-
const files = context.operation?.conflictedFiles;
|
|
41113
|
-
if (!files || files.length === 0)
|
|
41114
|
-
return undefined;
|
|
41115
|
-
const clamped = Math.min(state.selectedConflictFileIndex, files.length - 1);
|
|
41116
|
-
return files[clamped]?.path;
|
|
41117
|
-
})(),
|
|
41118
|
-
// H / gH need the actual diff text (not just hunk offsets) to
|
|
41119
|
-
// slice the cursored hunk into a `git apply` patch. Stash uses
|
|
41120
|
-
// the full `git stash show -p` output; commit-diff uses the
|
|
41121
|
-
// per-file `filePreview.hunks` array. Either way, extractDiffHunk
|
|
41122
|
-
// walks `@@` headers and synthesizes a fresh diff --git / --- /
|
|
41123
|
-
// +++ header set using the path the caller already resolved.
|
|
41124
|
-
diffLinesForHunkApply: state.diffSource === 'stash'
|
|
41125
|
-
? stashDiffLines
|
|
41126
|
-
: state.diffSource === 'commit'
|
|
41127
|
-
? filePreview?.hunks
|
|
41128
|
-
: undefined,
|
|
41129
|
-
// Line count of the changelog text, used by the changelog view's
|
|
41130
|
-
// j/k/PgUp/PgDn scroll bindings to clamp `pageChangelog` deltas.
|
|
41131
|
-
// Computed from view state rather than threaded through context
|
|
41132
|
-
// because the surface owns its own content — no external loader.
|
|
41133
|
-
changelogLineCount: state.changelogView.text?.split('\n').length,
|
|
41134
|
-
// Approximate line count for the split-plan overlay. Each group
|
|
41135
|
-
// renders as a header + (body if any) + files block + (rationale
|
|
41136
|
-
// if any) + blank separator. Used by j/k/PgUp/PgDn to clamp the
|
|
41137
|
-
// scroll offset. The exact render math lives in the overlay
|
|
41138
|
-
// module — this is a close-enough heuristic for clamping.
|
|
41139
|
-
// #879 item 3 — short sha of the bisect terminator (if any).
|
|
41140
|
-
// Gates `y`/`Y` yank on the completion panel and lets the
|
|
41141
|
-
// runtime resolve the value without re-parsing the log.
|
|
41142
|
-
bisectCompletionSha: context.bisect?.active
|
|
41143
|
-
? getBisectCompletion(context.bisect.log)?.sha
|
|
41144
|
-
: undefined,
|
|
41145
|
-
// #879 item 4 — disambiguates the bisect view's `s` keystroke
|
|
41146
|
-
// (skip current candidate vs. start the wizard).
|
|
41147
|
-
bisectActive: Boolean(context.bisect?.active),
|
|
41148
|
-
splitPlanLineCount: state.splitPlan?.plan
|
|
41149
|
-
? state.splitPlan.plan.groups.reduce((sum, group) => {
|
|
41150
|
-
let lines = 2; // title + separator
|
|
41151
|
-
if (group.body)
|
|
41152
|
-
lines += group.body.split('\n').length + 1;
|
|
41153
|
-
if (group.rationale)
|
|
41154
|
-
lines += 2;
|
|
41155
|
-
lines += (group.files?.length || 0) + 1;
|
|
41156
|
-
if ((group.hunks?.length || 0) > 0)
|
|
41157
|
-
lines += group.hunks.length + 1;
|
|
41158
|
-
return sum + lines;
|
|
41159
|
-
}, 0)
|
|
41160
|
-
: undefined,
|
|
41161
|
-
}).forEach((event) => {
|
|
41162
|
-
if (event.type === 'exit') {
|
|
41163
|
-
exit();
|
|
41164
|
-
}
|
|
41165
|
-
else if (event.type === 'refreshContext') {
|
|
41166
|
-
void refreshContext();
|
|
41167
|
-
}
|
|
41168
|
-
else if (event.type === 'toggleSelectedFileStage') {
|
|
41169
|
-
void toggleSelectedFileStage();
|
|
41170
|
-
}
|
|
41171
|
-
else if (event.type === 'toggleSelectedHunkStage') {
|
|
41172
|
-
void toggleSelectedHunkStage();
|
|
41173
|
-
}
|
|
41174
|
-
else if (event.type === 'revertSelectedFile') {
|
|
41175
|
-
void revertSelectedFile();
|
|
41176
|
-
}
|
|
41177
|
-
else if (event.type === 'revertSelectedHunk') {
|
|
41178
|
-
void revertSelectedHunk();
|
|
41179
|
-
}
|
|
41180
|
-
else if (event.type === 'createManualCommit') {
|
|
41181
|
-
void createCommitFromCompose();
|
|
41182
|
-
}
|
|
41183
|
-
else if (event.type === 'runAiCommitDraft') {
|
|
41184
|
-
void runAiCommitDraft();
|
|
41185
|
-
}
|
|
41186
|
-
else if (event.type === 'cancelAiCommitDraft') {
|
|
41187
|
-
cancelAiCommitDraft();
|
|
41188
|
-
}
|
|
41189
|
-
else if (event.type === 'startCreatePullRequest') {
|
|
41190
|
-
void startCreatePullRequest();
|
|
41191
|
-
}
|
|
41192
|
-
else if (event.type === 'cancelPullRequestBodyDraft') {
|
|
41193
|
-
cancelPullRequestBodyDraft();
|
|
41194
|
-
}
|
|
41195
|
-
else if (event.type === 'startChangelogView') {
|
|
41196
|
-
void startChangelogView();
|
|
41197
|
-
}
|
|
41198
|
-
else if (event.type === 'regenerateChangelog') {
|
|
41199
|
-
regenerateChangelog();
|
|
41200
|
-
}
|
|
41201
|
-
else if (event.type === 'yankChangelog') {
|
|
41202
|
-
yankChangelog();
|
|
41203
|
-
}
|
|
41204
|
-
else if (event.type === 'openChangelogInEditor') {
|
|
41205
|
-
openChangelogInEditor();
|
|
41206
|
-
}
|
|
41207
|
-
else if (event.type === 'openComposeInEditor') {
|
|
41208
|
-
openComposeInEditor();
|
|
41209
|
-
}
|
|
41210
|
-
else if (event.type === 'startCommitSplit') {
|
|
41211
|
-
void startCommitSplit();
|
|
41212
|
-
}
|
|
41213
|
-
else if (event.type === 'applyCommitSplit') {
|
|
41214
|
-
void applyCommitSplit();
|
|
41215
|
-
}
|
|
41216
|
-
else if (event.type === 'cancelCommitSplit') {
|
|
41217
|
-
cancelCommitSplit();
|
|
41218
|
-
}
|
|
41219
|
-
else if (event.type === 'yankText') {
|
|
41220
|
-
void yankText(event.value, event.label);
|
|
41221
|
-
}
|
|
41222
|
-
else if (event.type === 'runWorkflowAction') {
|
|
41223
|
-
void runWorkflowAction(event.id, event.payload);
|
|
41224
|
-
}
|
|
41225
|
-
else if (event.type === 'openFileInEditor') {
|
|
41226
|
-
openInEditor(event.path);
|
|
41227
|
-
}
|
|
41228
|
-
else if (event.type === 'yankFromActiveView') {
|
|
41229
|
-
void yankFromActiveView(event.short);
|
|
41230
|
-
}
|
|
41231
|
-
else {
|
|
41232
|
-
// P4.5: enrich filter-mutating actions with a precomputed
|
|
41233
|
-
// selection snapshot so the reducer can preserve the cursor on
|
|
41234
|
-
// the same item when it's still in the filtered result, only
|
|
41235
|
-
// snapping to result[0] when the previously selected item drops
|
|
41236
|
-
// out. The snapshot lives in the action so the reducer never
|
|
41237
|
-
// needs context items.
|
|
41238
|
-
const enriched = enrichFilterActionWithRectification(event.action, state, context);
|
|
41239
|
-
dispatch(enriched);
|
|
41240
|
-
}
|
|
41241
|
-
});
|
|
41242
|
-
});
|
|
41243
|
-
// Layout depends on focus (sidebar grows when focused), so it's
|
|
41244
|
-
// computed here — after state is in scope but before the render path.
|
|
41245
|
-
const layout = getLogInkLayout({
|
|
41246
|
-
columns: windowSize.columns || process.stdout.columns || LOG_INK_DEFAULT_COLUMNS,
|
|
41247
|
-
rows: windowSize.rows || process.stdout.rows || LOG_INK_DEFAULT_ROWS,
|
|
41248
|
-
sidebarFocused: state.focus === 'sidebar',
|
|
41249
|
-
inspectorFocused: state.focus === 'detail',
|
|
41250
|
-
helpOverlayActive: state.showHelp,
|
|
41251
|
-
});
|
|
41252
|
-
if (layout.tooSmall) {
|
|
41253
|
-
return h(Box, {
|
|
41254
|
-
flexDirection: 'column',
|
|
41255
|
-
height: layout.rows,
|
|
41256
|
-
paddingX: 1,
|
|
41257
|
-
paddingY: 1,
|
|
41258
|
-
}, h(Text, { bold: true }, appLabel), h(Text, undefined, `Terminal too small: ${layout.columns}x${layout.rows}`), h(Text, { dimColor: true }, `Minimum size is ${LOG_INK_MIN_COLUMNS}x${LOG_INK_MIN_ROWS}.`), h(Text, { dimColor: true }, 'Resize the terminal or run plain coco log.'));
|
|
41259
|
-
}
|
|
41260
|
-
// First-launch onboarding overlay (P1.3) replaces the entire UI for
|
|
41261
|
-
// one render — any keystroke dismisses it and persists the seen-marker.
|
|
41262
|
-
if (showOnboarding) {
|
|
41263
|
-
return renderOnboardingOverlay(h, { Box, Text }, layout.rows, layout.columns, theme, appLabel);
|
|
41264
|
-
}
|
|
41265
|
-
return h(Box, { flexDirection: 'column', height: layout.rows }, renderHeader$1(h, { Box, Text }, state, context, contextStatus, layout.columns, theme, appLabel), h(Box, { flexDirection: 'row', height: layout.bodyRows }, renderSidebar$1(h, { Box, Text }, state, context, contextStatus, layout.sidebarWidth, layout.bodyRows, theme, layout.sidebarRailed), renderMainPanel(h, { Box, Text }, state, context, contextStatus, worktreeDiff, worktreeDiffLoading, worktreeHunks, worktreeHunksLoading, filePreview, filePreviewLoading, commitDiffHunkOffsets, selectedDetailFile, stashDiffLines, stashDiffLoading, compareDiffLines, compareDiffLoading, bisectCandidateDetail, bisectCandidateLoading, layout.bodyRows, layout.mainPanelWidth, theme, hasMoreCommits, loadingMoreCommits, spinnerFrame, layout.density, layout.historyRowMode, Boolean(dateBucketingEnabled)), renderDetailPanel(h, { Box, Text }, state, context, contextStatus, detail, detailLoading, filePreview, filePreviewLoading, layout.detailWidth, layout.inspectorTabbed, theme, layout.inspectorRailed, layout.bodyRows)), renderFooter$1(h, { Box, Text }, state, context, theme, idleTip, spinnerFrame));
|
|
41266
|
-
}
|
|
41267
|
-
|
|
41268
|
-
/**
|
|
41269
|
-
* Explicit color-level detection for the Ink TUI (P5.2).
|
|
41270
|
-
*
|
|
41271
|
-
* Chalk already approximates hex colors when the terminal can't render
|
|
41272
|
-
* truecolor — but we want an explicit signal so the catppuccin / gruvbox
|
|
41273
|
-
* presets (which use hex) can fall back to the ANSI-named `default` preset
|
|
41274
|
-
* cleanly on minimal SSH sessions, instead of relying on chalk's
|
|
41275
|
-
* heuristics. Users who set `NO_COLOR` or pick the `monochrome` preset
|
|
41276
|
-
* still get the manual override.
|
|
41277
|
-
*
|
|
41278
|
-
* Levels (matching the chalk taxonomy):
|
|
41279
|
-
* - 'mono' → no ANSI escapes at all (NO_COLOR / TERM=dumb)
|
|
41280
|
-
* - '16' → standard 16-color ANSI palette
|
|
41281
|
-
* - '256' → xterm-256color
|
|
41282
|
-
* - 'truecolor' → 24-bit RGB (COLORTERM=truecolor or known terminals)
|
|
41283
|
-
*/
|
|
41284
|
-
function getColorLevel(env = process.env) {
|
|
41285
|
-
if (env.NO_COLOR)
|
|
41286
|
-
return 'mono';
|
|
41287
|
-
switch (env.FORCE_COLOR) {
|
|
41288
|
-
case '0':
|
|
41289
|
-
return 'mono';
|
|
41290
|
-
case '1':
|
|
41291
|
-
return '16';
|
|
41292
|
-
case '2':
|
|
41293
|
-
return '256';
|
|
41294
|
-
case '3':
|
|
41295
|
-
return 'truecolor';
|
|
41296
|
-
}
|
|
41297
|
-
const colorterm = env.COLORTERM?.toLowerCase();
|
|
41298
|
-
if (colorterm === 'truecolor' || colorterm === '24bit') {
|
|
41299
|
-
return 'truecolor';
|
|
41300
|
-
}
|
|
41301
|
-
// Modern terminal emulators that publicly advertise truecolor support.
|
|
41302
|
-
if (env.KITTY_WINDOW_ID || env.TERM === 'xterm-kitty') {
|
|
41303
|
-
return 'truecolor';
|
|
41304
|
-
}
|
|
41305
|
-
if (env.WT_SESSION) {
|
|
41306
|
-
return 'truecolor';
|
|
41307
|
-
}
|
|
41308
|
-
switch (env.TERM_PROGRAM) {
|
|
41309
|
-
case 'iTerm.app':
|
|
41310
|
-
case 'WezTerm':
|
|
41311
|
-
case 'vscode':
|
|
41312
|
-
case 'ghostty':
|
|
41313
|
-
case 'Hyper':
|
|
41314
|
-
return 'truecolor';
|
|
41315
|
-
}
|
|
41316
|
-
if (env.TERM === 'dumb')
|
|
41317
|
-
return 'mono';
|
|
41318
|
-
if (env.TERM?.includes('256color'))
|
|
41319
|
-
return '256';
|
|
41320
|
-
return '16';
|
|
41321
|
-
}
|
|
41322
|
-
const TRUECOLOR_PRESETS = new Set(['catppuccin', 'gruvbox', 'dracula', 'nord', 'solarized-dark', 'tokyo-night', 'one-dark', 'rose-pine', 'kanagawa', 'everforest', 'monokai', 'synthwave', 'ayu-dark', 'palenight', 'github-dark', 'horizon', 'nightfox', 'carbonfox', 'tokyonight-storm', 'catppuccin-latte', 'solarized-light', 'github-light', 'iceberg', 'material-ocean', 'moonlight', 'poimandres', 'vitesse-dark', 'vesper', 'flexoki', 'mellow']);
|
|
41323
|
-
/**
|
|
41324
|
-
* `true` when the named preset relies on hex colors that look best under
|
|
41325
|
-
* 24-bit RGB. Used by `createLogInkTheme` to decide whether to downgrade
|
|
41326
|
-
* to the ANSI-named `default` palette on lower-capability terminals.
|
|
41327
|
-
*/
|
|
41328
|
-
function presetUsesTrueColor(preset) {
|
|
41329
|
-
return preset !== undefined && TRUECOLOR_PRESETS.has(preset);
|
|
41330
|
-
}
|
|
41331
|
-
|
|
41332
|
-
const THEME_PRESET_COLORS = {
|
|
41333
|
-
default: {
|
|
41334
|
-
accent: 'cyan',
|
|
41335
|
-
border: 'gray',
|
|
41336
|
-
danger: 'red',
|
|
41337
|
-
focusBorder: 'cyan',
|
|
41338
|
-
gitAdded: 'green',
|
|
41339
|
-
gitDeleted: 'red',
|
|
41340
|
-
gitModified: 'yellow',
|
|
41341
|
-
info: 'blue',
|
|
41342
|
-
muted: 'gray',
|
|
41343
|
-
selection: 'cyan',
|
|
41344
|
-
success: 'green',
|
|
41345
|
-
warning: 'yellow',
|
|
41346
|
-
},
|
|
41347
|
-
catppuccin: {
|
|
41348
|
-
accent: '#89b4fa',
|
|
41349
|
-
border: '#585b70',
|
|
41350
|
-
danger: '#f38ba8',
|
|
41351
|
-
focusBorder: '#89dceb',
|
|
41352
|
-
gitAdded: '#a6e3a1',
|
|
41353
|
-
gitDeleted: '#f38ba8',
|
|
41354
|
-
gitModified: '#f9e2af',
|
|
41355
|
-
info: '#89b4fa',
|
|
41356
|
-
muted: '#6c7086',
|
|
41357
|
-
selection: '#45475a',
|
|
41358
|
-
success: '#a6e3a1',
|
|
41359
|
-
warning: '#f9e2af',
|
|
41360
|
-
},
|
|
41361
|
-
gruvbox: {
|
|
41362
|
-
accent: '#83a598',
|
|
41363
|
-
border: '#665c54',
|
|
41364
|
-
danger: '#fb4934',
|
|
41365
|
-
focusBorder: '#8ec07c',
|
|
41366
|
-
gitAdded: '#b8bb26',
|
|
41367
|
-
gitDeleted: '#fb4934',
|
|
41368
|
-
gitModified: '#fabd2f',
|
|
41369
|
-
info: '#83a598',
|
|
41370
|
-
muted: '#928374',
|
|
41371
|
-
selection: '#504945',
|
|
41372
|
-
success: '#b8bb26',
|
|
41373
|
-
warning: '#fabd2f',
|
|
41374
|
-
},
|
|
41375
|
-
dracula: {
|
|
41376
|
-
accent: '#bd93f9',
|
|
41377
|
-
border: '#44475a',
|
|
41378
|
-
danger: '#ff5555',
|
|
41379
|
-
focusBorder: '#ff79c6',
|
|
41380
|
-
gitAdded: '#50fa7b',
|
|
41381
|
-
gitDeleted: '#ff5555',
|
|
41382
|
-
gitModified: '#f1fa8c',
|
|
41383
|
-
info: '#8be9fd',
|
|
41384
|
-
muted: '#6272a4',
|
|
41385
|
-
selection: '#44475a',
|
|
41386
|
-
success: '#50fa7b',
|
|
41387
|
-
warning: '#f1fa8c',
|
|
41388
|
-
},
|
|
41389
|
-
nord: {
|
|
41390
|
-
accent: '#88c0d0',
|
|
41391
|
-
border: '#3b4252',
|
|
41392
|
-
danger: '#bf616a',
|
|
41393
|
-
focusBorder: '#81a1c1',
|
|
41394
|
-
gitAdded: '#a3be8c',
|
|
41395
|
-
gitDeleted: '#bf616a',
|
|
41396
|
-
gitModified: '#ebcb8b',
|
|
41397
|
-
info: '#81a1c1',
|
|
41398
|
-
muted: '#4c566a',
|
|
41399
|
-
selection: '#3b4252',
|
|
41400
|
-
success: '#a3be8c',
|
|
41401
|
-
warning: '#ebcb8b',
|
|
41402
|
-
},
|
|
41403
|
-
'solarized-dark': {
|
|
41404
|
-
accent: '#268bd2',
|
|
41405
|
-
border: '#073642',
|
|
41406
|
-
danger: '#dc322f',
|
|
41407
|
-
focusBorder: '#2aa198',
|
|
41408
|
-
gitAdded: '#859900',
|
|
41409
|
-
gitDeleted: '#dc322f',
|
|
41410
|
-
gitModified: '#b58900',
|
|
41411
|
-
info: '#268bd2',
|
|
41412
|
-
muted: '#586e75',
|
|
41413
|
-
selection: '#073642',
|
|
41414
|
-
success: '#859900',
|
|
41415
|
-
warning: '#b58900',
|
|
41416
|
-
},
|
|
41417
|
-
'tokyo-night': {
|
|
41418
|
-
accent: '#7aa2f7',
|
|
41419
|
-
border: '#3b4261',
|
|
41420
|
-
danger: '#f7768e',
|
|
41421
|
-
focusBorder: '#7dcfff',
|
|
41422
|
-
gitAdded: '#9ece6a',
|
|
41423
|
-
gitDeleted: '#f7768e',
|
|
41424
|
-
gitModified: '#e0af68',
|
|
41425
|
-
info: '#7aa2f7',
|
|
41426
|
-
muted: '#565f89',
|
|
41427
|
-
selection: '#33467c',
|
|
41428
|
-
success: '#9ece6a',
|
|
41429
|
-
warning: '#e0af68',
|
|
41430
|
-
},
|
|
41431
|
-
'one-dark': {
|
|
41432
|
-
accent: '#61afef',
|
|
41433
|
-
border: '#3e4452',
|
|
41434
|
-
danger: '#e06c75',
|
|
41435
|
-
focusBorder: '#56b6c2',
|
|
41436
|
-
gitAdded: '#98c379',
|
|
41437
|
-
gitDeleted: '#e06c75',
|
|
41438
|
-
gitModified: '#e5c07b',
|
|
41439
|
-
info: '#61afef',
|
|
41440
|
-
muted: '#5c6370',
|
|
41441
|
-
selection: '#3e4452',
|
|
41442
|
-
success: '#98c379',
|
|
41443
|
-
warning: '#e5c07b',
|
|
41444
|
-
},
|
|
41445
|
-
'rose-pine': {
|
|
41446
|
-
accent: '#c4a7e7',
|
|
41447
|
-
border: '#26233a',
|
|
41448
|
-
danger: '#eb6f92',
|
|
41449
|
-
focusBorder: '#9ccfd8',
|
|
41450
|
-
gitAdded: '#31748f',
|
|
41451
|
-
gitDeleted: '#eb6f92',
|
|
41452
|
-
gitModified: '#f6c177',
|
|
41453
|
-
info: '#9ccfd8',
|
|
41454
|
-
muted: '#6e6a86',
|
|
41455
|
-
selection: '#2a273f',
|
|
41456
|
-
success: '#31748f',
|
|
41457
|
-
warning: '#f6c177',
|
|
41458
|
-
},
|
|
41459
|
-
kanagawa: {
|
|
41460
|
-
accent: '#7e9cd8',
|
|
41461
|
-
border: '#2a2a37',
|
|
41462
|
-
danger: '#e82424',
|
|
41463
|
-
focusBorder: '#7fb4ca',
|
|
41464
|
-
gitAdded: '#76946a',
|
|
41465
|
-
gitDeleted: '#e82424',
|
|
41466
|
-
gitModified: '#dca561',
|
|
41467
|
-
info: '#7e9cd8',
|
|
41468
|
-
muted: '#727169',
|
|
41469
|
-
selection: '#2d4f67',
|
|
41470
|
-
success: '#76946a',
|
|
41471
|
-
warning: '#dca561',
|
|
41472
|
-
},
|
|
41473
|
-
everforest: {
|
|
41474
|
-
accent: '#a7c080',
|
|
41475
|
-
border: '#374145',
|
|
41476
|
-
danger: '#e67e80',
|
|
41477
|
-
focusBorder: '#83c092',
|
|
41478
|
-
gitAdded: '#a7c080',
|
|
41479
|
-
gitDeleted: '#e67e80',
|
|
41480
|
-
gitModified: '#dbbc7f',
|
|
41481
|
-
info: '#7fbbb3',
|
|
41482
|
-
muted: '#859289',
|
|
41483
|
-
selection: '#374145',
|
|
41484
|
-
success: '#a7c080',
|
|
41485
|
-
warning: '#dbbc7f',
|
|
41486
|
-
},
|
|
41487
|
-
monokai: {
|
|
41488
|
-
accent: '#66d9ef',
|
|
41489
|
-
border: '#49483e',
|
|
41490
|
-
danger: '#f92672',
|
|
41491
|
-
focusBorder: '#a6e22e',
|
|
41492
|
-
gitAdded: '#a6e22e',
|
|
41493
|
-
gitDeleted: '#f92672',
|
|
41494
|
-
gitModified: '#e6db74',
|
|
41495
|
-
info: '#66d9ef',
|
|
41496
|
-
muted: '#75715e',
|
|
41497
|
-
selection: '#49483e',
|
|
41498
|
-
success: '#a6e22e',
|
|
41499
|
-
warning: '#e6db74',
|
|
41500
|
-
},
|
|
41501
|
-
synthwave: {
|
|
41502
|
-
accent: '#f97e72',
|
|
41503
|
-
border: '#34294f',
|
|
41504
|
-
danger: '#fe4450',
|
|
41505
|
-
focusBorder: '#36f9f6',
|
|
41506
|
-
gitAdded: '#72f1b8',
|
|
41507
|
-
gitDeleted: '#fe4450',
|
|
41508
|
-
gitModified: '#fede5d',
|
|
41509
|
-
info: '#36f9f6',
|
|
41510
|
-
muted: '#848bbd',
|
|
41511
|
-
selection: '#34294f',
|
|
41512
|
-
success: '#72f1b8',
|
|
41513
|
-
warning: '#fede5d',
|
|
41514
|
-
},
|
|
41515
|
-
'ayu-dark': {
|
|
41516
|
-
accent: '#e6b450',
|
|
41517
|
-
border: '#11151c',
|
|
41518
|
-
danger: '#f07178',
|
|
41519
|
-
focusBorder: '#39bae6',
|
|
41520
|
-
gitAdded: '#7fd962',
|
|
41521
|
-
gitDeleted: '#f07178',
|
|
41522
|
-
gitModified: '#e6b450',
|
|
41523
|
-
info: '#39bae6',
|
|
41524
|
-
muted: '#565b66',
|
|
41525
|
-
selection: '#1a1f29',
|
|
41526
|
-
success: '#7fd962',
|
|
41527
|
-
warning: '#e6b450',
|
|
41528
|
-
},
|
|
41529
|
-
palenight: {
|
|
41530
|
-
accent: '#82aaff',
|
|
41531
|
-
border: '#3a3f58',
|
|
41532
|
-
danger: '#ff5370',
|
|
41533
|
-
focusBorder: '#89ddff',
|
|
41534
|
-
gitAdded: '#c3e88d',
|
|
41535
|
-
gitDeleted: '#ff5370',
|
|
41536
|
-
gitModified: '#ffcb6b',
|
|
41537
|
-
info: '#82aaff',
|
|
41538
|
-
muted: '#676e95',
|
|
41539
|
-
selection: '#3a3f58',
|
|
41540
|
-
success: '#c3e88d',
|
|
41541
|
-
warning: '#ffcb6b',
|
|
41542
|
-
},
|
|
41543
|
-
'github-dark': {
|
|
41544
|
-
accent: '#58a6ff',
|
|
41545
|
-
border: '#30363d',
|
|
41546
|
-
danger: '#f85149',
|
|
41547
|
-
focusBorder: '#58a6ff',
|
|
41548
|
-
gitAdded: '#3fb950',
|
|
41549
|
-
gitDeleted: '#f85149',
|
|
41550
|
-
gitModified: '#d29922',
|
|
41551
|
-
info: '#58a6ff',
|
|
41552
|
-
muted: '#8b949e',
|
|
41553
|
-
selection: '#264f78',
|
|
41554
|
-
success: '#3fb950',
|
|
41555
|
-
warning: '#d29922',
|
|
41556
|
-
},
|
|
41557
|
-
horizon: {
|
|
41558
|
-
accent: '#e95678',
|
|
41559
|
-
border: '#2e303e',
|
|
41560
|
-
danger: '#e95678',
|
|
41561
|
-
focusBorder: '#25b0bc',
|
|
41562
|
-
gitAdded: '#09f7a0',
|
|
41563
|
-
gitDeleted: '#e95678',
|
|
41564
|
-
gitModified: '#fab795',
|
|
41565
|
-
info: '#25b0bc',
|
|
41566
|
-
muted: '#6c6f93',
|
|
41567
|
-
selection: '#2e303e',
|
|
41568
|
-
success: '#09f7a0',
|
|
41569
|
-
warning: '#fab795',
|
|
41570
|
-
},
|
|
41571
|
-
nightfox: {
|
|
41572
|
-
accent: '#719cd6',
|
|
41573
|
-
border: '#2b3b51',
|
|
41574
|
-
danger: '#c94f6d',
|
|
41575
|
-
focusBorder: '#63cdcf',
|
|
41576
|
-
gitAdded: '#81b29a',
|
|
41577
|
-
gitDeleted: '#c94f6d',
|
|
41578
|
-
gitModified: '#dbc074',
|
|
41579
|
-
info: '#719cd6',
|
|
41580
|
-
muted: '#738091',
|
|
41581
|
-
selection: '#2b3b51',
|
|
41582
|
-
success: '#81b29a',
|
|
41583
|
-
warning: '#dbc074',
|
|
41584
|
-
},
|
|
41585
|
-
carbonfox: {
|
|
41586
|
-
accent: '#78a9ff',
|
|
41587
|
-
border: '#353535',
|
|
41588
|
-
danger: '#ee5396',
|
|
41589
|
-
focusBorder: '#33b1ff',
|
|
41590
|
-
gitAdded: '#42be65',
|
|
41591
|
-
gitDeleted: '#ee5396',
|
|
41592
|
-
gitModified: '#ffe97b',
|
|
41593
|
-
info: '#78a9ff',
|
|
41594
|
-
muted: '#7b7c7e',
|
|
41595
|
-
selection: '#353535',
|
|
41596
|
-
success: '#42be65',
|
|
41597
|
-
warning: '#ffe97b',
|
|
41598
|
-
},
|
|
41599
|
-
'tokyonight-storm': {
|
|
41600
|
-
accent: '#7aa2f7',
|
|
41601
|
-
border: '#2f334d',
|
|
41602
|
-
danger: '#f7768e',
|
|
41603
|
-
focusBorder: '#2ac3de',
|
|
41604
|
-
gitAdded: '#9ece6a',
|
|
41605
|
-
gitDeleted: '#f7768e',
|
|
41606
|
-
gitModified: '#e0af68',
|
|
41607
|
-
info: '#2ac3de',
|
|
41608
|
-
muted: '#545c7e',
|
|
41609
|
-
selection: '#2f334d',
|
|
41610
|
-
success: '#9ece6a',
|
|
41611
|
-
warning: '#e0af68',
|
|
41612
|
-
},
|
|
41613
|
-
'catppuccin-latte': {
|
|
41614
|
-
accent: '#1e66f5',
|
|
41615
|
-
border: '#ccd0da',
|
|
41616
|
-
danger: '#d20f39',
|
|
41617
|
-
focusBorder: '#179299',
|
|
41618
|
-
gitAdded: '#40a02b',
|
|
41619
|
-
gitDeleted: '#d20f39',
|
|
41620
|
-
gitModified: '#df8e1d',
|
|
41621
|
-
info: '#1e66f5',
|
|
41622
|
-
muted: '#9ca0b0',
|
|
41623
|
-
selection: '#ccd0da',
|
|
41624
|
-
success: '#40a02b',
|
|
41625
|
-
warning: '#df8e1d',
|
|
41626
|
-
},
|
|
41627
|
-
'solarized-light': {
|
|
41628
|
-
accent: '#268bd2',
|
|
41629
|
-
border: '#eee8d5',
|
|
41630
|
-
danger: '#dc322f',
|
|
41631
|
-
focusBorder: '#2aa198',
|
|
41632
|
-
gitAdded: '#859900',
|
|
41633
|
-
gitDeleted: '#dc322f',
|
|
41634
|
-
gitModified: '#b58900',
|
|
41635
|
-
info: '#268bd2',
|
|
41636
|
-
muted: '#93a1a1',
|
|
41637
|
-
selection: '#eee8d5',
|
|
41638
|
-
success: '#859900',
|
|
41639
|
-
warning: '#b58900',
|
|
41640
|
-
},
|
|
41641
|
-
'github-light': {
|
|
41642
|
-
accent: '#0969da',
|
|
41643
|
-
border: '#d0d7de',
|
|
41644
|
-
danger: '#cf222e',
|
|
41645
|
-
focusBorder: '#0969da',
|
|
41646
|
-
gitAdded: '#1a7f37',
|
|
41647
|
-
gitDeleted: '#cf222e',
|
|
41648
|
-
gitModified: '#9a6700',
|
|
41649
|
-
info: '#0969da',
|
|
41650
|
-
muted: '#656d76',
|
|
41651
|
-
selection: '#ddf4ff',
|
|
41652
|
-
success: '#1a7f37',
|
|
41653
|
-
warning: '#9a6700',
|
|
41654
|
-
},
|
|
41655
|
-
iceberg: {
|
|
41656
|
-
accent: '#84a0c6',
|
|
41657
|
-
border: '#1e2132',
|
|
41658
|
-
danger: '#e27878',
|
|
41659
|
-
focusBorder: '#89b8c2',
|
|
41660
|
-
gitAdded: '#b4be82',
|
|
41661
|
-
gitDeleted: '#e27878',
|
|
41662
|
-
gitModified: '#e2a478',
|
|
41663
|
-
info: '#84a0c6',
|
|
41664
|
-
muted: '#6b7089',
|
|
41665
|
-
selection: '#1e2132',
|
|
41666
|
-
success: '#b4be82',
|
|
41667
|
-
warning: '#e2a478',
|
|
41668
|
-
},
|
|
41669
|
-
'material-ocean': {
|
|
41670
|
-
accent: '#82aaff',
|
|
41671
|
-
border: '#2b2f3a',
|
|
41672
|
-
danger: '#f07178',
|
|
41673
|
-
focusBorder: '#89ddff',
|
|
41674
|
-
gitAdded: '#c3e88d',
|
|
41675
|
-
gitDeleted: '#f07178',
|
|
41676
|
-
gitModified: '#ffcb6b',
|
|
41677
|
-
info: '#82aaff',
|
|
41678
|
-
muted: '#464b5d',
|
|
41679
|
-
selection: '#2b2f3a',
|
|
41680
|
-
success: '#c3e88d',
|
|
41681
|
-
warning: '#ffcb6b',
|
|
41682
|
-
},
|
|
41683
|
-
moonlight: {
|
|
41684
|
-
accent: '#82aaff',
|
|
41685
|
-
border: '#2f334d',
|
|
41686
|
-
danger: '#ff757f',
|
|
41687
|
-
focusBorder: '#86e1fc',
|
|
41688
|
-
gitAdded: '#c3e88d',
|
|
41689
|
-
gitDeleted: '#ff757f',
|
|
41690
|
-
gitModified: '#ffc777',
|
|
41691
|
-
info: '#82aaff',
|
|
41692
|
-
muted: '#636da6',
|
|
41693
|
-
selection: '#2f334d',
|
|
41694
|
-
success: '#c3e88d',
|
|
41695
|
-
warning: '#ffc777',
|
|
41696
|
-
},
|
|
41697
|
-
poimandres: {
|
|
41698
|
-
accent: '#add7ff',
|
|
41699
|
-
border: '#1b1e28',
|
|
41700
|
-
danger: '#d0679d',
|
|
41701
|
-
focusBorder: '#5de4c7',
|
|
41702
|
-
gitAdded: '#5de4c7',
|
|
41703
|
-
gitDeleted: '#d0679d',
|
|
41704
|
-
gitModified: '#fffac2',
|
|
41705
|
-
info: '#add7ff',
|
|
41706
|
-
muted: '#506477',
|
|
41707
|
-
selection: '#1b1e28',
|
|
41708
|
-
success: '#5de4c7',
|
|
41709
|
-
warning: '#fffac2',
|
|
41710
|
-
},
|
|
41711
|
-
'vitesse-dark': {
|
|
41712
|
-
accent: '#4d9375',
|
|
41713
|
-
border: '#282828',
|
|
41714
|
-
danger: '#cb7676',
|
|
41715
|
-
focusBorder: '#4d9375',
|
|
41716
|
-
gitAdded: '#4d9375',
|
|
41717
|
-
gitDeleted: '#cb7676',
|
|
41718
|
-
gitModified: '#e6cc77',
|
|
41719
|
-
info: '#6394bf',
|
|
41720
|
-
muted: '#758575',
|
|
41721
|
-
selection: '#282828',
|
|
41722
|
-
success: '#4d9375',
|
|
41723
|
-
warning: '#e6cc77',
|
|
41724
|
-
},
|
|
41725
|
-
vesper: {
|
|
41726
|
-
accent: '#ffc799',
|
|
41727
|
-
border: '#232323',
|
|
41728
|
-
danger: '#f5a191',
|
|
41729
|
-
focusBorder: '#99ffe4',
|
|
41730
|
-
gitAdded: '#99ffe4',
|
|
41731
|
-
gitDeleted: '#f5a191',
|
|
41732
|
-
gitModified: '#ffc799',
|
|
41733
|
-
info: '#a0c4ff',
|
|
41734
|
-
muted: '#575757',
|
|
41735
|
-
selection: '#232323',
|
|
41736
|
-
success: '#99ffe4',
|
|
41737
|
-
warning: '#ffc799',
|
|
41738
|
-
},
|
|
41739
|
-
flexoki: {
|
|
41740
|
-
accent: '#205ea6',
|
|
41741
|
-
border: '#343331',
|
|
41742
|
-
danger: '#af3029',
|
|
41743
|
-
focusBorder: '#24837b',
|
|
41744
|
-
gitAdded: '#66800b',
|
|
41745
|
-
gitDeleted: '#af3029',
|
|
41746
|
-
gitModified: '#ad8301',
|
|
41747
|
-
info: '#205ea6',
|
|
41748
|
-
muted: '#878580',
|
|
41749
|
-
selection: '#343331',
|
|
41750
|
-
success: '#66800b',
|
|
41751
|
-
warning: '#ad8301',
|
|
41752
|
-
},
|
|
41753
|
-
mellow: {
|
|
41754
|
-
accent: '#7eb8da',
|
|
41755
|
-
border: '#2a2a2a',
|
|
41756
|
-
danger: '#f5a191',
|
|
41757
|
-
focusBorder: '#a3d4a0',
|
|
41758
|
-
gitAdded: '#a3d4a0',
|
|
41759
|
-
gitDeleted: '#f5a191',
|
|
41760
|
-
gitModified: '#f0c674',
|
|
41761
|
-
info: '#7eb8da',
|
|
41762
|
-
muted: '#6b6b6b',
|
|
41763
|
-
selection: '#2a2a2a',
|
|
41764
|
-
success: '#a3d4a0',
|
|
41765
|
-
warning: '#f0c674',
|
|
41766
|
-
},
|
|
41767
|
-
};
|
|
41768
|
-
function shouldUseAscii(term) {
|
|
41769
|
-
if (!term) {
|
|
41770
|
-
return false;
|
|
41771
|
-
}
|
|
41772
|
-
return term === 'dumb' || term.startsWith('vt100');
|
|
41773
|
-
}
|
|
41774
|
-
function createLogInkTheme(options = {}) {
|
|
41775
|
-
const noColor = (options.noColor ?? Boolean(process.env.NO_COLOR)) ||
|
|
41776
|
-
options.preset === 'monochrome';
|
|
41777
|
-
const ascii = options.ascii ?? shouldUseAscii(options.term ?? process.env.TERM);
|
|
41778
|
-
const requestedPreset = options.preset && options.preset !== 'monochrome' ? options.preset : 'default';
|
|
41779
|
-
// P5.2 — gracefully downgrade hex presets (catppuccin / gruvbox) when
|
|
41780
|
-
// the host terminal can't render truecolor. Chalk approximates hex in
|
|
41781
|
-
// those modes anyway, but the default preset's ANSI-named palette
|
|
41782
|
-
// renders far more faithfully on 16-color terminals.
|
|
41783
|
-
const colorLevel = getColorLevel(options.env ?? process.env);
|
|
41784
|
-
const preset = !noColor && presetUsesTrueColor(requestedPreset) && colorLevel !== 'truecolor'
|
|
41785
|
-
? 'default'
|
|
41786
|
-
: requestedPreset;
|
|
41787
|
-
const colors = noColor
|
|
41788
|
-
? {}
|
|
41789
|
-
: {
|
|
41790
|
-
...THEME_PRESET_COLORS[preset],
|
|
41791
|
-
...options.colors,
|
|
42108
|
+
dispatch({
|
|
42109
|
+
type: 'setStatus',
|
|
42110
|
+
value: `${target.label} target commit returned no rows — orphan ref?`,
|
|
42111
|
+
kind: 'warning',
|
|
42112
|
+
});
|
|
42113
|
+
}
|
|
42114
|
+
}
|
|
42115
|
+
catch (error) {
|
|
42116
|
+
if (mountedRef.current) {
|
|
42117
|
+
dispatch({
|
|
42118
|
+
type: 'setStatus',
|
|
42119
|
+
value: `Failed to load context for ${target.label}: ${error instanceof Error ? error.message : String(error)}`,
|
|
42120
|
+
kind: 'error',
|
|
42121
|
+
});
|
|
42122
|
+
}
|
|
42123
|
+
}
|
|
42124
|
+
}, [dispatch, git]);
|
|
42125
|
+
React.useEffect(() => {
|
|
42126
|
+
loadCommitContextRef.current = loadCommitContext;
|
|
42127
|
+
}, [loadCommitContext]);
|
|
42128
|
+
// Server-side history filter (#776). When the user submits `path:foo`
|
|
42129
|
+
// or `author:foo`, the filter parser dispatches setHistoryFetchArgs;
|
|
42130
|
+
// this effect picks up the change, re-runs `getLogRows` with merged
|
|
42131
|
+
// args, and replaces the rows. Clearing the fetch args (Ctrl+U inside
|
|
42132
|
+
// filter mode) re-fetches with the original logArgv so the user gets
|
|
42133
|
+
// the live full log back, not a stale snapshot of the initial rows.
|
|
42134
|
+
const historyFetchEffectInitialized = React.useRef(false);
|
|
42135
|
+
const historyFetchRequestRef = React.useRef(0);
|
|
42136
|
+
React.useEffect(() => {
|
|
42137
|
+
if (!logArgv)
|
|
42138
|
+
return;
|
|
42139
|
+
// Skip the first run — initial rows came in via deps.rows; we only
|
|
42140
|
+
// want to fetch in response to *changes* to historyFetchArgs.
|
|
42141
|
+
if (!historyFetchEffectInitialized.current) {
|
|
42142
|
+
historyFetchEffectInitialized.current = true;
|
|
42143
|
+
return;
|
|
42144
|
+
}
|
|
42145
|
+
const requestId = historyFetchRequestRef.current + 1;
|
|
42146
|
+
historyFetchRequestRef.current = requestId;
|
|
42147
|
+
const fetchArgs = state.historyFetchArgs;
|
|
42148
|
+
const merged = {
|
|
42149
|
+
...logArgv,
|
|
42150
|
+
...(fetchArgs?.author ? { author: fetchArgs.author } : {}),
|
|
42151
|
+
...(fetchArgs?.path ? { path: fetchArgs.path } : {}),
|
|
41792
42152
|
};
|
|
41793
|
-
|
|
41794
|
-
|
|
41795
|
-
|
|
41796
|
-
|
|
41797
|
-
|
|
41798
|
-
|
|
42153
|
+
const description = fetchArgs?.author
|
|
42154
|
+
? `author:${fetchArgs.author}`
|
|
42155
|
+
: fetchArgs?.path
|
|
42156
|
+
? `path:${fetchArgs.path}`
|
|
42157
|
+
: undefined;
|
|
42158
|
+
dispatch({
|
|
42159
|
+
type: 'setStatus',
|
|
42160
|
+
value: description ? `Refetching with ${description}` : 'Restoring full log',
|
|
42161
|
+
});
|
|
42162
|
+
void (async () => {
|
|
42163
|
+
const stashHashes = await getStashCommitHashes(git).catch(() => []);
|
|
42164
|
+
const nextRows = await safe(getLogRows(git, merged, {
|
|
42165
|
+
limit: LOG_INTERACTIVE_DEFAULT_LIMIT,
|
|
42166
|
+
extraRefs: stashHashes,
|
|
42167
|
+
}));
|
|
42168
|
+
if (!mountedRef.current || historyFetchRequestRef.current !== requestId) {
|
|
42169
|
+
return;
|
|
42170
|
+
}
|
|
42171
|
+
if (!nextRows) {
|
|
42172
|
+
dispatch({ type: 'setStatus', value: 'Failed to refetch with active filter', kind: 'error' });
|
|
42173
|
+
return;
|
|
42174
|
+
}
|
|
42175
|
+
dispatch({ type: 'replaceRows', rows: nextRows });
|
|
42176
|
+
const matched = getCommitRows(nextRows).length;
|
|
42177
|
+
setHasMoreCommits(matched >= LOG_INTERACTIVE_DEFAULT_LIMIT);
|
|
42178
|
+
dispatch({
|
|
42179
|
+
type: 'setStatus',
|
|
42180
|
+
value: description
|
|
42181
|
+
? `Showing ${matched} commits matching ${description}`
|
|
42182
|
+
: 'Showing full log',
|
|
42183
|
+
kind: 'success',
|
|
42184
|
+
});
|
|
42185
|
+
})();
|
|
42186
|
+
}, [dispatch, git, logArgv, state.historyFetchArgs]);
|
|
42187
|
+
// Graph mode toggle (`g` key, #791 follow-up). The header label flips
|
|
42188
|
+
// between "compact graph" and "full graph", but unless we re-fetch with
|
|
42189
|
+
// the right `view`, the underlying rows still come from the user's
|
|
42190
|
+
// initial argv (default `--first-parent --no-merges`) and the renderer
|
|
42191
|
+
// has no topology to draw — defeating the per-lane / junction work.
|
|
42192
|
+
// Mirrors the historyFetchArgs effect: skip first run, request-id ref
|
|
42193
|
+
// for stale-completion guard, swap rows in place via replaceRows.
|
|
42194
|
+
const toggleGraphEffectInitialized = React.useRef(false);
|
|
42195
|
+
const toggleGraphRequestRef = React.useRef(0);
|
|
42196
|
+
React.useEffect(() => {
|
|
42197
|
+
if (!logArgv)
|
|
42198
|
+
return;
|
|
42199
|
+
if (!toggleGraphEffectInitialized.current) {
|
|
42200
|
+
toggleGraphEffectInitialized.current = true;
|
|
42201
|
+
return;
|
|
42202
|
+
}
|
|
42203
|
+
const requestId = toggleGraphRequestRef.current + 1;
|
|
42204
|
+
toggleGraphRequestRef.current = requestId;
|
|
42205
|
+
const merged = buildToggleGraphArgs(logArgv, state.fullGraph);
|
|
42206
|
+
dispatch({
|
|
42207
|
+
type: 'setStatus',
|
|
42208
|
+
value: state.fullGraph
|
|
42209
|
+
? 'Loading full topology…'
|
|
42210
|
+
: 'Loading compact history…',
|
|
42211
|
+
});
|
|
42212
|
+
void (async () => {
|
|
42213
|
+
// Include stash commits as graph roots so the toggle's re-fetch
|
|
42214
|
+
// sees the same rich graph the boot loader assembles. Without
|
|
42215
|
+
// this, flipping `\` into full mode and back loses the stash
|
|
42216
|
+
// anchors that loadRowsWithStashes seeded on boot.
|
|
42217
|
+
const stashHashes = await getStashCommitHashes(git).catch(() => []);
|
|
42218
|
+
const nextRows = await safe(getLogRows(git, merged, {
|
|
42219
|
+
limit: LOG_INTERACTIVE_DEFAULT_LIMIT,
|
|
42220
|
+
extraRefs: stashHashes,
|
|
42221
|
+
}));
|
|
42222
|
+
if (!mountedRef.current || toggleGraphRequestRef.current !== requestId) {
|
|
42223
|
+
return;
|
|
42224
|
+
}
|
|
42225
|
+
if (!nextRows) {
|
|
42226
|
+
dispatch({ type: 'setStatus', value: 'Failed to refetch graph rows', kind: 'error' });
|
|
42227
|
+
return;
|
|
42228
|
+
}
|
|
42229
|
+
dispatch({ type: 'replaceRows', rows: nextRows });
|
|
42230
|
+
const matched = getCommitRows(nextRows).length;
|
|
42231
|
+
setHasMoreCommits(matched >= LOG_INTERACTIVE_DEFAULT_LIMIT);
|
|
42232
|
+
dispatch({
|
|
42233
|
+
type: 'setStatus',
|
|
42234
|
+
value: state.fullGraph
|
|
42235
|
+
? `Showing ${matched} commits across all branches`
|
|
42236
|
+
: `Showing ${matched} commits (compact)`,
|
|
42237
|
+
kind: 'success',
|
|
42238
|
+
});
|
|
42239
|
+
})();
|
|
42240
|
+
}, [dispatch, git, logArgv, state.fullGraph]);
|
|
42241
|
+
const commitDiffHunkOffsets = React.useMemo(() => (filePreview?.hunks
|
|
42242
|
+
.map((line, index) => (line.startsWith('@@') ? index : -1))
|
|
42243
|
+
.filter((index) => index >= 0)), [filePreview]);
|
|
42244
|
+
const worktreeDirty = Boolean(context.worktree &&
|
|
42245
|
+
(context.worktree.stagedCount + context.worktree.unstagedCount + context.worktree.untrackedCount) > 0);
|
|
42246
|
+
useInput((inputValue, key) => {
|
|
42247
|
+
// First-launch onboarding (P1.3): any keystroke dismisses the overlay
|
|
42248
|
+
// and writes the seen-marker. Swallow the keystroke so the same key
|
|
42249
|
+
// doesn't also trigger normal input dispatch.
|
|
42250
|
+
if (showOnboarding) {
|
|
42251
|
+
setShowOnboarding(false);
|
|
42252
|
+
markOnboardingSeen();
|
|
42253
|
+
return;
|
|
42254
|
+
}
|
|
42255
|
+
// P4.5: navigation in branches/tags/stash uses the FILTERED list
|
|
42256
|
+
// length when a filter is active so j/k stay live instead of getting
|
|
42257
|
+
// stuck against a full-list count that no longer matches what's on
|
|
42258
|
+
// screen. The filtered lists are memoized at LogInkApp scope (#808
|
|
42259
|
+
// perf pass) — reading them here is O(1) instead of O(branches +
|
|
42260
|
+
// tags + stashes + worktrees) per keystroke.
|
|
42261
|
+
const branchVisibleCount = filteredBranchList.length;
|
|
42262
|
+
const branchSelectedShortName = filteredBranchList[Math.min(state.selectedBranchIndex, Math.max(0, filteredBranchList.length - 1))]?.shortName;
|
|
42263
|
+
const tagVisibleCount = filteredTagList.length;
|
|
42264
|
+
const tagSelectedName = filteredTagList[Math.min(state.selectedTagIndex, Math.max(0, filteredTagList.length - 1))]?.name;
|
|
42265
|
+
const stashVisibleCount = filteredStashList.length;
|
|
42266
|
+
const stashSelectedRef = filteredStashList[Math.min(state.selectedStashIndex, Math.max(0, filteredStashList.length - 1))]?.ref;
|
|
42267
|
+
const reflogVisibleCount = filteredReflogList.length;
|
|
42268
|
+
const reflogSelectedHash = filteredReflogList[Math.min(state.selectedReflogIndex, Math.max(0, filteredReflogList.length - 1))]?.hash;
|
|
42269
|
+
const submoduleVisibleCount = filteredSubmoduleList.length;
|
|
42270
|
+
filteredSubmoduleList[Math.min(state.selectedSubmoduleIndex, Math.max(0, filteredSubmoduleList.length - 1))]?.path;
|
|
42271
|
+
const issueVisibleCount = filteredIssueList.length;
|
|
42272
|
+
const issueSelectedUrl = filteredIssueList[Math.min(state.selectedIssueIndex, Math.max(0, filteredIssueList.length - 1))]?.url;
|
|
42273
|
+
const pullRequestTriageVisibleCount = filteredPullRequestTriageList.length;
|
|
42274
|
+
const pullRequestTriageSelectedUrl = filteredPullRequestTriageList[Math.min(state.selectedPullRequestTriageIndex, Math.max(0, filteredPullRequestTriageList.length - 1))]?.url;
|
|
42275
|
+
const worktreeVisibleCount = filteredWorktreeList.length;
|
|
42276
|
+
// When the diff view is showing a stash patch, swap the previewLineCount
|
|
42277
|
+
// to the stash diff length so the existing pageDetailPreview path
|
|
42278
|
+
// (j/k, PgUp/PgDn) scrolls through it without a parallel pipeline.
|
|
42279
|
+
const diffPreviewLineCount = state.diffSource === 'stash'
|
|
42280
|
+
? stashDiffLines?.length
|
|
42281
|
+
: filePreview?.hunks.length;
|
|
42282
|
+
// Per-file segmentation for stash diffs reads the LogInkApp-scoped
|
|
42283
|
+
// memo so navigation keys + the input-context derivation share a
|
|
42284
|
+
// single parse pass per stash patch instead of re-walking the
|
|
42285
|
+
// entire patch text on every keystroke.
|
|
42286
|
+
const stashDiffFiles = state.diffSource === 'stash' ? stashDiffParsedFiles : [];
|
|
42287
|
+
const stashDiffFileOffsets = stashDiffFiles.map((file) => file.startLine);
|
|
42288
|
+
const stashDiffSelectedPath = state.diffSource === 'stash'
|
|
42289
|
+
? findStashFileForOffset(stashDiffFiles, state.diffPreviewOffset)?.path
|
|
42290
|
+
: undefined;
|
|
42291
|
+
getLogInkInputEvents(state, inputValue, key, {
|
|
42292
|
+
detailFileCount: detail?.files.length,
|
|
42293
|
+
previewLineCount: diffPreviewLineCount,
|
|
42294
|
+
worktreeDiffLineCount: worktreeDiff?.lines.length,
|
|
42295
|
+
worktreeFileCount: visibleWorktreeFilesGrouped.length,
|
|
42296
|
+
worktreeHunkOffsets: worktreeDiff?.hunkOffsets,
|
|
42297
|
+
commitDiffHunkOffsets,
|
|
42298
|
+
branchCount: branchVisibleCount,
|
|
42299
|
+
branchSelectedShortName,
|
|
42300
|
+
tagCount: tagVisibleCount,
|
|
42301
|
+
tagSelectedName,
|
|
42302
|
+
stashCount: stashVisibleCount,
|
|
42303
|
+
reflogCount: reflogVisibleCount,
|
|
42304
|
+
reflogSelectedHash,
|
|
42305
|
+
submoduleCount: submoduleVisibleCount,
|
|
42306
|
+
issueCount: issueVisibleCount,
|
|
42307
|
+
issueSelectedUrl,
|
|
42308
|
+
pullRequestTriageCount: pullRequestTriageVisibleCount,
|
|
42309
|
+
pullRequestTriageSelectedUrl,
|
|
42310
|
+
stashSelectedRef,
|
|
42311
|
+
stashDiffFileOffsets: stashDiffFileOffsets.length ? stashDiffFileOffsets : undefined,
|
|
42312
|
+
stashDiffSelectedPath,
|
|
42313
|
+
worktreeListCount: worktreeVisibleCount,
|
|
42314
|
+
worktreeSelectedPath: visibleWorktreeFilesGrouped[state.selectedWorktreeFileIndex]?.path,
|
|
42315
|
+
statusGroups: visibleWorktreeGroups.map((group) => ({
|
|
42316
|
+
state: group.state,
|
|
42317
|
+
count: group.files.length,
|
|
42318
|
+
startIndex: group.startIndex,
|
|
42319
|
+
})),
|
|
42320
|
+
inspectorActionCount: getInspectorActionsForState(state).length,
|
|
42321
|
+
commitDiffSelectedPath: state.diffSource === 'commit'
|
|
42322
|
+
? selectedDetailFile?.path
|
|
42323
|
+
: undefined,
|
|
42324
|
+
commitDiffSelectedSha: state.diffSource === 'commit'
|
|
42325
|
+
? selected?.hash
|
|
42326
|
+
: undefined,
|
|
42327
|
+
// #931 PR 3b — Submodule drill-in target for the cursored file
|
|
42328
|
+
// in a commit diff. Resolved per-render so the Enter handler in
|
|
42329
|
+
// `inkInput.ts` doesn't have to re-walk the submodule overview;
|
|
42330
|
+
// undefined whenever the cursored file isn't a registered
|
|
42331
|
+
// submodule (or the overview / repo root haven't loaded yet).
|
|
42332
|
+
commitDiffSubmoduleDrillIn: state.diffSource === 'commit' && selectedDetailFile
|
|
42333
|
+
? resolveCommitDiffDrillInTarget({
|
|
42334
|
+
selectedFile: {
|
|
42335
|
+
path: selectedDetailFile.path,
|
|
42336
|
+
submoduleChange: filePreview?.path === selectedDetailFile.path
|
|
42337
|
+
? filePreview.submoduleChange
|
|
42338
|
+
: undefined,
|
|
42339
|
+
},
|
|
42340
|
+
submodules: context.submodules,
|
|
42341
|
+
activeRepoRoot,
|
|
42342
|
+
})
|
|
42343
|
+
: undefined,
|
|
42344
|
+
// #931 PR 4 / #932 — Submodule drill-in target for the cursored
|
|
42345
|
+
// row in the dedicated submodules view. Resolved per-render so
|
|
42346
|
+
// the Enter handler in `inkInput.ts` doesn't have to re-walk the
|
|
42347
|
+
// submodule overview. Gated on `activeView === 'submodules'` so
|
|
42348
|
+
// a stale resolution from a different view can't accidentally
|
|
42349
|
+
// fire — the runtime only ever populates it when the user is
|
|
42350
|
+
// actually on the view.
|
|
42351
|
+
submoduleViewDrillIn: state.activeView === 'submodules'
|
|
42352
|
+
? resolveSubmoduleViewDrillInTarget({
|
|
42353
|
+
selectedIndex: state.selectedSubmoduleIndex,
|
|
42354
|
+
submodules: context.submodules,
|
|
42355
|
+
activeRepoRoot,
|
|
42356
|
+
})
|
|
42357
|
+
: undefined,
|
|
42358
|
+
worktreeDirty,
|
|
42359
|
+
conflictFileCount: context.operation?.conflictedFiles.length,
|
|
42360
|
+
conflictSelectedPath: (() => {
|
|
42361
|
+
const files = context.operation?.conflictedFiles;
|
|
42362
|
+
if (!files || files.length === 0)
|
|
42363
|
+
return undefined;
|
|
42364
|
+
const clamped = Math.min(state.selectedConflictFileIndex, files.length - 1);
|
|
42365
|
+
return files[clamped]?.path;
|
|
42366
|
+
})(),
|
|
42367
|
+
// H / gH need the actual diff text (not just hunk offsets) to
|
|
42368
|
+
// slice the cursored hunk into a `git apply` patch. Stash uses
|
|
42369
|
+
// the full `git stash show -p` output; commit-diff uses the
|
|
42370
|
+
// per-file `filePreview.hunks` array. Either way, extractDiffHunk
|
|
42371
|
+
// walks `@@` headers and synthesizes a fresh diff --git / --- /
|
|
42372
|
+
// +++ header set using the path the caller already resolved.
|
|
42373
|
+
diffLinesForHunkApply: state.diffSource === 'stash'
|
|
42374
|
+
? stashDiffLines
|
|
42375
|
+
: state.diffSource === 'commit'
|
|
42376
|
+
? filePreview?.hunks
|
|
42377
|
+
: undefined,
|
|
42378
|
+
// Line count of the changelog text, used by the changelog view's
|
|
42379
|
+
// j/k/PgUp/PgDn scroll bindings to clamp `pageChangelog` deltas.
|
|
42380
|
+
// Computed from view state rather than threaded through context
|
|
42381
|
+
// because the surface owns its own content — no external loader.
|
|
42382
|
+
changelogLineCount: state.changelogView.text?.split('\n').length,
|
|
42383
|
+
// Approximate line count for the split-plan overlay. Each group
|
|
42384
|
+
// renders as a header + (body if any) + files block + (rationale
|
|
42385
|
+
// if any) + blank separator. Used by j/k/PgUp/PgDn to clamp the
|
|
42386
|
+
// scroll offset. The exact render math lives in the overlay
|
|
42387
|
+
// module — this is a close-enough heuristic for clamping.
|
|
42388
|
+
// #879 item 3 — short sha of the bisect terminator (if any).
|
|
42389
|
+
// Gates `y`/`Y` yank on the completion panel and lets the
|
|
42390
|
+
// runtime resolve the value without re-parsing the log.
|
|
42391
|
+
bisectCompletionSha: context.bisect?.active
|
|
42392
|
+
? getBisectCompletion(context.bisect.log)?.sha
|
|
42393
|
+
: undefined,
|
|
42394
|
+
// #879 item 4 — disambiguates the bisect view's `s` keystroke
|
|
42395
|
+
// (skip current candidate vs. start the wizard).
|
|
42396
|
+
bisectActive: Boolean(context.bisect?.active),
|
|
42397
|
+
splitPlanLineCount: state.splitPlan?.plan
|
|
42398
|
+
? state.splitPlan.plan.groups.reduce((sum, group) => {
|
|
42399
|
+
let lines = 2; // title + separator
|
|
42400
|
+
if (group.body)
|
|
42401
|
+
lines += group.body.split('\n').length + 1;
|
|
42402
|
+
if (group.rationale)
|
|
42403
|
+
lines += 2;
|
|
42404
|
+
lines += (group.files?.length || 0) + 1;
|
|
42405
|
+
const hunkCount = group.hunks?.length || 0;
|
|
42406
|
+
if (hunkCount > 0)
|
|
42407
|
+
lines += hunkCount + 1;
|
|
42408
|
+
return sum + lines;
|
|
42409
|
+
}, 0)
|
|
42410
|
+
: undefined,
|
|
42411
|
+
}).forEach((event) => {
|
|
42412
|
+
if (event.type === 'exit') {
|
|
42413
|
+
exit();
|
|
42414
|
+
}
|
|
42415
|
+
else if (event.type === 'refreshContext') {
|
|
42416
|
+
void refreshContext();
|
|
42417
|
+
}
|
|
42418
|
+
else if (event.type === 'toggleSelectedFileStage') {
|
|
42419
|
+
void toggleSelectedFileStage();
|
|
42420
|
+
}
|
|
42421
|
+
else if (event.type === 'toggleSelectedHunkStage') {
|
|
42422
|
+
void toggleSelectedHunkStage();
|
|
42423
|
+
}
|
|
42424
|
+
else if (event.type === 'revertSelectedFile') {
|
|
42425
|
+
void revertSelectedFile();
|
|
42426
|
+
}
|
|
42427
|
+
else if (event.type === 'revertSelectedHunk') {
|
|
42428
|
+
void revertSelectedHunk();
|
|
42429
|
+
}
|
|
42430
|
+
else if (event.type === 'createManualCommit') {
|
|
42431
|
+
void createCommitFromCompose();
|
|
42432
|
+
}
|
|
42433
|
+
else if (event.type === 'runAiCommitDraft') {
|
|
42434
|
+
void runAiCommitDraft();
|
|
42435
|
+
}
|
|
42436
|
+
else if (event.type === 'cancelAiCommitDraft') {
|
|
42437
|
+
cancelAiCommitDraft();
|
|
42438
|
+
}
|
|
42439
|
+
else if (event.type === 'startCreatePullRequest') {
|
|
42440
|
+
void startCreatePullRequest();
|
|
42441
|
+
}
|
|
42442
|
+
else if (event.type === 'cancelPullRequestBodyDraft') {
|
|
42443
|
+
cancelPullRequestBodyDraft();
|
|
42444
|
+
}
|
|
42445
|
+
else if (event.type === 'startChangelogView') {
|
|
42446
|
+
void startChangelogView();
|
|
42447
|
+
}
|
|
42448
|
+
else if (event.type === 'regenerateChangelog') {
|
|
42449
|
+
regenerateChangelog();
|
|
42450
|
+
}
|
|
42451
|
+
else if (event.type === 'yankChangelog') {
|
|
42452
|
+
yankChangelog();
|
|
42453
|
+
}
|
|
42454
|
+
else if (event.type === 'openChangelogInEditor') {
|
|
42455
|
+
openChangelogInEditor();
|
|
42456
|
+
}
|
|
42457
|
+
else if (event.type === 'openComposeInEditor') {
|
|
42458
|
+
openComposeInEditor();
|
|
42459
|
+
}
|
|
42460
|
+
else if (event.type === 'startCommitSplit') {
|
|
42461
|
+
void startCommitSplit();
|
|
42462
|
+
}
|
|
42463
|
+
else if (event.type === 'applyCommitSplit') {
|
|
42464
|
+
void applyCommitSplit();
|
|
42465
|
+
}
|
|
42466
|
+
else if (event.type === 'cancelCommitSplit') {
|
|
42467
|
+
cancelCommitSplit();
|
|
42468
|
+
}
|
|
42469
|
+
else if (event.type === 'yankText') {
|
|
42470
|
+
void yankText(event.value, event.label);
|
|
42471
|
+
}
|
|
42472
|
+
else if (event.type === 'runWorkflowAction') {
|
|
42473
|
+
void runWorkflowAction(event.id, event.payload);
|
|
42474
|
+
}
|
|
42475
|
+
else if (event.type === 'openFileInEditor') {
|
|
42476
|
+
openInEditor(event.path);
|
|
42477
|
+
}
|
|
42478
|
+
else if (event.type === 'yankFromActiveView') {
|
|
42479
|
+
void yankFromActiveView(event.short);
|
|
42480
|
+
}
|
|
42481
|
+
else if (event.type === 'applyThemePreset') {
|
|
42482
|
+
// Apply for the session immediately, and best-effort persist to the
|
|
42483
|
+
// global config so it sticks across launches. The picker has already
|
|
42484
|
+
// dispatched `toggleThemePicker` (closing it), which clears the
|
|
42485
|
+
// preview via the sync effect below — the session preset takes over.
|
|
42486
|
+
const preset = event.preset;
|
|
42487
|
+
setThemeSessionPreset(preset);
|
|
42488
|
+
saveThemePreset(preset);
|
|
42489
|
+
}
|
|
42490
|
+
else {
|
|
42491
|
+
// P4.5: enrich filter-mutating actions with a precomputed
|
|
42492
|
+
// selection snapshot so the reducer can preserve the cursor on
|
|
42493
|
+
// the same item when it's still in the filtered result, only
|
|
42494
|
+
// snapping to result[0] when the previously selected item drops
|
|
42495
|
+
// out. The snapshot lives in the action so the reducer never
|
|
42496
|
+
// needs context items.
|
|
42497
|
+
const enriched = enrichFilterActionWithRectification(event.action, state, context);
|
|
42498
|
+
dispatch(enriched);
|
|
42499
|
+
}
|
|
42500
|
+
});
|
|
42501
|
+
});
|
|
42502
|
+
// Layout depends on focus (sidebar grows when focused), so it's
|
|
42503
|
+
// computed here — after state is in scope but before the render path.
|
|
42504
|
+
const layout = getLogInkLayout({
|
|
42505
|
+
columns: windowSize.columns || process.stdout.columns || LOG_INK_DEFAULT_COLUMNS,
|
|
42506
|
+
rows: windowSize.rows || process.stdout.rows || LOG_INK_DEFAULT_ROWS,
|
|
42507
|
+
sidebarFocused: state.focus === 'sidebar',
|
|
42508
|
+
inspectorFocused: state.focus === 'detail',
|
|
42509
|
+
helpOverlayActive: state.showHelp,
|
|
42510
|
+
});
|
|
42511
|
+
if (layout.tooSmall) {
|
|
42512
|
+
return h(Box, {
|
|
42513
|
+
flexDirection: 'column',
|
|
42514
|
+
height: layout.rows,
|
|
42515
|
+
paddingX: 1,
|
|
42516
|
+
paddingY: 1,
|
|
42517
|
+
}, h(Text, { bold: true }, appLabel), h(Text, undefined, `Terminal too small: ${layout.columns}x${layout.rows}`), h(Text, { dimColor: true }, `Minimum size is ${LOG_INK_MIN_COLUMNS}x${LOG_INK_MIN_ROWS}.`), h(Text, { dimColor: true }, 'Resize the terminal or run plain coco log.'));
|
|
42518
|
+
}
|
|
42519
|
+
// First-launch onboarding overlay (P1.3) replaces the entire UI for
|
|
42520
|
+
// one render — any keystroke dismisses it and persists the seen-marker.
|
|
42521
|
+
if (showOnboarding) {
|
|
42522
|
+
return renderOnboardingOverlay(h, { Box, Text }, layout.rows, layout.columns, theme, appLabel);
|
|
42523
|
+
}
|
|
42524
|
+
return h(Box, { flexDirection: 'column', height: layout.rows }, renderHeader$1(h, { Box, Text }, state, context, contextStatus, layout.columns, theme, appLabel), h(Box, { flexDirection: 'row', height: layout.bodyRows }, renderSidebar$1(h, { Box, Text }, state, context, contextStatus, layout.sidebarWidth, layout.bodyRows, theme, layout.sidebarRailed), renderMainPanel(h, { Box, Text }, state, context, contextStatus, worktreeDiff, worktreeDiffLoading, worktreeHunks, worktreeHunksLoading, filePreview, filePreviewLoading, commitDiffHunkOffsets, selectedDetailFile, stashDiffLines, stashDiffLoading, compareDiffLines, compareDiffLoading, bisectCandidateDetail, bisectCandidateLoading, layout.bodyRows, layout.mainPanelWidth, theme, hasMoreCommits, loadingMoreCommits, spinnerFrame, layout.density, layout.historyRowMode, Boolean(dateBucketingEnabled)), renderDetailPanel(h, { Box, Text }, state, context, contextStatus, detail, detailLoading, filePreview, filePreviewLoading, layout.detailWidth, layout.inspectorTabbed, theme, layout.inspectorRailed, layout.bodyRows)), renderFooter$1(h, { Box, Text }, state, context, theme, idleTip, spinnerFrame));
|
|
41799
42525
|
}
|
|
41800
42526
|
|
|
41801
42527
|
/**
|
|
@@ -41973,6 +42699,7 @@ async function startInkInteractiveLog(git, rows, streams = {}, options = {}) {
|
|
|
41973
42699
|
React,
|
|
41974
42700
|
rows,
|
|
41975
42701
|
theme: createLogInkTheme(options.theme),
|
|
42702
|
+
themeConfig: options.theme,
|
|
41976
42703
|
resumeRef,
|
|
41977
42704
|
});
|
|
41978
42705
|
const instance = ink.render(app, getLogInkRenderOptions({ input, output, error }));
|
|
@@ -43418,7 +44145,7 @@ const options$1 = {
|
|
|
43418
44145
|
},
|
|
43419
44146
|
theme: {
|
|
43420
44147
|
description: 'TUI theme preset',
|
|
43421
|
-
choices: ['default', 'monochrome', 'catppuccin', 'gruvbox', 'dracula', 'nord', 'solarized-dark', 'tokyo-night', 'one-dark', 'rose-pine', 'kanagawa', 'everforest', 'monokai', 'synthwave', 'ayu-dark', 'palenight', 'github-dark', 'horizon', 'nightfox', 'carbonfox', 'tokyonight-storm', 'catppuccin-latte', 'solarized-light', 'github-light', 'iceberg', 'material-ocean', 'moonlight', 'poimandres', 'vitesse-dark', 'vesper', 'flexoki', 'mellow'],
|
|
44148
|
+
choices: ['default', 'monochrome', 'catppuccin', 'gruvbox', 'dracula', 'nord', 'solarized-dark', 'tokyo-night', 'one-dark', 'rose-pine', 'kanagawa', 'everforest', 'monokai', 'synthwave', 'ayu-dark', 'palenight', 'github-dark', 'horizon', 'nightfox', 'carbonfox', 'tokyonight-storm', 'catppuccin-latte', 'solarized-light', 'github-light', 'iceberg', 'material-ocean', 'moonlight', 'poimandres', 'vitesse-dark', 'vesper', 'flexoki', 'mellow', 'night-owl', 'cobalt2', 'oceanic-next', 'catppuccin-macchiato', 'gruvbox-light', 'tokyo-night-day', 'one-light', 'ayu-light', 'rose-pine-dawn', 'everforest-light', 'vitesse-light', 'dayfox', 'night-owl-light', 'flexoki-light', 'material-lighter', 'papercolor-light', 'modus-operandi', 'quiet-light'],
|
|
43422
44149
|
},
|
|
43423
44150
|
};
|
|
43424
44151
|
const builder$1 = (yargs) => {
|
|
@@ -43446,7 +44173,7 @@ const options = {
|
|
|
43446
44173
|
},
|
|
43447
44174
|
theme: {
|
|
43448
44175
|
description: 'TUI theme preset',
|
|
43449
|
-
choices: ['default', 'monochrome', 'catppuccin', 'gruvbox', 'dracula', 'nord', 'solarized-dark', 'tokyo-night', 'one-dark', 'rose-pine', 'kanagawa', 'everforest', 'monokai', 'synthwave', 'ayu-dark', 'palenight', 'github-dark', 'horizon', 'nightfox', 'carbonfox', 'tokyonight-storm', 'catppuccin-latte', 'solarized-light', 'github-light', 'iceberg', 'material-ocean', 'moonlight', 'poimandres', 'vitesse-dark', 'vesper', 'flexoki', 'mellow'],
|
|
44176
|
+
choices: ['default', 'monochrome', 'catppuccin', 'gruvbox', 'dracula', 'nord', 'solarized-dark', 'tokyo-night', 'one-dark', 'rose-pine', 'kanagawa', 'everforest', 'monokai', 'synthwave', 'ayu-dark', 'palenight', 'github-dark', 'horizon', 'nightfox', 'carbonfox', 'tokyonight-storm', 'catppuccin-latte', 'solarized-light', 'github-light', 'iceberg', 'material-ocean', 'moonlight', 'poimandres', 'vitesse-dark', 'vesper', 'flexoki', 'mellow', 'night-owl', 'cobalt2', 'oceanic-next', 'catppuccin-macchiato', 'gruvbox-light', 'tokyo-night-day', 'one-light', 'ayu-light', 'rose-pine-dawn', 'everforest-light', 'vitesse-light', 'dayfox', 'night-owl-light', 'flexoki-light', 'material-lighter', 'papercolor-light', 'modus-operandi', 'quiet-light'],
|
|
43450
44177
|
},
|
|
43451
44178
|
};
|
|
43452
44179
|
const builder = (yargs) => {
|
|
@@ -44149,6 +44876,9 @@ function createWorkspaceState(init) {
|
|
|
44149
44876
|
roots: init.roots,
|
|
44150
44877
|
showHelp: false,
|
|
44151
44878
|
showOnboarding: Boolean(init.showOnboarding),
|
|
44879
|
+
showThemePicker: false,
|
|
44880
|
+
themePickerFilter: '',
|
|
44881
|
+
themePickerIndex: 0,
|
|
44152
44882
|
knownRepoPaths: init.knownRepoPaths ?? [],
|
|
44153
44883
|
pullRequestFetching: [],
|
|
44154
44884
|
};
|
|
@@ -44319,6 +45049,39 @@ function applyWorkspaceAction(state, action) {
|
|
|
44319
45049
|
case 'close-help': {
|
|
44320
45050
|
return { ...state, showHelp: false };
|
|
44321
45051
|
}
|
|
45052
|
+
case 'toggle-theme-picker': {
|
|
45053
|
+
return {
|
|
45054
|
+
...state,
|
|
45055
|
+
showThemePicker: !state.showThemePicker,
|
|
45056
|
+
showHelp: false,
|
|
45057
|
+
showOnboarding: false,
|
|
45058
|
+
themePickerFilter: '',
|
|
45059
|
+
themePickerIndex: 0,
|
|
45060
|
+
};
|
|
45061
|
+
}
|
|
45062
|
+
case 'move-theme-picker': {
|
|
45063
|
+
return {
|
|
45064
|
+
...state,
|
|
45065
|
+
themePickerIndex: clampCursor(state.themePickerIndex + action.delta, action.presetCount),
|
|
45066
|
+
};
|
|
45067
|
+
}
|
|
45068
|
+
case 'append-theme-picker-filter': {
|
|
45069
|
+
return {
|
|
45070
|
+
...state,
|
|
45071
|
+
themePickerFilter: `${state.themePickerFilter}${action.value}`,
|
|
45072
|
+
themePickerIndex: 0,
|
|
45073
|
+
};
|
|
45074
|
+
}
|
|
45075
|
+
case 'backspace-theme-picker-filter': {
|
|
45076
|
+
return {
|
|
45077
|
+
...state,
|
|
45078
|
+
themePickerFilter: state.themePickerFilter.slice(0, -1),
|
|
45079
|
+
themePickerIndex: 0,
|
|
45080
|
+
};
|
|
45081
|
+
}
|
|
45082
|
+
case 'clear-theme-picker-filter': {
|
|
45083
|
+
return { ...state, themePickerFilter: '', themePickerIndex: 0 };
|
|
45084
|
+
}
|
|
44322
45085
|
case 'dismiss-onboarding': {
|
|
44323
45086
|
return { ...state, showOnboarding: false };
|
|
44324
45087
|
}
|
|
@@ -44730,33 +45493,48 @@ function buildWorkspaceHeaderChips(state, options = { focusLabel: 'List' }) {
|
|
|
44730
45493
|
});
|
|
44731
45494
|
return chips;
|
|
44732
45495
|
}
|
|
44733
|
-
// Footer hints
|
|
44734
|
-
//
|
|
44735
|
-
//
|
|
44736
|
-
//
|
|
44737
|
-
|
|
44738
|
-
|
|
44739
|
-
|
|
44740
|
-
const
|
|
44741
|
-
const
|
|
44742
|
-
|
|
45496
|
+
// Footer hints are split into two slots — same pattern as `coco ui`:
|
|
45497
|
+
// - contextual : per-mode actions (sort, filter, refresh, add/remove)
|
|
45498
|
+
// - global : always-on essentials (help, quit) — never crowded out
|
|
45499
|
+
//
|
|
45500
|
+
// The contextual slot drops bindings users can find via the help
|
|
45501
|
+
// overlay (arrow keys, tab); the global slot is the safety net so
|
|
45502
|
+
// `? help` and `q quit` never disappear.
|
|
45503
|
+
const LIST_CONTEXTUAL = ['s sort', '/ filter', 'r/R refresh', 'a add', 'd remove'];
|
|
45504
|
+
const SIDEBAR_CONTEXTUAL = ['↑/↓ cycle tab', 'enter open'];
|
|
45505
|
+
const FILTER_CONTEXTUAL = ['type to filter', 'enter apply', 'esc cancel'];
|
|
45506
|
+
const ADD_REPO_CONTEXTUAL = ['type path', 'tab to complete', 'enter to add', 'esc to cancel'];
|
|
45507
|
+
const CONFIRM_DELETE_CONTEXTUAL = ['y confirm', 'any other key cancels'];
|
|
45508
|
+
const GLOBAL_HINTS = ['? help', 'q quit'];
|
|
45509
|
+
function contextualHintsFor(focus) {
|
|
44743
45510
|
switch (focus) {
|
|
44744
45511
|
case 'sidebar':
|
|
44745
|
-
return
|
|
45512
|
+
return SIDEBAR_CONTEXTUAL;
|
|
44746
45513
|
case 'filter':
|
|
44747
|
-
return
|
|
45514
|
+
return FILTER_CONTEXTUAL;
|
|
44748
45515
|
case 'add-repo':
|
|
44749
|
-
return
|
|
45516
|
+
return ADD_REPO_CONTEXTUAL;
|
|
44750
45517
|
case 'confirm-delete':
|
|
44751
|
-
return
|
|
45518
|
+
return CONFIRM_DELETE_CONTEXTUAL;
|
|
44752
45519
|
case 'list':
|
|
44753
45520
|
default:
|
|
44754
|
-
return
|
|
45521
|
+
return LIST_CONTEXTUAL;
|
|
44755
45522
|
}
|
|
44756
45523
|
}
|
|
44757
45524
|
function buildWorkspaceFooter(state) {
|
|
45525
|
+
const contextual = contextualHintsFor(state.focus);
|
|
45526
|
+
// Modal modes (filter / add-repo / confirm-delete) suppress the
|
|
45527
|
+
// global hints — those bindings are not reachable while a prompt
|
|
45528
|
+
// is open and showing them would be misleading.
|
|
45529
|
+
const isModal = state.focus === 'filter' ||
|
|
45530
|
+
state.focus === 'add-repo' ||
|
|
45531
|
+
state.focus === 'confirm-delete';
|
|
45532
|
+
const global = isModal ? [] : GLOBAL_HINTS;
|
|
45533
|
+
const allHints = [...contextual, ...global];
|
|
44758
45534
|
return {
|
|
44759
|
-
hint:
|
|
45535
|
+
hint: allHints.join(' · '),
|
|
45536
|
+
contextual,
|
|
45537
|
+
global,
|
|
44760
45538
|
status: state.status,
|
|
44761
45539
|
filterMode: state.focus === 'filter',
|
|
44762
45540
|
};
|
|
@@ -44766,11 +45544,26 @@ function buildWorkspaceFooter(state) {
|
|
|
44766
45544
|
* sections so users can scan by intent ("how do I navigate?" "how
|
|
44767
45545
|
* do I act?") rather than reading a flat alphabetized list.
|
|
44768
45546
|
*
|
|
45547
|
+
* Section order mirrors `coco ui`'s help convention — Essentials
|
|
45548
|
+
* first so newcomers see `?`/`esc`/`q` immediately, then move outward
|
|
45549
|
+
* to navigation, modification, and the destructive verbs last.
|
|
45550
|
+
*
|
|
44769
45551
|
* The view layer composes these into a panel with section titles,
|
|
44770
45552
|
* a leading app/title bar, and a closing hint at the bottom.
|
|
44771
45553
|
*/
|
|
44772
45554
|
function buildWorkspaceHelpSections() {
|
|
44773
45555
|
return [
|
|
45556
|
+
{
|
|
45557
|
+
title: 'Essentials',
|
|
45558
|
+
subtitle: 'The keys you reach for most often.',
|
|
45559
|
+
rows: [
|
|
45560
|
+
{ glyph: '?', keys: '?', description: 'Toggle this help overlay' },
|
|
45561
|
+
{ glyph: '⎋', keys: 'esc', description: 'Clear filter / close overlay / cancel prompt' },
|
|
45562
|
+
{ glyph: '◴', keys: 'q · ctrl+c', description: 'Quit the workspace surface' },
|
|
45563
|
+
{ glyph: '↵', keys: 'enter', description: 'Drill into the cursored repo (coco ui)' },
|
|
45564
|
+
{ glyph: '◧', keys: 'T', description: 'Theme picker — browse, live-preview & apply a color theme' },
|
|
45565
|
+
],
|
|
45566
|
+
},
|
|
44774
45567
|
{
|
|
44775
45568
|
title: 'Navigate',
|
|
44776
45569
|
subtitle: 'Move the cursor and switch focus between panels.',
|
|
@@ -44780,7 +45573,6 @@ function buildWorkspaceHelpSections() {
|
|
|
44780
45573
|
{ glyph: '←', keys: 'h', description: 'Jump focus to the sidebar' },
|
|
44781
45574
|
{ glyph: '→', keys: 'l', description: 'Jump focus to the list' },
|
|
44782
45575
|
{ glyph: '⤒', keys: 'g / G', description: 'Jump to top / bottom of the list' },
|
|
44783
|
-
{ glyph: '↵', keys: 'enter', description: 'Drill into the cursored repo (coco ui)' },
|
|
44784
45576
|
],
|
|
44785
45577
|
},
|
|
44786
45578
|
{
|
|
@@ -44800,14 +45592,6 @@ function buildWorkspaceHelpSections() {
|
|
|
44800
45592
|
{ glyph: '✕', keys: 'd', description: 'Remove the cursored repo from the known-repos store' },
|
|
44801
45593
|
],
|
|
44802
45594
|
},
|
|
44803
|
-
{
|
|
44804
|
-
title: 'General',
|
|
44805
|
-
rows: [
|
|
44806
|
-
{ glyph: '?', keys: '?', description: 'Toggle this help overlay' },
|
|
44807
|
-
{ glyph: '⎋', keys: 'esc', description: 'Clear filter / close overlay / cancel prompt' },
|
|
44808
|
-
{ glyph: '◴', keys: 'q · ctrl+c', description: 'Quit the workspace surface' },
|
|
44809
|
-
],
|
|
44810
|
-
},
|
|
44811
45595
|
];
|
|
44812
45596
|
}
|
|
44813
45597
|
function buildWorkspaceOnboarding(state) {
|
|
@@ -45254,13 +46038,19 @@ function renderFooter(deps) {
|
|
|
45254
46038
|
// height shifted by a row every time a status banner came and went,
|
|
45255
46039
|
// forcing the panel chrome to reflow.
|
|
45256
46040
|
const statusContent = model.status ?? '';
|
|
46041
|
+
const contextualText = model.contextual.join(' ');
|
|
46042
|
+
const globalText = model.global.join(' · ');
|
|
45257
46043
|
return React.createElement(Box, {
|
|
45258
46044
|
borderColor: focusBorderColor(theme, false),
|
|
45259
46045
|
borderStyle: theme.borderStyle,
|
|
45260
46046
|
paddingX: 1,
|
|
45261
46047
|
flexDirection: 'column',
|
|
45262
46048
|
height: FOOTER_HEIGHT,
|
|
45263
|
-
},
|
|
46049
|
+
},
|
|
46050
|
+
// Row 1: contextual ↔ global hints. justifyContent pushes them
|
|
46051
|
+
// to opposite edges so the eye scans each cluster as one block —
|
|
46052
|
+
// same shape as `coco ui`'s footer post-0.54.2 redesign.
|
|
46053
|
+
React.createElement(Box, { flexDirection: 'row', justifyContent: 'space-between' }, React.createElement(Text, { dimColor: true }, contextualText), React.createElement(Text, { dimColor: true }, globalText)), React.createElement(Text, {
|
|
45264
46054
|
color: model.status ? toneColor('warn', theme) : undefined,
|
|
45265
46055
|
dimColor: !model.status,
|
|
45266
46056
|
}, statusContent || ' '));
|
|
@@ -45296,6 +46086,11 @@ function renderWorkspaceApp(deps) {
|
|
|
45296
46086
|
if (deps.state.showHelp) {
|
|
45297
46087
|
return React.createElement(Box, { flexDirection: 'column', height: rootHeight }, renderHeader(deps), renderHelpOverlay(deps), renderFooter(deps));
|
|
45298
46088
|
}
|
|
46089
|
+
// Theme picker is modal too — the chrome live-previews underneath via
|
|
46090
|
+
// the reactive `deps.theme`, while the overlay replaces the body.
|
|
46091
|
+
if (deps.state.showThemePicker) {
|
|
46092
|
+
return React.createElement(Box, { flexDirection: 'column', height: rootHeight }, renderHeader(deps), renderThemePickerOverlay(React.createElement, { Box: ink.Box, Text: ink.Text }, deps.state.themePickerFilter, deps.state.themePickerIndex, bodyWidth, deps.theme, true), renderFooter(deps));
|
|
46093
|
+
}
|
|
45299
46094
|
const bodyHeight = computeBodyHeight(deps);
|
|
45300
46095
|
return React.createElement(Box, { flexDirection: 'column', height: rootHeight }, renderHeader(deps), React.createElement(Box, { flexDirection: 'row', height: bodyHeight }, renderSidebar(deps, bodyHeight), renderListBody(deps, bodyWidth - sidebarWidthFor(deps) - 2, bodyHeight)), renderOnboardingBanner(deps), renderAddRepoPrompt(deps), renderConfirmDelete(deps), renderFooter(deps));
|
|
45301
46096
|
}
|
|
@@ -45336,6 +46131,37 @@ function resolveWorkspaceInput(input, key, state) {
|
|
|
45336
46131
|
}
|
|
45337
46132
|
return { kind: 'noop' };
|
|
45338
46133
|
}
|
|
46134
|
+
// Theme picker is modal (like `coco ui`'s gC): type to filter, ↑/↓ to
|
|
46135
|
+
// move, Enter applies + persists, Esc clears the filter then closes.
|
|
46136
|
+
if (state.showThemePicker) {
|
|
46137
|
+
const filtered = filterThemePresets(state.themePickerFilter);
|
|
46138
|
+
if (key.escape) {
|
|
46139
|
+
if (state.themePickerFilter.length > 0) {
|
|
46140
|
+
return { kind: 'action', action: { type: 'clear-theme-picker-filter' } };
|
|
46141
|
+
}
|
|
46142
|
+
return { kind: 'action', action: { type: 'toggle-theme-picker' } };
|
|
46143
|
+
}
|
|
46144
|
+
if (key.return) {
|
|
46145
|
+
const selected = getThemePickerSelectionFor(state.themePickerFilter, state.themePickerIndex);
|
|
46146
|
+
if (!selected) {
|
|
46147
|
+
return { kind: 'action', action: { type: 'toggle-theme-picker' } };
|
|
46148
|
+
}
|
|
46149
|
+
return { kind: 'apply-theme', preset: selected };
|
|
46150
|
+
}
|
|
46151
|
+
if (key.upArrow || (key.ctrl && input === 'p')) {
|
|
46152
|
+
return { kind: 'action', action: { type: 'move-theme-picker', delta: -1, presetCount: filtered.length } };
|
|
46153
|
+
}
|
|
46154
|
+
if (key.downArrow || (key.ctrl && input === 'n')) {
|
|
46155
|
+
return { kind: 'action', action: { type: 'move-theme-picker', delta: 1, presetCount: filtered.length } };
|
|
46156
|
+
}
|
|
46157
|
+
if (key.backspace || key.delete) {
|
|
46158
|
+
return { kind: 'action', action: { type: 'backspace-theme-picker-filter' } };
|
|
46159
|
+
}
|
|
46160
|
+
if (input && !key.ctrl && !key.meta) {
|
|
46161
|
+
return { kind: 'action', action: { type: 'append-theme-picker-filter', value: input } };
|
|
46162
|
+
}
|
|
46163
|
+
return { kind: 'noop' };
|
|
46164
|
+
}
|
|
45339
46165
|
if (state.focus === 'filter') {
|
|
45340
46166
|
if (key.escape) {
|
|
45341
46167
|
return { kind: 'action', action: { type: 'clear-filter' } };
|
|
@@ -45453,6 +46279,12 @@ function resolveWorkspaceInput(input, key, state) {
|
|
|
45453
46279
|
if (input === '?') {
|
|
45454
46280
|
return { kind: 'action', action: { type: 'toggle-help' } };
|
|
45455
46281
|
}
|
|
46282
|
+
// `T` opens the theme picker (browse + live-preview + apply a color
|
|
46283
|
+
// theme). Single key here rather than `coco ui`'s gC chord since the
|
|
46284
|
+
// workspace keymap is flat (no g-prefix continuations).
|
|
46285
|
+
if (input === 'T') {
|
|
46286
|
+
return { kind: 'action', action: { type: 'toggle-theme-picker' } };
|
|
46287
|
+
}
|
|
45456
46288
|
return { kind: 'noop' };
|
|
45457
46289
|
}
|
|
45458
46290
|
|
|
@@ -45834,6 +46666,7 @@ async function startWorkspace(options) {
|
|
|
45834
46666
|
ink,
|
|
45835
46667
|
React,
|
|
45836
46668
|
theme,
|
|
46669
|
+
themeConfig: options.theme,
|
|
45837
46670
|
resumeRef,
|
|
45838
46671
|
});
|
|
45839
46672
|
// Override exitOnCtrlC. Ink's default ctrl+c handler reaches into
|
|
@@ -45999,6 +46832,17 @@ function WorkspaceInkApp(props) {
|
|
|
45999
46832
|
// (see effect below) so idle workspaces don't burn CPU on animation
|
|
46000
46833
|
// frames.
|
|
46001
46834
|
const [spinnerTick, setSpinnerTick] = React.useState(0);
|
|
46835
|
+
// Theme picker (`T`) — reactive theme so the chrome live-previews the
|
|
46836
|
+
// cursored theme. `themePreviewPreset` follows the picker cursor while
|
|
46837
|
+
// open; `themeSessionPreset` is the applied choice. The effective theme
|
|
46838
|
+
// rebuilds from the original config; when neither is set we use the
|
|
46839
|
+
// static `props.theme` unchanged (mirrors `coco ui`).
|
|
46840
|
+
const [themePreviewPreset, setThemePreviewPreset] = React.useState(undefined);
|
|
46841
|
+
const [themeSessionPreset, setThemeSessionPreset] = React.useState(undefined);
|
|
46842
|
+
const effectiveThemePreset = themePreviewPreset ?? themeSessionPreset;
|
|
46843
|
+
const theme = React.useMemo(() => effectiveThemePreset
|
|
46844
|
+
? createLogInkTheme({ ...props.themeConfig, preset: effectiveThemePreset })
|
|
46845
|
+
: props.theme, [effectiveThemePreset, props.themeConfig, props.theme]);
|
|
46002
46846
|
const dispatch = React.useCallback((action) => {
|
|
46003
46847
|
setState((prev) => applyWorkspaceAction(prev, action));
|
|
46004
46848
|
}, []);
|
|
@@ -46373,6 +47217,13 @@ function WorkspaceInkApp(props) {
|
|
|
46373
47217
|
case 'action':
|
|
46374
47218
|
dispatch(intent.action);
|
|
46375
47219
|
break;
|
|
47220
|
+
case 'apply-theme':
|
|
47221
|
+
// Apply for the session + persist to the global config (best-effort),
|
|
47222
|
+
// then close the picker (clearing the preview via the sync effect).
|
|
47223
|
+
setThemeSessionPreset(intent.preset);
|
|
47224
|
+
saveThemePreset(intent.preset);
|
|
47225
|
+
dispatch({ type: 'toggle-theme-picker' });
|
|
47226
|
+
break;
|
|
46376
47227
|
case 'quit':
|
|
46377
47228
|
workspaceDebug('→ exit() called from quit intent');
|
|
46378
47229
|
exitRefHolder.current.current = { kind: 'quit' };
|
|
@@ -46422,11 +47273,20 @@ function WorkspaceInkApp(props) {
|
|
|
46422
47273
|
filter: state.filter,
|
|
46423
47274
|
});
|
|
46424
47275
|
}, [props.roots, state.sortMode, state.tab, state.filter]);
|
|
47276
|
+
// Keep the live preview in sync with the preset under the picker cursor
|
|
47277
|
+
// while the overlay is open; clear it on close so the theme reverts to
|
|
47278
|
+
// the applied session preset (or the original config theme).
|
|
47279
|
+
const themePickerSelection = state.showThemePicker
|
|
47280
|
+
? getThemePickerSelectionFor(state.themePickerFilter, state.themePickerIndex)
|
|
47281
|
+
: undefined;
|
|
47282
|
+
React.useEffect(() => {
|
|
47283
|
+
setThemePreviewPreset(state.showThemePicker ? themePickerSelection : undefined);
|
|
47284
|
+
}, [state.showThemePicker, themePickerSelection]);
|
|
46425
47285
|
return renderWorkspaceApp({
|
|
46426
47286
|
React,
|
|
46427
47287
|
ink: { Box: ink.Box, Text: ink.Text },
|
|
46428
47288
|
state,
|
|
46429
|
-
theme
|
|
47289
|
+
theme,
|
|
46430
47290
|
appLabel: props.appLabel,
|
|
46431
47291
|
filterDraft,
|
|
46432
47292
|
addRepoDraft,
|
|
@@ -46437,8 +47297,20 @@ function WorkspaceInkApp(props) {
|
|
|
46437
47297
|
});
|
|
46438
47298
|
}
|
|
46439
47299
|
|
|
46440
|
-
|
|
46441
|
-
|
|
47300
|
+
/**
|
|
47301
|
+
* Resolve the directories the workspace scans for git repos, in
|
|
47302
|
+
* precedence order:
|
|
47303
|
+
* 1. `--root` CLI flag(s)
|
|
47304
|
+
* 2. `workspace.roots` from config
|
|
47305
|
+
* 3. the current working directory (`cwd`)
|
|
47306
|
+
*
|
|
47307
|
+
* Falling back to `cwd` (rather than a hardcoded `~/code`) means a bare
|
|
47308
|
+
* `coco` / `coco ws` discovers repos wherever you launched it — the
|
|
47309
|
+
* handler chdir's to honor `--repo` / `--cwd` before this runs, so `cwd`
|
|
47310
|
+
* already reflects the targeted directory. `cwd` is a parameter (not a
|
|
47311
|
+
* direct `process.cwd()` call) so the resolver stays a pure unit.
|
|
47312
|
+
*/
|
|
47313
|
+
function resolveWorkspaceRoots(argv, config, cwd = process.cwd()) {
|
|
46442
47314
|
const raw = argv.root;
|
|
46443
47315
|
if (Array.isArray(raw) && raw.length > 0) {
|
|
46444
47316
|
return raw.map((entry) => String(entry));
|
|
@@ -46449,7 +47321,7 @@ function resolveWorkspaceRoots(argv, config) {
|
|
|
46449
47321
|
if (config.workspace?.roots && config.workspace.roots.length > 0) {
|
|
46450
47322
|
return [...config.workspace.roots];
|
|
46451
47323
|
}
|
|
46452
|
-
return [
|
|
47324
|
+
return [cwd];
|
|
46453
47325
|
}
|
|
46454
47326
|
function resolveWorkspaceKnownRepos(config) {
|
|
46455
47327
|
return config.workspace?.knownRepos ? [...config.workspace.knownRepos] : [];
|
|
@@ -46563,6 +47435,148 @@ var workspace = {
|
|
|
46563
47435
|
options,
|
|
46564
47436
|
};
|
|
46565
47437
|
|
|
47438
|
+
/**
|
|
47439
|
+
* Default-command router for `coco` invoked with no positional
|
|
47440
|
+
* arguments. Decides where to send the user based on the state of
|
|
47441
|
+
* their machine:
|
|
47442
|
+
*
|
|
47443
|
+
* ┌─────────────────────────┬─────────────────────┬──────────────┐
|
|
47444
|
+
* │ Config present? │ In a git repo? │ Action │
|
|
47445
|
+
* ├─────────────────────────┼─────────────────────┼──────────────┤
|
|
47446
|
+
* │ No (default-only) │ — │ run `init` │
|
|
47447
|
+
* │ Yes │ Yes (worktree) │ run `ui` │
|
|
47448
|
+
* │ Yes │ No │ run `ws` │
|
|
47449
|
+
* └─────────────────────────┴─────────────────────┴──────────────┘
|
|
47450
|
+
*
|
|
47451
|
+
* The pre-existing default — fall through to `commit` — was hostile
|
|
47452
|
+
* to first-time users: a fresh install with no config landed
|
|
47453
|
+
* straight in the API-key error path, with no hint that `coco init`
|
|
47454
|
+
* was the right next step. Routing fresh installs to `init` and
|
|
47455
|
+
* configured users to the workstation/UI matches what every other
|
|
47456
|
+
* git-aware CLI does (lazygit, tig, gitui all open their TUI on bare
|
|
47457
|
+
* invocation).
|
|
47458
|
+
*
|
|
47459
|
+
* `coco commit` keeps its dedicated subcommand entry so existing
|
|
47460
|
+
* scripts (`git aliases`, hook integrations, CI jobs) that call
|
|
47461
|
+
* `coco commit` continue to work unchanged.
|
|
47462
|
+
*
|
|
47463
|
+
* The router is a thin shim — it forwards to the existing handlers
|
|
47464
|
+
* via their public exports rather than re-implementing the logic.
|
|
47465
|
+
*/
|
|
47466
|
+
/**
|
|
47467
|
+
* Pure decision function — given probed signals (whether config
|
|
47468
|
+
* exists, whether the current directory is a git repo, whether the
|
|
47469
|
+
* user opted into legacy commit-by-default), decides which command
|
|
47470
|
+
* to invoke. Kept pure so unit tests can cover every quadrant of
|
|
47471
|
+
* the router table without spawning processes.
|
|
47472
|
+
*
|
|
47473
|
+
* "Config exists" is defined as: the loader detected at least one
|
|
47474
|
+
* source beyond `default` — i.e., the user has either a project
|
|
47475
|
+
* config, a git config `[coco]` section, an env var, or an XDG
|
|
47476
|
+
* config. A pure-defaults run is treated as "never been configured"
|
|
47477
|
+
* because `coco init` is the only way to populate any of those
|
|
47478
|
+
* sources.
|
|
47479
|
+
*/
|
|
47480
|
+
function decideDefaultRoute(input) {
|
|
47481
|
+
if (input.envOverride === 'commit' || input.explicitCommit) {
|
|
47482
|
+
return {
|
|
47483
|
+
kind: 'commit',
|
|
47484
|
+
reason: input.envOverride === 'commit' ? 'env-override' : 'explicit-flag',
|
|
47485
|
+
};
|
|
47486
|
+
}
|
|
47487
|
+
if (!input.hasConfigSource) {
|
|
47488
|
+
return { kind: 'init', reason: 'no-config' };
|
|
47489
|
+
}
|
|
47490
|
+
if (input.isGitRepo) {
|
|
47491
|
+
return { kind: 'ui', reason: 'config-and-repo' };
|
|
47492
|
+
}
|
|
47493
|
+
return { kind: 'workspace', reason: 'config-no-repo' };
|
|
47494
|
+
}
|
|
47495
|
+
/**
|
|
47496
|
+
* Probe whether the cwd (after `--repo` is honored) is inside a git
|
|
47497
|
+
* worktree. Tolerant of every error class — a thrown simple-git
|
|
47498
|
+
* call should never block the router; it should fall back to
|
|
47499
|
+
* "not a repo" so the user lands somewhere sensible (workspace
|
|
47500
|
+
* surface) rather than crashing on an empty machine.
|
|
47501
|
+
*/
|
|
47502
|
+
async function probeIsGitRepo() {
|
|
47503
|
+
try {
|
|
47504
|
+
// Lazy-import simple-git so the cold-start path stays fast for
|
|
47505
|
+
// users running `coco --help` / `coco doctor` etc.
|
|
47506
|
+
const { default: simpleGit } = await import('simple-git');
|
|
47507
|
+
const git = simpleGit();
|
|
47508
|
+
return await git.checkIsRepo();
|
|
47509
|
+
}
|
|
47510
|
+
catch {
|
|
47511
|
+
return false;
|
|
47512
|
+
}
|
|
47513
|
+
}
|
|
47514
|
+
/**
|
|
47515
|
+
* Build a synthetic argv for one of the targeted handlers. Each
|
|
47516
|
+
* handler reads its own typed argv contract (`CommitArgv`,
|
|
47517
|
+
* `InitArgv`, `UiArgv`, `WorkspaceArgv`) so we can't just spread the
|
|
47518
|
+
* raw default argv — we have to project the shared fields and let
|
|
47519
|
+
* the handler fill in command-specific defaults.
|
|
47520
|
+
*/
|
|
47521
|
+
function buildSyntheticArgv(argv) {
|
|
47522
|
+
return {
|
|
47523
|
+
_: ['$0'],
|
|
47524
|
+
$0: argv.$0,
|
|
47525
|
+
repo: argv.repo,
|
|
47526
|
+
cwd: argv.cwd,
|
|
47527
|
+
verbose: argv.verbose ?? false,
|
|
47528
|
+
interactive: true,
|
|
47529
|
+
version: false,
|
|
47530
|
+
help: false,
|
|
47531
|
+
};
|
|
47532
|
+
}
|
|
47533
|
+
/**
|
|
47534
|
+
* Default-command handler installed under yargs's `$0` slot. Probes
|
|
47535
|
+
* the environment, computes the right route, and forwards to the
|
|
47536
|
+
* matching command handler. Falls through to commit if any
|
|
47537
|
+
* unexpected error blocks routing — preserves backwards-compat
|
|
47538
|
+
* for users on weird setups while still giving newcomers the
|
|
47539
|
+
* onboarding path they deserve.
|
|
47540
|
+
*/
|
|
47541
|
+
const defaultRouteHandler = async (argv, logger) => {
|
|
47542
|
+
// The `--repo` flag has to land before any probe runs — otherwise
|
|
47543
|
+
// we'd sniff the launcher's cwd instead of the targeted repo.
|
|
47544
|
+
applyRepoCwd(argv);
|
|
47545
|
+
// Trigger a config load so `getConfigSources()` returns the active
|
|
47546
|
+
// source list. We discard the config object — the decision only
|
|
47547
|
+
// cares about which sources contributed.
|
|
47548
|
+
void loadConfig(argv);
|
|
47549
|
+
const sources = getConfigSources();
|
|
47550
|
+
const hasConfigSource = sources.some((source) => source.source !== 'default');
|
|
47551
|
+
const isGitRepo = await probeIsGitRepo();
|
|
47552
|
+
const decision = decideDefaultRoute({
|
|
47553
|
+
hasConfigSource,
|
|
47554
|
+
isGitRepo,
|
|
47555
|
+
explicitCommit: Boolean(argv.commit),
|
|
47556
|
+
envOverride: process.env.COCO_DEFAULT,
|
|
47557
|
+
});
|
|
47558
|
+
switch (decision.kind) {
|
|
47559
|
+
case 'init':
|
|
47560
|
+
// Friendly hint before the wizard kicks in — sets expectations
|
|
47561
|
+
// that the user is being walked through setup, not silently
|
|
47562
|
+
// routed to a different command.
|
|
47563
|
+
logger.log('No coco config detected — running `coco init` to set up your provider + key.', { color: 'cyan' });
|
|
47564
|
+
logger.log('');
|
|
47565
|
+
await handler$7(buildSyntheticArgv(argv), logger);
|
|
47566
|
+
return;
|
|
47567
|
+
case 'ui':
|
|
47568
|
+
await handler$5(buildSyntheticArgv(argv));
|
|
47569
|
+
return;
|
|
47570
|
+
case 'workspace':
|
|
47571
|
+
await handler(buildSyntheticArgv(argv));
|
|
47572
|
+
return;
|
|
47573
|
+
case 'commit':
|
|
47574
|
+
default:
|
|
47575
|
+
await handler$9(buildSyntheticArgv(argv), logger);
|
|
47576
|
+
return;
|
|
47577
|
+
}
|
|
47578
|
+
};
|
|
47579
|
+
|
|
46566
47580
|
var types = /*#__PURE__*/Object.freeze({
|
|
46567
47581
|
__proto__: null
|
|
46568
47582
|
});
|
|
@@ -46581,7 +47595,37 @@ y.option('repo', {
|
|
|
46581
47595
|
description: 'Target a specific repository directory instead of the current working directory.',
|
|
46582
47596
|
global: true,
|
|
46583
47597
|
});
|
|
46584
|
-
|
|
47598
|
+
// Global `--verbose` (alias `-v`) — every subcommand inherits it.
|
|
47599
|
+
// Flips `argv.verbose: true` so `commandExecutor` and `Logger` print
|
|
47600
|
+
// stack traces / debug spans. Previously only settable via the
|
|
47601
|
+
// `COCO_VERBOSE=true` env var or `coco.verbose` git/json config —
|
|
47602
|
+
// `BaseArgvOptions.verbose` was typed but never declared as a yargs
|
|
47603
|
+
// option, so passing `--verbose` from the CLI was a silent no-op.
|
|
47604
|
+
y.option('verbose', {
|
|
47605
|
+
type: 'boolean',
|
|
47606
|
+
alias: 'v',
|
|
47607
|
+
description: 'Print verbose diagnostic output (stack traces on errors, debug spans).',
|
|
47608
|
+
default: false,
|
|
47609
|
+
global: true,
|
|
47610
|
+
});
|
|
47611
|
+
// `$0` (no positional args) routes through the smart default router
|
|
47612
|
+
// rather than aliasing directly to `coco commit`. The router probes
|
|
47613
|
+
// the user's environment (config presence, git-repo presence) and
|
|
47614
|
+
// forwards to `init` / `ui` / `workspace` / `commit` based on which
|
|
47615
|
+
// of those is most likely to be helpful. Mirrors what other modern
|
|
47616
|
+
// git-aware CLIs do (lazygit / tig / gitui) — fresh installs land in
|
|
47617
|
+
// a setup wizard, configured users land in the TUI, scripts that
|
|
47618
|
+
// rely on `coco commit` keep their dedicated subcommand entry.
|
|
47619
|
+
y.command('$0', 'Smart entry point — routes to init / ui / workspace / commit based on your environment.', (yargs) => yargs.option('commit', {
|
|
47620
|
+
type: 'boolean',
|
|
47621
|
+
description: 'Force the legacy default — run `coco commit` regardless of routing.',
|
|
47622
|
+
default: false,
|
|
47623
|
+
}),
|
|
47624
|
+
// `commandExecutor` wraps every command with config loading, error
|
|
47625
|
+
// formatting, and exit-code handling. The router is a regular
|
|
47626
|
+
// command so it lights up the same plumbing for free.
|
|
47627
|
+
commandExecutor(defaultRouteHandler));
|
|
47628
|
+
y.command(commit.command, commit.desc, commit.builder, commit.handler);
|
|
46585
47629
|
y.command(changelog.command, changelog.desc, changelog.builder, changelog.handler);
|
|
46586
47630
|
y.command(recap.command, recap.desc, recap.builder, recap.handler);
|
|
46587
47631
|
y.command(review.command, review.desc, review.builder, review.handler);
|