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.esm.mjs
CHANGED
|
@@ -22,7 +22,7 @@ import ora from 'ora';
|
|
|
22
22
|
import now from 'performance-now';
|
|
23
23
|
import prettyMilliseconds from 'pretty-ms';
|
|
24
24
|
import * as fs$1 from 'node:fs';
|
|
25
|
-
import { existsSync,
|
|
25
|
+
import { existsSync, unlinkSync, statSync, mkdirSync, writeFileSync, renameSync, mkdtempSync, rmSync, readFileSync as readFileSync$1 } from 'node:fs';
|
|
26
26
|
import * as os$1 from 'node:os';
|
|
27
27
|
import { platform, homedir, tmpdir as tmpdir$1 } from 'node:os';
|
|
28
28
|
import * as path$1 from 'node:path';
|
|
@@ -61,7 +61,7 @@ import { pathToFileURL } from 'url';
|
|
|
61
61
|
/**
|
|
62
62
|
* Current build version from package.json
|
|
63
63
|
*/
|
|
64
|
-
const BUILD_VERSION = "0.
|
|
64
|
+
const BUILD_VERSION = "0.58.0";
|
|
65
65
|
|
|
66
66
|
const isInteractive = (config) => {
|
|
67
67
|
return config?.mode === 'interactive' || !!config?.interactive;
|
|
@@ -1160,10 +1160,7 @@ const schema$1 = {
|
|
|
1160
1160
|
"items": {
|
|
1161
1161
|
"type": "string"
|
|
1162
1162
|
},
|
|
1163
|
-
"description": "Directories to scan for git repositories. Each entry may use a `~` prefix; resolved against the user's home directory.
|
|
1164
|
-
"default": [
|
|
1165
|
-
"~/code"
|
|
1166
|
-
]
|
|
1163
|
+
"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.)"
|
|
1167
1164
|
},
|
|
1168
1165
|
"knownRepos": {
|
|
1169
1166
|
"type": "array",
|
|
@@ -2139,6 +2136,10 @@ const schema$1 = {
|
|
|
2139
2136
|
"selection": {
|
|
2140
2137
|
"type": "string"
|
|
2141
2138
|
},
|
|
2139
|
+
"selectionForeground": {
|
|
2140
|
+
"type": "string",
|
|
2141
|
+
"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`."
|
|
2142
|
+
},
|
|
2142
2143
|
"success": {
|
|
2143
2144
|
"type": "string"
|
|
2144
2145
|
},
|
|
@@ -2182,7 +2183,25 @@ const schema$1 = {
|
|
|
2182
2183
|
"vitesse-dark",
|
|
2183
2184
|
"vesper",
|
|
2184
2185
|
"flexoki",
|
|
2185
|
-
"mellow"
|
|
2186
|
+
"mellow",
|
|
2187
|
+
"night-owl",
|
|
2188
|
+
"cobalt2",
|
|
2189
|
+
"oceanic-next",
|
|
2190
|
+
"catppuccin-macchiato",
|
|
2191
|
+
"gruvbox-light",
|
|
2192
|
+
"tokyo-night-day",
|
|
2193
|
+
"one-light",
|
|
2194
|
+
"ayu-light",
|
|
2195
|
+
"rose-pine-dawn",
|
|
2196
|
+
"everforest-light",
|
|
2197
|
+
"vitesse-light",
|
|
2198
|
+
"dayfox",
|
|
2199
|
+
"night-owl-light",
|
|
2200
|
+
"flexoki-light",
|
|
2201
|
+
"material-lighter",
|
|
2202
|
+
"papercolor-light",
|
|
2203
|
+
"modus-operandi",
|
|
2204
|
+
"quiet-light"
|
|
2186
2205
|
]
|
|
2187
2206
|
}
|
|
2188
2207
|
}
|
|
@@ -15212,10 +15231,19 @@ const CommitSplitPlanSchema = objectType({
|
|
|
15212
15231
|
title: stringType().min(1),
|
|
15213
15232
|
body: stringType().optional(),
|
|
15214
15233
|
rationale: stringType().optional(),
|
|
15215
|
-
|
|
15216
|
-
hunks
|
|
15234
|
+
// Both optional: the model legitimately emits a group with *either*
|
|
15235
|
+
// `files` or `hunks` (a file-level vs hunk-level grouping), not always
|
|
15236
|
+
// both. Requiring both made Zod throw "Required" and the whole split
|
|
15237
|
+
// chain failed to parse before the refine could run. The refine below
|
|
15238
|
+
// still enforces "at least one", and every downstream consumer already
|
|
15239
|
+
// reads these as `group.files || []`. (Kept `.optional()` rather than
|
|
15240
|
+
// `.default([])` so the schema's input and output types stay identical
|
|
15241
|
+
// — `executeChainWithSchema` takes a `z.ZodSchema<T>`, which requires
|
|
15242
|
+
// that.)
|
|
15243
|
+
files: arrayType(stringType()).optional(),
|
|
15244
|
+
hunks: arrayType(stringType()).optional(),
|
|
15217
15245
|
})
|
|
15218
|
-
.refine((group) => group.files
|
|
15246
|
+
.refine((group) => (group.files?.length ?? 0) > 0 || (group.hunks?.length ?? 0) > 0, {
|
|
15219
15247
|
message: 'Each group must include at least one file or hunk',
|
|
15220
15248
|
}))
|
|
15221
15249
|
.min(1),
|
|
@@ -22813,6 +22841,13 @@ const LOG_INK_KEY_BINDINGS = [
|
|
|
22813
22841
|
description: 'Create a lightweight tag at the cursored commit.',
|
|
22814
22842
|
contexts: ['history'],
|
|
22815
22843
|
},
|
|
22844
|
+
{
|
|
22845
|
+
id: 'themePicker',
|
|
22846
|
+
keys: ['gC'],
|
|
22847
|
+
label: 'theme picker',
|
|
22848
|
+
description: 'Browse, live-preview, and apply a color theme.',
|
|
22849
|
+
contexts: ['normal'],
|
|
22850
|
+
},
|
|
22816
22851
|
{
|
|
22817
22852
|
id: 'viewChangelog',
|
|
22818
22853
|
keys: ['L'],
|
|
@@ -22902,6 +22937,7 @@ const BINDING_CATEGORY_BY_ID = {
|
|
|
22902
22937
|
// them above everything else.
|
|
22903
22938
|
help: 'essentials',
|
|
22904
22939
|
commandPalette: 'essentials',
|
|
22940
|
+
themePicker: 'view',
|
|
22905
22941
|
quit: 'essentials',
|
|
22906
22942
|
refresh: 'essentials',
|
|
22907
22943
|
navigateBack: 'essentials',
|
|
@@ -23537,635 +23573,1496 @@ function filterLogInkPaletteCommands(commands, filter, recent) {
|
|
|
23537
23573
|
}
|
|
23538
23574
|
|
|
23539
23575
|
/**
|
|
23540
|
-
*
|
|
23541
|
-
* (#882 phase 6). Each preset compiles to the same shape the
|
|
23542
|
-
* underlying list fetchers (`getIssueList` / `getPullRequestList`)
|
|
23543
|
-
* already accept — there's no new `gh` surface area, just a
|
|
23544
|
-
* curated set of common triage angles surfaced as a single
|
|
23545
|
-
* keystroke (`f` cycles).
|
|
23576
|
+
* Explicit color-level detection for the Ink TUI (P5.2).
|
|
23546
23577
|
*
|
|
23547
|
-
*
|
|
23548
|
-
*
|
|
23578
|
+
* Chalk already approximates hex colors when the terminal can't render
|
|
23579
|
+
* truecolor — but we want an explicit signal so the catppuccin / gruvbox
|
|
23580
|
+
* presets (which use hex) can fall back to the ANSI-named `default` preset
|
|
23581
|
+
* cleanly on minimal SSH sessions, instead of relying on chalk's
|
|
23582
|
+
* heuristics. Users who set `NO_COLOR` or pick the `monochrome` preset
|
|
23583
|
+
* still get the manual override.
|
|
23549
23584
|
*
|
|
23550
|
-
*
|
|
23551
|
-
*
|
|
23552
|
-
* -
|
|
23553
|
-
*
|
|
23554
|
-
* -
|
|
23555
|
-
* mean "I'm the assignee" (issues are tasks people pick up);
|
|
23556
|
-
* for PRs it means "I'm the author" (PRs are work people
|
|
23557
|
-
* post). The presets bake those in so the user doesn't have
|
|
23558
|
-
* to think about it.
|
|
23559
|
-
*/
|
|
23560
|
-
/** Cycle order — must match the keystroke walk on `f`. */
|
|
23561
|
-
const ISSUE_FILTER_PRESETS = [
|
|
23562
|
-
'open',
|
|
23563
|
-
'closed',
|
|
23564
|
-
'mine',
|
|
23565
|
-
'assigned',
|
|
23566
|
-
];
|
|
23567
|
-
const PULL_REQUEST_FILTER_PRESETS = [
|
|
23568
|
-
'open',
|
|
23569
|
-
'draft',
|
|
23570
|
-
'mine',
|
|
23571
|
-
'assigned',
|
|
23572
|
-
'closed',
|
|
23573
|
-
'merged',
|
|
23574
|
-
];
|
|
23575
|
-
const ISSUE_FILTER_LABELS = {
|
|
23576
|
-
open: 'open',
|
|
23577
|
-
closed: 'closed',
|
|
23578
|
-
mine: 'mine (assigned)',
|
|
23579
|
-
assigned: 'assigned to me',
|
|
23580
|
-
};
|
|
23581
|
-
const PULL_REQUEST_FILTER_LABELS = {
|
|
23582
|
-
open: 'open',
|
|
23583
|
-
draft: 'draft',
|
|
23584
|
-
mine: 'mine (authored)',
|
|
23585
|
-
assigned: 'assigned to me',
|
|
23586
|
-
closed: 'closed',
|
|
23587
|
-
merged: 'merged',
|
|
23588
|
-
};
|
|
23589
|
-
/**
|
|
23590
|
-
* Resolve a preset to the filter object the data fetcher accepts.
|
|
23591
|
-
* Pure mapping — no `gh` calls. Kept separate from `getIssueList` /
|
|
23592
|
-
* `getPullRequestList` so unit tests can assert the mapping
|
|
23593
|
-
* independently from the fetch pipeline.
|
|
23585
|
+
* Levels (matching the chalk taxonomy):
|
|
23586
|
+
* - 'mono' → no ANSI escapes at all (NO_COLOR / TERM=dumb)
|
|
23587
|
+
* - '16' → standard 16-color ANSI palette
|
|
23588
|
+
* - '256' → xterm-256color
|
|
23589
|
+
* - 'truecolor' → 24-bit RGB (COLORTERM=truecolor or known terminals)
|
|
23594
23590
|
*/
|
|
23595
|
-
function
|
|
23596
|
-
|
|
23597
|
-
|
|
23598
|
-
|
|
23599
|
-
case '
|
|
23600
|
-
return
|
|
23601
|
-
case '
|
|
23602
|
-
|
|
23603
|
-
|
|
23604
|
-
|
|
23605
|
-
|
|
23606
|
-
return
|
|
23607
|
-
case 'assigned':
|
|
23608
|
-
return { assignee: '@me' };
|
|
23591
|
+
function getColorLevel(env = process.env) {
|
|
23592
|
+
if (env.NO_COLOR)
|
|
23593
|
+
return 'mono';
|
|
23594
|
+
switch (env.FORCE_COLOR) {
|
|
23595
|
+
case '0':
|
|
23596
|
+
return 'mono';
|
|
23597
|
+
case '1':
|
|
23598
|
+
return '16';
|
|
23599
|
+
case '2':
|
|
23600
|
+
return '256';
|
|
23601
|
+
case '3':
|
|
23602
|
+
return 'truecolor';
|
|
23609
23603
|
}
|
|
23610
|
-
|
|
23611
|
-
|
|
23612
|
-
|
|
23613
|
-
case 'open':
|
|
23614
|
-
return { state: 'open' };
|
|
23615
|
-
case 'draft':
|
|
23616
|
-
// gh's `--draft` flag implies `--state open`; surface that
|
|
23617
|
-
// explicitly so the canonicalize step doesn't elide it.
|
|
23618
|
-
return { state: 'open', draft: true };
|
|
23619
|
-
case 'mine':
|
|
23620
|
-
// PRs are work — "mine" is what *I authored*. Most useful
|
|
23621
|
-
// when looking at one's own backlog of in-flight PRs.
|
|
23622
|
-
return { state: 'open', author: '@me' };
|
|
23623
|
-
case 'assigned':
|
|
23624
|
-
return { assignee: '@me' };
|
|
23625
|
-
case 'closed':
|
|
23626
|
-
return { state: 'closed' };
|
|
23627
|
-
case 'merged':
|
|
23628
|
-
return { state: 'merged' };
|
|
23604
|
+
const colorterm = env.COLORTERM?.toLowerCase();
|
|
23605
|
+
if (colorterm === 'truecolor' || colorterm === '24bit') {
|
|
23606
|
+
return 'truecolor';
|
|
23629
23607
|
}
|
|
23630
|
-
|
|
23631
|
-
|
|
23632
|
-
|
|
23633
|
-
const next = (index + 1) % ISSUE_FILTER_PRESETS.length;
|
|
23634
|
-
return ISSUE_FILTER_PRESETS[next];
|
|
23635
|
-
}
|
|
23636
|
-
function cyclePullRequestFilterPreset(current) {
|
|
23637
|
-
const index = PULL_REQUEST_FILTER_PRESETS.indexOf(current);
|
|
23638
|
-
const next = (index + 1) % PULL_REQUEST_FILTER_PRESETS.length;
|
|
23639
|
-
return PULL_REQUEST_FILTER_PRESETS[next];
|
|
23640
|
-
}
|
|
23641
|
-
|
|
23642
|
-
/**
|
|
23643
|
-
* Sort modes for the promoted views (P4.2).
|
|
23644
|
-
*
|
|
23645
|
-
* Pure: takes existing context entries + the active mode, returns a sorted
|
|
23646
|
-
* copy. Tested in isolation; the runtime just calls these helpers.
|
|
23647
|
-
*
|
|
23648
|
-
* Display label uses `▼` (U+25BC) under truecolor / UTF-8 and falls back to
|
|
23649
|
-
* `v` under ASCII. Letters (`recent` / `name` / `ahead`) carry meaning;
|
|
23650
|
-
* shape enhances.
|
|
23651
|
-
*/
|
|
23652
|
-
const BRANCH_SORT_MODES = ['name', 'recent', 'ahead'];
|
|
23653
|
-
const DEFAULT_BRANCH_SORT_MODE = 'name';
|
|
23654
|
-
function cycleBranchSort(mode) {
|
|
23655
|
-
const index = BRANCH_SORT_MODES.indexOf(mode);
|
|
23656
|
-
if (index < 0)
|
|
23657
|
-
return BRANCH_SORT_MODES[0];
|
|
23658
|
-
return BRANCH_SORT_MODES[(index + 1) % BRANCH_SORT_MODES.length];
|
|
23659
|
-
}
|
|
23660
|
-
function sortBranches(branches, mode) {
|
|
23661
|
-
// Pin the current branch at index 0 regardless of sort mode (#806
|
|
23662
|
-
// follow-up). Lands the user's cursor on the active branch by
|
|
23663
|
-
// default and keeps the most-relevant row glued to the top of the
|
|
23664
|
-
// list as they cycle sorts.
|
|
23665
|
-
const current = branches.find((entry) => entry.current);
|
|
23666
|
-
const rest = branches.filter((entry) => !entry.current);
|
|
23667
|
-
const sortedRest = rest.slice();
|
|
23668
|
-
switch (mode) {
|
|
23669
|
-
case 'name':
|
|
23670
|
-
sortedRest.sort((a, b) => a.shortName.localeCompare(b.shortName));
|
|
23671
|
-
break;
|
|
23672
|
-
case 'recent':
|
|
23673
|
-
// ISO-shaped dates compare byte-for-byte; descending so the freshest
|
|
23674
|
-
// branch sits at the top.
|
|
23675
|
-
sortedRest.sort((a, b) => (b.date || '').localeCompare(a.date || '') ||
|
|
23676
|
-
a.shortName.localeCompare(b.shortName));
|
|
23677
|
-
break;
|
|
23678
|
-
case 'ahead':
|
|
23679
|
-
// ahead-first; ties broken by behind, then by name. Keeps "this branch
|
|
23680
|
-
// has unmerged work" in the user's first scroll.
|
|
23681
|
-
sortedRest.sort((a, b) => b.ahead - a.ahead || b.behind - a.behind ||
|
|
23682
|
-
a.shortName.localeCompare(b.shortName));
|
|
23683
|
-
break;
|
|
23608
|
+
// Modern terminal emulators that publicly advertise truecolor support.
|
|
23609
|
+
if (env.KITTY_WINDOW_ID || env.TERM === 'xterm-kitty') {
|
|
23610
|
+
return 'truecolor';
|
|
23684
23611
|
}
|
|
23685
|
-
|
|
23686
|
-
|
|
23687
|
-
const TAG_SORT_MODES = ['recent', 'name'];
|
|
23688
|
-
const DEFAULT_TAG_SORT_MODE = 'recent';
|
|
23689
|
-
function cycleTagSort(mode) {
|
|
23690
|
-
const index = TAG_SORT_MODES.indexOf(mode);
|
|
23691
|
-
if (index < 0)
|
|
23692
|
-
return TAG_SORT_MODES[0];
|
|
23693
|
-
return TAG_SORT_MODES[(index + 1) % TAG_SORT_MODES.length];
|
|
23694
|
-
}
|
|
23695
|
-
function sortTags(tags, mode) {
|
|
23696
|
-
const copy = tags.slice();
|
|
23697
|
-
switch (mode) {
|
|
23698
|
-
case 'name':
|
|
23699
|
-
return copy.sort((a, b) => a.name.localeCompare(b.name));
|
|
23700
|
-
case 'recent':
|
|
23701
|
-
return copy.sort((a, b) => (b.date || '').localeCompare(a.date || '') ||
|
|
23702
|
-
a.name.localeCompare(b.name));
|
|
23703
|
-
default:
|
|
23704
|
-
return copy;
|
|
23612
|
+
if (env.WT_SESSION) {
|
|
23613
|
+
return 'truecolor';
|
|
23705
23614
|
}
|
|
23615
|
+
switch (env.TERM_PROGRAM) {
|
|
23616
|
+
case 'iTerm.app':
|
|
23617
|
+
case 'WezTerm':
|
|
23618
|
+
case 'vscode':
|
|
23619
|
+
case 'ghostty':
|
|
23620
|
+
case 'Hyper':
|
|
23621
|
+
return 'truecolor';
|
|
23622
|
+
}
|
|
23623
|
+
if (env.TERM === 'dumb')
|
|
23624
|
+
return 'mono';
|
|
23625
|
+
if (env.TERM?.includes('256color'))
|
|
23626
|
+
return '256';
|
|
23627
|
+
return '16';
|
|
23706
23628
|
}
|
|
23707
|
-
|
|
23708
|
-
function formatSortIndicator(mode, options = {}) {
|
|
23709
|
-
return `${options.ascii ? 'v' : '▼'} ${mode}`;
|
|
23710
|
-
}
|
|
23711
|
-
|
|
23712
|
-
const DEFAULT_CHANGELOG_VIEW_STATE = {
|
|
23713
|
-
status: 'idle',
|
|
23714
|
-
scrollOffset: 0,
|
|
23715
|
-
};
|
|
23716
|
-
const DEFAULT_LOG_INK_STATUS_FILTER_MASK = {
|
|
23717
|
-
staged: true,
|
|
23718
|
-
unstaged: true,
|
|
23719
|
-
untracked: true,
|
|
23720
|
-
};
|
|
23629
|
+
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']);
|
|
23721
23630
|
/**
|
|
23722
|
-
*
|
|
23723
|
-
* `
|
|
23724
|
-
*
|
|
23725
|
-
* remainder of the string (post-prefix) becomes the value — paths and
|
|
23726
|
-
* author names commonly contain spaces, and we don't try to parse
|
|
23727
|
-
* shell-like syntax.
|
|
23631
|
+
* `true` when the named preset relies on hex colors that look best under
|
|
23632
|
+
* 24-bit RGB. Used by `createLogInkTheme` to decide whether to downgrade
|
|
23633
|
+
* to the ANSI-named `default` palette on lower-capability terminals.
|
|
23728
23634
|
*/
|
|
23729
|
-
function
|
|
23730
|
-
|
|
23731
|
-
if (trimmed.startsWith('path:')) {
|
|
23732
|
-
const value = trimmed.slice('path:'.length).trim();
|
|
23733
|
-
return value ? { path: value } : undefined;
|
|
23734
|
-
}
|
|
23735
|
-
if (trimmed.startsWith('author:')) {
|
|
23736
|
-
const value = trimmed.slice('author:'.length).trim();
|
|
23737
|
-
return value ? { author: value } : undefined;
|
|
23738
|
-
}
|
|
23739
|
-
return undefined;
|
|
23740
|
-
}
|
|
23741
|
-
const FOCUS_ORDER = ['sidebar', 'commits', 'detail'];
|
|
23742
|
-
const SIDEBAR_TABS = ['status', 'branches', 'tags', 'stashes', 'worktrees'];
|
|
23743
|
-
function searchableFields(commit) {
|
|
23744
|
-
return [
|
|
23745
|
-
commit.shortHash,
|
|
23746
|
-
commit.hash,
|
|
23747
|
-
commit.date,
|
|
23748
|
-
commit.author,
|
|
23749
|
-
commit.message,
|
|
23750
|
-
...commit.refs,
|
|
23751
|
-
];
|
|
23752
|
-
}
|
|
23753
|
-
function scoreField(field, term) {
|
|
23754
|
-
const value = field.toLowerCase();
|
|
23755
|
-
const normalized = term.toLowerCase();
|
|
23756
|
-
if (!normalized) {
|
|
23757
|
-
return 0;
|
|
23758
|
-
}
|
|
23759
|
-
if (value === normalized) {
|
|
23760
|
-
return 1000;
|
|
23761
|
-
}
|
|
23762
|
-
if (value.startsWith(normalized)) {
|
|
23763
|
-
return 800 - Math.min(value.length - normalized.length, 200);
|
|
23764
|
-
}
|
|
23765
|
-
const substringIndex = value.indexOf(normalized);
|
|
23766
|
-
if (substringIndex >= 0) {
|
|
23767
|
-
return 600 - Math.min(substringIndex, 200);
|
|
23768
|
-
}
|
|
23769
|
-
let searchIndex = 0;
|
|
23770
|
-
let distance = 0;
|
|
23771
|
-
for (const character of normalized) {
|
|
23772
|
-
const nextIndex = value.indexOf(character, searchIndex);
|
|
23773
|
-
if (nextIndex < 0) {
|
|
23774
|
-
return undefined;
|
|
23775
|
-
}
|
|
23776
|
-
distance += nextIndex - searchIndex;
|
|
23777
|
-
searchIndex = nextIndex + 1;
|
|
23778
|
-
}
|
|
23779
|
-
return 300 - Math.min(distance, 200);
|
|
23780
|
-
}
|
|
23781
|
-
function scoreLogInkCommitFilter(commit, filter) {
|
|
23782
|
-
const terms = filter.trim().split(/\s+/).filter(Boolean);
|
|
23783
|
-
if (terms.length === 0) {
|
|
23784
|
-
return 0;
|
|
23785
|
-
}
|
|
23786
|
-
const fields = searchableFields(commit);
|
|
23787
|
-
let score = 0;
|
|
23788
|
-
for (const term of terms) {
|
|
23789
|
-
const bestFieldScore = fields.reduce((best, field) => {
|
|
23790
|
-
const fieldScore = scoreField(field, term);
|
|
23791
|
-
if (fieldScore === undefined) {
|
|
23792
|
-
return best;
|
|
23793
|
-
}
|
|
23794
|
-
return best === undefined ? fieldScore : Math.max(best, fieldScore);
|
|
23795
|
-
}, undefined);
|
|
23796
|
-
if (bestFieldScore === undefined) {
|
|
23797
|
-
return undefined;
|
|
23798
|
-
}
|
|
23799
|
-
score += bestFieldScore;
|
|
23800
|
-
}
|
|
23801
|
-
return score;
|
|
23802
|
-
}
|
|
23803
|
-
function filterCommits(commits, filter) {
|
|
23804
|
-
return commits
|
|
23805
|
-
.map((commit, index) => ({
|
|
23806
|
-
commit,
|
|
23807
|
-
index,
|
|
23808
|
-
score: scoreLogInkCommitFilter(commit, filter),
|
|
23809
|
-
}))
|
|
23810
|
-
.filter((entry) => entry.score !== undefined)
|
|
23811
|
-
.sort((a, b) => b.score - a.score || a.index - b.index)
|
|
23812
|
-
.map((entry) => entry.commit);
|
|
23813
|
-
}
|
|
23814
|
-
function clampIndex(index, length) {
|
|
23815
|
-
if (length === 0) {
|
|
23816
|
-
return 0;
|
|
23817
|
-
}
|
|
23818
|
-
return Math.max(0, Math.min(index, length - 1));
|
|
23819
|
-
}
|
|
23820
|
-
function cycleValue(values, current, delta) {
|
|
23821
|
-
const currentIndex = Math.max(0, values.indexOf(current));
|
|
23822
|
-
const nextIndex = (currentIndex + delta + values.length) % values.length;
|
|
23823
|
-
return values[nextIndex];
|
|
23824
|
-
}
|
|
23825
|
-
const HOME_VIEW = 'history';
|
|
23826
|
-
function topOfStack(stack) {
|
|
23827
|
-
return stack[stack.length - 1];
|
|
23828
|
-
}
|
|
23829
|
-
function withPushedView(state, value) {
|
|
23830
|
-
if (topOfStack(state.viewStack) === value) {
|
|
23831
|
-
return { ...state, pendingKey: undefined };
|
|
23832
|
-
}
|
|
23833
|
-
const viewStack = [...state.viewStack, value];
|
|
23834
|
-
return {
|
|
23835
|
-
...state,
|
|
23836
|
-
activeView: value,
|
|
23837
|
-
viewStack,
|
|
23838
|
-
// The compose + status views' right detail panels already show
|
|
23839
|
-
// worktree info, so keeping the left sidebar on the Status tab
|
|
23840
|
-
// duplicates that information. Auto-switch to Branches when entering
|
|
23841
|
-
// either view; the user can swap back with [/] if they want.
|
|
23842
|
-
//
|
|
23843
|
-
// We update only the rendered `sidebarTab` here, never
|
|
23844
|
-
// `userSidebarTab`, so this auto-switch is invisible to per-repo
|
|
23845
|
-
// persistence and pop-view restores the previous tab.
|
|
23846
|
-
sidebarTab: value === 'compose' || value === 'status' ? 'branches' : state.sidebarTab,
|
|
23847
|
-
worktreeDiffOffset: value === 'diff' ? state.worktreeDiffOffset : 0,
|
|
23848
|
-
selectedWorktreeHunkIndex: value === 'diff' ? state.selectedWorktreeHunkIndex : 0,
|
|
23849
|
-
diffSource: value === 'diff' ? state.diffSource : undefined,
|
|
23850
|
-
stashDiffRef: value === 'diff' ? state.stashDiffRef : undefined,
|
|
23851
|
-
compareHead: value === 'diff' ? state.compareHead : undefined,
|
|
23852
|
-
pendingCommitFocused: value === 'history' ? state.pendingCommitFocused : false,
|
|
23853
|
-
statusGroupHeaderFocused: value === 'status' ? state.statusGroupHeaderFocused : false,
|
|
23854
|
-
pendingKey: undefined,
|
|
23855
|
-
};
|
|
23856
|
-
}
|
|
23857
|
-
function withPoppedView(state) {
|
|
23858
|
-
if (state.viewStack.length <= 1) {
|
|
23859
|
-
return { ...state, pendingKey: undefined };
|
|
23860
|
-
}
|
|
23861
|
-
const viewStack = state.viewStack.slice(0, -1);
|
|
23862
|
-
const next = topOfStack(viewStack);
|
|
23863
|
-
// #779 — compareBase is "cleared when the diff view is popped." We
|
|
23864
|
-
// detect that case by checking if the *previous* top was 'diff'.
|
|
23865
|
-
// The compare workflow ends when the user backs out of the compare
|
|
23866
|
-
// diff; on the next mark they re-set the base. Other view pops
|
|
23867
|
-
// preserve compareBase so the user can move between branches / tags /
|
|
23868
|
-
// history while hunting for a head ref.
|
|
23869
|
-
const wasOnDiff = state.activeView === 'diff';
|
|
23870
|
-
return {
|
|
23871
|
-
...state,
|
|
23872
|
-
activeView: next,
|
|
23873
|
-
viewStack,
|
|
23874
|
-
// Restore the user's last explicit tab choice so popping out of
|
|
23875
|
-
// compose / status (which auto-switch the sidebar to Branches)
|
|
23876
|
-
// returns the user to whatever they actually had open before.
|
|
23877
|
-
sidebarTab: state.userSidebarTab,
|
|
23878
|
-
worktreeDiffOffset: next === 'diff' ? state.worktreeDiffOffset : 0,
|
|
23879
|
-
selectedWorktreeHunkIndex: next === 'diff' ? state.selectedWorktreeHunkIndex : 0,
|
|
23880
|
-
diffSource: next === 'diff' ? state.diffSource : undefined,
|
|
23881
|
-
stashDiffRef: next === 'diff' ? state.stashDiffRef : undefined,
|
|
23882
|
-
compareBase: wasOnDiff ? undefined : state.compareBase,
|
|
23883
|
-
compareHead: next === 'diff' ? state.compareHead : undefined,
|
|
23884
|
-
pendingCommitFocused: next === 'history' ? state.pendingCommitFocused : false,
|
|
23885
|
-
statusGroupHeaderFocused: next === 'status' ? state.statusGroupHeaderFocused : false,
|
|
23886
|
-
pendingKey: undefined,
|
|
23887
|
-
};
|
|
23635
|
+
function presetUsesTrueColor(preset) {
|
|
23636
|
+
return preset !== undefined && TRUECOLOR_PRESETS.has(preset);
|
|
23888
23637
|
}
|
|
23889
23638
|
/**
|
|
23890
|
-
*
|
|
23891
|
-
*
|
|
23892
|
-
*
|
|
23893
|
-
* resets the per-frame navigation state (active view, view stack,
|
|
23894
|
-
* row / file / submodule cursors, filter) so the nested frame opens
|
|
23895
|
-
* in a clean slate — the mental equivalent of a fresh `coco ui`
|
|
23896
|
-
* launched against the submodule's working dir.
|
|
23897
|
-
*
|
|
23898
|
-
* Sidebar tab + branch / tag sort are also captured into the return
|
|
23899
|
-
* snapshot (#995) so popping back restores the parent's choices
|
|
23900
|
-
* instead of letting the submodule's tab/sort bleed across the
|
|
23901
|
-
* boundary. The values on the *new* frame are left as-is (carried
|
|
23902
|
-
* over from the parent) — the load effect in app.ts re-reads
|
|
23903
|
-
* persistence keyed on the submodule's workdir and dispatches a
|
|
23904
|
-
* restore if the user has a submodule-specific saved preference.
|
|
23905
|
-
*
|
|
23906
|
-
* Other preferences (palette recents, inspector tab, diff view mode)
|
|
23907
|
-
* stay global by design — the user's preference shouldn't reset when
|
|
23908
|
-
* they cross a submodule boundary.
|
|
23909
|
-
*
|
|
23910
|
-
* Live runtime objects (`SimpleGit`, loaded `LogInkContext`) live
|
|
23911
|
-
* outside the reducer in `app.ts`'s parallel ref structure — this
|
|
23912
|
-
* helper only manages the pure view-model side of the push.
|
|
23639
|
+
* WCAG 2.x relative luminance of a `#rrggbb` color, 0 (black) … 1 (white).
|
|
23640
|
+
* Returns `null` for anything that isn't a 6-digit hex (e.g. ANSI-named
|
|
23641
|
+
* colors), so callers can fall back rather than guess.
|
|
23913
23642
|
*/
|
|
23914
|
-
function
|
|
23915
|
-
const
|
|
23916
|
-
|
|
23917
|
-
|
|
23918
|
-
|
|
23919
|
-
|
|
23920
|
-
|
|
23921
|
-
|
|
23922
|
-
selectedFileIndex: state.selectedFileIndex,
|
|
23923
|
-
selectedSubmoduleIndex: state.selectedSubmoduleIndex,
|
|
23924
|
-
filter: state.filter,
|
|
23925
|
-
sidebarTab: state.sidebarTab,
|
|
23926
|
-
userSidebarTab: state.userSidebarTab,
|
|
23927
|
-
branchSort: state.branchSort,
|
|
23928
|
-
tagSort: state.tagSort,
|
|
23929
|
-
},
|
|
23930
|
-
};
|
|
23931
|
-
return {
|
|
23932
|
-
...state,
|
|
23933
|
-
repoStack: [...state.repoStack, newFrame],
|
|
23934
|
-
activeView: 'history',
|
|
23935
|
-
viewStack: ['history'],
|
|
23936
|
-
selectedIndex: 0,
|
|
23937
|
-
selectedFileIndex: 0,
|
|
23938
|
-
selectedSubmoduleIndex: 0,
|
|
23939
|
-
filter: '',
|
|
23940
|
-
filterMode: false,
|
|
23941
|
-
pendingCommitFocused: false,
|
|
23942
|
-
pendingKey: undefined,
|
|
23943
|
-
pendingConfirmationId: undefined,
|
|
23944
|
-
pendingConfirmationPayload: undefined,
|
|
23945
|
-
pendingMutationConfirmation: undefined,
|
|
23643
|
+
function relativeLuminance(hex) {
|
|
23644
|
+
const match = /^#?([0-9a-f]{6})$/i.exec(hex.trim());
|
|
23645
|
+
if (!match)
|
|
23646
|
+
return null;
|
|
23647
|
+
const int = parseInt(match[1], 16);
|
|
23648
|
+
const channel = (c) => {
|
|
23649
|
+
const x = c / 255;
|
|
23650
|
+
return x <= 0.03928 ? x / 12.92 : ((x + 0.055) / 1.055) ** 2.4;
|
|
23946
23651
|
};
|
|
23652
|
+
const r = channel((int >> 16) & 0xff);
|
|
23653
|
+
const g = channel((int >> 8) & 0xff);
|
|
23654
|
+
const b = channel(int & 0xff);
|
|
23655
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
23947
23656
|
}
|
|
23948
23657
|
/**
|
|
23949
|
-
*
|
|
23950
|
-
*
|
|
23951
|
-
*
|
|
23952
|
-
*
|
|
23953
|
-
* Esc auto-pop wiring that lands in a follow-up PR).
|
|
23658
|
+
* Pick a foreground guaranteed to stay readable on `bg` — black for light
|
|
23659
|
+
* backgrounds, white for dark ones. The 0.179 threshold is the luminance
|
|
23660
|
+
* crossover where black and white yield identical contrast, so the choice
|
|
23661
|
+
* always maximizes it; every background clears WCAG AA (≥ 4.5:1).
|
|
23954
23662
|
*
|
|
23955
|
-
*
|
|
23956
|
-
*
|
|
23957
|
-
*
|
|
23958
|
-
*
|
|
23663
|
+
* This is how the selected-row text stays legible across every theme:
|
|
23664
|
+
* coco controls the selection *background* but not the user's terminal
|
|
23665
|
+
* default foreground, so it must supply its own contrasting foreground
|
|
23666
|
+
* instead of hoping the terminal's happens to contrast. Returns
|
|
23667
|
+
* `undefined` for non-hex backgrounds (let the caller leave color alone).
|
|
23959
23668
|
*/
|
|
23960
|
-
function
|
|
23961
|
-
if (
|
|
23962
|
-
return
|
|
23963
|
-
|
|
23964
|
-
|
|
23965
|
-
|
|
23966
|
-
|
|
23967
|
-
if (!ret) {
|
|
23968
|
-
return { ...state, repoStack, pendingKey: undefined };
|
|
23969
|
-
}
|
|
23970
|
-
return {
|
|
23971
|
-
...state,
|
|
23972
|
-
repoStack,
|
|
23973
|
-
activeView: ret.activeView,
|
|
23974
|
-
viewStack: [ret.activeView],
|
|
23975
|
-
selectedIndex: ret.selectedIndex,
|
|
23976
|
-
selectedFileIndex: ret.selectedFileIndex,
|
|
23977
|
-
selectedSubmoduleIndex: ret.selectedSubmoduleIndex,
|
|
23978
|
-
filter: ret.filter,
|
|
23979
|
-
filterMode: false,
|
|
23980
|
-
pendingCommitFocused: false,
|
|
23981
|
-
// #995 — restore sidebar tab + sort preferences from the captured
|
|
23982
|
-
// parentReturn. Without this, the submodule's tab / sort choice
|
|
23983
|
-
// bleeds back into the parent after pop: the user picks 'tags' in
|
|
23984
|
-
// a vendored submodule, pops back to the parent, and finds the
|
|
23985
|
-
// parent's previously-selected 'branches' tab quietly replaced.
|
|
23986
|
-
sidebarTab: ret.sidebarTab,
|
|
23987
|
-
userSidebarTab: ret.userSidebarTab,
|
|
23988
|
-
branchSort: ret.branchSort,
|
|
23989
|
-
tagSort: ret.tagSort,
|
|
23990
|
-
pendingKey: undefined,
|
|
23991
|
-
pendingConfirmationId: undefined,
|
|
23992
|
-
pendingConfirmationPayload: undefined,
|
|
23993
|
-
pendingMutationConfirmation: undefined,
|
|
23994
|
-
};
|
|
23669
|
+
function readableForegroundFor(bg) {
|
|
23670
|
+
if (!bg)
|
|
23671
|
+
return undefined;
|
|
23672
|
+
const luminance = relativeLuminance(bg);
|
|
23673
|
+
if (luminance === null)
|
|
23674
|
+
return undefined;
|
|
23675
|
+
return luminance > 0.179 ? '#000000' : '#ffffff';
|
|
23995
23676
|
}
|
|
23996
|
-
|
|
23997
|
-
|
|
23998
|
-
|
|
23999
|
-
|
|
24000
|
-
|
|
24001
|
-
|
|
24002
|
-
|
|
24003
|
-
|
|
24004
|
-
|
|
24005
|
-
|
|
24006
|
-
|
|
24007
|
-
|
|
24008
|
-
|
|
24009
|
-
|
|
24010
|
-
|
|
24011
|
-
|
|
24012
|
-
|
|
24013
|
-
|
|
23677
|
+
|
|
23678
|
+
const THEME_PRESET_COLORS = {
|
|
23679
|
+
default: {
|
|
23680
|
+
accent: 'cyan',
|
|
23681
|
+
border: 'gray',
|
|
23682
|
+
danger: 'red',
|
|
23683
|
+
focusBorder: 'cyan',
|
|
23684
|
+
gitAdded: 'green',
|
|
23685
|
+
gitDeleted: 'red',
|
|
23686
|
+
gitModified: 'yellow',
|
|
23687
|
+
info: 'blue',
|
|
23688
|
+
muted: 'gray',
|
|
23689
|
+
selection: '#1a3a4a',
|
|
23690
|
+
success: 'green',
|
|
23691
|
+
warning: 'yellow',
|
|
23692
|
+
},
|
|
23693
|
+
catppuccin: {
|
|
23694
|
+
accent: '#89b4fa',
|
|
23695
|
+
border: '#585b70',
|
|
23696
|
+
danger: '#f38ba8',
|
|
23697
|
+
focusBorder: '#89dceb',
|
|
23698
|
+
gitAdded: '#a6e3a1',
|
|
23699
|
+
gitDeleted: '#f38ba8',
|
|
23700
|
+
gitModified: '#f9e2af',
|
|
23701
|
+
info: '#89b4fa',
|
|
23702
|
+
muted: '#6c7086',
|
|
23703
|
+
selection: '#45475a',
|
|
23704
|
+
success: '#a6e3a1',
|
|
23705
|
+
warning: '#f9e2af',
|
|
23706
|
+
},
|
|
23707
|
+
gruvbox: {
|
|
23708
|
+
accent: '#83a598',
|
|
23709
|
+
border: '#665c54',
|
|
23710
|
+
danger: '#fb4934',
|
|
23711
|
+
focusBorder: '#8ec07c',
|
|
23712
|
+
gitAdded: '#b8bb26',
|
|
23713
|
+
gitDeleted: '#fb4934',
|
|
23714
|
+
gitModified: '#fabd2f',
|
|
23715
|
+
info: '#83a598',
|
|
23716
|
+
muted: '#928374',
|
|
23717
|
+
selection: '#504945',
|
|
23718
|
+
success: '#b8bb26',
|
|
23719
|
+
warning: '#fabd2f',
|
|
23720
|
+
},
|
|
23721
|
+
dracula: {
|
|
23722
|
+
accent: '#bd93f9',
|
|
23723
|
+
border: '#44475a',
|
|
23724
|
+
danger: '#ff5555',
|
|
23725
|
+
focusBorder: '#ff79c6',
|
|
23726
|
+
gitAdded: '#50fa7b',
|
|
23727
|
+
gitDeleted: '#ff5555',
|
|
23728
|
+
gitModified: '#f1fa8c',
|
|
23729
|
+
info: '#8be9fd',
|
|
23730
|
+
muted: '#6272a4',
|
|
23731
|
+
selection: '#44475a',
|
|
23732
|
+
success: '#50fa7b',
|
|
23733
|
+
warning: '#f1fa8c',
|
|
23734
|
+
},
|
|
23735
|
+
nord: {
|
|
23736
|
+
accent: '#88c0d0',
|
|
23737
|
+
border: '#3b4252',
|
|
23738
|
+
danger: '#bf616a',
|
|
23739
|
+
focusBorder: '#81a1c1',
|
|
23740
|
+
gitAdded: '#a3be8c',
|
|
23741
|
+
gitDeleted: '#bf616a',
|
|
23742
|
+
gitModified: '#ebcb8b',
|
|
23743
|
+
info: '#81a1c1',
|
|
23744
|
+
muted: '#4c566a',
|
|
23745
|
+
selection: '#3b4252',
|
|
23746
|
+
success: '#a3be8c',
|
|
23747
|
+
warning: '#ebcb8b',
|
|
23748
|
+
},
|
|
23749
|
+
'solarized-dark': {
|
|
23750
|
+
accent: '#268bd2',
|
|
23751
|
+
border: '#073642',
|
|
23752
|
+
danger: '#dc322f',
|
|
23753
|
+
focusBorder: '#2aa198',
|
|
23754
|
+
gitAdded: '#859900',
|
|
23755
|
+
gitDeleted: '#dc322f',
|
|
23756
|
+
gitModified: '#b58900',
|
|
23757
|
+
info: '#268bd2',
|
|
23758
|
+
muted: '#586e75',
|
|
23759
|
+
selection: '#073642',
|
|
23760
|
+
success: '#859900',
|
|
23761
|
+
warning: '#b58900',
|
|
23762
|
+
},
|
|
23763
|
+
'tokyo-night': {
|
|
23764
|
+
accent: '#7aa2f7',
|
|
23765
|
+
border: '#3b4261',
|
|
23766
|
+
danger: '#f7768e',
|
|
23767
|
+
focusBorder: '#7dcfff',
|
|
23768
|
+
gitAdded: '#9ece6a',
|
|
23769
|
+
gitDeleted: '#f7768e',
|
|
23770
|
+
gitModified: '#e0af68',
|
|
23771
|
+
info: '#7aa2f7',
|
|
23772
|
+
muted: '#565f89',
|
|
23773
|
+
selection: '#33467c',
|
|
23774
|
+
success: '#9ece6a',
|
|
23775
|
+
warning: '#e0af68',
|
|
23776
|
+
},
|
|
23777
|
+
'one-dark': {
|
|
23778
|
+
accent: '#61afef',
|
|
23779
|
+
border: '#3e4452',
|
|
23780
|
+
danger: '#e06c75',
|
|
23781
|
+
focusBorder: '#56b6c2',
|
|
23782
|
+
gitAdded: '#98c379',
|
|
23783
|
+
gitDeleted: '#e06c75',
|
|
23784
|
+
gitModified: '#e5c07b',
|
|
23785
|
+
info: '#61afef',
|
|
23786
|
+
muted: '#5c6370',
|
|
23787
|
+
selection: '#3e4452',
|
|
23788
|
+
success: '#98c379',
|
|
23789
|
+
warning: '#e5c07b',
|
|
23790
|
+
},
|
|
23791
|
+
'rose-pine': {
|
|
23792
|
+
accent: '#c4a7e7',
|
|
23793
|
+
border: '#26233a',
|
|
23794
|
+
danger: '#eb6f92',
|
|
23795
|
+
focusBorder: '#9ccfd8',
|
|
23796
|
+
gitAdded: '#31748f',
|
|
23797
|
+
gitDeleted: '#eb6f92',
|
|
23798
|
+
gitModified: '#f6c177',
|
|
23799
|
+
info: '#9ccfd8',
|
|
23800
|
+
muted: '#6e6a86',
|
|
23801
|
+
selection: '#2a273f',
|
|
23802
|
+
success: '#31748f',
|
|
23803
|
+
warning: '#f6c177',
|
|
23804
|
+
},
|
|
23805
|
+
kanagawa: {
|
|
23806
|
+
accent: '#7e9cd8',
|
|
23807
|
+
border: '#2a2a37',
|
|
23808
|
+
danger: '#e82424',
|
|
23809
|
+
focusBorder: '#7fb4ca',
|
|
23810
|
+
gitAdded: '#76946a',
|
|
23811
|
+
gitDeleted: '#e82424',
|
|
23812
|
+
gitModified: '#dca561',
|
|
23813
|
+
info: '#7e9cd8',
|
|
23814
|
+
muted: '#727169',
|
|
23815
|
+
selection: '#2d4f67',
|
|
23816
|
+
success: '#76946a',
|
|
23817
|
+
warning: '#dca561',
|
|
23818
|
+
},
|
|
23819
|
+
everforest: {
|
|
23820
|
+
accent: '#a7c080',
|
|
23821
|
+
border: '#374145',
|
|
23822
|
+
danger: '#e67e80',
|
|
23823
|
+
focusBorder: '#83c092',
|
|
23824
|
+
gitAdded: '#a7c080',
|
|
23825
|
+
gitDeleted: '#e67e80',
|
|
23826
|
+
gitModified: '#dbbc7f',
|
|
23827
|
+
info: '#7fbbb3',
|
|
23828
|
+
muted: '#859289',
|
|
23829
|
+
selection: '#374145',
|
|
23830
|
+
success: '#a7c080',
|
|
23831
|
+
warning: '#dbbc7f',
|
|
23832
|
+
},
|
|
23833
|
+
monokai: {
|
|
23834
|
+
accent: '#66d9ef',
|
|
23835
|
+
border: '#49483e',
|
|
23836
|
+
danger: '#f92672',
|
|
23837
|
+
focusBorder: '#a6e22e',
|
|
23838
|
+
gitAdded: '#a6e22e',
|
|
23839
|
+
gitDeleted: '#f92672',
|
|
23840
|
+
gitModified: '#e6db74',
|
|
23841
|
+
info: '#66d9ef',
|
|
23842
|
+
muted: '#75715e',
|
|
23843
|
+
selection: '#49483e',
|
|
23844
|
+
success: '#a6e22e',
|
|
23845
|
+
warning: '#e6db74',
|
|
23846
|
+
},
|
|
23847
|
+
synthwave: {
|
|
23848
|
+
accent: '#f97e72',
|
|
23849
|
+
border: '#34294f',
|
|
23850
|
+
danger: '#fe4450',
|
|
23851
|
+
focusBorder: '#36f9f6',
|
|
23852
|
+
gitAdded: '#72f1b8',
|
|
23853
|
+
gitDeleted: '#fe4450',
|
|
23854
|
+
gitModified: '#fede5d',
|
|
23855
|
+
info: '#36f9f6',
|
|
23856
|
+
muted: '#848bbd',
|
|
23857
|
+
selection: '#34294f',
|
|
23858
|
+
success: '#72f1b8',
|
|
23859
|
+
warning: '#fede5d',
|
|
23860
|
+
},
|
|
23861
|
+
'ayu-dark': {
|
|
23862
|
+
accent: '#e6b450',
|
|
23863
|
+
border: '#11151c',
|
|
23864
|
+
danger: '#f07178',
|
|
23865
|
+
focusBorder: '#39bae6',
|
|
23866
|
+
gitAdded: '#7fd962',
|
|
23867
|
+
gitDeleted: '#f07178',
|
|
23868
|
+
gitModified: '#e6b450',
|
|
23869
|
+
info: '#39bae6',
|
|
23870
|
+
muted: '#565b66',
|
|
23871
|
+
selection: '#1a1f29',
|
|
23872
|
+
success: '#7fd962',
|
|
23873
|
+
warning: '#e6b450',
|
|
23874
|
+
},
|
|
23875
|
+
palenight: {
|
|
23876
|
+
accent: '#82aaff',
|
|
23877
|
+
border: '#3a3f58',
|
|
23878
|
+
danger: '#ff5370',
|
|
23879
|
+
focusBorder: '#89ddff',
|
|
23880
|
+
gitAdded: '#c3e88d',
|
|
23881
|
+
gitDeleted: '#ff5370',
|
|
23882
|
+
gitModified: '#ffcb6b',
|
|
23883
|
+
info: '#82aaff',
|
|
23884
|
+
muted: '#676e95',
|
|
23885
|
+
selection: '#3a3f58',
|
|
23886
|
+
success: '#c3e88d',
|
|
23887
|
+
warning: '#ffcb6b',
|
|
23888
|
+
},
|
|
23889
|
+
'github-dark': {
|
|
23890
|
+
accent: '#58a6ff',
|
|
23891
|
+
border: '#30363d',
|
|
23892
|
+
danger: '#f85149',
|
|
23893
|
+
focusBorder: '#58a6ff',
|
|
23894
|
+
gitAdded: '#3fb950',
|
|
23895
|
+
gitDeleted: '#f85149',
|
|
23896
|
+
gitModified: '#d29922',
|
|
23897
|
+
info: '#58a6ff',
|
|
23898
|
+
muted: '#8b949e',
|
|
23899
|
+
selection: '#264f78',
|
|
23900
|
+
success: '#3fb950',
|
|
23901
|
+
warning: '#d29922',
|
|
23902
|
+
},
|
|
23903
|
+
horizon: {
|
|
23904
|
+
accent: '#e95678',
|
|
23905
|
+
border: '#2e303e',
|
|
23906
|
+
danger: '#e95678',
|
|
23907
|
+
focusBorder: '#25b0bc',
|
|
23908
|
+
gitAdded: '#09f7a0',
|
|
23909
|
+
gitDeleted: '#e95678',
|
|
23910
|
+
gitModified: '#fab795',
|
|
23911
|
+
info: '#25b0bc',
|
|
23912
|
+
muted: '#6c6f93',
|
|
23913
|
+
selection: '#2e303e',
|
|
23914
|
+
success: '#09f7a0',
|
|
23915
|
+
warning: '#fab795',
|
|
23916
|
+
},
|
|
23917
|
+
nightfox: {
|
|
23918
|
+
accent: '#719cd6',
|
|
23919
|
+
border: '#2b3b51',
|
|
23920
|
+
danger: '#c94f6d',
|
|
23921
|
+
focusBorder: '#63cdcf',
|
|
23922
|
+
gitAdded: '#81b29a',
|
|
23923
|
+
gitDeleted: '#c94f6d',
|
|
23924
|
+
gitModified: '#dbc074',
|
|
23925
|
+
info: '#719cd6',
|
|
23926
|
+
muted: '#738091',
|
|
23927
|
+
selection: '#2b3b51',
|
|
23928
|
+
success: '#81b29a',
|
|
23929
|
+
warning: '#dbc074',
|
|
23930
|
+
},
|
|
23931
|
+
carbonfox: {
|
|
23932
|
+
accent: '#78a9ff',
|
|
23933
|
+
border: '#353535',
|
|
23934
|
+
danger: '#ee5396',
|
|
23935
|
+
focusBorder: '#33b1ff',
|
|
23936
|
+
gitAdded: '#42be65',
|
|
23937
|
+
gitDeleted: '#ee5396',
|
|
23938
|
+
gitModified: '#ffe97b',
|
|
23939
|
+
info: '#78a9ff',
|
|
23940
|
+
muted: '#7b7c7e',
|
|
23941
|
+
selection: '#353535',
|
|
23942
|
+
success: '#42be65',
|
|
23943
|
+
warning: '#ffe97b',
|
|
23944
|
+
},
|
|
23945
|
+
'tokyonight-storm': {
|
|
23946
|
+
accent: '#7aa2f7',
|
|
23947
|
+
border: '#2f334d',
|
|
23948
|
+
danger: '#f7768e',
|
|
23949
|
+
focusBorder: '#2ac3de',
|
|
23950
|
+
gitAdded: '#9ece6a',
|
|
23951
|
+
gitDeleted: '#f7768e',
|
|
23952
|
+
gitModified: '#e0af68',
|
|
23953
|
+
info: '#2ac3de',
|
|
23954
|
+
muted: '#545c7e',
|
|
23955
|
+
selection: '#2f334d',
|
|
23956
|
+
success: '#9ece6a',
|
|
23957
|
+
warning: '#e0af68',
|
|
23958
|
+
},
|
|
23959
|
+
'catppuccin-latte': {
|
|
23960
|
+
accent: '#1e66f5',
|
|
23961
|
+
border: '#ccd0da',
|
|
23962
|
+
danger: '#d20f39',
|
|
23963
|
+
focusBorder: '#179299',
|
|
23964
|
+
gitAdded: '#40a02b',
|
|
23965
|
+
gitDeleted: '#d20f39',
|
|
23966
|
+
gitModified: '#df8e1d',
|
|
23967
|
+
info: '#1e66f5',
|
|
23968
|
+
muted: '#9ca0b0',
|
|
23969
|
+
selection: '#ccd0da',
|
|
23970
|
+
success: '#40a02b',
|
|
23971
|
+
warning: '#df8e1d',
|
|
23972
|
+
},
|
|
23973
|
+
'solarized-light': {
|
|
23974
|
+
accent: '#268bd2',
|
|
23975
|
+
border: '#eee8d5',
|
|
23976
|
+
danger: '#dc322f',
|
|
23977
|
+
focusBorder: '#2aa198',
|
|
23978
|
+
gitAdded: '#859900',
|
|
23979
|
+
gitDeleted: '#dc322f',
|
|
23980
|
+
gitModified: '#b58900',
|
|
23981
|
+
info: '#268bd2',
|
|
23982
|
+
muted: '#93a1a1',
|
|
23983
|
+
selection: '#eee8d5',
|
|
23984
|
+
success: '#859900',
|
|
23985
|
+
warning: '#b58900',
|
|
23986
|
+
},
|
|
23987
|
+
'github-light': {
|
|
23988
|
+
accent: '#0969da',
|
|
23989
|
+
border: '#d0d7de',
|
|
23990
|
+
danger: '#cf222e',
|
|
23991
|
+
focusBorder: '#0969da',
|
|
23992
|
+
gitAdded: '#1a7f37',
|
|
23993
|
+
gitDeleted: '#cf222e',
|
|
23994
|
+
gitModified: '#9a6700',
|
|
23995
|
+
info: '#0969da',
|
|
23996
|
+
muted: '#656d76',
|
|
23997
|
+
selection: '#ddf4ff',
|
|
23998
|
+
success: '#1a7f37',
|
|
23999
|
+
warning: '#9a6700',
|
|
24000
|
+
},
|
|
24001
|
+
iceberg: {
|
|
24002
|
+
accent: '#84a0c6',
|
|
24003
|
+
border: '#1e2132',
|
|
24004
|
+
danger: '#e27878',
|
|
24005
|
+
focusBorder: '#89b8c2',
|
|
24006
|
+
gitAdded: '#b4be82',
|
|
24007
|
+
gitDeleted: '#e27878',
|
|
24008
|
+
gitModified: '#e2a478',
|
|
24009
|
+
info: '#84a0c6',
|
|
24010
|
+
muted: '#6b7089',
|
|
24011
|
+
selection: '#1e2132',
|
|
24012
|
+
success: '#b4be82',
|
|
24013
|
+
warning: '#e2a478',
|
|
24014
|
+
},
|
|
24015
|
+
'material-ocean': {
|
|
24016
|
+
accent: '#82aaff',
|
|
24017
|
+
border: '#2b2f3a',
|
|
24018
|
+
danger: '#f07178',
|
|
24019
|
+
focusBorder: '#89ddff',
|
|
24020
|
+
gitAdded: '#c3e88d',
|
|
24021
|
+
gitDeleted: '#f07178',
|
|
24022
|
+
gitModified: '#ffcb6b',
|
|
24023
|
+
info: '#82aaff',
|
|
24024
|
+
muted: '#464b5d',
|
|
24025
|
+
selection: '#2b2f3a',
|
|
24026
|
+
success: '#c3e88d',
|
|
24027
|
+
warning: '#ffcb6b',
|
|
24028
|
+
},
|
|
24029
|
+
moonlight: {
|
|
24030
|
+
accent: '#82aaff',
|
|
24031
|
+
border: '#2f334d',
|
|
24032
|
+
danger: '#ff757f',
|
|
24033
|
+
focusBorder: '#86e1fc',
|
|
24034
|
+
gitAdded: '#c3e88d',
|
|
24035
|
+
gitDeleted: '#ff757f',
|
|
24036
|
+
gitModified: '#ffc777',
|
|
24037
|
+
info: '#82aaff',
|
|
24038
|
+
muted: '#636da6',
|
|
24039
|
+
selection: '#2f334d',
|
|
24040
|
+
success: '#c3e88d',
|
|
24041
|
+
warning: '#ffc777',
|
|
24042
|
+
},
|
|
24043
|
+
poimandres: {
|
|
24044
|
+
accent: '#add7ff',
|
|
24045
|
+
border: '#1b1e28',
|
|
24046
|
+
danger: '#d0679d',
|
|
24047
|
+
focusBorder: '#5de4c7',
|
|
24048
|
+
gitAdded: '#5de4c7',
|
|
24049
|
+
gitDeleted: '#d0679d',
|
|
24050
|
+
gitModified: '#fffac2',
|
|
24051
|
+
info: '#add7ff',
|
|
24052
|
+
muted: '#506477',
|
|
24053
|
+
selection: '#1b1e28',
|
|
24054
|
+
success: '#5de4c7',
|
|
24055
|
+
warning: '#fffac2',
|
|
24056
|
+
},
|
|
24057
|
+
'vitesse-dark': {
|
|
24058
|
+
accent: '#4d9375',
|
|
24059
|
+
border: '#282828',
|
|
24060
|
+
danger: '#cb7676',
|
|
24061
|
+
focusBorder: '#4d9375',
|
|
24062
|
+
gitAdded: '#4d9375',
|
|
24063
|
+
gitDeleted: '#cb7676',
|
|
24064
|
+
gitModified: '#e6cc77',
|
|
24065
|
+
info: '#6394bf',
|
|
24066
|
+
muted: '#758575',
|
|
24067
|
+
selection: '#282828',
|
|
24068
|
+
success: '#4d9375',
|
|
24069
|
+
warning: '#e6cc77',
|
|
24070
|
+
},
|
|
24071
|
+
vesper: {
|
|
24072
|
+
accent: '#ffc799',
|
|
24073
|
+
border: '#232323',
|
|
24074
|
+
danger: '#f5a191',
|
|
24075
|
+
focusBorder: '#99ffe4',
|
|
24076
|
+
gitAdded: '#99ffe4',
|
|
24077
|
+
gitDeleted: '#f5a191',
|
|
24078
|
+
gitModified: '#ffc799',
|
|
24079
|
+
info: '#a0c4ff',
|
|
24080
|
+
muted: '#575757',
|
|
24081
|
+
selection: '#232323',
|
|
24082
|
+
success: '#99ffe4',
|
|
24083
|
+
warning: '#ffc799',
|
|
24084
|
+
},
|
|
24085
|
+
flexoki: {
|
|
24086
|
+
accent: '#205ea6',
|
|
24087
|
+
border: '#343331',
|
|
24088
|
+
danger: '#af3029',
|
|
24089
|
+
focusBorder: '#24837b',
|
|
24090
|
+
gitAdded: '#66800b',
|
|
24091
|
+
gitDeleted: '#af3029',
|
|
24092
|
+
gitModified: '#ad8301',
|
|
24093
|
+
info: '#205ea6',
|
|
24094
|
+
muted: '#878580',
|
|
24095
|
+
selection: '#343331',
|
|
24096
|
+
success: '#66800b',
|
|
24097
|
+
warning: '#ad8301',
|
|
24098
|
+
},
|
|
24099
|
+
mellow: {
|
|
24100
|
+
accent: '#7eb8da',
|
|
24101
|
+
border: '#2a2a2a',
|
|
24102
|
+
danger: '#f5a191',
|
|
24103
|
+
focusBorder: '#a3d4a0',
|
|
24104
|
+
gitAdded: '#a3d4a0',
|
|
24105
|
+
gitDeleted: '#f5a191',
|
|
24106
|
+
gitModified: '#f0c674',
|
|
24107
|
+
info: '#7eb8da',
|
|
24108
|
+
muted: '#6b6b6b',
|
|
24109
|
+
selection: '#2a2a2a',
|
|
24110
|
+
success: '#a3d4a0',
|
|
24111
|
+
warning: '#f0c674',
|
|
24112
|
+
},
|
|
24113
|
+
'night-owl': {
|
|
24114
|
+
accent: '#82aaff',
|
|
24115
|
+
border: '#1d3b53',
|
|
24116
|
+
danger: '#ef5350',
|
|
24117
|
+
focusBorder: '#7fdbca',
|
|
24118
|
+
gitAdded: '#addb67',
|
|
24119
|
+
gitDeleted: '#ef5350',
|
|
24120
|
+
gitModified: '#ecc48d',
|
|
24121
|
+
info: '#82aaff',
|
|
24122
|
+
muted: '#637777',
|
|
24123
|
+
selection: '#1d3b53',
|
|
24124
|
+
success: '#addb67',
|
|
24125
|
+
warning: '#ecc48d',
|
|
24126
|
+
},
|
|
24127
|
+
cobalt2: {
|
|
24128
|
+
accent: '#ffc600',
|
|
24129
|
+
border: '#234e6d',
|
|
24130
|
+
danger: '#ff628c',
|
|
24131
|
+
focusBorder: '#9effff',
|
|
24132
|
+
gitAdded: '#3ad900',
|
|
24133
|
+
gitDeleted: '#ff628c',
|
|
24134
|
+
gitModified: '#ffc600',
|
|
24135
|
+
info: '#9effff',
|
|
24136
|
+
muted: '#627e99',
|
|
24137
|
+
selection: '#0d3a58',
|
|
24138
|
+
success: '#3ad900',
|
|
24139
|
+
warning: '#ffc600',
|
|
24140
|
+
},
|
|
24141
|
+
'oceanic-next': {
|
|
24142
|
+
accent: '#6699cc',
|
|
24143
|
+
border: '#343d46',
|
|
24144
|
+
danger: '#ec5f67',
|
|
24145
|
+
focusBorder: '#5fb3b3',
|
|
24146
|
+
gitAdded: '#99c794',
|
|
24147
|
+
gitDeleted: '#ec5f67',
|
|
24148
|
+
gitModified: '#fac863',
|
|
24149
|
+
info: '#6699cc',
|
|
24150
|
+
muted: '#65737e',
|
|
24151
|
+
selection: '#4f5b66',
|
|
24152
|
+
success: '#99c794',
|
|
24153
|
+
warning: '#fac863',
|
|
24154
|
+
},
|
|
24155
|
+
'catppuccin-macchiato': {
|
|
24156
|
+
accent: '#8aadf4',
|
|
24157
|
+
border: '#494d64',
|
|
24158
|
+
danger: '#ed8796',
|
|
24159
|
+
focusBorder: '#91d7e3',
|
|
24160
|
+
gitAdded: '#a6da95',
|
|
24161
|
+
gitDeleted: '#ed8796',
|
|
24162
|
+
gitModified: '#eed49f',
|
|
24163
|
+
info: '#8aadf4',
|
|
24164
|
+
muted: '#6e738d',
|
|
24165
|
+
selection: '#363a4f',
|
|
24166
|
+
success: '#a6da95',
|
|
24167
|
+
warning: '#eed49f',
|
|
24168
|
+
},
|
|
24169
|
+
'gruvbox-light': {
|
|
24170
|
+
accent: '#076678',
|
|
24171
|
+
border: '#bdae93',
|
|
24172
|
+
danger: '#9d0006',
|
|
24173
|
+
focusBorder: '#427b58',
|
|
24174
|
+
gitAdded: '#79740e',
|
|
24175
|
+
gitDeleted: '#9d0006',
|
|
24176
|
+
gitModified: '#b57614',
|
|
24177
|
+
info: '#076678',
|
|
24178
|
+
muted: '#7c6f64',
|
|
24179
|
+
selection: '#ebdbb2',
|
|
24180
|
+
success: '#79740e',
|
|
24181
|
+
warning: '#b57614',
|
|
24182
|
+
},
|
|
24183
|
+
'tokyo-night-day': {
|
|
24184
|
+
accent: '#2e7de9',
|
|
24185
|
+
border: '#b7c1e3',
|
|
24186
|
+
danger: '#f52a65',
|
|
24187
|
+
focusBorder: '#007197',
|
|
24188
|
+
gitAdded: '#587539',
|
|
24189
|
+
gitDeleted: '#f52a65',
|
|
24190
|
+
gitModified: '#8c6c3e',
|
|
24191
|
+
info: '#2e7de9',
|
|
24192
|
+
muted: '#848cb5',
|
|
24193
|
+
selection: '#b7c1e3',
|
|
24194
|
+
success: '#587539',
|
|
24195
|
+
warning: '#8c6c3e',
|
|
24196
|
+
},
|
|
24197
|
+
'one-light': {
|
|
24198
|
+
accent: '#4078f2',
|
|
24199
|
+
border: '#d4d4d4',
|
|
24200
|
+
danger: '#e45649',
|
|
24201
|
+
focusBorder: '#0184bc',
|
|
24202
|
+
gitAdded: '#50a14f',
|
|
24203
|
+
gitDeleted: '#e45649',
|
|
24204
|
+
gitModified: '#c18401',
|
|
24205
|
+
info: '#4078f2',
|
|
24206
|
+
muted: '#a0a1a7',
|
|
24207
|
+
selection: '#e5e5e6',
|
|
24208
|
+
success: '#50a14f',
|
|
24209
|
+
warning: '#c18401',
|
|
24210
|
+
},
|
|
24211
|
+
'ayu-light': {
|
|
24212
|
+
accent: '#fa8d3e',
|
|
24213
|
+
border: '#e6e6e6',
|
|
24214
|
+
danger: '#e65050',
|
|
24215
|
+
focusBorder: '#4cbf99',
|
|
24216
|
+
gitAdded: '#6cbf43',
|
|
24217
|
+
gitDeleted: '#e65050',
|
|
24218
|
+
gitModified: '#f2ae49',
|
|
24219
|
+
info: '#399ee6',
|
|
24220
|
+
muted: '#abb0b6',
|
|
24221
|
+
selection: '#d1e4f4',
|
|
24222
|
+
success: '#6cbf43',
|
|
24223
|
+
warning: '#f2ae49',
|
|
24224
|
+
},
|
|
24225
|
+
'rose-pine-dawn': {
|
|
24226
|
+
accent: '#907aa9',
|
|
24227
|
+
border: '#dfdad9',
|
|
24228
|
+
danger: '#b4637a',
|
|
24229
|
+
focusBorder: '#56949f',
|
|
24230
|
+
gitAdded: '#286983',
|
|
24231
|
+
gitDeleted: '#b4637a',
|
|
24232
|
+
gitModified: '#ea9d34',
|
|
24233
|
+
info: '#56949f',
|
|
24234
|
+
muted: '#9893a5',
|
|
24235
|
+
selection: '#dfdad9',
|
|
24236
|
+
success: '#286983',
|
|
24237
|
+
warning: '#ea9d34',
|
|
24238
|
+
},
|
|
24239
|
+
'everforest-light': {
|
|
24240
|
+
accent: '#8da101',
|
|
24241
|
+
border: '#ddd8be',
|
|
24242
|
+
danger: '#f85552',
|
|
24243
|
+
focusBorder: '#35a77c',
|
|
24244
|
+
gitAdded: '#8da101',
|
|
24245
|
+
gitDeleted: '#f85552',
|
|
24246
|
+
gitModified: '#dfa000',
|
|
24247
|
+
info: '#3a94c5',
|
|
24248
|
+
muted: '#939f91',
|
|
24249
|
+
selection: '#edeada',
|
|
24250
|
+
success: '#8da101',
|
|
24251
|
+
warning: '#dfa000',
|
|
24252
|
+
},
|
|
24253
|
+
'vitesse-light': {
|
|
24254
|
+
accent: '#1e754f',
|
|
24255
|
+
border: '#e0e0e0',
|
|
24256
|
+
danger: '#ab5959',
|
|
24257
|
+
focusBorder: '#2993a3',
|
|
24258
|
+
gitAdded: '#1e754f',
|
|
24259
|
+
gitDeleted: '#ab5959',
|
|
24260
|
+
gitModified: '#b07d48',
|
|
24261
|
+
info: '#296aa3',
|
|
24262
|
+
muted: '#999fa6',
|
|
24263
|
+
selection: '#eaeaeb',
|
|
24264
|
+
success: '#1e754f',
|
|
24265
|
+
warning: '#b07d48',
|
|
24266
|
+
},
|
|
24267
|
+
dayfox: {
|
|
24268
|
+
accent: '#2848a9',
|
|
24269
|
+
border: '#e4dcd4',
|
|
24270
|
+
danger: '#a5222f',
|
|
24271
|
+
focusBorder: '#287980',
|
|
24272
|
+
gitAdded: '#396847',
|
|
24273
|
+
gitDeleted: '#a5222f',
|
|
24274
|
+
gitModified: '#ac5402',
|
|
24275
|
+
info: '#2848a9',
|
|
24276
|
+
muted: '#908479',
|
|
24277
|
+
selection: '#e7d2be',
|
|
24278
|
+
success: '#396847',
|
|
24279
|
+
warning: '#ac5402',
|
|
24280
|
+
},
|
|
24281
|
+
'night-owl-light': {
|
|
24282
|
+
accent: '#288ed7',
|
|
24283
|
+
border: '#d9d9d9',
|
|
24284
|
+
danger: '#d3423e',
|
|
24285
|
+
focusBorder: '#2aa298',
|
|
24286
|
+
gitAdded: '#08916a',
|
|
24287
|
+
gitDeleted: '#d3423e',
|
|
24288
|
+
gitModified: '#daaa01',
|
|
24289
|
+
info: '#288ed7',
|
|
24290
|
+
muted: '#989fb1',
|
|
24291
|
+
selection: '#e4e8f0',
|
|
24292
|
+
success: '#08916a',
|
|
24293
|
+
warning: '#daaa01',
|
|
24294
|
+
},
|
|
24295
|
+
'flexoki-light': {
|
|
24296
|
+
accent: '#205ea6',
|
|
24297
|
+
border: '#cecdc3',
|
|
24298
|
+
danger: '#af3029',
|
|
24299
|
+
focusBorder: '#24837b',
|
|
24300
|
+
gitAdded: '#66800b',
|
|
24301
|
+
gitDeleted: '#af3029',
|
|
24302
|
+
gitModified: '#ad8301',
|
|
24303
|
+
info: '#205ea6',
|
|
24304
|
+
muted: '#6f6e69',
|
|
24305
|
+
selection: '#e6e4d9',
|
|
24306
|
+
success: '#66800b',
|
|
24307
|
+
warning: '#ad8301',
|
|
24308
|
+
},
|
|
24309
|
+
'material-lighter': {
|
|
24310
|
+
accent: '#39adb5',
|
|
24311
|
+
border: '#e7eaec',
|
|
24312
|
+
danger: '#e53935',
|
|
24313
|
+
focusBorder: '#39adb5',
|
|
24314
|
+
gitAdded: '#91b859',
|
|
24315
|
+
gitDeleted: '#e53935',
|
|
24316
|
+
gitModified: '#f6a434',
|
|
24317
|
+
info: '#6182b8',
|
|
24318
|
+
muted: '#90a4ae',
|
|
24319
|
+
selection: '#d3e1e8',
|
|
24320
|
+
success: '#91b859',
|
|
24321
|
+
warning: '#f6a434',
|
|
24322
|
+
},
|
|
24323
|
+
'papercolor-light': {
|
|
24324
|
+
accent: '#0087af',
|
|
24325
|
+
border: '#d7d7d7',
|
|
24326
|
+
danger: '#af0000',
|
|
24327
|
+
focusBorder: '#005f87',
|
|
24328
|
+
gitAdded: '#008700',
|
|
24329
|
+
gitDeleted: '#af0000',
|
|
24330
|
+
gitModified: '#d75f00',
|
|
24331
|
+
info: '#0087af',
|
|
24332
|
+
muted: '#878787',
|
|
24333
|
+
selection: '#d0d0d0',
|
|
24334
|
+
success: '#008700',
|
|
24335
|
+
warning: '#d75f00',
|
|
24336
|
+
},
|
|
24337
|
+
'modus-operandi': {
|
|
24338
|
+
accent: '#0031a9',
|
|
24339
|
+
border: '#d7d7d7',
|
|
24340
|
+
danger: '#a60000',
|
|
24341
|
+
focusBorder: '#005e8b',
|
|
24342
|
+
gitAdded: '#006800',
|
|
24343
|
+
gitDeleted: '#a60000',
|
|
24344
|
+
gitModified: '#6f5500',
|
|
24345
|
+
info: '#0031a9',
|
|
24346
|
+
muted: '#595959',
|
|
24347
|
+
selection: '#c0deff',
|
|
24348
|
+
success: '#006800',
|
|
24349
|
+
warning: '#6f5500',
|
|
24350
|
+
},
|
|
24351
|
+
'quiet-light': {
|
|
24352
|
+
accent: '#4b83cd',
|
|
24353
|
+
border: '#e0e0e0',
|
|
24354
|
+
danger: '#aa3731',
|
|
24355
|
+
focusBorder: '#4b83cd',
|
|
24356
|
+
gitAdded: '#448c27',
|
|
24357
|
+
gitDeleted: '#aa3731',
|
|
24358
|
+
gitModified: '#a67d00',
|
|
24359
|
+
info: '#4b83cd',
|
|
24360
|
+
muted: '#a3a6ad',
|
|
24361
|
+
selection: '#c9d0d9',
|
|
24362
|
+
success: '#448c27',
|
|
24363
|
+
warning: '#a67d00',
|
|
24364
|
+
},
|
|
24365
|
+
};
|
|
24366
|
+
/**
|
|
24367
|
+
* Ordered list of every selectable theme preset, for the `coco ui` theme
|
|
24368
|
+
* picker and any UI that enumerates themes. `monochrome` isn't a key in
|
|
24369
|
+
* `THEME_PRESET_COLORS` (it's handled via `noColor`), so it's spliced in
|
|
24370
|
+
* right after `default` — the two non-color baselines sit together at the
|
|
24371
|
+
* top, followed by the color themes in catalog order.
|
|
24372
|
+
*/
|
|
24373
|
+
function getLogInkThemePresets() {
|
|
24374
|
+
const keys = Object.keys(THEME_PRESET_COLORS);
|
|
24375
|
+
const [first, ...rest] = keys;
|
|
24376
|
+
return first === 'default'
|
|
24377
|
+
? ['default', 'monochrome', ...rest]
|
|
24378
|
+
: ['monochrome', ...keys];
|
|
24014
24379
|
}
|
|
24015
|
-
function
|
|
24016
|
-
|
|
24017
|
-
|
|
24018
|
-
|
|
24019
|
-
|
|
24020
|
-
// when the previously-selected item dropped out. Falls back to the older
|
|
24021
|
-
// "snap to 0" behavior when no snapshot was provided (test paths,
|
|
24022
|
-
// dispatchers without context).
|
|
24023
|
-
const filterChanged = state.filter !== filter;
|
|
24024
|
-
const branchIndex = promotedSelections?.branchIndex ??
|
|
24025
|
-
(filterChanged ? 0 : state.selectedBranchIndex);
|
|
24026
|
-
const tagIndex = promotedSelections?.tagIndex ??
|
|
24027
|
-
(filterChanged ? 0 : state.selectedTagIndex);
|
|
24028
|
-
const stashIndex = promotedSelections?.stashIndex ??
|
|
24029
|
-
(filterChanged ? 0 : state.selectedStashIndex);
|
|
24030
|
-
// Reflog (#781) snaps to 0 on filter change rather than rectifying.
|
|
24031
|
-
// The list is chronological and the user is unlikely to be tracking
|
|
24032
|
-
// a specific entry through filter changes — the simpler reset
|
|
24033
|
-
// matches the "find recovery target by typing" interaction.
|
|
24034
|
-
const reflogIndex = filterChanged ? 0 : state.selectedReflogIndex;
|
|
24035
|
-
return {
|
|
24036
|
-
...state,
|
|
24037
|
-
filter,
|
|
24038
|
-
filteredCommits,
|
|
24039
|
-
selectedIndex: clampIndex(state.selectedIndex, filteredCommits.length),
|
|
24040
|
-
selectedFileIndex: 0,
|
|
24041
|
-
selectedBranchIndex: branchIndex,
|
|
24042
|
-
selectedTagIndex: tagIndex,
|
|
24043
|
-
selectedStashIndex: stashIndex,
|
|
24044
|
-
selectedReflogIndex: reflogIndex,
|
|
24045
|
-
diffPreviewOffset: 0,
|
|
24046
|
-
pendingKey: undefined,
|
|
24047
|
-
};
|
|
24380
|
+
function shouldUseAscii(term) {
|
|
24381
|
+
if (!term) {
|
|
24382
|
+
return false;
|
|
24383
|
+
}
|
|
24384
|
+
return term === 'dumb' || term.startsWith('vt100');
|
|
24048
24385
|
}
|
|
24049
|
-
function
|
|
24050
|
-
|
|
24051
|
-
|
|
24052
|
-
|
|
24053
|
-
|
|
24054
|
-
|
|
24055
|
-
|
|
24386
|
+
function createLogInkTheme(options = {}) {
|
|
24387
|
+
const noColor = (options.noColor ?? Boolean(process.env.NO_COLOR)) ||
|
|
24388
|
+
options.preset === 'monochrome';
|
|
24389
|
+
const ascii = options.ascii ?? shouldUseAscii(options.term ?? process.env.TERM);
|
|
24390
|
+
const requestedPreset = options.preset && options.preset !== 'monochrome' ? options.preset : 'default';
|
|
24391
|
+
// P5.2 — gracefully downgrade hex presets (catppuccin / gruvbox) when
|
|
24392
|
+
// the host terminal can't render truecolor. Chalk approximates hex in
|
|
24393
|
+
// those modes anyway, but the default preset's ANSI-named palette
|
|
24394
|
+
// renders far more faithfully on 16-color terminals.
|
|
24395
|
+
const colorLevel = getColorLevel(options.env ?? process.env);
|
|
24396
|
+
const preset = !noColor && presetUsesTrueColor(requestedPreset) && colorLevel !== 'truecolor'
|
|
24397
|
+
? 'default'
|
|
24398
|
+
: requestedPreset;
|
|
24399
|
+
const colors = noColor
|
|
24400
|
+
? {}
|
|
24401
|
+
: {
|
|
24402
|
+
...THEME_PRESET_COLORS[preset],
|
|
24403
|
+
// Preserve the requested theme's selection background even when the
|
|
24404
|
+
// rest of the palette downgrades to `default`. The selection is a
|
|
24405
|
+
// single background color the terminal can approximate; without this,
|
|
24406
|
+
// a light theme inherits `default`'s dark selection (#1a3a4a) and the
|
|
24407
|
+
// selected row renders as a dark bar on a light background.
|
|
24408
|
+
...(preset !== requestedPreset && THEME_PRESET_COLORS[requestedPreset]?.selection
|
|
24409
|
+
? { selection: THEME_PRESET_COLORS[requestedPreset].selection }
|
|
24410
|
+
: {}),
|
|
24411
|
+
...options.colors,
|
|
24412
|
+
};
|
|
24413
|
+
// Derive a contrasting foreground for the selected row from its own
|
|
24414
|
+
// selection background, unless the caller supplied one explicitly. coco
|
|
24415
|
+
// owns the selection background but not the terminal's default foreground,
|
|
24416
|
+
// so without this the selected row's text falls back to whatever the
|
|
24417
|
+
// user's terminal foreground is — which may not contrast with the bar at
|
|
24418
|
+
// all (the bug behind unreadable selected rows on many themes).
|
|
24419
|
+
if (!noColor && colors.selection && !colors.selectionForeground) {
|
|
24420
|
+
const selectionForeground = readableForegroundFor(colors.selection);
|
|
24421
|
+
if (selectionForeground) {
|
|
24422
|
+
colors.selectionForeground = selectionForeground;
|
|
24423
|
+
}
|
|
24424
|
+
}
|
|
24056
24425
|
return {
|
|
24057
|
-
|
|
24058
|
-
|
|
24059
|
-
|
|
24060
|
-
|
|
24061
|
-
selectedIndex: 0,
|
|
24062
|
-
selectedFileIndex: 0,
|
|
24063
|
-
pendingCommitFocused: false,
|
|
24064
|
-
pendingKey: undefined,
|
|
24065
|
-
// Rows just landed — clear the boot-loading flag so the history
|
|
24066
|
-
// surface drops the "Loading commits…" placeholder. Safe to clear
|
|
24067
|
-
// unconditionally because `replaceRows` only fires after a real
|
|
24068
|
-
// git log returns.
|
|
24069
|
-
bootLoading: false,
|
|
24426
|
+
noColor,
|
|
24427
|
+
ascii,
|
|
24428
|
+
borderStyle: options.borderStyle || (ascii ? 'classic' : 'round'),
|
|
24429
|
+
colors,
|
|
24070
24430
|
};
|
|
24071
24431
|
}
|
|
24072
|
-
|
|
24073
|
-
|
|
24074
|
-
|
|
24075
|
-
|
|
24076
|
-
|
|
24077
|
-
|
|
24078
|
-
|
|
24432
|
+
|
|
24433
|
+
/**
|
|
24434
|
+
* Canned filter presets for the issue / PR triage TUI views
|
|
24435
|
+
* (#882 phase 6). Each preset compiles to the same shape the
|
|
24436
|
+
* underlying list fetchers (`getIssueList` / `getPullRequestList`)
|
|
24437
|
+
* already accept — there's no new `gh` surface area, just a
|
|
24438
|
+
* curated set of common triage angles surfaced as a single
|
|
24439
|
+
* keystroke (`f` cycles).
|
|
24440
|
+
*
|
|
24441
|
+
* The presets are deliberately *not* a 1:1 mirror across the two
|
|
24442
|
+
* surfaces:
|
|
24443
|
+
*
|
|
24444
|
+
* - Issues have no draft / mergeable concept, so `draft` /
|
|
24445
|
+
* `mergeable` are skipped on the issue list.
|
|
24446
|
+
* - PRs have a `merged` state distinct from `closed`; issues
|
|
24447
|
+
* don't.
|
|
24448
|
+
* - `mine` semantics differ subtly: for issues it tends to
|
|
24449
|
+
* mean "I'm the assignee" (issues are tasks people pick up);
|
|
24450
|
+
* for PRs it means "I'm the author" (PRs are work people
|
|
24451
|
+
* post). The presets bake those in so the user doesn't have
|
|
24452
|
+
* to think about it.
|
|
24453
|
+
*/
|
|
24454
|
+
/** Cycle order — must match the keystroke walk on `f`. */
|
|
24455
|
+
const ISSUE_FILTER_PRESETS = [
|
|
24456
|
+
'open',
|
|
24457
|
+
'closed',
|
|
24458
|
+
'mine',
|
|
24459
|
+
'assigned',
|
|
24460
|
+
];
|
|
24461
|
+
const PULL_REQUEST_FILTER_PRESETS = [
|
|
24462
|
+
'open',
|
|
24463
|
+
'draft',
|
|
24464
|
+
'mine',
|
|
24465
|
+
'assigned',
|
|
24466
|
+
'closed',
|
|
24467
|
+
'merged',
|
|
24468
|
+
];
|
|
24469
|
+
const ISSUE_FILTER_LABELS = {
|
|
24470
|
+
open: 'open',
|
|
24471
|
+
closed: 'closed',
|
|
24472
|
+
mine: 'mine (assigned)',
|
|
24473
|
+
assigned: 'assigned to me',
|
|
24474
|
+
};
|
|
24475
|
+
const PULL_REQUEST_FILTER_LABELS = {
|
|
24476
|
+
open: 'open',
|
|
24477
|
+
draft: 'draft',
|
|
24478
|
+
mine: 'mine (authored)',
|
|
24479
|
+
assigned: 'assigned to me',
|
|
24480
|
+
closed: 'closed',
|
|
24481
|
+
merged: 'merged',
|
|
24482
|
+
};
|
|
24483
|
+
/**
|
|
24484
|
+
* Resolve a preset to the filter object the data fetcher accepts.
|
|
24485
|
+
* Pure mapping — no `gh` calls. Kept separate from `getIssueList` /
|
|
24486
|
+
* `getPullRequestList` so unit tests can assert the mapping
|
|
24487
|
+
* independently from the fetch pipeline.
|
|
24488
|
+
*/
|
|
24489
|
+
function issueFilterForPreset(preset) {
|
|
24490
|
+
switch (preset) {
|
|
24491
|
+
case 'open':
|
|
24492
|
+
return { state: 'open' };
|
|
24493
|
+
case 'closed':
|
|
24494
|
+
return { state: 'closed' };
|
|
24495
|
+
case 'mine':
|
|
24496
|
+
// Issues are tasks — "mine" is what *I'm working on*, i.e.
|
|
24497
|
+
// assigned to me + still open. Same as `assigned` plus the
|
|
24498
|
+
// open-state filter for ergonomic single-keystroke focus on
|
|
24499
|
+
// the active backlog.
|
|
24500
|
+
return { state: 'open', assignee: '@me' };
|
|
24501
|
+
case 'assigned':
|
|
24502
|
+
return { assignee: '@me' };
|
|
24503
|
+
}
|
|
24504
|
+
}
|
|
24505
|
+
function pullRequestFilterForPreset(preset) {
|
|
24506
|
+
switch (preset) {
|
|
24507
|
+
case 'open':
|
|
24508
|
+
return { state: 'open' };
|
|
24509
|
+
case 'draft':
|
|
24510
|
+
// gh's `--draft` flag implies `--state open`; surface that
|
|
24511
|
+
// explicitly so the canonicalize step doesn't elide it.
|
|
24512
|
+
return { state: 'open', draft: true };
|
|
24513
|
+
case 'mine':
|
|
24514
|
+
// PRs are work — "mine" is what *I authored*. Most useful
|
|
24515
|
+
// when looking at one's own backlog of in-flight PRs.
|
|
24516
|
+
return { state: 'open', author: '@me' };
|
|
24517
|
+
case 'assigned':
|
|
24518
|
+
return { assignee: '@me' };
|
|
24519
|
+
case 'closed':
|
|
24520
|
+
return { state: 'closed' };
|
|
24521
|
+
case 'merged':
|
|
24522
|
+
return { state: 'merged' };
|
|
24523
|
+
}
|
|
24524
|
+
}
|
|
24525
|
+
function cycleIssueFilterPreset(current) {
|
|
24526
|
+
const index = ISSUE_FILTER_PRESETS.indexOf(current);
|
|
24527
|
+
const next = (index + 1) % ISSUE_FILTER_PRESETS.length;
|
|
24528
|
+
return ISSUE_FILTER_PRESETS[next];
|
|
24529
|
+
}
|
|
24530
|
+
function cyclePullRequestFilterPreset(current) {
|
|
24531
|
+
const index = PULL_REQUEST_FILTER_PRESETS.indexOf(current);
|
|
24532
|
+
const next = (index + 1) % PULL_REQUEST_FILTER_PRESETS.length;
|
|
24533
|
+
return PULL_REQUEST_FILTER_PRESETS[next];
|
|
24534
|
+
}
|
|
24535
|
+
|
|
24536
|
+
/**
|
|
24537
|
+
* Sort modes for the promoted views (P4.2).
|
|
24538
|
+
*
|
|
24539
|
+
* Pure: takes existing context entries + the active mode, returns a sorted
|
|
24540
|
+
* copy. Tested in isolation; the runtime just calls these helpers.
|
|
24541
|
+
*
|
|
24542
|
+
* Display label uses `▼` (U+25BC) under truecolor / UTF-8 and falls back to
|
|
24543
|
+
* `v` under ASCII. Letters (`recent` / `name` / `ahead`) carry meaning;
|
|
24544
|
+
* shape enhances.
|
|
24545
|
+
*/
|
|
24546
|
+
const BRANCH_SORT_MODES = ['name', 'recent', 'ahead'];
|
|
24547
|
+
const DEFAULT_BRANCH_SORT_MODE = 'name';
|
|
24548
|
+
function cycleBranchSort(mode) {
|
|
24549
|
+
const index = BRANCH_SORT_MODES.indexOf(mode);
|
|
24550
|
+
if (index < 0)
|
|
24551
|
+
return BRANCH_SORT_MODES[0];
|
|
24552
|
+
return BRANCH_SORT_MODES[(index + 1) % BRANCH_SORT_MODES.length];
|
|
24553
|
+
}
|
|
24554
|
+
function sortBranches(branches, mode) {
|
|
24555
|
+
// Pin the current branch at index 0 regardless of sort mode (#806
|
|
24556
|
+
// follow-up). Lands the user's cursor on the active branch by
|
|
24557
|
+
// default and keeps the most-relevant row glued to the top of the
|
|
24558
|
+
// list as they cycle sorts.
|
|
24559
|
+
const current = branches.find((entry) => entry.current);
|
|
24560
|
+
const rest = branches.filter((entry) => !entry.current);
|
|
24561
|
+
const sortedRest = rest.slice();
|
|
24562
|
+
switch (mode) {
|
|
24563
|
+
case 'name':
|
|
24564
|
+
sortedRest.sort((a, b) => a.shortName.localeCompare(b.shortName));
|
|
24565
|
+
break;
|
|
24566
|
+
case 'recent':
|
|
24567
|
+
// ISO-shaped dates compare byte-for-byte; descending so the freshest
|
|
24568
|
+
// branch sits at the top.
|
|
24569
|
+
sortedRest.sort((a, b) => (b.date || '').localeCompare(a.date || '') ||
|
|
24570
|
+
a.shortName.localeCompare(b.shortName));
|
|
24571
|
+
break;
|
|
24572
|
+
case 'ahead':
|
|
24573
|
+
// ahead-first; ties broken by behind, then by name. Keeps "this branch
|
|
24574
|
+
// has unmerged work" in the user's first scroll.
|
|
24575
|
+
sortedRest.sort((a, b) => b.ahead - a.ahead || b.behind - a.behind ||
|
|
24576
|
+
a.shortName.localeCompare(b.shortName));
|
|
24577
|
+
break;
|
|
24578
|
+
}
|
|
24579
|
+
return current ? [current, ...sortedRest] : sortedRest;
|
|
24580
|
+
}
|
|
24581
|
+
const TAG_SORT_MODES = ['recent', 'name'];
|
|
24582
|
+
const DEFAULT_TAG_SORT_MODE = 'recent';
|
|
24583
|
+
function cycleTagSort(mode) {
|
|
24584
|
+
const index = TAG_SORT_MODES.indexOf(mode);
|
|
24585
|
+
if (index < 0)
|
|
24586
|
+
return TAG_SORT_MODES[0];
|
|
24587
|
+
return TAG_SORT_MODES[(index + 1) % TAG_SORT_MODES.length];
|
|
24588
|
+
}
|
|
24589
|
+
function sortTags(tags, mode) {
|
|
24590
|
+
const copy = tags.slice();
|
|
24591
|
+
switch (mode) {
|
|
24592
|
+
case 'name':
|
|
24593
|
+
return copy.sort((a, b) => a.name.localeCompare(b.name));
|
|
24594
|
+
case 'recent':
|
|
24595
|
+
return copy.sort((a, b) => (b.date || '').localeCompare(a.date || '') ||
|
|
24596
|
+
a.name.localeCompare(b.name));
|
|
24597
|
+
default:
|
|
24598
|
+
return copy;
|
|
24599
|
+
}
|
|
24600
|
+
}
|
|
24601
|
+
/* ---------------------------- header indicator -------------------------- */
|
|
24602
|
+
function formatSortIndicator(mode, options = {}) {
|
|
24603
|
+
return `${options.ascii ? 'v' : '▼'} ${mode}`;
|
|
24604
|
+
}
|
|
24605
|
+
|
|
24606
|
+
const DEFAULT_CHANGELOG_VIEW_STATE = {
|
|
24607
|
+
status: 'idle',
|
|
24608
|
+
scrollOffset: 0,
|
|
24609
|
+
};
|
|
24610
|
+
const DEFAULT_LOG_INK_STATUS_FILTER_MASK = {
|
|
24611
|
+
staged: true,
|
|
24612
|
+
unstaged: true,
|
|
24613
|
+
untracked: true,
|
|
24614
|
+
};
|
|
24615
|
+
/**
|
|
24616
|
+
* Detect a history server-side filter prefix (#776). Returns the parsed
|
|
24617
|
+
* `LogInkHistoryFetchArgs` for `path:<value>` and `author:<value>`
|
|
24618
|
+
* prefixes, or `undefined` for a plain (client-side) filter. The whole
|
|
24619
|
+
* remainder of the string (post-prefix) becomes the value — paths and
|
|
24620
|
+
* author names commonly contain spaces, and we don't try to parse
|
|
24621
|
+
* shell-like syntax.
|
|
24622
|
+
*/
|
|
24623
|
+
function parseLogInkHistoryFetchPrefix(filter) {
|
|
24624
|
+
const trimmed = filter.trim();
|
|
24625
|
+
if (trimmed.startsWith('path:')) {
|
|
24626
|
+
const value = trimmed.slice('path:'.length).trim();
|
|
24627
|
+
return value ? { path: value } : undefined;
|
|
24628
|
+
}
|
|
24629
|
+
if (trimmed.startsWith('author:')) {
|
|
24630
|
+
const value = trimmed.slice('author:'.length).trim();
|
|
24631
|
+
return value ? { author: value } : undefined;
|
|
24632
|
+
}
|
|
24633
|
+
return undefined;
|
|
24634
|
+
}
|
|
24635
|
+
const FOCUS_ORDER = ['sidebar', 'commits', 'detail'];
|
|
24636
|
+
const SIDEBAR_TABS = ['status', 'branches', 'tags', 'stashes', 'worktrees'];
|
|
24637
|
+
function searchableFields(commit) {
|
|
24638
|
+
return [
|
|
24639
|
+
commit.shortHash,
|
|
24640
|
+
commit.hash,
|
|
24641
|
+
commit.date,
|
|
24642
|
+
commit.author,
|
|
24643
|
+
commit.message,
|
|
24644
|
+
...commit.refs,
|
|
24645
|
+
];
|
|
24646
|
+
}
|
|
24647
|
+
function scoreField(field, term) {
|
|
24648
|
+
const value = field.toLowerCase();
|
|
24649
|
+
const normalized = term.toLowerCase();
|
|
24650
|
+
if (!normalized) {
|
|
24651
|
+
return 0;
|
|
24652
|
+
}
|
|
24653
|
+
if (value === normalized) {
|
|
24654
|
+
return 1000;
|
|
24655
|
+
}
|
|
24656
|
+
if (value.startsWith(normalized)) {
|
|
24657
|
+
return 800 - Math.min(value.length - normalized.length, 200);
|
|
24658
|
+
}
|
|
24659
|
+
const substringIndex = value.indexOf(normalized);
|
|
24660
|
+
if (substringIndex >= 0) {
|
|
24661
|
+
return 600 - Math.min(substringIndex, 200);
|
|
24662
|
+
}
|
|
24663
|
+
let searchIndex = 0;
|
|
24664
|
+
let distance = 0;
|
|
24665
|
+
for (const character of normalized) {
|
|
24666
|
+
const nextIndex = value.indexOf(character, searchIndex);
|
|
24667
|
+
if (nextIndex < 0) {
|
|
24668
|
+
return undefined;
|
|
24079
24669
|
}
|
|
24080
|
-
|
|
24081
|
-
|
|
24082
|
-
}
|
|
24083
|
-
|
|
24084
|
-
|
|
24085
|
-
|
|
24086
|
-
|
|
24670
|
+
distance += nextIndex - searchIndex;
|
|
24671
|
+
searchIndex = nextIndex + 1;
|
|
24672
|
+
}
|
|
24673
|
+
return 300 - Math.min(distance, 200);
|
|
24674
|
+
}
|
|
24675
|
+
function scoreLogInkCommitFilter(commit, filter) {
|
|
24676
|
+
const terms = filter.trim().split(/\s+/).filter(Boolean);
|
|
24677
|
+
if (terms.length === 0) {
|
|
24678
|
+
return 0;
|
|
24679
|
+
}
|
|
24680
|
+
const fields = searchableFields(commit);
|
|
24681
|
+
let score = 0;
|
|
24682
|
+
for (const term of terms) {
|
|
24683
|
+
const bestFieldScore = fields.reduce((best, field) => {
|
|
24684
|
+
const fieldScore = scoreField(field, term);
|
|
24685
|
+
if (fieldScore === undefined) {
|
|
24686
|
+
return best;
|
|
24687
|
+
}
|
|
24688
|
+
return best === undefined ? fieldScore : Math.max(best, fieldScore);
|
|
24689
|
+
}, undefined);
|
|
24690
|
+
if (bestFieldScore === undefined) {
|
|
24691
|
+
return undefined;
|
|
24692
|
+
}
|
|
24693
|
+
score += bestFieldScore;
|
|
24694
|
+
}
|
|
24695
|
+
return score;
|
|
24696
|
+
}
|
|
24697
|
+
function filterCommits(commits, filter) {
|
|
24698
|
+
return commits
|
|
24699
|
+
.map((commit, index) => ({
|
|
24700
|
+
commit,
|
|
24701
|
+
index,
|
|
24702
|
+
score: scoreLogInkCommitFilter(commit, filter),
|
|
24703
|
+
}))
|
|
24704
|
+
.filter((entry) => entry.score !== undefined)
|
|
24705
|
+
.sort((a, b) => b.score - a.score || a.index - b.index)
|
|
24706
|
+
.map((entry) => entry.commit);
|
|
24707
|
+
}
|
|
24708
|
+
function clampIndex(index, length) {
|
|
24709
|
+
if (length === 0) {
|
|
24710
|
+
return 0;
|
|
24711
|
+
}
|
|
24712
|
+
return Math.max(0, Math.min(index, length - 1));
|
|
24713
|
+
}
|
|
24714
|
+
function cycleValue(values, current, delta) {
|
|
24715
|
+
const currentIndex = Math.max(0, values.indexOf(current));
|
|
24716
|
+
const nextIndex = (currentIndex + delta + values.length) % values.length;
|
|
24717
|
+
return values[nextIndex];
|
|
24718
|
+
}
|
|
24719
|
+
const HOME_VIEW = 'history';
|
|
24720
|
+
function topOfStack(stack) {
|
|
24721
|
+
return stack[stack.length - 1];
|
|
24722
|
+
}
|
|
24723
|
+
function withPushedView(state, value) {
|
|
24724
|
+
if (topOfStack(state.viewStack) === value) {
|
|
24725
|
+
return { ...state, pendingKey: undefined };
|
|
24726
|
+
}
|
|
24727
|
+
const viewStack = [...state.viewStack, value];
|
|
24087
24728
|
return {
|
|
24088
24729
|
...state,
|
|
24089
|
-
|
|
24090
|
-
|
|
24091
|
-
|
|
24092
|
-
|
|
24093
|
-
|
|
24094
|
-
|
|
24730
|
+
activeView: value,
|
|
24731
|
+
viewStack,
|
|
24732
|
+
// The compose + status views' right detail panels already show
|
|
24733
|
+
// worktree info, so keeping the left sidebar on the Status tab
|
|
24734
|
+
// duplicates that information. Auto-switch to Branches when entering
|
|
24735
|
+
// either view; the user can swap back with [/] if they want.
|
|
24736
|
+
//
|
|
24737
|
+
// We update only the rendered `sidebarTab` here, never
|
|
24738
|
+
// `userSidebarTab`, so this auto-switch is invisible to per-repo
|
|
24739
|
+
// persistence and pop-view restores the previous tab.
|
|
24740
|
+
sidebarTab: value === 'compose' || value === 'status' ? 'branches' : state.sidebarTab,
|
|
24741
|
+
worktreeDiffOffset: value === 'diff' ? state.worktreeDiffOffset : 0,
|
|
24742
|
+
selectedWorktreeHunkIndex: value === 'diff' ? state.selectedWorktreeHunkIndex : 0,
|
|
24743
|
+
diffSource: value === 'diff' ? state.diffSource : undefined,
|
|
24744
|
+
stashDiffRef: value === 'diff' ? state.stashDiffRef : undefined,
|
|
24745
|
+
compareHead: value === 'diff' ? state.compareHead : undefined,
|
|
24746
|
+
pendingCommitFocused: value === 'history' ? state.pendingCommitFocused : false,
|
|
24747
|
+
statusGroupHeaderFocused: value === 'status' ? state.statusGroupHeaderFocused : false,
|
|
24095
24748
|
pendingKey: undefined,
|
|
24096
24749
|
};
|
|
24097
24750
|
}
|
|
24098
|
-
function
|
|
24099
|
-
if (
|
|
24100
|
-
return
|
|
24101
|
-
}
|
|
24102
|
-
if (delta > 0) {
|
|
24103
|
-
const nextOffset = hunkOffsets.find((offset) => offset > currentOffset);
|
|
24104
|
-
return nextOffset === undefined ? currentOffset : nextOffset;
|
|
24751
|
+
function withPoppedView(state) {
|
|
24752
|
+
if (state.viewStack.length <= 1) {
|
|
24753
|
+
return { ...state, pendingKey: undefined };
|
|
24105
24754
|
}
|
|
24106
|
-
const
|
|
24107
|
-
|
|
24108
|
-
|
|
24109
|
-
|
|
24110
|
-
|
|
24111
|
-
|
|
24112
|
-
|
|
24113
|
-
|
|
24114
|
-
|
|
24755
|
+
const viewStack = state.viewStack.slice(0, -1);
|
|
24756
|
+
const next = topOfStack(viewStack);
|
|
24757
|
+
// #779 — compareBase is "cleared when the diff view is popped." We
|
|
24758
|
+
// detect that case by checking if the *previous* top was 'diff'.
|
|
24759
|
+
// The compare workflow ends when the user backs out of the compare
|
|
24760
|
+
// diff; on the next mark they re-set the base. Other view pops
|
|
24761
|
+
// preserve compareBase so the user can move between branches / tags /
|
|
24762
|
+
// history while hunting for a head ref.
|
|
24763
|
+
const wasOnDiff = state.activeView === 'diff';
|
|
24764
|
+
return {
|
|
24765
|
+
...state,
|
|
24766
|
+
activeView: next,
|
|
24767
|
+
viewStack,
|
|
24768
|
+
// Restore the user's last explicit tab choice so popping out of
|
|
24769
|
+
// compose / status (which auto-switch the sidebar to Branches)
|
|
24770
|
+
// returns the user to whatever they actually had open before.
|
|
24771
|
+
sidebarTab: state.userSidebarTab,
|
|
24772
|
+
worktreeDiffOffset: next === 'diff' ? state.worktreeDiffOffset : 0,
|
|
24773
|
+
selectedWorktreeHunkIndex: next === 'diff' ? state.selectedWorktreeHunkIndex : 0,
|
|
24774
|
+
diffSource: next === 'diff' ? state.diffSource : undefined,
|
|
24775
|
+
stashDiffRef: next === 'diff' ? state.stashDiffRef : undefined,
|
|
24776
|
+
compareBase: wasOnDiff ? undefined : state.compareBase,
|
|
24777
|
+
compareHead: next === 'diff' ? state.compareHead : undefined,
|
|
24778
|
+
pendingCommitFocused: next === 'history' ? state.pendingCommitFocused : false,
|
|
24779
|
+
statusGroupHeaderFocused: next === 'status' ? state.statusGroupHeaderFocused : false,
|
|
24780
|
+
pendingKey: undefined,
|
|
24781
|
+
};
|
|
24115
24782
|
}
|
|
24116
|
-
|
|
24117
|
-
|
|
24118
|
-
|
|
24783
|
+
/**
|
|
24784
|
+
* Push a nested-repo frame onto `state.repoStack` (#931). Snapshots
|
|
24785
|
+
* the active view position into the new frame's `parentReturn` so a
|
|
24786
|
+
* subsequent pop lands the user back where they came from, then
|
|
24787
|
+
* resets the per-frame navigation state (active view, view stack,
|
|
24788
|
+
* row / file / submodule cursors, filter) so the nested frame opens
|
|
24789
|
+
* in a clean slate — the mental equivalent of a fresh `coco ui`
|
|
24790
|
+
* launched against the submodule's working dir.
|
|
24791
|
+
*
|
|
24792
|
+
* Sidebar tab + branch / tag sort are also captured into the return
|
|
24793
|
+
* snapshot (#995) so popping back restores the parent's choices
|
|
24794
|
+
* instead of letting the submodule's tab/sort bleed across the
|
|
24795
|
+
* boundary. The values on the *new* frame are left as-is (carried
|
|
24796
|
+
* over from the parent) — the load effect in app.ts re-reads
|
|
24797
|
+
* persistence keyed on the submodule's workdir and dispatches a
|
|
24798
|
+
* restore if the user has a submodule-specific saved preference.
|
|
24799
|
+
*
|
|
24800
|
+
* Other preferences (palette recents, inspector tab, diff view mode)
|
|
24801
|
+
* stay global by design — the user's preference shouldn't reset when
|
|
24802
|
+
* they cross a submodule boundary.
|
|
24803
|
+
*
|
|
24804
|
+
* Live runtime objects (`SimpleGit`, loaded `LogInkContext`) live
|
|
24805
|
+
* outside the reducer in `app.ts`'s parallel ref structure — this
|
|
24806
|
+
* helper only manages the pure view-model side of the push.
|
|
24807
|
+
*/
|
|
24808
|
+
function withPushedRepoFrame(state, payload) {
|
|
24809
|
+
const newFrame = {
|
|
24810
|
+
label: payload.label,
|
|
24811
|
+
workdir: payload.workdir,
|
|
24812
|
+
entryRange: payload.entryRange,
|
|
24813
|
+
parentReturn: {
|
|
24814
|
+
activeView: state.activeView,
|
|
24815
|
+
selectedIndex: state.selectedIndex,
|
|
24816
|
+
selectedFileIndex: state.selectedFileIndex,
|
|
24817
|
+
selectedSubmoduleIndex: state.selectedSubmoduleIndex,
|
|
24818
|
+
filter: state.filter,
|
|
24819
|
+
sidebarTab: state.sidebarTab,
|
|
24820
|
+
userSidebarTab: state.userSidebarTab,
|
|
24821
|
+
branchSort: state.branchSort,
|
|
24822
|
+
tagSort: state.tagSort,
|
|
24823
|
+
},
|
|
24824
|
+
};
|
|
24119
24825
|
return {
|
|
24120
|
-
|
|
24121
|
-
|
|
24122
|
-
|
|
24123
|
-
|
|
24124
|
-
filteredCommits: commits,
|
|
24826
|
+
...state,
|
|
24827
|
+
repoStack: [...state.repoStack, newFrame],
|
|
24828
|
+
activeView: 'history',
|
|
24829
|
+
viewStack: ['history'],
|
|
24125
24830
|
selectedIndex: 0,
|
|
24126
24831
|
selectedFileIndex: 0,
|
|
24127
|
-
selectedWorktreeFileIndex: 0,
|
|
24128
|
-
selectedWorktreeHunkIndex: 0,
|
|
24129
|
-
selectedBranchIndex: 0,
|
|
24130
|
-
selectedTagIndex: 0,
|
|
24131
|
-
selectedStashIndex: 0,
|
|
24132
|
-
selectedWorktreeListIndex: 0,
|
|
24133
|
-
selectedConflictFileIndex: 0,
|
|
24134
|
-
selectedReflogIndex: 0,
|
|
24135
24832
|
selectedSubmoduleIndex: 0,
|
|
24136
|
-
selectedIssueIndex: 0,
|
|
24137
|
-
selectedPullRequestTriageIndex: 0,
|
|
24138
|
-
selectedIssueFilter: 'open',
|
|
24139
|
-
selectedPullRequestFilter: 'open',
|
|
24140
|
-
repoStack: [{ label: options.repoLabel || 'root', workdir: options.repoWorkdir }],
|
|
24141
|
-
branchSort: DEFAULT_BRANCH_SORT_MODE,
|
|
24142
|
-
tagSort: DEFAULT_TAG_SORT_MODE,
|
|
24143
|
-
paletteFilter: '',
|
|
24144
|
-
paletteSelectedIndex: 0,
|
|
24145
|
-
paletteRecent: [],
|
|
24146
|
-
commitCompose: createCommitComposeState(),
|
|
24147
|
-
diffPreviewOffset: 0,
|
|
24148
|
-
worktreeDiffOffset: 0,
|
|
24149
24833
|
filter: '',
|
|
24150
24834
|
filterMode: false,
|
|
24151
|
-
|
|
24152
|
-
|
|
24153
|
-
// out of the box. Pre-0.54.x this defaulted to false (current
|
|
24154
|
-
// branch only); user feedback consistently asked for the
|
|
24155
|
-
// GitKraken-style "see everything" view as the starting state.
|
|
24156
|
-
// The `\` toggle still flips back to compact / current-branch
|
|
24157
|
-
// mode for users who want the cleaner single-line graph. Tests
|
|
24158
|
-
// override via `options.fullGraph` when they need the compact
|
|
24159
|
-
// case explicitly.
|
|
24160
|
-
fullGraph: options.fullGraph ?? true,
|
|
24161
|
-
showHelp: false,
|
|
24162
|
-
helpScrollOffset: 0,
|
|
24163
|
-
showCommandPalette: false,
|
|
24164
|
-
workflowActionId: undefined,
|
|
24835
|
+
pendingCommitFocused: false,
|
|
24836
|
+
pendingKey: undefined,
|
|
24165
24837
|
pendingConfirmationId: undefined,
|
|
24166
24838
|
pendingConfirmationPayload: undefined,
|
|
24167
24839
|
pendingMutationConfirmation: undefined,
|
|
24168
|
-
|
|
24840
|
+
};
|
|
24841
|
+
}
|
|
24842
|
+
/**
|
|
24843
|
+
* Pop the top repo frame off `state.repoStack` (#931) and restore
|
|
24844
|
+
* the parent's view position from the captured `parentReturn`. A
|
|
24845
|
+
* no-op when the stack is already at its single root frame so this
|
|
24846
|
+
* action is safe to dispatch from generic input handlers (e.g. the
|
|
24847
|
+
* Esc auto-pop wiring that lands in a follow-up PR).
|
|
24848
|
+
*
|
|
24849
|
+
* The defensive `parentReturn` fallback handles the never-supposed-
|
|
24850
|
+
* to-happen case where a non-root frame somehow has no return state
|
|
24851
|
+
* recorded — drop the frame but leave the user's view position
|
|
24852
|
+
* alone rather than crash mid-session.
|
|
24853
|
+
*/
|
|
24854
|
+
function withPoppedRepoFrame(state) {
|
|
24855
|
+
if (state.repoStack.length <= 1) {
|
|
24856
|
+
return { ...state, pendingKey: undefined };
|
|
24857
|
+
}
|
|
24858
|
+
const topFrame = state.repoStack[state.repoStack.length - 1];
|
|
24859
|
+
const ret = topFrame.parentReturn;
|
|
24860
|
+
const repoStack = state.repoStack.slice(0, -1);
|
|
24861
|
+
if (!ret) {
|
|
24862
|
+
return { ...state, repoStack, pendingKey: undefined };
|
|
24863
|
+
}
|
|
24864
|
+
return {
|
|
24865
|
+
...state,
|
|
24866
|
+
repoStack,
|
|
24867
|
+
activeView: ret.activeView,
|
|
24868
|
+
viewStack: [ret.activeView],
|
|
24869
|
+
selectedIndex: ret.selectedIndex,
|
|
24870
|
+
selectedFileIndex: ret.selectedFileIndex,
|
|
24871
|
+
selectedSubmoduleIndex: ret.selectedSubmoduleIndex,
|
|
24872
|
+
filter: ret.filter,
|
|
24873
|
+
filterMode: false,
|
|
24874
|
+
pendingCommitFocused: false,
|
|
24875
|
+
// #995 — restore sidebar tab + sort preferences from the captured
|
|
24876
|
+
// parentReturn. Without this, the submodule's tab / sort choice
|
|
24877
|
+
// bleeds back into the parent after pop: the user picks 'tags' in
|
|
24878
|
+
// a vendored submodule, pops back to the parent, and finds the
|
|
24879
|
+
// parent's previously-selected 'branches' tab quietly replaced.
|
|
24880
|
+
sidebarTab: ret.sidebarTab,
|
|
24881
|
+
userSidebarTab: ret.userSidebarTab,
|
|
24882
|
+
branchSort: ret.branchSort,
|
|
24883
|
+
tagSort: ret.tagSort,
|
|
24884
|
+
pendingKey: undefined,
|
|
24885
|
+
pendingConfirmationId: undefined,
|
|
24886
|
+
pendingConfirmationPayload: undefined,
|
|
24887
|
+
pendingMutationConfirmation: undefined,
|
|
24888
|
+
};
|
|
24889
|
+
}
|
|
24890
|
+
function withReplacedView(state, value) {
|
|
24891
|
+
if (topOfStack(state.viewStack) === value) {
|
|
24892
|
+
return { ...state, pendingKey: undefined };
|
|
24893
|
+
}
|
|
24894
|
+
const viewStack = [...state.viewStack.slice(0, -1), value];
|
|
24895
|
+
return {
|
|
24896
|
+
...state,
|
|
24897
|
+
activeView: value,
|
|
24898
|
+
viewStack,
|
|
24899
|
+
worktreeDiffOffset: value === 'diff' ? state.worktreeDiffOffset : 0,
|
|
24900
|
+
selectedWorktreeHunkIndex: value === 'diff' ? state.selectedWorktreeHunkIndex : 0,
|
|
24901
|
+
diffSource: value === 'diff' ? state.diffSource : undefined,
|
|
24902
|
+
stashDiffRef: value === 'diff' ? state.stashDiffRef : undefined,
|
|
24903
|
+
compareHead: value === 'diff' ? state.compareHead : undefined,
|
|
24904
|
+
pendingCommitFocused: value === 'history' ? state.pendingCommitFocused : false,
|
|
24905
|
+
statusGroupHeaderFocused: value === 'status' ? state.statusGroupHeaderFocused : false,
|
|
24906
|
+
pendingKey: undefined,
|
|
24907
|
+
};
|
|
24908
|
+
}
|
|
24909
|
+
function withFilter(state, filter, promotedSelections) {
|
|
24910
|
+
const filteredCommits = filterCommits(state.commits, filter);
|
|
24911
|
+
// P4.5: rectify promoted-view selections when the filter changes. Prefer
|
|
24912
|
+
// the runtime-supplied snapshot — which preserves the cursor on the same
|
|
24913
|
+
// item when it's still in the filtered list and only snaps to result[0]
|
|
24914
|
+
// when the previously-selected item dropped out. Falls back to the older
|
|
24915
|
+
// "snap to 0" behavior when no snapshot was provided (test paths,
|
|
24916
|
+
// dispatchers without context).
|
|
24917
|
+
const filterChanged = state.filter !== filter;
|
|
24918
|
+
const branchIndex = promotedSelections?.branchIndex ??
|
|
24919
|
+
(filterChanged ? 0 : state.selectedBranchIndex);
|
|
24920
|
+
const tagIndex = promotedSelections?.tagIndex ??
|
|
24921
|
+
(filterChanged ? 0 : state.selectedTagIndex);
|
|
24922
|
+
const stashIndex = promotedSelections?.stashIndex ??
|
|
24923
|
+
(filterChanged ? 0 : state.selectedStashIndex);
|
|
24924
|
+
// Reflog (#781) snaps to 0 on filter change rather than rectifying.
|
|
24925
|
+
// The list is chronological and the user is unlikely to be tracking
|
|
24926
|
+
// a specific entry through filter changes — the simpler reset
|
|
24927
|
+
// matches the "find recovery target by typing" interaction.
|
|
24928
|
+
const reflogIndex = filterChanged ? 0 : state.selectedReflogIndex;
|
|
24929
|
+
return {
|
|
24930
|
+
...state,
|
|
24931
|
+
filter,
|
|
24932
|
+
filteredCommits,
|
|
24933
|
+
selectedIndex: clampIndex(state.selectedIndex, filteredCommits.length),
|
|
24934
|
+
selectedFileIndex: 0,
|
|
24935
|
+
selectedBranchIndex: branchIndex,
|
|
24936
|
+
selectedTagIndex: tagIndex,
|
|
24937
|
+
selectedStashIndex: stashIndex,
|
|
24938
|
+
selectedReflogIndex: reflogIndex,
|
|
24939
|
+
diffPreviewOffset: 0,
|
|
24940
|
+
pendingKey: undefined,
|
|
24941
|
+
};
|
|
24942
|
+
}
|
|
24943
|
+
function replaceRows(state, rows) {
|
|
24944
|
+
// Wholesale row replacement after a server-side re-fetch (#776).
|
|
24945
|
+
// Resets the cursor to the top because the new commit set may not
|
|
24946
|
+
// share any hashes with the old one (e.g. switching from `--all` to
|
|
24947
|
+
// `-- some/path` typically dumps the previous selection).
|
|
24948
|
+
const commits = getCommitRows(rows);
|
|
24949
|
+
const filteredCommits = filterCommits(commits, state.filter);
|
|
24950
|
+
return {
|
|
24951
|
+
...state,
|
|
24952
|
+
rows,
|
|
24953
|
+
commits,
|
|
24954
|
+
filteredCommits,
|
|
24955
|
+
selectedIndex: 0,
|
|
24956
|
+
selectedFileIndex: 0,
|
|
24957
|
+
pendingCommitFocused: false,
|
|
24958
|
+
pendingKey: undefined,
|
|
24959
|
+
// Rows just landed — clear the boot-loading flag so the history
|
|
24960
|
+
// surface drops the "Loading commits…" placeholder. Safe to clear
|
|
24961
|
+
// unconditionally because `replaceRows` only fires after a real
|
|
24962
|
+
// git log returns.
|
|
24963
|
+
bootLoading: false,
|
|
24964
|
+
};
|
|
24965
|
+
}
|
|
24966
|
+
function appendRows(state, rows) {
|
|
24967
|
+
const selected = getSelectedInkCommit(state);
|
|
24968
|
+
const nextRows = [...state.rows, ...rows];
|
|
24969
|
+
const seen = new Set();
|
|
24970
|
+
const commits = getCommitRows(nextRows).filter((commit) => {
|
|
24971
|
+
if (seen.has(commit.hash)) {
|
|
24972
|
+
return false;
|
|
24973
|
+
}
|
|
24974
|
+
seen.add(commit.hash);
|
|
24975
|
+
return true;
|
|
24976
|
+
});
|
|
24977
|
+
const filteredCommits = filterCommits(commits, state.filter);
|
|
24978
|
+
const selectedIndex = selected
|
|
24979
|
+
? filteredCommits.findIndex((commit) => commit.hash === selected.hash)
|
|
24980
|
+
: state.selectedIndex;
|
|
24981
|
+
return {
|
|
24982
|
+
...state,
|
|
24983
|
+
rows: nextRows,
|
|
24984
|
+
commits,
|
|
24985
|
+
filteredCommits,
|
|
24986
|
+
selectedIndex: selectedIndex >= 0
|
|
24987
|
+
? selectedIndex
|
|
24988
|
+
: clampIndex(state.selectedIndex, filteredCommits.length),
|
|
24989
|
+
pendingKey: undefined,
|
|
24990
|
+
};
|
|
24991
|
+
}
|
|
24992
|
+
function nextHunkOffset(currentOffset, hunkOffsets, delta) {
|
|
24993
|
+
if (hunkOffsets.length === 0) {
|
|
24994
|
+
return currentOffset;
|
|
24995
|
+
}
|
|
24996
|
+
if (delta > 0) {
|
|
24997
|
+
const nextOffset = hunkOffsets.find((offset) => offset > currentOffset);
|
|
24998
|
+
return nextOffset === undefined ? currentOffset : nextOffset;
|
|
24999
|
+
}
|
|
25000
|
+
const previousOffset = [...hunkOffsets].reverse().find((offset) => offset < currentOffset);
|
|
25001
|
+
return previousOffset === undefined ? currentOffset : previousOffset;
|
|
25002
|
+
}
|
|
25003
|
+
function nextHunkIndex(currentOffset, hunkOffsets, delta) {
|
|
25004
|
+
const offset = nextHunkOffset(currentOffset, hunkOffsets, delta);
|
|
25005
|
+
return Math.max(0, hunkOffsets.indexOf(offset));
|
|
25006
|
+
}
|
|
25007
|
+
function getLogInkSidebarTabs() {
|
|
25008
|
+
return [...SIDEBAR_TABS];
|
|
25009
|
+
}
|
|
25010
|
+
function createLogInkState(rows, options = {}) {
|
|
25011
|
+
const commits = getCommitRows(rows);
|
|
25012
|
+
const initialView = options.activeView || 'history';
|
|
25013
|
+
return {
|
|
25014
|
+
activeView: initialView,
|
|
25015
|
+
viewStack: [initialView],
|
|
25016
|
+
rows,
|
|
25017
|
+
commits,
|
|
25018
|
+
filteredCommits: commits,
|
|
25019
|
+
selectedIndex: 0,
|
|
25020
|
+
selectedFileIndex: 0,
|
|
25021
|
+
selectedWorktreeFileIndex: 0,
|
|
25022
|
+
selectedWorktreeHunkIndex: 0,
|
|
25023
|
+
selectedBranchIndex: 0,
|
|
25024
|
+
selectedTagIndex: 0,
|
|
25025
|
+
selectedStashIndex: 0,
|
|
25026
|
+
selectedWorktreeListIndex: 0,
|
|
25027
|
+
selectedConflictFileIndex: 0,
|
|
25028
|
+
selectedReflogIndex: 0,
|
|
25029
|
+
selectedSubmoduleIndex: 0,
|
|
25030
|
+
selectedIssueIndex: 0,
|
|
25031
|
+
selectedPullRequestTriageIndex: 0,
|
|
25032
|
+
selectedIssueFilter: 'open',
|
|
25033
|
+
selectedPullRequestFilter: 'open',
|
|
25034
|
+
repoStack: [{ label: options.repoLabel || 'root', workdir: options.repoWorkdir }],
|
|
25035
|
+
branchSort: DEFAULT_BRANCH_SORT_MODE,
|
|
25036
|
+
tagSort: DEFAULT_TAG_SORT_MODE,
|
|
25037
|
+
paletteFilter: '',
|
|
25038
|
+
paletteSelectedIndex: 0,
|
|
25039
|
+
paletteRecent: [],
|
|
25040
|
+
showThemePicker: false,
|
|
25041
|
+
themePickerFilter: '',
|
|
25042
|
+
themePickerIndex: 0,
|
|
25043
|
+
commitCompose: createCommitComposeState(),
|
|
25044
|
+
diffPreviewOffset: 0,
|
|
25045
|
+
worktreeDiffOffset: 0,
|
|
25046
|
+
filter: '',
|
|
25047
|
+
filterMode: false,
|
|
25048
|
+
// Default to the full multi-ref graph (`git log --all`) so users
|
|
25049
|
+
// see how branches, tags, and stashes weave through the history
|
|
25050
|
+
// out of the box. Pre-0.54.x this defaulted to false (current
|
|
25051
|
+
// branch only); user feedback consistently asked for the
|
|
25052
|
+
// GitKraken-style "see everything" view as the starting state.
|
|
25053
|
+
// The `\` toggle still flips back to compact / current-branch
|
|
25054
|
+
// mode for users who want the cleaner single-line graph. Tests
|
|
25055
|
+
// override via `options.fullGraph` when they need the compact
|
|
25056
|
+
// case explicitly.
|
|
25057
|
+
fullGraph: options.fullGraph ?? true,
|
|
25058
|
+
showHelp: false,
|
|
25059
|
+
helpScrollOffset: 0,
|
|
25060
|
+
showCommandPalette: false,
|
|
25061
|
+
workflowActionId: undefined,
|
|
25062
|
+
pendingConfirmationId: undefined,
|
|
25063
|
+
pendingConfirmationPayload: undefined,
|
|
25064
|
+
pendingMutationConfirmation: undefined,
|
|
25065
|
+
pendingKey: undefined,
|
|
24169
25066
|
focus: 'commits',
|
|
24170
25067
|
// Default first-time tab is 'branches' — it's the most useful
|
|
24171
25068
|
// landing surface in the workstation (current branch + recent
|
|
@@ -24904,6 +25801,46 @@ function applyLogInkAction(state, action) {
|
|
|
24904
25801
|
pendingKey: undefined,
|
|
24905
25802
|
};
|
|
24906
25803
|
}
|
|
25804
|
+
case 'toggleThemePicker': {
|
|
25805
|
+
const opening = !state.showThemePicker;
|
|
25806
|
+
return {
|
|
25807
|
+
...state,
|
|
25808
|
+
showThemePicker: opening,
|
|
25809
|
+
// Only one overlay at a time — close help / palette on open.
|
|
25810
|
+
showHelp: false,
|
|
25811
|
+
showCommandPalette: false,
|
|
25812
|
+
themePickerFilter: '',
|
|
25813
|
+
themePickerIndex: 0,
|
|
25814
|
+
pendingKey: undefined,
|
|
25815
|
+
};
|
|
25816
|
+
}
|
|
25817
|
+
case 'moveThemePicker':
|
|
25818
|
+
return {
|
|
25819
|
+
...state,
|
|
25820
|
+
themePickerIndex: clampIndex(state.themePickerIndex + action.delta, action.presetCount),
|
|
25821
|
+
pendingKey: undefined,
|
|
25822
|
+
};
|
|
25823
|
+
case 'appendThemePickerFilter':
|
|
25824
|
+
return {
|
|
25825
|
+
...state,
|
|
25826
|
+
themePickerFilter: `${state.themePickerFilter}${action.value}`,
|
|
25827
|
+
themePickerIndex: 0,
|
|
25828
|
+
pendingKey: undefined,
|
|
25829
|
+
};
|
|
25830
|
+
case 'backspaceThemePickerFilter':
|
|
25831
|
+
return {
|
|
25832
|
+
...state,
|
|
25833
|
+
themePickerFilter: state.themePickerFilter.slice(0, -1),
|
|
25834
|
+
themePickerIndex: 0,
|
|
25835
|
+
pendingKey: undefined,
|
|
25836
|
+
};
|
|
25837
|
+
case 'clearThemePickerFilter':
|
|
25838
|
+
return {
|
|
25839
|
+
...state,
|
|
25840
|
+
themePickerFilter: '',
|
|
25841
|
+
themePickerIndex: 0,
|
|
25842
|
+
pendingKey: undefined,
|
|
25843
|
+
};
|
|
24907
25844
|
case 'setChangelogLoading':
|
|
24908
25845
|
return {
|
|
24909
25846
|
...state,
|
|
@@ -25111,6 +26048,71 @@ function applyLogInkAction(state, action) {
|
|
|
25111
26048
|
return state;
|
|
25112
26049
|
}
|
|
25113
26050
|
}
|
|
26051
|
+
/**
|
|
26052
|
+
* Fuzzy (subsequence) score for a preset id against a lowercase query.
|
|
26053
|
+
* Returns `null` when the query chars don't appear in order; otherwise a
|
|
26054
|
+
* score where contiguous runs, a start-of-string match, and matches right
|
|
26055
|
+
* after a `-` separator are rewarded — so `gl` ranks `gruvbox-light` /
|
|
26056
|
+
* `github-light` above incidental matches, and `tn` finds `tokyo-night`.
|
|
26057
|
+
*/
|
|
26058
|
+
function fuzzyScoreThemePreset(preset, query) {
|
|
26059
|
+
const target = preset.toLowerCase();
|
|
26060
|
+
let qi = 0;
|
|
26061
|
+
let score = 0;
|
|
26062
|
+
let lastMatch = -2;
|
|
26063
|
+
for (let i = 0; i < target.length && qi < query.length; i += 1) {
|
|
26064
|
+
if (target[i] === query[qi]) {
|
|
26065
|
+
score += 1;
|
|
26066
|
+
if (i === lastMatch + 1)
|
|
26067
|
+
score += 4; // contiguous run
|
|
26068
|
+
if (i === 0)
|
|
26069
|
+
score += 8; // matches the very start
|
|
26070
|
+
else if (target[i - 1] === '-')
|
|
26071
|
+
score += 4; // start of a word segment
|
|
26072
|
+
lastMatch = i;
|
|
26073
|
+
qi += 1;
|
|
26074
|
+
}
|
|
26075
|
+
}
|
|
26076
|
+
return qi === query.length ? score : null;
|
|
26077
|
+
}
|
|
26078
|
+
/**
|
|
26079
|
+
* Filter the full preset list by a fuzzy (subsequence) query, ranked best
|
|
26080
|
+
* match first (ties broken by catalog order). An empty query returns every
|
|
26081
|
+
* preset in catalog order. Shared by the theme picker overlay renderer, the
|
|
26082
|
+
* input handler (for cursor bounds), and the live-preview selector so all
|
|
26083
|
+
* three agree on the same filtered list.
|
|
26084
|
+
*/
|
|
26085
|
+
function filterThemePresets(filter) {
|
|
26086
|
+
const query = filter.trim().toLowerCase();
|
|
26087
|
+
const all = getLogInkThemePresets();
|
|
26088
|
+
if (!query) {
|
|
26089
|
+
return all;
|
|
26090
|
+
}
|
|
26091
|
+
return all
|
|
26092
|
+
.map((preset, index) => ({ preset, index, score: fuzzyScoreThemePreset(preset, query) }))
|
|
26093
|
+
.filter((entry) => entry.score !== null)
|
|
26094
|
+
.sort((a, b) => b.score - a.score || a.index - b.index)
|
|
26095
|
+
.map((entry) => entry.preset);
|
|
26096
|
+
}
|
|
26097
|
+
/**
|
|
26098
|
+
* The preset currently under the theme-picker cursor (clamped to the
|
|
26099
|
+
* filtered list). `undefined` when the filter matches nothing.
|
|
26100
|
+
*/
|
|
26101
|
+
function getThemePickerSelection(state) {
|
|
26102
|
+
return getThemePickerSelectionFor(state.themePickerFilter, state.themePickerIndex);
|
|
26103
|
+
}
|
|
26104
|
+
/**
|
|
26105
|
+
* State-model-agnostic variant: the preset under the picker cursor for a
|
|
26106
|
+
* raw `filter` + `index`. Used by the workspace top-level surface, which
|
|
26107
|
+
* keeps its own state shape but shares the picker filtering.
|
|
26108
|
+
*/
|
|
26109
|
+
function getThemePickerSelectionFor(filter, index) {
|
|
26110
|
+
const filtered = filterThemePresets(filter);
|
|
26111
|
+
if (filtered.length === 0) {
|
|
26112
|
+
return undefined;
|
|
26113
|
+
}
|
|
26114
|
+
return filtered[clampIndex(index, filtered.length)];
|
|
26115
|
+
}
|
|
25114
26116
|
|
|
25115
26117
|
/**
|
|
25116
26118
|
* In-sidebar selection helpers (#791 follow-up — sidebar entity ops).
|
|
@@ -25572,6 +26574,10 @@ function getLogInkPaletteExecuteEvents(command, state) {
|
|
|
25572
26574
|
case 'commandPalette':
|
|
25573
26575
|
// Re-toggling closes; the dispatcher will close after execute anyway.
|
|
25574
26576
|
return [];
|
|
26577
|
+
case 'themePicker':
|
|
26578
|
+
// Palette closes on execute (toggleCommandPalette runs first), then
|
|
26579
|
+
// this opens the theme picker.
|
|
26580
|
+
return [action({ type: 'toggleThemePicker' })];
|
|
25575
26581
|
case 'workflowDeleteBranch':
|
|
25576
26582
|
case 'workflowDeleteTag':
|
|
25577
26583
|
case 'workflowDropStash':
|
|
@@ -26104,6 +27110,45 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
|
|
|
26104
27110
|
// overlay open without dispatching any state change.
|
|
26105
27111
|
return [];
|
|
26106
27112
|
}
|
|
27113
|
+
if (state.showThemePicker) {
|
|
27114
|
+
const filtered = filterThemePresets(state.themePickerFilter);
|
|
27115
|
+
if (key.escape) {
|
|
27116
|
+
// Two-stage Esc: clear a non-empty filter first, then close (and
|
|
27117
|
+
// revert the live preview to the previously-active theme).
|
|
27118
|
+
if (state.themePickerFilter.length > 0) {
|
|
27119
|
+
return [action({ type: 'clearThemePickerFilter' })];
|
|
27120
|
+
}
|
|
27121
|
+
return [action({ type: 'toggleThemePicker' })];
|
|
27122
|
+
}
|
|
27123
|
+
if (key.return) {
|
|
27124
|
+
const selected = getThemePickerSelection(state);
|
|
27125
|
+
if (!selected) {
|
|
27126
|
+
return [action({ type: 'toggleThemePicker' })];
|
|
27127
|
+
}
|
|
27128
|
+
return [
|
|
27129
|
+
action({ type: 'toggleThemePicker' }),
|
|
27130
|
+
{ type: 'applyThemePreset', preset: selected },
|
|
27131
|
+
];
|
|
27132
|
+
}
|
|
27133
|
+
if (key.upArrow || (key.ctrl && inputValue === 'p')) {
|
|
27134
|
+
return [action({ type: 'moveThemePicker', delta: -1, presetCount: filtered.length })];
|
|
27135
|
+
}
|
|
27136
|
+
if (key.downArrow || (key.ctrl && inputValue === 'n')) {
|
|
27137
|
+
return [action({ type: 'moveThemePicker', delta: 1, presetCount: filtered.length })];
|
|
27138
|
+
}
|
|
27139
|
+
if (key.backspace || key.delete) {
|
|
27140
|
+
return [action({ type: 'backspaceThemePickerFilter' })];
|
|
27141
|
+
}
|
|
27142
|
+
if (key.ctrl && inputValue === 'u') {
|
|
27143
|
+
return [action({ type: 'clearThemePickerFilter' })];
|
|
27144
|
+
}
|
|
27145
|
+
// All other printable input filters the list (so `j`/`k` type into the
|
|
27146
|
+
// filter rather than navigating — matching the command palette).
|
|
27147
|
+
if (inputValue && !key.ctrl && !key.meta) {
|
|
27148
|
+
return [action({ type: 'appendThemePickerFilter', value: inputValue })];
|
|
27149
|
+
}
|
|
27150
|
+
return [];
|
|
27151
|
+
}
|
|
26107
27152
|
if (state.showCommandPalette) {
|
|
26108
27153
|
const filtered = filterLogInkPaletteCommands(getLogInkPaletteCommands(), state.paletteFilter, state.paletteRecent);
|
|
26109
27154
|
if (key.escape) {
|
|
@@ -26374,6 +27419,13 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
|
|
|
26374
27419
|
action({ type: 'setStatus', value: 'gT creates a tag at the cursored commit on the history view', kind: 'warning' }),
|
|
26375
27420
|
];
|
|
26376
27421
|
}
|
|
27422
|
+
// gC — open the theme picker (browse + live-preview + apply a color theme).
|
|
27423
|
+
if (state.pendingKey === 'g' && inputValue === 'C') {
|
|
27424
|
+
return [
|
|
27425
|
+
action({ type: 'setPendingKey', value: undefined }),
|
|
27426
|
+
action({ type: 'toggleThemePicker' }),
|
|
27427
|
+
];
|
|
27428
|
+
}
|
|
26377
27429
|
// #784 — bisect view action keys. Scoped to `state.activeView ===
|
|
26378
27430
|
// 'bisect' && state.focus === 'commits'` so the single-letter keys
|
|
26379
27431
|
// stay free everywhere else. `g` and `b` collide with the global
|
|
@@ -27845,6 +28897,60 @@ function markOnboardingSeen() {
|
|
|
27845
28897
|
}
|
|
27846
28898
|
}
|
|
27847
28899
|
|
|
28900
|
+
/**
|
|
28901
|
+
* Persist the user's chosen `coco ui` theme preset to the global XDG
|
|
28902
|
+
* config (`$XDG_CONFIG_HOME/coco/config.json`, default `~/.config/...`),
|
|
28903
|
+
* so a theme picked in the workstation sticks across every repo and
|
|
28904
|
+
* launch. This is the same file `loadXDGConfig` reads.
|
|
28905
|
+
*
|
|
28906
|
+
* Read-modify-write that preserves every other key (we only touch
|
|
28907
|
+
* `logTui.theme.preset`), unlike the whole-object project-config writer.
|
|
28908
|
+
* Best-effort: a read-only HOME or malformed file never throws — the
|
|
28909
|
+
* picker still applies the theme for the session.
|
|
28910
|
+
*/
|
|
28911
|
+
const VALID_PRESETS = new Set(getLogInkThemePresets());
|
|
28912
|
+
function getXdgConfigPath() {
|
|
28913
|
+
const home = process.env.XDG_CONFIG_HOME || path$1.join(os$1.homedir(), '.config');
|
|
28914
|
+
return path$1.join(home, 'coco', 'config.json');
|
|
28915
|
+
}
|
|
28916
|
+
function isRecord(value) {
|
|
28917
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
28918
|
+
}
|
|
28919
|
+
/**
|
|
28920
|
+
* Write `logTui.theme.preset = <preset>` into the global config, merging
|
|
28921
|
+
* into any existing content. Returns `true` on success, `false` if the
|
|
28922
|
+
* preset is unknown or the write failed (caller treats failure as
|
|
28923
|
+
* "applied for this session only").
|
|
28924
|
+
*/
|
|
28925
|
+
function saveThemePreset(preset) {
|
|
28926
|
+
if (!VALID_PRESETS.has(preset)) {
|
|
28927
|
+
return false;
|
|
28928
|
+
}
|
|
28929
|
+
const file = getXdgConfigPath();
|
|
28930
|
+
try {
|
|
28931
|
+
let config = {};
|
|
28932
|
+
try {
|
|
28933
|
+
const parsed = JSON.parse(fs$1.readFileSync(file, 'utf8'));
|
|
28934
|
+
if (isRecord(parsed)) {
|
|
28935
|
+
config = parsed;
|
|
28936
|
+
}
|
|
28937
|
+
}
|
|
28938
|
+
catch {
|
|
28939
|
+
// No existing file (or unreadable/malformed) — start fresh.
|
|
28940
|
+
config = {};
|
|
28941
|
+
}
|
|
28942
|
+
const logTui = isRecord(config.logTui) ? config.logTui : {};
|
|
28943
|
+
const theme = isRecord(logTui.theme) ? logTui.theme : {};
|
|
28944
|
+
config.logTui = { ...logTui, theme: { ...theme, preset } };
|
|
28945
|
+
fs$1.mkdirSync(path$1.dirname(file), { recursive: true });
|
|
28946
|
+
fs$1.writeFileSync(file, `${JSON.stringify(config, null, 2)}\n`);
|
|
28947
|
+
return true;
|
|
28948
|
+
}
|
|
28949
|
+
catch {
|
|
28950
|
+
return false;
|
|
28951
|
+
}
|
|
28952
|
+
}
|
|
28953
|
+
|
|
27848
28954
|
/**
|
|
27849
28955
|
* Status-line hints for "what to do next" after a workflow that
|
|
27850
28956
|
* mutates the worktree (split-apply, etc.). Pure formatting — the
|
|
@@ -32180,6 +33286,10 @@ function renderBranchesSurface(h, components, state, context, contextStatus, bod
|
|
|
32180
33286
|
// row's dim and read as quiet chrome.
|
|
32181
33287
|
h(Text, { color: markerColor, dimColor: markerColor ? false : undefined }, marker.glyph), trailingName, h(Text, { dimColor: true }, timestampPadded), trailingDivergence);
|
|
32182
33288
|
});
|
|
33289
|
+
// Scroll indicators — same "N more above/below" pattern as the
|
|
33290
|
+
// sidebar and help overlay so the user knows the list continues.
|
|
33291
|
+
const branchesHasMoreAbove = startIndex > 0 && localBranches.length > 0;
|
|
33292
|
+
const branchesHasMoreBelow = startIndex + listRows < localBranches.length;
|
|
32183
33293
|
return h(Box, {
|
|
32184
33294
|
borderColor: focusBorderColor(theme, focused),
|
|
32185
33295
|
borderStyle: theme.borderStyle,
|
|
@@ -32187,7 +33297,11 @@ function renderBranchesSurface(h, components, state, context, contextStatus, bod
|
|
|
32187
33297
|
flexShrink: 0,
|
|
32188
33298
|
paddingX: 1,
|
|
32189
33299
|
width,
|
|
32190
|
-
}, h(Box, { justifyContent: 'space-between' }, h(Text, { bold: true }, panelTitle('Branches', focused)), h(Text, { dimColor: true }, headerRight)), ...renderPromotedFilterAffordance(h, Text, state, theme), ...
|
|
33300
|
+
}, h(Box, { justifyContent: 'space-between' }, h(Text, { bold: true }, panelTitle('Branches', focused)), h(Text, { dimColor: true }, headerRight)), ...renderPromotedFilterAffordance(h, Text, state, theme), ...(branchesHasMoreAbove
|
|
33301
|
+
? [h(Text, { key: 'branches-more-above', dimColor: true }, ` ↑ ${startIndex} more above`)]
|
|
33302
|
+
: []), ...lines, ...(branchesHasMoreBelow
|
|
33303
|
+
? [h(Text, { key: 'branches-more-below', dimColor: true }, ` ↓ ${localBranches.length - (startIndex + listRows)} more below`)]
|
|
33304
|
+
: []));
|
|
32191
33305
|
}
|
|
32192
33306
|
|
|
32193
33307
|
/**
|
|
@@ -32960,12 +34074,19 @@ function renderDiffSurface(h, components, state, context, contextStatus, worktre
|
|
|
32960
34074
|
// sees at a glance which file the cursor is inside.
|
|
32961
34075
|
const isActive = absoluteIndex === activeStartLine;
|
|
32962
34076
|
const arrow = theme.ascii ? '> ' : '▾ ';
|
|
34077
|
+
const activeHeader = isActive && focused && !theme.noColor;
|
|
32963
34078
|
return h(Text, {
|
|
32964
34079
|
key: `stash-diff-line-${absoluteIndex}`,
|
|
32965
34080
|
bold: true,
|
|
32966
|
-
|
|
32967
|
-
|
|
32968
|
-
inverse
|
|
34081
|
+
// Active header sits on the selection bar with a
|
|
34082
|
+
// contrast-guaranteed foreground (matches history/status).
|
|
34083
|
+
// The old `inverse` swap turned the accent into the bar and
|
|
34084
|
+
// left the path in the selection color — low-contrast on
|
|
34085
|
+
// light themes (e.g. accent blue bar + light-gray text).
|
|
34086
|
+
color: activeHeader
|
|
34087
|
+
? theme.colors.selectionForeground
|
|
34088
|
+
: (theme.noColor ? undefined : theme.colors.accent),
|
|
34089
|
+
backgroundColor: activeHeader ? theme.colors.selection : undefined,
|
|
32969
34090
|
}, (() => {
|
|
32970
34091
|
// Smart path truncation for the diff file header: keep
|
|
32971
34092
|
// the leading arrow glyph and elide middle path
|
|
@@ -34043,7 +35164,7 @@ function formatHistoryFetchArgs(args) {
|
|
|
34043
35164
|
* Returns the spans flat so the caller can splat them into the row's
|
|
34044
35165
|
* outer Text alongside other segments without an extra wrapper.
|
|
34045
35166
|
*/
|
|
34046
|
-
function renderTypedSubject(h, Text, text, theme, key) {
|
|
35167
|
+
function renderTypedSubject(h, Text, text, theme, key, suppressColor = false) {
|
|
34047
35168
|
const parsed = parseConventionalCommitPrefix(text);
|
|
34048
35169
|
if (!parsed) {
|
|
34049
35170
|
return [h(Text, { key: `${key}-msg` }, text)];
|
|
@@ -34051,7 +35172,9 @@ function renderTypedSubject(h, Text, text, theme, key) {
|
|
|
34051
35172
|
if (text.length < parsed.prefix.length) {
|
|
34052
35173
|
return [h(Text, { key: `${key}-msg` }, text)];
|
|
34053
35174
|
}
|
|
34054
|
-
|
|
35175
|
+
// When the row is selected (inverted), suppress the type color so
|
|
35176
|
+
// text inherits the dark inverted foreground and stays readable.
|
|
35177
|
+
const color = suppressColor ? undefined : getConventionalCommitColor(parsed, theme);
|
|
34055
35178
|
return [
|
|
34056
35179
|
h(Text, { key: `${key}-type`, color, bold: parsed.breaking }, parsed.prefix),
|
|
34057
35180
|
h(Text, { key: `${key}-rest` }, text.slice(parsed.prefix.length)),
|
|
@@ -34072,15 +35195,10 @@ function renderLaneSegmentSpans(h, Text, segments, theme, padTo, keyPrefix, opti
|
|
|
34072
35195
|
const elements = [];
|
|
34073
35196
|
let totalLen = 0;
|
|
34074
35197
|
segments.forEach((seg, idx) => {
|
|
34075
|
-
const laneColor = getLaneColor(seg.laneId, theme);
|
|
35198
|
+
const laneColor = options.suppressColor ? undefined : (getLaneColor(seg.laneId, theme) ?? muted);
|
|
34076
35199
|
elements.push(h(Text, {
|
|
34077
35200
|
key: `${keyPrefix}-${idx}`,
|
|
34078
|
-
color: laneColor
|
|
34079
|
-
// Ink does not cascade dimColor from a parent Text to children,
|
|
34080
|
-
// so the caller's "this whole row should fade" intent has to
|
|
34081
|
-
// travel here as an explicit flag (#831). Used for graph-only
|
|
34082
|
-
// lane-closure rows, where the lane colors otherwise compete
|
|
34083
|
-
// for attention with the commits they connect.
|
|
35201
|
+
color: laneColor,
|
|
34084
35202
|
dimColor: options.forceDim || (theme.noColor && seg.laneId === undefined),
|
|
34085
35203
|
}, seg.text));
|
|
34086
35204
|
totalLen += seg.text.length;
|
|
@@ -34131,18 +35249,26 @@ function renderCommitHistoryRow(h, Text, commit, graph, graphWidth, selected, th
|
|
|
34131
35249
|
const messageRoom = Math.max(8, totalWidth - fixedWidth - cellWidth(refsTrunc));
|
|
34132
35250
|
const message = truncateCells(commit.message, messageRoom);
|
|
34133
35251
|
const selectedBg = selected && !theme.noColor ? theme.colors.selection : undefined;
|
|
34134
|
-
|
|
34135
|
-
|
|
35252
|
+
// Don't use inverse — it makes child colors unreadable. Instead, set a
|
|
35253
|
+
// background on the row AND an explicit, contrast-guaranteed foreground
|
|
35254
|
+
// (`selectionForeground`, derived from the selection bg) on the outer
|
|
35255
|
+
// span. Suppressing each child's own color to `undefined` then lets it
|
|
35256
|
+
// inherit that readable foreground — so the whole selected row stays
|
|
35257
|
+
// legible regardless of the user's terminal default foreground, which
|
|
35258
|
+
// is what the old "rely on the default fg" approach got wrong.
|
|
35259
|
+
const selectedFg = selected && !theme.noColor ? theme.colors.selectionForeground : undefined;
|
|
35260
|
+
const accent = selected ? undefined : (theme.noColor ? undefined : theme.colors.accent);
|
|
35261
|
+
const muted = selected ? undefined : (theme.noColor ? undefined : theme.colors.muted);
|
|
34136
35262
|
// Lane-colored graph spans when full graph mode + non-ASCII rendering
|
|
34137
35263
|
// is in play; otherwise fall back to the legacy single-muted span so
|
|
34138
35264
|
// compact mode and legacy terminals stay visually unchanged.
|
|
34139
35265
|
const graphChildren = laneSegments && !theme.ascii
|
|
34140
|
-
? renderLaneSegmentSpans(h, Text, laneSegments, theme, graphWidth, `c${index}
|
|
34141
|
-
: [h(Text, { color: muted, dimColor: theme.noColor }, substituteGraphChars(graph.padEnd(graphWidth), { ascii: theme.ascii }))];
|
|
35266
|
+
? renderLaneSegmentSpans(h, Text, laneSegments, theme, graphWidth, `c${index}`, { suppressColor: selected })
|
|
35267
|
+
: [h(Text, { color: muted, dimColor: !selected && theme.noColor }, substituteGraphChars(graph.padEnd(graphWidth), { ascii: theme.ascii }))];
|
|
34142
35268
|
return h(Text, {
|
|
34143
35269
|
key: `${commit.hash}-${index}`,
|
|
34144
35270
|
backgroundColor: selectedBg,
|
|
34145
|
-
|
|
35271
|
+
color: selectedFg,
|
|
34146
35272
|
}, ...graphChildren, ' ',
|
|
34147
35273
|
// "Just landed" marker — a single thick vertical bar in the
|
|
34148
35274
|
// accent color before the short hash. Fades when the runtime
|
|
@@ -34164,11 +35290,11 @@ function renderCommitHistoryRow(h, Text, commit, graph, graphWidth, selected, th
|
|
|
34164
35290
|
// Date column drops out entirely at `tight` density — no spacer
|
|
34165
35291
|
// either, so the message column slides left into the freed cells.
|
|
34166
35292
|
dateText
|
|
34167
|
-
? h(Text, { key: `${commit.hash}-${index}-date`, dimColor:
|
|
35293
|
+
? h(Text, { key: `${commit.hash}-${index}-date`, dimColor: !selected }, dateText, ' ')
|
|
34168
35294
|
: null,
|
|
34169
35295
|
// Branch chip prefix (full-graph mode only) lands right before the
|
|
34170
35296
|
// message so the eye reads "branch · subject" as a unit.
|
|
34171
|
-
chip.node, ...renderTypedSubject(h, Text, message, theme, `${commit.hash}-${index}-subj
|
|
35297
|
+
chip.node, ...renderTypedSubject(h, Text, message, theme, `${commit.hash}-${index}-subj`, selected), refsTrunc ? h(Text, { color: accent }, refsTrunc) : null);
|
|
34172
35298
|
}
|
|
34173
35299
|
/**
|
|
34174
35300
|
* Stacked variant used at `rowMode='stacked'` (rail tier). Each
|
|
@@ -34183,9 +35309,13 @@ function renderCommitHistoryRow(h, Text, commit, graph, graphWidth, selected, th
|
|
|
34183
35309
|
*/
|
|
34184
35310
|
function renderStackedCommitHistoryRow(h, Text, Box, commit, graph, graphWidth, selected, theme, index, panelWidth, fullGraph, now, laneSegments, isRecent = false, remoteNames) {
|
|
34185
35311
|
const totalWidth = Math.max(20, panelWidth - 4);
|
|
34186
|
-
|
|
34187
|
-
|
|
35312
|
+
// Suppress child colors on selected rows so each span inherits the
|
|
35313
|
+
// contrast-guaranteed `selectionForeground` set on the line-1 span,
|
|
35314
|
+
// keeping the selected row readable against the selection bg.
|
|
35315
|
+
const accent = selected ? undefined : (theme.noColor ? undefined : theme.colors.accent);
|
|
35316
|
+
const muted = selected ? undefined : (theme.noColor ? undefined : theme.colors.muted);
|
|
34188
35317
|
const selectedBg = selected && !theme.noColor ? theme.colors.selection : undefined;
|
|
35318
|
+
const selectedFg = selected && !theme.noColor ? theme.colors.selectionForeground : undefined;
|
|
34189
35319
|
// Line 1 — subject row. Mostly mirrors the single-line layout but
|
|
34190
35320
|
// skips the date and refs so the message has the whole tail to
|
|
34191
35321
|
// itself. Branch chip rides between the hash and the subject the
|
|
@@ -34197,15 +35327,15 @@ function renderStackedCommitHistoryRow(h, Text, Box, commit, graph, graphWidth,
|
|
|
34197
35327
|
const lineOneFixed = graphWidth + 1 + commit.shortHash.length + 1 + recentMarkerWidth + chip.width;
|
|
34198
35328
|
const subject = truncateCells(commit.message, Math.max(8, totalWidth - lineOneFixed));
|
|
34199
35329
|
const graphChildren = laneSegments && !theme.ascii
|
|
34200
|
-
? renderLaneSegmentSpans(h, Text, laneSegments, theme, graphWidth, `cs${index}
|
|
34201
|
-
: [h(Text, { color: muted, dimColor: theme.noColor }, substituteGraphChars(graph.padEnd(graphWidth), { ascii: theme.ascii }))];
|
|
35330
|
+
? renderLaneSegmentSpans(h, Text, laneSegments, theme, graphWidth, `cs${index}`, { suppressColor: selected })
|
|
35331
|
+
: [h(Text, { color: muted, dimColor: !selected && theme.noColor }, substituteGraphChars(graph.padEnd(graphWidth), { ascii: theme.ascii }))];
|
|
34202
35332
|
const lineOne = h(Text, {
|
|
34203
35333
|
key: `${commit.hash}-${index}-l1`,
|
|
34204
35334
|
backgroundColor: selectedBg,
|
|
34205
|
-
|
|
35335
|
+
color: selectedFg,
|
|
34206
35336
|
}, ...graphChildren, ' ', isRecent
|
|
34207
35337
|
? h(Text, { color: accent, bold: true }, theme.ascii ? '* ' : '▎ ')
|
|
34208
|
-
: null, h(Text, { color: accent, bold: selected || isRecent }, commit.shortHash), ' ', chip.node, ...renderTypedSubject(h, Text, subject, theme, `${commit.hash}-${index}-stk-subj
|
|
35338
|
+
: null, h(Text, { color: accent, bold: selected || isRecent }, commit.shortHash), ' ', chip.node, ...renderTypedSubject(h, Text, subject, theme, `${commit.hash}-${index}-stk-subj`, selected));
|
|
34209
35339
|
// Line 2 — metadata row, padded to align with the start of the
|
|
34210
35340
|
// shortHash on line 1 so the eye still groups them as one commit.
|
|
34211
35341
|
// Selection background does not extend here so we don't get a thick
|
|
@@ -34258,8 +35388,11 @@ function renderPendingCommitRow(h, Text, worktree, selected, theme) {
|
|
|
34258
35388
|
return h(Text, {
|
|
34259
35389
|
key: 'pending-commit-row',
|
|
34260
35390
|
bold: true,
|
|
34261
|
-
|
|
34262
|
-
|
|
35391
|
+
// On selection, swap to the contrast-guaranteed foreground so the
|
|
35392
|
+
// accent label doesn't wash out against the selection bar.
|
|
35393
|
+
color: selected && !theme.noColor
|
|
35394
|
+
? theme.colors.selectionForeground
|
|
35395
|
+
: (theme.noColor ? undefined : theme.colors.accent),
|
|
34263
35396
|
backgroundColor: selected && !theme.noColor ? theme.colors.selection : undefined,
|
|
34264
35397
|
}, truncateCells(label, 140));
|
|
34265
35398
|
}
|
|
@@ -34673,6 +35806,10 @@ function renderCommandPalette(h, components, state, width, theme, focused) {
|
|
|
34673
35806
|
dimColor: !isSelected,
|
|
34674
35807
|
}, truncateCells(line, width - 4));
|
|
34675
35808
|
});
|
|
35809
|
+
// Scroll indicators for the palette list — same pattern as the
|
|
35810
|
+
// sidebar and help overlay so the user knows there's more content.
|
|
35811
|
+
const paletteHasMoreAbove = startIndex > 0 && filtered.length > 0;
|
|
35812
|
+
const paletteHasMoreBelow = startIndex + listRows < filtered.length;
|
|
34676
35813
|
return h(Box, {
|
|
34677
35814
|
borderColor: focusBorderColor(theme, focused),
|
|
34678
35815
|
borderStyle: theme.borderStyle,
|
|
@@ -34681,7 +35818,66 @@ function renderCommandPalette(h, components, state, width, theme, focused) {
|
|
|
34681
35818
|
paddingX: 1,
|
|
34682
35819
|
}, 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
|
|
34683
35820
|
? [h(Text, { key: 'palette-recent-hint', dimColor: true }, '· marks recently-used')]
|
|
34684
|
-
: []), ...
|
|
35821
|
+
: []), ...(paletteHasMoreAbove
|
|
35822
|
+
? [h(Text, { key: 'palette-more-above', dimColor: true }, ` ↑ ${startIndex} more above`)]
|
|
35823
|
+
: []), ...itemLines, ...(paletteHasMoreBelow
|
|
35824
|
+
? [h(Text, { key: 'palette-more-below', dimColor: true }, ` ↓ ${filtered.length - (startIndex + listRows)} more below`)]
|
|
35825
|
+
: []));
|
|
35826
|
+
}
|
|
35827
|
+
/**
|
|
35828
|
+
* Theme picker overlay (`gC`). Renders like the command palette so the
|
|
35829
|
+
* rest of the surface live-previews the cursored theme underneath. Type to
|
|
35830
|
+
* filter, ↑/↓ to move, Enter applies (and persists), Esc cancels. Takes the
|
|
35831
|
+
* raw `filter` + `index` rather than a `LogInkState` so it's reusable by
|
|
35832
|
+
* the workspace top-level surface, which has its own state model.
|
|
35833
|
+
*/
|
|
35834
|
+
function renderThemePickerOverlay(h, components, filter, index, width, theme, focused) {
|
|
35835
|
+
const { Box, Text } = components;
|
|
35836
|
+
const filtered = filterThemePresets(filter);
|
|
35837
|
+
const selectedIndex = filtered.length === 0
|
|
35838
|
+
? 0
|
|
35839
|
+
: Math.max(0, Math.min(index, filtered.length - 1));
|
|
35840
|
+
const listRows = 14;
|
|
35841
|
+
const startIndex = Math.max(0, selectedIndex - Math.floor(listRows / 2));
|
|
35842
|
+
const visible = filtered.slice(startIndex, startIndex + listRows);
|
|
35843
|
+
const inputLine = `> ${filter}_`;
|
|
35844
|
+
const matchSummary = filtered.length === 0
|
|
35845
|
+
? 'no matches'
|
|
35846
|
+
: `${filtered.length} ${filtered.length === 1 ? 'theme' : 'themes'}`;
|
|
35847
|
+
const hint = '↑/↓ select · type to filter · enter apply · esc close';
|
|
35848
|
+
const itemLines = filtered.length === 0
|
|
35849
|
+
? [h(Text, { key: 'theme-empty', dimColor: true }, 'No themes match the current filter.')]
|
|
35850
|
+
: visible.map((preset, offset) => {
|
|
35851
|
+
const index = startIndex + offset;
|
|
35852
|
+
const isSelected = index === selectedIndex;
|
|
35853
|
+
const cursor = isSelected ? '>' : ' ';
|
|
35854
|
+
// Accent swatch per theme (no swatch for the monochrome baseline or
|
|
35855
|
+
// when color is off). `default` is the only ANSI-named accent.
|
|
35856
|
+
const accent = preset === 'monochrome'
|
|
35857
|
+
? undefined
|
|
35858
|
+
: THEME_PRESET_COLORS[preset]?.accent;
|
|
35859
|
+
const swatch = accent && !theme.noColor
|
|
35860
|
+
? h(Text, { key: `theme-swatch-${preset}`, color: accent }, '● ')
|
|
35861
|
+
: h(Text, { key: `theme-swatch-${preset}`, dimColor: true }, '· ');
|
|
35862
|
+
return h(Text, {
|
|
35863
|
+
key: `theme-${preset}`,
|
|
35864
|
+
bold: isSelected,
|
|
35865
|
+
dimColor: !isSelected,
|
|
35866
|
+
}, `${cursor} `, swatch, truncateCells(preset, width - 8));
|
|
35867
|
+
});
|
|
35868
|
+
const hasMoreAbove = startIndex > 0 && filtered.length > 0;
|
|
35869
|
+
const hasMoreBelow = startIndex + listRows < filtered.length;
|
|
35870
|
+
return h(Box, {
|
|
35871
|
+
borderColor: focusBorderColor(theme, focused),
|
|
35872
|
+
borderStyle: theme.borderStyle,
|
|
35873
|
+
flexDirection: 'column',
|
|
35874
|
+
width,
|
|
35875
|
+
paddingX: 1,
|
|
35876
|
+
}, 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
|
|
35877
|
+
? [h(Text, { key: 'theme-more-above', dimColor: true }, ` ↑ ${startIndex} more above`)]
|
|
35878
|
+
: []), ...itemLines, ...(hasMoreBelow
|
|
35879
|
+
? [h(Text, { key: 'theme-more-below', dimColor: true }, ` ↓ ${filtered.length - (startIndex + listRows)} more below`)]
|
|
35880
|
+
: []));
|
|
34685
35881
|
}
|
|
34686
35882
|
/**
|
|
34687
35883
|
* Split-plan overlay (#907) — renders the proposed commit groups for
|
|
@@ -35537,6 +36733,8 @@ function renderStashSurface(h, components, state, context, contextStatus, bodyRo
|
|
|
35537
36733
|
dimColor: !isSelected,
|
|
35538
36734
|
}, truncateCells(`${cursor} ${stash.ref.padEnd(12)} ${stash.message}`, 140));
|
|
35539
36735
|
});
|
|
36736
|
+
const stashHasMoreAbove = startIndex > 0 && stashes.length > 0;
|
|
36737
|
+
const stashHasMoreBelow = startIndex + listRows < stashes.length;
|
|
35540
36738
|
return h(Box, {
|
|
35541
36739
|
borderColor: focusBorderColor(theme, focused),
|
|
35542
36740
|
borderStyle: theme.borderStyle,
|
|
@@ -35544,7 +36742,11 @@ function renderStashSurface(h, components, state, context, contextStatus, bodyRo
|
|
|
35544
36742
|
flexShrink: 0,
|
|
35545
36743
|
paddingX: 1,
|
|
35546
36744
|
width,
|
|
35547
|
-
}, h(Box, { justifyContent: 'space-between' }, h(Text, { bold: true }, panelTitle('Stash', focused)), h(Text, { dimColor: true }, headerRight)), ...renderPromotedFilterAffordance(h, Text, state, theme), ...
|
|
36745
|
+
}, h(Box, { justifyContent: 'space-between' }, h(Text, { bold: true }, panelTitle('Stash', focused)), h(Text, { dimColor: true }, headerRight)), ...renderPromotedFilterAffordance(h, Text, state, theme), ...(stashHasMoreAbove
|
|
36746
|
+
? [h(Text, { key: 'stash-more-above', dimColor: true }, ` ↑ ${startIndex} more above`)]
|
|
36747
|
+
: []), ...lines, ...(stashHasMoreBelow
|
|
36748
|
+
? [h(Text, { key: 'stash-more-below', dimColor: true }, ` ↓ ${stashes.length - (startIndex + listRows)} more below`)]
|
|
36749
|
+
: []));
|
|
35548
36750
|
}
|
|
35549
36751
|
|
|
35550
36752
|
/**
|
|
@@ -35640,7 +36842,7 @@ function renderStatusSurface(h, components, state, context, contextStatus, bodyR
|
|
|
35640
36842
|
bold: true,
|
|
35641
36843
|
dimColor: !headerSelected && rowIndex > cursorRowIndex,
|
|
35642
36844
|
backgroundColor: headerSelected && !theme.noColor ? theme.colors.selection : undefined,
|
|
35643
|
-
|
|
36845
|
+
color: headerSelected && !theme.noColor ? theme.colors.selectionForeground : undefined,
|
|
35644
36846
|
}, truncateCells(text, 140));
|
|
35645
36847
|
}
|
|
35646
36848
|
const isSelected = !headerFocused && row.flatIndex === selectedIndex;
|
|
@@ -35660,8 +36862,11 @@ function renderStatusSurface(h, components, state, context, contextStatus, bodyR
|
|
|
35660
36862
|
key: `status-file-${row.flatIndex}-${rowIndex}`,
|
|
35661
36863
|
dimColor: !isSelected && rowIndex > cursorRowIndex,
|
|
35662
36864
|
backgroundColor: isSelected && focused && !theme.noColor ? theme.colors.selection : undefined,
|
|
35663
|
-
|
|
35664
|
-
}, ` ${cursorPart}`,
|
|
36865
|
+
color: isSelected && focused && !theme.noColor ? theme.colors.selectionForeground : undefined,
|
|
36866
|
+
}, ` ${cursorPart}`,
|
|
36867
|
+
// Suppress the dot's own color on selected rows so it inherits the
|
|
36868
|
+
// contrast-guaranteed selection foreground set on the row span.
|
|
36869
|
+
...(useDot ? [h(Text, { color: (isSelected && focused) ? undefined : dotColor }, STAGE_STATUS_DOT), ' '] : []), tailTrunc);
|
|
35665
36870
|
});
|
|
35666
36871
|
// When the mask narrows the list to nothing but the underlying repo
|
|
35667
36872
|
// is non-clean, surface why the panel looks empty so the user can
|
|
@@ -35676,6 +36881,10 @@ function renderStatusSurface(h, components, state, context, contextStatus, bodyR
|
|
|
35676
36881
|
: cleanHint
|
|
35677
36882
|
? [cleanHint]
|
|
35678
36883
|
: ['Worktree clean'];
|
|
36884
|
+
// Scroll indicators for the status file list — same pattern as
|
|
36885
|
+
// branches and the sidebar so the user knows there's more content.
|
|
36886
|
+
const statusHasMoreAbove = windowStart > 0 && surfaceRows.length > 0;
|
|
36887
|
+
const statusHasMoreBelow = windowStart + listRows < surfaceRows.length;
|
|
35679
36888
|
return h(Box, {
|
|
35680
36889
|
borderColor: focusBorderColor(theme, focused),
|
|
35681
36890
|
borderStyle: theme.borderStyle,
|
|
@@ -35691,7 +36900,11 @@ function renderStatusSurface(h, components, state, context, contextStatus, bodyR
|
|
|
35691
36900
|
// never touch the filter.
|
|
35692
36901
|
...(isStatusFilterMaskActive(state.statusFilterMask)
|
|
35693
36902
|
? [h(Text, { key: 'status-mask-indicator', dimColor: true }, `filter: ${formatStatusFilterMask(state.statusFilterMask)} (1/2/3 to toggle)`)]
|
|
35694
|
-
: []), ...
|
|
36903
|
+
: []), ...(statusHasMoreAbove
|
|
36904
|
+
? [h(Text, { key: 'status-more-above', dimColor: true }, ` ↑ ${windowStart} more above`)]
|
|
36905
|
+
: []), ...renderedRows, ...(statusHasMoreBelow
|
|
36906
|
+
? [h(Text, { key: 'status-more-below', dimColor: true }, ` ↓ ${surfaceRows.length - (windowStart + listRows)} more below`)]
|
|
36907
|
+
: []), ...fallbackLines.map((line, index) => h(Text, {
|
|
35695
36908
|
key: `status-surface-fallback-${index}`,
|
|
35696
36909
|
dimColor: index > 0,
|
|
35697
36910
|
}, truncateCells(line, 140))));
|
|
@@ -35921,6 +37134,8 @@ function renderTagsSurface(h, components, state, context, contextStatus, bodyRow
|
|
|
35921
37134
|
dimColor: !isSelected,
|
|
35922
37135
|
}, before, formatHyperlink(namePadded, url), after);
|
|
35923
37136
|
});
|
|
37137
|
+
const tagsHasMoreAbove = startIndex > 0 && tags.length > 0;
|
|
37138
|
+
const tagsHasMoreBelow = startIndex + listRows < tags.length;
|
|
35924
37139
|
return h(Box, {
|
|
35925
37140
|
borderColor: focusBorderColor(theme, focused),
|
|
35926
37141
|
borderStyle: theme.borderStyle,
|
|
@@ -35928,7 +37143,11 @@ function renderTagsSurface(h, components, state, context, contextStatus, bodyRow
|
|
|
35928
37143
|
flexShrink: 0,
|
|
35929
37144
|
paddingX: 1,
|
|
35930
37145
|
width,
|
|
35931
|
-
}, h(Box, { justifyContent: 'space-between' }, h(Text, { bold: true }, panelTitle('Tags', focused)), h(Text, { dimColor: true }, headerRight)), ...renderPromotedFilterAffordance(h, Text, state, theme), ...
|
|
37146
|
+
}, h(Box, { justifyContent: 'space-between' }, h(Text, { bold: true }, panelTitle('Tags', focused)), h(Text, { dimColor: true }, headerRight)), ...renderPromotedFilterAffordance(h, Text, state, theme), ...(tagsHasMoreAbove
|
|
37147
|
+
? [h(Text, { key: 'tags-more-above', dimColor: true }, ` ↑ ${startIndex} more above`)]
|
|
37148
|
+
: []), ...lines, ...(tagsHasMoreBelow
|
|
37149
|
+
? [h(Text, { key: 'tags-more-below', dimColor: true }, ` ↓ ${tags.length - (startIndex + listRows)} more below`)]
|
|
37150
|
+
: []));
|
|
35932
37151
|
}
|
|
35933
37152
|
|
|
35934
37153
|
/**
|
|
@@ -36447,12 +37666,17 @@ function renderInspectorActionsSection(h, Text, context, width, theme, options =
|
|
|
36447
37666
|
h(Text, { key: 'actions-title' }, cursorActive ? '[Actions]' : 'Actions:'),
|
|
36448
37667
|
...actions.map((action, index) => {
|
|
36449
37668
|
const isSelected = cursorActive && index === cursorIndex;
|
|
37669
|
+
// On the selected row, swap every span to the contrast-guaranteed
|
|
37670
|
+
// selection foreground so the key glyph / destructive marker don't
|
|
37671
|
+
// wash out against the selection bar; the row is already highlighted,
|
|
37672
|
+
// and the label text still conveys which actions are destructive.
|
|
37673
|
+
const selectedFg = isSelected && !theme.noColor ? theme.colors.selectionForeground : undefined;
|
|
36450
37674
|
const keyCell = action.key.padEnd(KEY_COLUMN);
|
|
36451
37675
|
const label = truncateCells(action.label, labelBudget);
|
|
36452
37676
|
const children = [
|
|
36453
37677
|
h(Text, {
|
|
36454
37678
|
key: `actions-${index}-key`,
|
|
36455
|
-
color: action.destructive ? theme.colors.danger : theme.colors.accent,
|
|
37679
|
+
color: selectedFg ?? (action.destructive ? theme.colors.danger : theme.colors.accent),
|
|
36456
37680
|
}, keyCell),
|
|
36457
37681
|
GAP,
|
|
36458
37682
|
label,
|
|
@@ -36460,14 +37684,14 @@ function renderInspectorActionsSection(h, Text, context, width, theme, options =
|
|
|
36460
37684
|
if (action.destructive) {
|
|
36461
37685
|
children.push(h(Text, {
|
|
36462
37686
|
key: `actions-${index}-mark`,
|
|
36463
|
-
color: theme.colors.danger,
|
|
37687
|
+
color: selectedFg ?? theme.colors.danger,
|
|
36464
37688
|
dimColor: false,
|
|
36465
37689
|
}, DESTRUCTIVE_SUFFIX));
|
|
36466
37690
|
}
|
|
36467
37691
|
return h(Text, {
|
|
36468
37692
|
key: `actions-${index}`,
|
|
36469
37693
|
backgroundColor: isSelected && !theme.noColor ? theme.colors.selection : undefined,
|
|
36470
|
-
|
|
37694
|
+
color: selectedFg,
|
|
36471
37695
|
}, ...children);
|
|
36472
37696
|
}),
|
|
36473
37697
|
];
|
|
@@ -36544,7 +37768,6 @@ function renderCommitFileList(h, Text, files, selectedIndex, focused, maxRows, w
|
|
|
36544
37768
|
return h(Text, {
|
|
36545
37769
|
key: `commit-file-${index}`,
|
|
36546
37770
|
color: statusCodeColor(file.status, theme),
|
|
36547
|
-
inverse: isSelected && focused && !theme.noColor,
|
|
36548
37771
|
bold: isSelected,
|
|
36549
37772
|
}, label);
|
|
36550
37773
|
});
|
|
@@ -37092,6 +38315,9 @@ function renderDetailPanel(h, components, state, context, contextStatus, detail,
|
|
|
37092
38315
|
if (state.showCommandPalette) {
|
|
37093
38316
|
return renderCommandPalette(h, components, state, width, theme, focused);
|
|
37094
38317
|
}
|
|
38318
|
+
if (state.showThemePicker) {
|
|
38319
|
+
return renderThemePickerOverlay(h, components, state.themePickerFilter, state.themePickerIndex, width, theme, focused);
|
|
38320
|
+
}
|
|
37095
38321
|
if (state.inputPrompt) {
|
|
37096
38322
|
return renderInputPromptPanel(h, components, state, width, theme, focused);
|
|
37097
38323
|
}
|
|
@@ -37386,9 +38612,21 @@ function enrichFilterActionWithRectification(action, state, context) {
|
|
|
37386
38612
|
}
|
|
37387
38613
|
}
|
|
37388
38614
|
function LogInkApp(deps) {
|
|
37389
|
-
const { appLabel, clipboardRunner, dateBucketingEnabled, git: rootGit, idleTipsEnabled, ink, initialView, loadRows, logArgv, React, resumeRef, rows, theme } = deps;
|
|
38615
|
+
const { appLabel, clipboardRunner, dateBucketingEnabled, git: rootGit, idleTipsEnabled, ink, initialView, loadRows, logArgv, React, resumeRef, rows, theme: baseTheme, themeConfig } = deps;
|
|
37390
38616
|
const { Box, Text, useApp, useInput, useWindowSize } = ink;
|
|
37391
38617
|
const h = React.createElement;
|
|
38618
|
+
// Theme picker (gC) — live preview + apply. `themePreviewPreset` follows
|
|
38619
|
+
// the picker cursor while the overlay is open; `themeSessionPreset` is the
|
|
38620
|
+
// applied choice that survives close. The effective theme is rebuilt from
|
|
38621
|
+
// the original `themeConfig` so ascii/border/noColor + truecolor-downgrade
|
|
38622
|
+
// semantics are preserved; when neither override is set we use the static
|
|
38623
|
+
// `baseTheme` unchanged (so behavior is identical until the picker is used).
|
|
38624
|
+
const [themePreviewPreset, setThemePreviewPreset] = React.useState(undefined);
|
|
38625
|
+
const [themeSessionPreset, setThemeSessionPreset] = React.useState(undefined);
|
|
38626
|
+
const effectiveThemePreset = themePreviewPreset ?? themeSessionPreset;
|
|
38627
|
+
const theme = React.useMemo(() => effectiveThemePreset
|
|
38628
|
+
? createLogInkTheme({ ...themeConfig, preset: effectiveThemePreset })
|
|
38629
|
+
: baseTheme, [effectiveThemePreset, themeConfig, baseTheme]);
|
|
37392
38630
|
const { exit } = useApp();
|
|
37393
38631
|
const windowSize = useWindowSize();
|
|
37394
38632
|
// Bumping this on SIGCONT forces the existing tree to repaint so users
|
|
@@ -37417,6 +38655,17 @@ function LogInkApp(deps) {
|
|
|
37417
38655
|
// immediately while the chrome still flags the refresh.
|
|
37418
38656
|
bootLoading: Boolean(loadRows),
|
|
37419
38657
|
}));
|
|
38658
|
+
// Theme picker live preview: keep `themePreviewPreset` in sync with the
|
|
38659
|
+
// preset under the picker cursor while the overlay is open; clear it when
|
|
38660
|
+
// the overlay closes so the theme reverts to the applied session preset
|
|
38661
|
+
// (or the original config theme). The derived-theme `useMemo` above does
|
|
38662
|
+
// the actual re-render from this state.
|
|
38663
|
+
const themePickerSelection = state.showThemePicker
|
|
38664
|
+
? getThemePickerSelection(state)
|
|
38665
|
+
: undefined;
|
|
38666
|
+
React.useEffect(() => {
|
|
38667
|
+
setThemePreviewPreset(state.showThemePicker ? themePickerSelection : undefined);
|
|
38668
|
+
}, [state.showThemePicker, themePickerSelection]);
|
|
37420
38669
|
// Nested-repo runtime stack (#931). Each frame holds the live
|
|
37421
38670
|
// `SimpleGit`, the loaded `LogInkContext`, and the per-key load
|
|
37422
38671
|
// status the chrome reads. The active (top-of-stack) entry drives
|
|
@@ -40839,946 +42088,423 @@ function LogInkApp(deps) {
|
|
|
40839
42088
|
// (failure), surfacing the right message.
|
|
40840
42089
|
}
|
|
40841
42090
|
else {
|
|
40842
|
-
dispatch({
|
|
40843
|
-
type: 'setStatus',
|
|
40844
|
-
value: `${target.label} target commit returned no rows — orphan ref?`,
|
|
40845
|
-
kind: 'warning',
|
|
40846
|
-
});
|
|
40847
|
-
}
|
|
40848
|
-
}
|
|
40849
|
-
catch (error) {
|
|
40850
|
-
if (mountedRef.current) {
|
|
40851
|
-
dispatch({
|
|
40852
|
-
type: 'setStatus',
|
|
40853
|
-
value: `Failed to load context for ${target.label}: ${error instanceof Error ? error.message : String(error)}`,
|
|
40854
|
-
kind: 'error',
|
|
40855
|
-
});
|
|
40856
|
-
}
|
|
40857
|
-
}
|
|
40858
|
-
}, [dispatch, git]);
|
|
40859
|
-
React.useEffect(() => {
|
|
40860
|
-
loadCommitContextRef.current = loadCommitContext;
|
|
40861
|
-
}, [loadCommitContext]);
|
|
40862
|
-
// Server-side history filter (#776). When the user submits `path:foo`
|
|
40863
|
-
// or `author:foo`, the filter parser dispatches setHistoryFetchArgs;
|
|
40864
|
-
// this effect picks up the change, re-runs `getLogRows` with merged
|
|
40865
|
-
// args, and replaces the rows. Clearing the fetch args (Ctrl+U inside
|
|
40866
|
-
// filter mode) re-fetches with the original logArgv so the user gets
|
|
40867
|
-
// the live full log back, not a stale snapshot of the initial rows.
|
|
40868
|
-
const historyFetchEffectInitialized = React.useRef(false);
|
|
40869
|
-
const historyFetchRequestRef = React.useRef(0);
|
|
40870
|
-
React.useEffect(() => {
|
|
40871
|
-
if (!logArgv)
|
|
40872
|
-
return;
|
|
40873
|
-
// Skip the first run — initial rows came in via deps.rows; we only
|
|
40874
|
-
// want to fetch in response to *changes* to historyFetchArgs.
|
|
40875
|
-
if (!historyFetchEffectInitialized.current) {
|
|
40876
|
-
historyFetchEffectInitialized.current = true;
|
|
40877
|
-
return;
|
|
40878
|
-
}
|
|
40879
|
-
const requestId = historyFetchRequestRef.current + 1;
|
|
40880
|
-
historyFetchRequestRef.current = requestId;
|
|
40881
|
-
const fetchArgs = state.historyFetchArgs;
|
|
40882
|
-
const merged = {
|
|
40883
|
-
...logArgv,
|
|
40884
|
-
...(fetchArgs?.author ? { author: fetchArgs.author } : {}),
|
|
40885
|
-
...(fetchArgs?.path ? { path: fetchArgs.path } : {}),
|
|
40886
|
-
};
|
|
40887
|
-
const description = fetchArgs?.author
|
|
40888
|
-
? `author:${fetchArgs.author}`
|
|
40889
|
-
: fetchArgs?.path
|
|
40890
|
-
? `path:${fetchArgs.path}`
|
|
40891
|
-
: undefined;
|
|
40892
|
-
dispatch({
|
|
40893
|
-
type: 'setStatus',
|
|
40894
|
-
value: description ? `Refetching with ${description}` : 'Restoring full log',
|
|
40895
|
-
});
|
|
40896
|
-
void (async () => {
|
|
40897
|
-
const stashHashes = await getStashCommitHashes(git).catch(() => []);
|
|
40898
|
-
const nextRows = await safe(getLogRows(git, merged, {
|
|
40899
|
-
limit: LOG_INTERACTIVE_DEFAULT_LIMIT,
|
|
40900
|
-
extraRefs: stashHashes,
|
|
40901
|
-
}));
|
|
40902
|
-
if (!mountedRef.current || historyFetchRequestRef.current !== requestId) {
|
|
40903
|
-
return;
|
|
40904
|
-
}
|
|
40905
|
-
if (!nextRows) {
|
|
40906
|
-
dispatch({ type: 'setStatus', value: 'Failed to refetch with active filter', kind: 'error' });
|
|
40907
|
-
return;
|
|
40908
|
-
}
|
|
40909
|
-
dispatch({ type: 'replaceRows', rows: nextRows });
|
|
40910
|
-
const matched = getCommitRows(nextRows).length;
|
|
40911
|
-
setHasMoreCommits(matched >= LOG_INTERACTIVE_DEFAULT_LIMIT);
|
|
40912
|
-
dispatch({
|
|
40913
|
-
type: 'setStatus',
|
|
40914
|
-
value: description
|
|
40915
|
-
? `Showing ${matched} commits matching ${description}`
|
|
40916
|
-
: 'Showing full log',
|
|
40917
|
-
kind: 'success',
|
|
40918
|
-
});
|
|
40919
|
-
})();
|
|
40920
|
-
}, [dispatch, git, logArgv, state.historyFetchArgs]);
|
|
40921
|
-
// Graph mode toggle (`g` key, #791 follow-up). The header label flips
|
|
40922
|
-
// between "compact graph" and "full graph", but unless we re-fetch with
|
|
40923
|
-
// the right `view`, the underlying rows still come from the user's
|
|
40924
|
-
// initial argv (default `--first-parent --no-merges`) and the renderer
|
|
40925
|
-
// has no topology to draw — defeating the per-lane / junction work.
|
|
40926
|
-
// Mirrors the historyFetchArgs effect: skip first run, request-id ref
|
|
40927
|
-
// for stale-completion guard, swap rows in place via replaceRows.
|
|
40928
|
-
const toggleGraphEffectInitialized = React.useRef(false);
|
|
40929
|
-
const toggleGraphRequestRef = React.useRef(0);
|
|
40930
|
-
React.useEffect(() => {
|
|
40931
|
-
if (!logArgv)
|
|
40932
|
-
return;
|
|
40933
|
-
if (!toggleGraphEffectInitialized.current) {
|
|
40934
|
-
toggleGraphEffectInitialized.current = true;
|
|
40935
|
-
return;
|
|
40936
|
-
}
|
|
40937
|
-
const requestId = toggleGraphRequestRef.current + 1;
|
|
40938
|
-
toggleGraphRequestRef.current = requestId;
|
|
40939
|
-
const merged = buildToggleGraphArgs(logArgv, state.fullGraph);
|
|
40940
|
-
dispatch({
|
|
40941
|
-
type: 'setStatus',
|
|
40942
|
-
value: state.fullGraph
|
|
40943
|
-
? 'Loading full topology…'
|
|
40944
|
-
: 'Loading compact history…',
|
|
40945
|
-
});
|
|
40946
|
-
void (async () => {
|
|
40947
|
-
// Include stash commits as graph roots so the toggle's re-fetch
|
|
40948
|
-
// sees the same rich graph the boot loader assembles. Without
|
|
40949
|
-
// this, flipping `\` into full mode and back loses the stash
|
|
40950
|
-
// anchors that loadRowsWithStashes seeded on boot.
|
|
40951
|
-
const stashHashes = await getStashCommitHashes(git).catch(() => []);
|
|
40952
|
-
const nextRows = await safe(getLogRows(git, merged, {
|
|
40953
|
-
limit: LOG_INTERACTIVE_DEFAULT_LIMIT,
|
|
40954
|
-
extraRefs: stashHashes,
|
|
40955
|
-
}));
|
|
40956
|
-
if (!mountedRef.current || toggleGraphRequestRef.current !== requestId) {
|
|
40957
|
-
return;
|
|
40958
|
-
}
|
|
40959
|
-
if (!nextRows) {
|
|
40960
|
-
dispatch({ type: 'setStatus', value: 'Failed to refetch graph rows', kind: 'error' });
|
|
40961
|
-
return;
|
|
40962
|
-
}
|
|
40963
|
-
dispatch({ type: 'replaceRows', rows: nextRows });
|
|
40964
|
-
const matched = getCommitRows(nextRows).length;
|
|
40965
|
-
setHasMoreCommits(matched >= LOG_INTERACTIVE_DEFAULT_LIMIT);
|
|
40966
|
-
dispatch({
|
|
40967
|
-
type: 'setStatus',
|
|
40968
|
-
value: state.fullGraph
|
|
40969
|
-
? `Showing ${matched} commits across all branches`
|
|
40970
|
-
: `Showing ${matched} commits (compact)`,
|
|
40971
|
-
kind: 'success',
|
|
40972
|
-
});
|
|
40973
|
-
})();
|
|
40974
|
-
}, [dispatch, git, logArgv, state.fullGraph]);
|
|
40975
|
-
const commitDiffHunkOffsets = React.useMemo(() => (filePreview?.hunks
|
|
40976
|
-
.map((line, index) => (line.startsWith('@@') ? index : -1))
|
|
40977
|
-
.filter((index) => index >= 0)), [filePreview]);
|
|
40978
|
-
const worktreeDirty = Boolean(context.worktree &&
|
|
40979
|
-
(context.worktree.stagedCount + context.worktree.unstagedCount + context.worktree.untrackedCount) > 0);
|
|
40980
|
-
useInput((inputValue, key) => {
|
|
40981
|
-
// First-launch onboarding (P1.3): any keystroke dismisses the overlay
|
|
40982
|
-
// and writes the seen-marker. Swallow the keystroke so the same key
|
|
40983
|
-
// doesn't also trigger normal input dispatch.
|
|
40984
|
-
if (showOnboarding) {
|
|
40985
|
-
setShowOnboarding(false);
|
|
40986
|
-
markOnboardingSeen();
|
|
40987
|
-
return;
|
|
40988
|
-
}
|
|
40989
|
-
// P4.5: navigation in branches/tags/stash uses the FILTERED list
|
|
40990
|
-
// length when a filter is active so j/k stay live instead of getting
|
|
40991
|
-
// stuck against a full-list count that no longer matches what's on
|
|
40992
|
-
// screen. The filtered lists are memoized at LogInkApp scope (#808
|
|
40993
|
-
// perf pass) — reading them here is O(1) instead of O(branches +
|
|
40994
|
-
// tags + stashes + worktrees) per keystroke.
|
|
40995
|
-
const branchVisibleCount = filteredBranchList.length;
|
|
40996
|
-
const branchSelectedShortName = filteredBranchList[Math.min(state.selectedBranchIndex, Math.max(0, filteredBranchList.length - 1))]?.shortName;
|
|
40997
|
-
const tagVisibleCount = filteredTagList.length;
|
|
40998
|
-
const tagSelectedName = filteredTagList[Math.min(state.selectedTagIndex, Math.max(0, filteredTagList.length - 1))]?.name;
|
|
40999
|
-
const stashVisibleCount = filteredStashList.length;
|
|
41000
|
-
const stashSelectedRef = filteredStashList[Math.min(state.selectedStashIndex, Math.max(0, filteredStashList.length - 1))]?.ref;
|
|
41001
|
-
const reflogVisibleCount = filteredReflogList.length;
|
|
41002
|
-
const reflogSelectedHash = filteredReflogList[Math.min(state.selectedReflogIndex, Math.max(0, filteredReflogList.length - 1))]?.hash;
|
|
41003
|
-
const submoduleVisibleCount = filteredSubmoduleList.length;
|
|
41004
|
-
filteredSubmoduleList[Math.min(state.selectedSubmoduleIndex, Math.max(0, filteredSubmoduleList.length - 1))]?.path;
|
|
41005
|
-
const issueVisibleCount = filteredIssueList.length;
|
|
41006
|
-
const issueSelectedUrl = filteredIssueList[Math.min(state.selectedIssueIndex, Math.max(0, filteredIssueList.length - 1))]?.url;
|
|
41007
|
-
const pullRequestTriageVisibleCount = filteredPullRequestTriageList.length;
|
|
41008
|
-
const pullRequestTriageSelectedUrl = filteredPullRequestTriageList[Math.min(state.selectedPullRequestTriageIndex, Math.max(0, filteredPullRequestTriageList.length - 1))]?.url;
|
|
41009
|
-
const worktreeVisibleCount = filteredWorktreeList.length;
|
|
41010
|
-
// When the diff view is showing a stash patch, swap the previewLineCount
|
|
41011
|
-
// to the stash diff length so the existing pageDetailPreview path
|
|
41012
|
-
// (j/k, PgUp/PgDn) scrolls through it without a parallel pipeline.
|
|
41013
|
-
const diffPreviewLineCount = state.diffSource === 'stash'
|
|
41014
|
-
? stashDiffLines?.length
|
|
41015
|
-
: filePreview?.hunks.length;
|
|
41016
|
-
// Per-file segmentation for stash diffs reads the LogInkApp-scoped
|
|
41017
|
-
// memo so navigation keys + the input-context derivation share a
|
|
41018
|
-
// single parse pass per stash patch instead of re-walking the
|
|
41019
|
-
// entire patch text on every keystroke.
|
|
41020
|
-
const stashDiffFiles = state.diffSource === 'stash' ? stashDiffParsedFiles : [];
|
|
41021
|
-
const stashDiffFileOffsets = stashDiffFiles.map((file) => file.startLine);
|
|
41022
|
-
const stashDiffSelectedPath = state.diffSource === 'stash'
|
|
41023
|
-
? findStashFileForOffset(stashDiffFiles, state.diffPreviewOffset)?.path
|
|
41024
|
-
: undefined;
|
|
41025
|
-
getLogInkInputEvents(state, inputValue, key, {
|
|
41026
|
-
detailFileCount: detail?.files.length,
|
|
41027
|
-
previewLineCount: diffPreviewLineCount,
|
|
41028
|
-
worktreeDiffLineCount: worktreeDiff?.lines.length,
|
|
41029
|
-
worktreeFileCount: visibleWorktreeFilesGrouped.length,
|
|
41030
|
-
worktreeHunkOffsets: worktreeDiff?.hunkOffsets,
|
|
41031
|
-
commitDiffHunkOffsets,
|
|
41032
|
-
branchCount: branchVisibleCount,
|
|
41033
|
-
branchSelectedShortName,
|
|
41034
|
-
tagCount: tagVisibleCount,
|
|
41035
|
-
tagSelectedName,
|
|
41036
|
-
stashCount: stashVisibleCount,
|
|
41037
|
-
reflogCount: reflogVisibleCount,
|
|
41038
|
-
reflogSelectedHash,
|
|
41039
|
-
submoduleCount: submoduleVisibleCount,
|
|
41040
|
-
issueCount: issueVisibleCount,
|
|
41041
|
-
issueSelectedUrl,
|
|
41042
|
-
pullRequestTriageCount: pullRequestTriageVisibleCount,
|
|
41043
|
-
pullRequestTriageSelectedUrl,
|
|
41044
|
-
stashSelectedRef,
|
|
41045
|
-
stashDiffFileOffsets: stashDiffFileOffsets.length ? stashDiffFileOffsets : undefined,
|
|
41046
|
-
stashDiffSelectedPath,
|
|
41047
|
-
worktreeListCount: worktreeVisibleCount,
|
|
41048
|
-
worktreeSelectedPath: visibleWorktreeFilesGrouped[state.selectedWorktreeFileIndex]?.path,
|
|
41049
|
-
statusGroups: visibleWorktreeGroups.map((group) => ({
|
|
41050
|
-
state: group.state,
|
|
41051
|
-
count: group.files.length,
|
|
41052
|
-
startIndex: group.startIndex,
|
|
41053
|
-
})),
|
|
41054
|
-
inspectorActionCount: getInspectorActionsForState(state).length,
|
|
41055
|
-
commitDiffSelectedPath: state.diffSource === 'commit'
|
|
41056
|
-
? selectedDetailFile?.path
|
|
41057
|
-
: undefined,
|
|
41058
|
-
commitDiffSelectedSha: state.diffSource === 'commit'
|
|
41059
|
-
? selected?.hash
|
|
41060
|
-
: undefined,
|
|
41061
|
-
// #931 PR 3b — Submodule drill-in target for the cursored file
|
|
41062
|
-
// in a commit diff. Resolved per-render so the Enter handler in
|
|
41063
|
-
// `inkInput.ts` doesn't have to re-walk the submodule overview;
|
|
41064
|
-
// undefined whenever the cursored file isn't a registered
|
|
41065
|
-
// submodule (or the overview / repo root haven't loaded yet).
|
|
41066
|
-
commitDiffSubmoduleDrillIn: state.diffSource === 'commit' && selectedDetailFile
|
|
41067
|
-
? resolveCommitDiffDrillInTarget({
|
|
41068
|
-
selectedFile: {
|
|
41069
|
-
path: selectedDetailFile.path,
|
|
41070
|
-
submoduleChange: filePreview?.path === selectedDetailFile.path
|
|
41071
|
-
? filePreview.submoduleChange
|
|
41072
|
-
: undefined,
|
|
41073
|
-
},
|
|
41074
|
-
submodules: context.submodules,
|
|
41075
|
-
activeRepoRoot,
|
|
41076
|
-
})
|
|
41077
|
-
: undefined,
|
|
41078
|
-
// #931 PR 4 / #932 — Submodule drill-in target for the cursored
|
|
41079
|
-
// row in the dedicated submodules view. Resolved per-render so
|
|
41080
|
-
// the Enter handler in `inkInput.ts` doesn't have to re-walk the
|
|
41081
|
-
// submodule overview. Gated on `activeView === 'submodules'` so
|
|
41082
|
-
// a stale resolution from a different view can't accidentally
|
|
41083
|
-
// fire — the runtime only ever populates it when the user is
|
|
41084
|
-
// actually on the view.
|
|
41085
|
-
submoduleViewDrillIn: state.activeView === 'submodules'
|
|
41086
|
-
? resolveSubmoduleViewDrillInTarget({
|
|
41087
|
-
selectedIndex: state.selectedSubmoduleIndex,
|
|
41088
|
-
submodules: context.submodules,
|
|
41089
|
-
activeRepoRoot,
|
|
41090
|
-
})
|
|
41091
|
-
: undefined,
|
|
41092
|
-
worktreeDirty,
|
|
41093
|
-
conflictFileCount: context.operation?.conflictedFiles.length,
|
|
41094
|
-
conflictSelectedPath: (() => {
|
|
41095
|
-
const files = context.operation?.conflictedFiles;
|
|
41096
|
-
if (!files || files.length === 0)
|
|
41097
|
-
return undefined;
|
|
41098
|
-
const clamped = Math.min(state.selectedConflictFileIndex, files.length - 1);
|
|
41099
|
-
return files[clamped]?.path;
|
|
41100
|
-
})(),
|
|
41101
|
-
// H / gH need the actual diff text (not just hunk offsets) to
|
|
41102
|
-
// slice the cursored hunk into a `git apply` patch. Stash uses
|
|
41103
|
-
// the full `git stash show -p` output; commit-diff uses the
|
|
41104
|
-
// per-file `filePreview.hunks` array. Either way, extractDiffHunk
|
|
41105
|
-
// walks `@@` headers and synthesizes a fresh diff --git / --- /
|
|
41106
|
-
// +++ header set using the path the caller already resolved.
|
|
41107
|
-
diffLinesForHunkApply: state.diffSource === 'stash'
|
|
41108
|
-
? stashDiffLines
|
|
41109
|
-
: state.diffSource === 'commit'
|
|
41110
|
-
? filePreview?.hunks
|
|
41111
|
-
: undefined,
|
|
41112
|
-
// Line count of the changelog text, used by the changelog view's
|
|
41113
|
-
// j/k/PgUp/PgDn scroll bindings to clamp `pageChangelog` deltas.
|
|
41114
|
-
// Computed from view state rather than threaded through context
|
|
41115
|
-
// because the surface owns its own content — no external loader.
|
|
41116
|
-
changelogLineCount: state.changelogView.text?.split('\n').length,
|
|
41117
|
-
// Approximate line count for the split-plan overlay. Each group
|
|
41118
|
-
// renders as a header + (body if any) + files block + (rationale
|
|
41119
|
-
// if any) + blank separator. Used by j/k/PgUp/PgDn to clamp the
|
|
41120
|
-
// scroll offset. The exact render math lives in the overlay
|
|
41121
|
-
// module — this is a close-enough heuristic for clamping.
|
|
41122
|
-
// #879 item 3 — short sha of the bisect terminator (if any).
|
|
41123
|
-
// Gates `y`/`Y` yank on the completion panel and lets the
|
|
41124
|
-
// runtime resolve the value without re-parsing the log.
|
|
41125
|
-
bisectCompletionSha: context.bisect?.active
|
|
41126
|
-
? getBisectCompletion(context.bisect.log)?.sha
|
|
41127
|
-
: undefined,
|
|
41128
|
-
// #879 item 4 — disambiguates the bisect view's `s` keystroke
|
|
41129
|
-
// (skip current candidate vs. start the wizard).
|
|
41130
|
-
bisectActive: Boolean(context.bisect?.active),
|
|
41131
|
-
splitPlanLineCount: state.splitPlan?.plan
|
|
41132
|
-
? state.splitPlan.plan.groups.reduce((sum, group) => {
|
|
41133
|
-
let lines = 2; // title + separator
|
|
41134
|
-
if (group.body)
|
|
41135
|
-
lines += group.body.split('\n').length + 1;
|
|
41136
|
-
if (group.rationale)
|
|
41137
|
-
lines += 2;
|
|
41138
|
-
lines += (group.files?.length || 0) + 1;
|
|
41139
|
-
if ((group.hunks?.length || 0) > 0)
|
|
41140
|
-
lines += group.hunks.length + 1;
|
|
41141
|
-
return sum + lines;
|
|
41142
|
-
}, 0)
|
|
41143
|
-
: undefined,
|
|
41144
|
-
}).forEach((event) => {
|
|
41145
|
-
if (event.type === 'exit') {
|
|
41146
|
-
exit();
|
|
41147
|
-
}
|
|
41148
|
-
else if (event.type === 'refreshContext') {
|
|
41149
|
-
void refreshContext();
|
|
41150
|
-
}
|
|
41151
|
-
else if (event.type === 'toggleSelectedFileStage') {
|
|
41152
|
-
void toggleSelectedFileStage();
|
|
41153
|
-
}
|
|
41154
|
-
else if (event.type === 'toggleSelectedHunkStage') {
|
|
41155
|
-
void toggleSelectedHunkStage();
|
|
41156
|
-
}
|
|
41157
|
-
else if (event.type === 'revertSelectedFile') {
|
|
41158
|
-
void revertSelectedFile();
|
|
41159
|
-
}
|
|
41160
|
-
else if (event.type === 'revertSelectedHunk') {
|
|
41161
|
-
void revertSelectedHunk();
|
|
41162
|
-
}
|
|
41163
|
-
else if (event.type === 'createManualCommit') {
|
|
41164
|
-
void createCommitFromCompose();
|
|
41165
|
-
}
|
|
41166
|
-
else if (event.type === 'runAiCommitDraft') {
|
|
41167
|
-
void runAiCommitDraft();
|
|
41168
|
-
}
|
|
41169
|
-
else if (event.type === 'cancelAiCommitDraft') {
|
|
41170
|
-
cancelAiCommitDraft();
|
|
41171
|
-
}
|
|
41172
|
-
else if (event.type === 'startCreatePullRequest') {
|
|
41173
|
-
void startCreatePullRequest();
|
|
41174
|
-
}
|
|
41175
|
-
else if (event.type === 'cancelPullRequestBodyDraft') {
|
|
41176
|
-
cancelPullRequestBodyDraft();
|
|
41177
|
-
}
|
|
41178
|
-
else if (event.type === 'startChangelogView') {
|
|
41179
|
-
void startChangelogView();
|
|
41180
|
-
}
|
|
41181
|
-
else if (event.type === 'regenerateChangelog') {
|
|
41182
|
-
regenerateChangelog();
|
|
41183
|
-
}
|
|
41184
|
-
else if (event.type === 'yankChangelog') {
|
|
41185
|
-
yankChangelog();
|
|
41186
|
-
}
|
|
41187
|
-
else if (event.type === 'openChangelogInEditor') {
|
|
41188
|
-
openChangelogInEditor();
|
|
41189
|
-
}
|
|
41190
|
-
else if (event.type === 'openComposeInEditor') {
|
|
41191
|
-
openComposeInEditor();
|
|
41192
|
-
}
|
|
41193
|
-
else if (event.type === 'startCommitSplit') {
|
|
41194
|
-
void startCommitSplit();
|
|
41195
|
-
}
|
|
41196
|
-
else if (event.type === 'applyCommitSplit') {
|
|
41197
|
-
void applyCommitSplit();
|
|
41198
|
-
}
|
|
41199
|
-
else if (event.type === 'cancelCommitSplit') {
|
|
41200
|
-
cancelCommitSplit();
|
|
41201
|
-
}
|
|
41202
|
-
else if (event.type === 'yankText') {
|
|
41203
|
-
void yankText(event.value, event.label);
|
|
41204
|
-
}
|
|
41205
|
-
else if (event.type === 'runWorkflowAction') {
|
|
41206
|
-
void runWorkflowAction(event.id, event.payload);
|
|
41207
|
-
}
|
|
41208
|
-
else if (event.type === 'openFileInEditor') {
|
|
41209
|
-
openInEditor(event.path);
|
|
41210
|
-
}
|
|
41211
|
-
else if (event.type === 'yankFromActiveView') {
|
|
41212
|
-
void yankFromActiveView(event.short);
|
|
41213
|
-
}
|
|
41214
|
-
else {
|
|
41215
|
-
// P4.5: enrich filter-mutating actions with a precomputed
|
|
41216
|
-
// selection snapshot so the reducer can preserve the cursor on
|
|
41217
|
-
// the same item when it's still in the filtered result, only
|
|
41218
|
-
// snapping to result[0] when the previously selected item drops
|
|
41219
|
-
// out. The snapshot lives in the action so the reducer never
|
|
41220
|
-
// needs context items.
|
|
41221
|
-
const enriched = enrichFilterActionWithRectification(event.action, state, context);
|
|
41222
|
-
dispatch(enriched);
|
|
41223
|
-
}
|
|
41224
|
-
});
|
|
41225
|
-
});
|
|
41226
|
-
// Layout depends on focus (sidebar grows when focused), so it's
|
|
41227
|
-
// computed here — after state is in scope but before the render path.
|
|
41228
|
-
const layout = getLogInkLayout({
|
|
41229
|
-
columns: windowSize.columns || process.stdout.columns || LOG_INK_DEFAULT_COLUMNS,
|
|
41230
|
-
rows: windowSize.rows || process.stdout.rows || LOG_INK_DEFAULT_ROWS,
|
|
41231
|
-
sidebarFocused: state.focus === 'sidebar',
|
|
41232
|
-
inspectorFocused: state.focus === 'detail',
|
|
41233
|
-
helpOverlayActive: state.showHelp,
|
|
41234
|
-
});
|
|
41235
|
-
if (layout.tooSmall) {
|
|
41236
|
-
return h(Box, {
|
|
41237
|
-
flexDirection: 'column',
|
|
41238
|
-
height: layout.rows,
|
|
41239
|
-
paddingX: 1,
|
|
41240
|
-
paddingY: 1,
|
|
41241
|
-
}, 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.'));
|
|
41242
|
-
}
|
|
41243
|
-
// First-launch onboarding overlay (P1.3) replaces the entire UI for
|
|
41244
|
-
// one render — any keystroke dismisses it and persists the seen-marker.
|
|
41245
|
-
if (showOnboarding) {
|
|
41246
|
-
return renderOnboardingOverlay(h, { Box, Text }, layout.rows, layout.columns, theme, appLabel);
|
|
41247
|
-
}
|
|
41248
|
-
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));
|
|
41249
|
-
}
|
|
41250
|
-
|
|
41251
|
-
/**
|
|
41252
|
-
* Explicit color-level detection for the Ink TUI (P5.2).
|
|
41253
|
-
*
|
|
41254
|
-
* Chalk already approximates hex colors when the terminal can't render
|
|
41255
|
-
* truecolor — but we want an explicit signal so the catppuccin / gruvbox
|
|
41256
|
-
* presets (which use hex) can fall back to the ANSI-named `default` preset
|
|
41257
|
-
* cleanly on minimal SSH sessions, instead of relying on chalk's
|
|
41258
|
-
* heuristics. Users who set `NO_COLOR` or pick the `monochrome` preset
|
|
41259
|
-
* still get the manual override.
|
|
41260
|
-
*
|
|
41261
|
-
* Levels (matching the chalk taxonomy):
|
|
41262
|
-
* - 'mono' → no ANSI escapes at all (NO_COLOR / TERM=dumb)
|
|
41263
|
-
* - '16' → standard 16-color ANSI palette
|
|
41264
|
-
* - '256' → xterm-256color
|
|
41265
|
-
* - 'truecolor' → 24-bit RGB (COLORTERM=truecolor or known terminals)
|
|
41266
|
-
*/
|
|
41267
|
-
function getColorLevel(env = process.env) {
|
|
41268
|
-
if (env.NO_COLOR)
|
|
41269
|
-
return 'mono';
|
|
41270
|
-
switch (env.FORCE_COLOR) {
|
|
41271
|
-
case '0':
|
|
41272
|
-
return 'mono';
|
|
41273
|
-
case '1':
|
|
41274
|
-
return '16';
|
|
41275
|
-
case '2':
|
|
41276
|
-
return '256';
|
|
41277
|
-
case '3':
|
|
41278
|
-
return 'truecolor';
|
|
41279
|
-
}
|
|
41280
|
-
const colorterm = env.COLORTERM?.toLowerCase();
|
|
41281
|
-
if (colorterm === 'truecolor' || colorterm === '24bit') {
|
|
41282
|
-
return 'truecolor';
|
|
41283
|
-
}
|
|
41284
|
-
// Modern terminal emulators that publicly advertise truecolor support.
|
|
41285
|
-
if (env.KITTY_WINDOW_ID || env.TERM === 'xterm-kitty') {
|
|
41286
|
-
return 'truecolor';
|
|
41287
|
-
}
|
|
41288
|
-
if (env.WT_SESSION) {
|
|
41289
|
-
return 'truecolor';
|
|
41290
|
-
}
|
|
41291
|
-
switch (env.TERM_PROGRAM) {
|
|
41292
|
-
case 'iTerm.app':
|
|
41293
|
-
case 'WezTerm':
|
|
41294
|
-
case 'vscode':
|
|
41295
|
-
case 'ghostty':
|
|
41296
|
-
case 'Hyper':
|
|
41297
|
-
return 'truecolor';
|
|
41298
|
-
}
|
|
41299
|
-
if (env.TERM === 'dumb')
|
|
41300
|
-
return 'mono';
|
|
41301
|
-
if (env.TERM?.includes('256color'))
|
|
41302
|
-
return '256';
|
|
41303
|
-
return '16';
|
|
41304
|
-
}
|
|
41305
|
-
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']);
|
|
41306
|
-
/**
|
|
41307
|
-
* `true` when the named preset relies on hex colors that look best under
|
|
41308
|
-
* 24-bit RGB. Used by `createLogInkTheme` to decide whether to downgrade
|
|
41309
|
-
* to the ANSI-named `default` palette on lower-capability terminals.
|
|
41310
|
-
*/
|
|
41311
|
-
function presetUsesTrueColor(preset) {
|
|
41312
|
-
return preset !== undefined && TRUECOLOR_PRESETS.has(preset);
|
|
41313
|
-
}
|
|
41314
|
-
|
|
41315
|
-
const THEME_PRESET_COLORS = {
|
|
41316
|
-
default: {
|
|
41317
|
-
accent: 'cyan',
|
|
41318
|
-
border: 'gray',
|
|
41319
|
-
danger: 'red',
|
|
41320
|
-
focusBorder: 'cyan',
|
|
41321
|
-
gitAdded: 'green',
|
|
41322
|
-
gitDeleted: 'red',
|
|
41323
|
-
gitModified: 'yellow',
|
|
41324
|
-
info: 'blue',
|
|
41325
|
-
muted: 'gray',
|
|
41326
|
-
selection: 'cyan',
|
|
41327
|
-
success: 'green',
|
|
41328
|
-
warning: 'yellow',
|
|
41329
|
-
},
|
|
41330
|
-
catppuccin: {
|
|
41331
|
-
accent: '#89b4fa',
|
|
41332
|
-
border: '#585b70',
|
|
41333
|
-
danger: '#f38ba8',
|
|
41334
|
-
focusBorder: '#89dceb',
|
|
41335
|
-
gitAdded: '#a6e3a1',
|
|
41336
|
-
gitDeleted: '#f38ba8',
|
|
41337
|
-
gitModified: '#f9e2af',
|
|
41338
|
-
info: '#89b4fa',
|
|
41339
|
-
muted: '#6c7086',
|
|
41340
|
-
selection: '#45475a',
|
|
41341
|
-
success: '#a6e3a1',
|
|
41342
|
-
warning: '#f9e2af',
|
|
41343
|
-
},
|
|
41344
|
-
gruvbox: {
|
|
41345
|
-
accent: '#83a598',
|
|
41346
|
-
border: '#665c54',
|
|
41347
|
-
danger: '#fb4934',
|
|
41348
|
-
focusBorder: '#8ec07c',
|
|
41349
|
-
gitAdded: '#b8bb26',
|
|
41350
|
-
gitDeleted: '#fb4934',
|
|
41351
|
-
gitModified: '#fabd2f',
|
|
41352
|
-
info: '#83a598',
|
|
41353
|
-
muted: '#928374',
|
|
41354
|
-
selection: '#504945',
|
|
41355
|
-
success: '#b8bb26',
|
|
41356
|
-
warning: '#fabd2f',
|
|
41357
|
-
},
|
|
41358
|
-
dracula: {
|
|
41359
|
-
accent: '#bd93f9',
|
|
41360
|
-
border: '#44475a',
|
|
41361
|
-
danger: '#ff5555',
|
|
41362
|
-
focusBorder: '#ff79c6',
|
|
41363
|
-
gitAdded: '#50fa7b',
|
|
41364
|
-
gitDeleted: '#ff5555',
|
|
41365
|
-
gitModified: '#f1fa8c',
|
|
41366
|
-
info: '#8be9fd',
|
|
41367
|
-
muted: '#6272a4',
|
|
41368
|
-
selection: '#44475a',
|
|
41369
|
-
success: '#50fa7b',
|
|
41370
|
-
warning: '#f1fa8c',
|
|
41371
|
-
},
|
|
41372
|
-
nord: {
|
|
41373
|
-
accent: '#88c0d0',
|
|
41374
|
-
border: '#3b4252',
|
|
41375
|
-
danger: '#bf616a',
|
|
41376
|
-
focusBorder: '#81a1c1',
|
|
41377
|
-
gitAdded: '#a3be8c',
|
|
41378
|
-
gitDeleted: '#bf616a',
|
|
41379
|
-
gitModified: '#ebcb8b',
|
|
41380
|
-
info: '#81a1c1',
|
|
41381
|
-
muted: '#4c566a',
|
|
41382
|
-
selection: '#3b4252',
|
|
41383
|
-
success: '#a3be8c',
|
|
41384
|
-
warning: '#ebcb8b',
|
|
41385
|
-
},
|
|
41386
|
-
'solarized-dark': {
|
|
41387
|
-
accent: '#268bd2',
|
|
41388
|
-
border: '#073642',
|
|
41389
|
-
danger: '#dc322f',
|
|
41390
|
-
focusBorder: '#2aa198',
|
|
41391
|
-
gitAdded: '#859900',
|
|
41392
|
-
gitDeleted: '#dc322f',
|
|
41393
|
-
gitModified: '#b58900',
|
|
41394
|
-
info: '#268bd2',
|
|
41395
|
-
muted: '#586e75',
|
|
41396
|
-
selection: '#073642',
|
|
41397
|
-
success: '#859900',
|
|
41398
|
-
warning: '#b58900',
|
|
41399
|
-
},
|
|
41400
|
-
'tokyo-night': {
|
|
41401
|
-
accent: '#7aa2f7',
|
|
41402
|
-
border: '#3b4261',
|
|
41403
|
-
danger: '#f7768e',
|
|
41404
|
-
focusBorder: '#7dcfff',
|
|
41405
|
-
gitAdded: '#9ece6a',
|
|
41406
|
-
gitDeleted: '#f7768e',
|
|
41407
|
-
gitModified: '#e0af68',
|
|
41408
|
-
info: '#7aa2f7',
|
|
41409
|
-
muted: '#565f89',
|
|
41410
|
-
selection: '#33467c',
|
|
41411
|
-
success: '#9ece6a',
|
|
41412
|
-
warning: '#e0af68',
|
|
41413
|
-
},
|
|
41414
|
-
'one-dark': {
|
|
41415
|
-
accent: '#61afef',
|
|
41416
|
-
border: '#3e4452',
|
|
41417
|
-
danger: '#e06c75',
|
|
41418
|
-
focusBorder: '#56b6c2',
|
|
41419
|
-
gitAdded: '#98c379',
|
|
41420
|
-
gitDeleted: '#e06c75',
|
|
41421
|
-
gitModified: '#e5c07b',
|
|
41422
|
-
info: '#61afef',
|
|
41423
|
-
muted: '#5c6370',
|
|
41424
|
-
selection: '#3e4452',
|
|
41425
|
-
success: '#98c379',
|
|
41426
|
-
warning: '#e5c07b',
|
|
41427
|
-
},
|
|
41428
|
-
'rose-pine': {
|
|
41429
|
-
accent: '#c4a7e7',
|
|
41430
|
-
border: '#26233a',
|
|
41431
|
-
danger: '#eb6f92',
|
|
41432
|
-
focusBorder: '#9ccfd8',
|
|
41433
|
-
gitAdded: '#31748f',
|
|
41434
|
-
gitDeleted: '#eb6f92',
|
|
41435
|
-
gitModified: '#f6c177',
|
|
41436
|
-
info: '#9ccfd8',
|
|
41437
|
-
muted: '#6e6a86',
|
|
41438
|
-
selection: '#2a273f',
|
|
41439
|
-
success: '#31748f',
|
|
41440
|
-
warning: '#f6c177',
|
|
41441
|
-
},
|
|
41442
|
-
kanagawa: {
|
|
41443
|
-
accent: '#7e9cd8',
|
|
41444
|
-
border: '#2a2a37',
|
|
41445
|
-
danger: '#e82424',
|
|
41446
|
-
focusBorder: '#7fb4ca',
|
|
41447
|
-
gitAdded: '#76946a',
|
|
41448
|
-
gitDeleted: '#e82424',
|
|
41449
|
-
gitModified: '#dca561',
|
|
41450
|
-
info: '#7e9cd8',
|
|
41451
|
-
muted: '#727169',
|
|
41452
|
-
selection: '#2d4f67',
|
|
41453
|
-
success: '#76946a',
|
|
41454
|
-
warning: '#dca561',
|
|
41455
|
-
},
|
|
41456
|
-
everforest: {
|
|
41457
|
-
accent: '#a7c080',
|
|
41458
|
-
border: '#374145',
|
|
41459
|
-
danger: '#e67e80',
|
|
41460
|
-
focusBorder: '#83c092',
|
|
41461
|
-
gitAdded: '#a7c080',
|
|
41462
|
-
gitDeleted: '#e67e80',
|
|
41463
|
-
gitModified: '#dbbc7f',
|
|
41464
|
-
info: '#7fbbb3',
|
|
41465
|
-
muted: '#859289',
|
|
41466
|
-
selection: '#374145',
|
|
41467
|
-
success: '#a7c080',
|
|
41468
|
-
warning: '#dbbc7f',
|
|
41469
|
-
},
|
|
41470
|
-
monokai: {
|
|
41471
|
-
accent: '#66d9ef',
|
|
41472
|
-
border: '#49483e',
|
|
41473
|
-
danger: '#f92672',
|
|
41474
|
-
focusBorder: '#a6e22e',
|
|
41475
|
-
gitAdded: '#a6e22e',
|
|
41476
|
-
gitDeleted: '#f92672',
|
|
41477
|
-
gitModified: '#e6db74',
|
|
41478
|
-
info: '#66d9ef',
|
|
41479
|
-
muted: '#75715e',
|
|
41480
|
-
selection: '#49483e',
|
|
41481
|
-
success: '#a6e22e',
|
|
41482
|
-
warning: '#e6db74',
|
|
41483
|
-
},
|
|
41484
|
-
synthwave: {
|
|
41485
|
-
accent: '#f97e72',
|
|
41486
|
-
border: '#34294f',
|
|
41487
|
-
danger: '#fe4450',
|
|
41488
|
-
focusBorder: '#36f9f6',
|
|
41489
|
-
gitAdded: '#72f1b8',
|
|
41490
|
-
gitDeleted: '#fe4450',
|
|
41491
|
-
gitModified: '#fede5d',
|
|
41492
|
-
info: '#36f9f6',
|
|
41493
|
-
muted: '#848bbd',
|
|
41494
|
-
selection: '#34294f',
|
|
41495
|
-
success: '#72f1b8',
|
|
41496
|
-
warning: '#fede5d',
|
|
41497
|
-
},
|
|
41498
|
-
'ayu-dark': {
|
|
41499
|
-
accent: '#e6b450',
|
|
41500
|
-
border: '#11151c',
|
|
41501
|
-
danger: '#f07178',
|
|
41502
|
-
focusBorder: '#39bae6',
|
|
41503
|
-
gitAdded: '#7fd962',
|
|
41504
|
-
gitDeleted: '#f07178',
|
|
41505
|
-
gitModified: '#e6b450',
|
|
41506
|
-
info: '#39bae6',
|
|
41507
|
-
muted: '#565b66',
|
|
41508
|
-
selection: '#1a1f29',
|
|
41509
|
-
success: '#7fd962',
|
|
41510
|
-
warning: '#e6b450',
|
|
41511
|
-
},
|
|
41512
|
-
palenight: {
|
|
41513
|
-
accent: '#82aaff',
|
|
41514
|
-
border: '#3a3f58',
|
|
41515
|
-
danger: '#ff5370',
|
|
41516
|
-
focusBorder: '#89ddff',
|
|
41517
|
-
gitAdded: '#c3e88d',
|
|
41518
|
-
gitDeleted: '#ff5370',
|
|
41519
|
-
gitModified: '#ffcb6b',
|
|
41520
|
-
info: '#82aaff',
|
|
41521
|
-
muted: '#676e95',
|
|
41522
|
-
selection: '#3a3f58',
|
|
41523
|
-
success: '#c3e88d',
|
|
41524
|
-
warning: '#ffcb6b',
|
|
41525
|
-
},
|
|
41526
|
-
'github-dark': {
|
|
41527
|
-
accent: '#58a6ff',
|
|
41528
|
-
border: '#30363d',
|
|
41529
|
-
danger: '#f85149',
|
|
41530
|
-
focusBorder: '#58a6ff',
|
|
41531
|
-
gitAdded: '#3fb950',
|
|
41532
|
-
gitDeleted: '#f85149',
|
|
41533
|
-
gitModified: '#d29922',
|
|
41534
|
-
info: '#58a6ff',
|
|
41535
|
-
muted: '#8b949e',
|
|
41536
|
-
selection: '#264f78',
|
|
41537
|
-
success: '#3fb950',
|
|
41538
|
-
warning: '#d29922',
|
|
41539
|
-
},
|
|
41540
|
-
horizon: {
|
|
41541
|
-
accent: '#e95678',
|
|
41542
|
-
border: '#2e303e',
|
|
41543
|
-
danger: '#e95678',
|
|
41544
|
-
focusBorder: '#25b0bc',
|
|
41545
|
-
gitAdded: '#09f7a0',
|
|
41546
|
-
gitDeleted: '#e95678',
|
|
41547
|
-
gitModified: '#fab795',
|
|
41548
|
-
info: '#25b0bc',
|
|
41549
|
-
muted: '#6c6f93',
|
|
41550
|
-
selection: '#2e303e',
|
|
41551
|
-
success: '#09f7a0',
|
|
41552
|
-
warning: '#fab795',
|
|
41553
|
-
},
|
|
41554
|
-
nightfox: {
|
|
41555
|
-
accent: '#719cd6',
|
|
41556
|
-
border: '#2b3b51',
|
|
41557
|
-
danger: '#c94f6d',
|
|
41558
|
-
focusBorder: '#63cdcf',
|
|
41559
|
-
gitAdded: '#81b29a',
|
|
41560
|
-
gitDeleted: '#c94f6d',
|
|
41561
|
-
gitModified: '#dbc074',
|
|
41562
|
-
info: '#719cd6',
|
|
41563
|
-
muted: '#738091',
|
|
41564
|
-
selection: '#2b3b51',
|
|
41565
|
-
success: '#81b29a',
|
|
41566
|
-
warning: '#dbc074',
|
|
41567
|
-
},
|
|
41568
|
-
carbonfox: {
|
|
41569
|
-
accent: '#78a9ff',
|
|
41570
|
-
border: '#353535',
|
|
41571
|
-
danger: '#ee5396',
|
|
41572
|
-
focusBorder: '#33b1ff',
|
|
41573
|
-
gitAdded: '#42be65',
|
|
41574
|
-
gitDeleted: '#ee5396',
|
|
41575
|
-
gitModified: '#ffe97b',
|
|
41576
|
-
info: '#78a9ff',
|
|
41577
|
-
muted: '#7b7c7e',
|
|
41578
|
-
selection: '#353535',
|
|
41579
|
-
success: '#42be65',
|
|
41580
|
-
warning: '#ffe97b',
|
|
41581
|
-
},
|
|
41582
|
-
'tokyonight-storm': {
|
|
41583
|
-
accent: '#7aa2f7',
|
|
41584
|
-
border: '#2f334d',
|
|
41585
|
-
danger: '#f7768e',
|
|
41586
|
-
focusBorder: '#2ac3de',
|
|
41587
|
-
gitAdded: '#9ece6a',
|
|
41588
|
-
gitDeleted: '#f7768e',
|
|
41589
|
-
gitModified: '#e0af68',
|
|
41590
|
-
info: '#2ac3de',
|
|
41591
|
-
muted: '#545c7e',
|
|
41592
|
-
selection: '#2f334d',
|
|
41593
|
-
success: '#9ece6a',
|
|
41594
|
-
warning: '#e0af68',
|
|
41595
|
-
},
|
|
41596
|
-
'catppuccin-latte': {
|
|
41597
|
-
accent: '#1e66f5',
|
|
41598
|
-
border: '#ccd0da',
|
|
41599
|
-
danger: '#d20f39',
|
|
41600
|
-
focusBorder: '#179299',
|
|
41601
|
-
gitAdded: '#40a02b',
|
|
41602
|
-
gitDeleted: '#d20f39',
|
|
41603
|
-
gitModified: '#df8e1d',
|
|
41604
|
-
info: '#1e66f5',
|
|
41605
|
-
muted: '#9ca0b0',
|
|
41606
|
-
selection: '#ccd0da',
|
|
41607
|
-
success: '#40a02b',
|
|
41608
|
-
warning: '#df8e1d',
|
|
41609
|
-
},
|
|
41610
|
-
'solarized-light': {
|
|
41611
|
-
accent: '#268bd2',
|
|
41612
|
-
border: '#eee8d5',
|
|
41613
|
-
danger: '#dc322f',
|
|
41614
|
-
focusBorder: '#2aa198',
|
|
41615
|
-
gitAdded: '#859900',
|
|
41616
|
-
gitDeleted: '#dc322f',
|
|
41617
|
-
gitModified: '#b58900',
|
|
41618
|
-
info: '#268bd2',
|
|
41619
|
-
muted: '#93a1a1',
|
|
41620
|
-
selection: '#eee8d5',
|
|
41621
|
-
success: '#859900',
|
|
41622
|
-
warning: '#b58900',
|
|
41623
|
-
},
|
|
41624
|
-
'github-light': {
|
|
41625
|
-
accent: '#0969da',
|
|
41626
|
-
border: '#d0d7de',
|
|
41627
|
-
danger: '#cf222e',
|
|
41628
|
-
focusBorder: '#0969da',
|
|
41629
|
-
gitAdded: '#1a7f37',
|
|
41630
|
-
gitDeleted: '#cf222e',
|
|
41631
|
-
gitModified: '#9a6700',
|
|
41632
|
-
info: '#0969da',
|
|
41633
|
-
muted: '#656d76',
|
|
41634
|
-
selection: '#ddf4ff',
|
|
41635
|
-
success: '#1a7f37',
|
|
41636
|
-
warning: '#9a6700',
|
|
41637
|
-
},
|
|
41638
|
-
iceberg: {
|
|
41639
|
-
accent: '#84a0c6',
|
|
41640
|
-
border: '#1e2132',
|
|
41641
|
-
danger: '#e27878',
|
|
41642
|
-
focusBorder: '#89b8c2',
|
|
41643
|
-
gitAdded: '#b4be82',
|
|
41644
|
-
gitDeleted: '#e27878',
|
|
41645
|
-
gitModified: '#e2a478',
|
|
41646
|
-
info: '#84a0c6',
|
|
41647
|
-
muted: '#6b7089',
|
|
41648
|
-
selection: '#1e2132',
|
|
41649
|
-
success: '#b4be82',
|
|
41650
|
-
warning: '#e2a478',
|
|
41651
|
-
},
|
|
41652
|
-
'material-ocean': {
|
|
41653
|
-
accent: '#82aaff',
|
|
41654
|
-
border: '#2b2f3a',
|
|
41655
|
-
danger: '#f07178',
|
|
41656
|
-
focusBorder: '#89ddff',
|
|
41657
|
-
gitAdded: '#c3e88d',
|
|
41658
|
-
gitDeleted: '#f07178',
|
|
41659
|
-
gitModified: '#ffcb6b',
|
|
41660
|
-
info: '#82aaff',
|
|
41661
|
-
muted: '#464b5d',
|
|
41662
|
-
selection: '#2b2f3a',
|
|
41663
|
-
success: '#c3e88d',
|
|
41664
|
-
warning: '#ffcb6b',
|
|
41665
|
-
},
|
|
41666
|
-
moonlight: {
|
|
41667
|
-
accent: '#82aaff',
|
|
41668
|
-
border: '#2f334d',
|
|
41669
|
-
danger: '#ff757f',
|
|
41670
|
-
focusBorder: '#86e1fc',
|
|
41671
|
-
gitAdded: '#c3e88d',
|
|
41672
|
-
gitDeleted: '#ff757f',
|
|
41673
|
-
gitModified: '#ffc777',
|
|
41674
|
-
info: '#82aaff',
|
|
41675
|
-
muted: '#636da6',
|
|
41676
|
-
selection: '#2f334d',
|
|
41677
|
-
success: '#c3e88d',
|
|
41678
|
-
warning: '#ffc777',
|
|
41679
|
-
},
|
|
41680
|
-
poimandres: {
|
|
41681
|
-
accent: '#add7ff',
|
|
41682
|
-
border: '#1b1e28',
|
|
41683
|
-
danger: '#d0679d',
|
|
41684
|
-
focusBorder: '#5de4c7',
|
|
41685
|
-
gitAdded: '#5de4c7',
|
|
41686
|
-
gitDeleted: '#d0679d',
|
|
41687
|
-
gitModified: '#fffac2',
|
|
41688
|
-
info: '#add7ff',
|
|
41689
|
-
muted: '#506477',
|
|
41690
|
-
selection: '#1b1e28',
|
|
41691
|
-
success: '#5de4c7',
|
|
41692
|
-
warning: '#fffac2',
|
|
41693
|
-
},
|
|
41694
|
-
'vitesse-dark': {
|
|
41695
|
-
accent: '#4d9375',
|
|
41696
|
-
border: '#282828',
|
|
41697
|
-
danger: '#cb7676',
|
|
41698
|
-
focusBorder: '#4d9375',
|
|
41699
|
-
gitAdded: '#4d9375',
|
|
41700
|
-
gitDeleted: '#cb7676',
|
|
41701
|
-
gitModified: '#e6cc77',
|
|
41702
|
-
info: '#6394bf',
|
|
41703
|
-
muted: '#758575',
|
|
41704
|
-
selection: '#282828',
|
|
41705
|
-
success: '#4d9375',
|
|
41706
|
-
warning: '#e6cc77',
|
|
41707
|
-
},
|
|
41708
|
-
vesper: {
|
|
41709
|
-
accent: '#ffc799',
|
|
41710
|
-
border: '#232323',
|
|
41711
|
-
danger: '#f5a191',
|
|
41712
|
-
focusBorder: '#99ffe4',
|
|
41713
|
-
gitAdded: '#99ffe4',
|
|
41714
|
-
gitDeleted: '#f5a191',
|
|
41715
|
-
gitModified: '#ffc799',
|
|
41716
|
-
info: '#a0c4ff',
|
|
41717
|
-
muted: '#575757',
|
|
41718
|
-
selection: '#232323',
|
|
41719
|
-
success: '#99ffe4',
|
|
41720
|
-
warning: '#ffc799',
|
|
41721
|
-
},
|
|
41722
|
-
flexoki: {
|
|
41723
|
-
accent: '#205ea6',
|
|
41724
|
-
border: '#343331',
|
|
41725
|
-
danger: '#af3029',
|
|
41726
|
-
focusBorder: '#24837b',
|
|
41727
|
-
gitAdded: '#66800b',
|
|
41728
|
-
gitDeleted: '#af3029',
|
|
41729
|
-
gitModified: '#ad8301',
|
|
41730
|
-
info: '#205ea6',
|
|
41731
|
-
muted: '#878580',
|
|
41732
|
-
selection: '#343331',
|
|
41733
|
-
success: '#66800b',
|
|
41734
|
-
warning: '#ad8301',
|
|
41735
|
-
},
|
|
41736
|
-
mellow: {
|
|
41737
|
-
accent: '#7eb8da',
|
|
41738
|
-
border: '#2a2a2a',
|
|
41739
|
-
danger: '#f5a191',
|
|
41740
|
-
focusBorder: '#a3d4a0',
|
|
41741
|
-
gitAdded: '#a3d4a0',
|
|
41742
|
-
gitDeleted: '#f5a191',
|
|
41743
|
-
gitModified: '#f0c674',
|
|
41744
|
-
info: '#7eb8da',
|
|
41745
|
-
muted: '#6b6b6b',
|
|
41746
|
-
selection: '#2a2a2a',
|
|
41747
|
-
success: '#a3d4a0',
|
|
41748
|
-
warning: '#f0c674',
|
|
41749
|
-
},
|
|
41750
|
-
};
|
|
41751
|
-
function shouldUseAscii(term) {
|
|
41752
|
-
if (!term) {
|
|
41753
|
-
return false;
|
|
41754
|
-
}
|
|
41755
|
-
return term === 'dumb' || term.startsWith('vt100');
|
|
41756
|
-
}
|
|
41757
|
-
function createLogInkTheme(options = {}) {
|
|
41758
|
-
const noColor = (options.noColor ?? Boolean(process.env.NO_COLOR)) ||
|
|
41759
|
-
options.preset === 'monochrome';
|
|
41760
|
-
const ascii = options.ascii ?? shouldUseAscii(options.term ?? process.env.TERM);
|
|
41761
|
-
const requestedPreset = options.preset && options.preset !== 'monochrome' ? options.preset : 'default';
|
|
41762
|
-
// P5.2 — gracefully downgrade hex presets (catppuccin / gruvbox) when
|
|
41763
|
-
// the host terminal can't render truecolor. Chalk approximates hex in
|
|
41764
|
-
// those modes anyway, but the default preset's ANSI-named palette
|
|
41765
|
-
// renders far more faithfully on 16-color terminals.
|
|
41766
|
-
const colorLevel = getColorLevel(options.env ?? process.env);
|
|
41767
|
-
const preset = !noColor && presetUsesTrueColor(requestedPreset) && colorLevel !== 'truecolor'
|
|
41768
|
-
? 'default'
|
|
41769
|
-
: requestedPreset;
|
|
41770
|
-
const colors = noColor
|
|
41771
|
-
? {}
|
|
41772
|
-
: {
|
|
41773
|
-
...THEME_PRESET_COLORS[preset],
|
|
41774
|
-
...options.colors,
|
|
42091
|
+
dispatch({
|
|
42092
|
+
type: 'setStatus',
|
|
42093
|
+
value: `${target.label} target commit returned no rows — orphan ref?`,
|
|
42094
|
+
kind: 'warning',
|
|
42095
|
+
});
|
|
42096
|
+
}
|
|
42097
|
+
}
|
|
42098
|
+
catch (error) {
|
|
42099
|
+
if (mountedRef.current) {
|
|
42100
|
+
dispatch({
|
|
42101
|
+
type: 'setStatus',
|
|
42102
|
+
value: `Failed to load context for ${target.label}: ${error instanceof Error ? error.message : String(error)}`,
|
|
42103
|
+
kind: 'error',
|
|
42104
|
+
});
|
|
42105
|
+
}
|
|
42106
|
+
}
|
|
42107
|
+
}, [dispatch, git]);
|
|
42108
|
+
React.useEffect(() => {
|
|
42109
|
+
loadCommitContextRef.current = loadCommitContext;
|
|
42110
|
+
}, [loadCommitContext]);
|
|
42111
|
+
// Server-side history filter (#776). When the user submits `path:foo`
|
|
42112
|
+
// or `author:foo`, the filter parser dispatches setHistoryFetchArgs;
|
|
42113
|
+
// this effect picks up the change, re-runs `getLogRows` with merged
|
|
42114
|
+
// args, and replaces the rows. Clearing the fetch args (Ctrl+U inside
|
|
42115
|
+
// filter mode) re-fetches with the original logArgv so the user gets
|
|
42116
|
+
// the live full log back, not a stale snapshot of the initial rows.
|
|
42117
|
+
const historyFetchEffectInitialized = React.useRef(false);
|
|
42118
|
+
const historyFetchRequestRef = React.useRef(0);
|
|
42119
|
+
React.useEffect(() => {
|
|
42120
|
+
if (!logArgv)
|
|
42121
|
+
return;
|
|
42122
|
+
// Skip the first run — initial rows came in via deps.rows; we only
|
|
42123
|
+
// want to fetch in response to *changes* to historyFetchArgs.
|
|
42124
|
+
if (!historyFetchEffectInitialized.current) {
|
|
42125
|
+
historyFetchEffectInitialized.current = true;
|
|
42126
|
+
return;
|
|
42127
|
+
}
|
|
42128
|
+
const requestId = historyFetchRequestRef.current + 1;
|
|
42129
|
+
historyFetchRequestRef.current = requestId;
|
|
42130
|
+
const fetchArgs = state.historyFetchArgs;
|
|
42131
|
+
const merged = {
|
|
42132
|
+
...logArgv,
|
|
42133
|
+
...(fetchArgs?.author ? { author: fetchArgs.author } : {}),
|
|
42134
|
+
...(fetchArgs?.path ? { path: fetchArgs.path } : {}),
|
|
41775
42135
|
};
|
|
41776
|
-
|
|
41777
|
-
|
|
41778
|
-
|
|
41779
|
-
|
|
41780
|
-
|
|
41781
|
-
|
|
42136
|
+
const description = fetchArgs?.author
|
|
42137
|
+
? `author:${fetchArgs.author}`
|
|
42138
|
+
: fetchArgs?.path
|
|
42139
|
+
? `path:${fetchArgs.path}`
|
|
42140
|
+
: undefined;
|
|
42141
|
+
dispatch({
|
|
42142
|
+
type: 'setStatus',
|
|
42143
|
+
value: description ? `Refetching with ${description}` : 'Restoring full log',
|
|
42144
|
+
});
|
|
42145
|
+
void (async () => {
|
|
42146
|
+
const stashHashes = await getStashCommitHashes(git).catch(() => []);
|
|
42147
|
+
const nextRows = await safe(getLogRows(git, merged, {
|
|
42148
|
+
limit: LOG_INTERACTIVE_DEFAULT_LIMIT,
|
|
42149
|
+
extraRefs: stashHashes,
|
|
42150
|
+
}));
|
|
42151
|
+
if (!mountedRef.current || historyFetchRequestRef.current !== requestId) {
|
|
42152
|
+
return;
|
|
42153
|
+
}
|
|
42154
|
+
if (!nextRows) {
|
|
42155
|
+
dispatch({ type: 'setStatus', value: 'Failed to refetch with active filter', kind: 'error' });
|
|
42156
|
+
return;
|
|
42157
|
+
}
|
|
42158
|
+
dispatch({ type: 'replaceRows', rows: nextRows });
|
|
42159
|
+
const matched = getCommitRows(nextRows).length;
|
|
42160
|
+
setHasMoreCommits(matched >= LOG_INTERACTIVE_DEFAULT_LIMIT);
|
|
42161
|
+
dispatch({
|
|
42162
|
+
type: 'setStatus',
|
|
42163
|
+
value: description
|
|
42164
|
+
? `Showing ${matched} commits matching ${description}`
|
|
42165
|
+
: 'Showing full log',
|
|
42166
|
+
kind: 'success',
|
|
42167
|
+
});
|
|
42168
|
+
})();
|
|
42169
|
+
}, [dispatch, git, logArgv, state.historyFetchArgs]);
|
|
42170
|
+
// Graph mode toggle (`g` key, #791 follow-up). The header label flips
|
|
42171
|
+
// between "compact graph" and "full graph", but unless we re-fetch with
|
|
42172
|
+
// the right `view`, the underlying rows still come from the user's
|
|
42173
|
+
// initial argv (default `--first-parent --no-merges`) and the renderer
|
|
42174
|
+
// has no topology to draw — defeating the per-lane / junction work.
|
|
42175
|
+
// Mirrors the historyFetchArgs effect: skip first run, request-id ref
|
|
42176
|
+
// for stale-completion guard, swap rows in place via replaceRows.
|
|
42177
|
+
const toggleGraphEffectInitialized = React.useRef(false);
|
|
42178
|
+
const toggleGraphRequestRef = React.useRef(0);
|
|
42179
|
+
React.useEffect(() => {
|
|
42180
|
+
if (!logArgv)
|
|
42181
|
+
return;
|
|
42182
|
+
if (!toggleGraphEffectInitialized.current) {
|
|
42183
|
+
toggleGraphEffectInitialized.current = true;
|
|
42184
|
+
return;
|
|
42185
|
+
}
|
|
42186
|
+
const requestId = toggleGraphRequestRef.current + 1;
|
|
42187
|
+
toggleGraphRequestRef.current = requestId;
|
|
42188
|
+
const merged = buildToggleGraphArgs(logArgv, state.fullGraph);
|
|
42189
|
+
dispatch({
|
|
42190
|
+
type: 'setStatus',
|
|
42191
|
+
value: state.fullGraph
|
|
42192
|
+
? 'Loading full topology…'
|
|
42193
|
+
: 'Loading compact history…',
|
|
42194
|
+
});
|
|
42195
|
+
void (async () => {
|
|
42196
|
+
// Include stash commits as graph roots so the toggle's re-fetch
|
|
42197
|
+
// sees the same rich graph the boot loader assembles. Without
|
|
42198
|
+
// this, flipping `\` into full mode and back loses the stash
|
|
42199
|
+
// anchors that loadRowsWithStashes seeded on boot.
|
|
42200
|
+
const stashHashes = await getStashCommitHashes(git).catch(() => []);
|
|
42201
|
+
const nextRows = await safe(getLogRows(git, merged, {
|
|
42202
|
+
limit: LOG_INTERACTIVE_DEFAULT_LIMIT,
|
|
42203
|
+
extraRefs: stashHashes,
|
|
42204
|
+
}));
|
|
42205
|
+
if (!mountedRef.current || toggleGraphRequestRef.current !== requestId) {
|
|
42206
|
+
return;
|
|
42207
|
+
}
|
|
42208
|
+
if (!nextRows) {
|
|
42209
|
+
dispatch({ type: 'setStatus', value: 'Failed to refetch graph rows', kind: 'error' });
|
|
42210
|
+
return;
|
|
42211
|
+
}
|
|
42212
|
+
dispatch({ type: 'replaceRows', rows: nextRows });
|
|
42213
|
+
const matched = getCommitRows(nextRows).length;
|
|
42214
|
+
setHasMoreCommits(matched >= LOG_INTERACTIVE_DEFAULT_LIMIT);
|
|
42215
|
+
dispatch({
|
|
42216
|
+
type: 'setStatus',
|
|
42217
|
+
value: state.fullGraph
|
|
42218
|
+
? `Showing ${matched} commits across all branches`
|
|
42219
|
+
: `Showing ${matched} commits (compact)`,
|
|
42220
|
+
kind: 'success',
|
|
42221
|
+
});
|
|
42222
|
+
})();
|
|
42223
|
+
}, [dispatch, git, logArgv, state.fullGraph]);
|
|
42224
|
+
const commitDiffHunkOffsets = React.useMemo(() => (filePreview?.hunks
|
|
42225
|
+
.map((line, index) => (line.startsWith('@@') ? index : -1))
|
|
42226
|
+
.filter((index) => index >= 0)), [filePreview]);
|
|
42227
|
+
const worktreeDirty = Boolean(context.worktree &&
|
|
42228
|
+
(context.worktree.stagedCount + context.worktree.unstagedCount + context.worktree.untrackedCount) > 0);
|
|
42229
|
+
useInput((inputValue, key) => {
|
|
42230
|
+
// First-launch onboarding (P1.3): any keystroke dismisses the overlay
|
|
42231
|
+
// and writes the seen-marker. Swallow the keystroke so the same key
|
|
42232
|
+
// doesn't also trigger normal input dispatch.
|
|
42233
|
+
if (showOnboarding) {
|
|
42234
|
+
setShowOnboarding(false);
|
|
42235
|
+
markOnboardingSeen();
|
|
42236
|
+
return;
|
|
42237
|
+
}
|
|
42238
|
+
// P4.5: navigation in branches/tags/stash uses the FILTERED list
|
|
42239
|
+
// length when a filter is active so j/k stay live instead of getting
|
|
42240
|
+
// stuck against a full-list count that no longer matches what's on
|
|
42241
|
+
// screen. The filtered lists are memoized at LogInkApp scope (#808
|
|
42242
|
+
// perf pass) — reading them here is O(1) instead of O(branches +
|
|
42243
|
+
// tags + stashes + worktrees) per keystroke.
|
|
42244
|
+
const branchVisibleCount = filteredBranchList.length;
|
|
42245
|
+
const branchSelectedShortName = filteredBranchList[Math.min(state.selectedBranchIndex, Math.max(0, filteredBranchList.length - 1))]?.shortName;
|
|
42246
|
+
const tagVisibleCount = filteredTagList.length;
|
|
42247
|
+
const tagSelectedName = filteredTagList[Math.min(state.selectedTagIndex, Math.max(0, filteredTagList.length - 1))]?.name;
|
|
42248
|
+
const stashVisibleCount = filteredStashList.length;
|
|
42249
|
+
const stashSelectedRef = filteredStashList[Math.min(state.selectedStashIndex, Math.max(0, filteredStashList.length - 1))]?.ref;
|
|
42250
|
+
const reflogVisibleCount = filteredReflogList.length;
|
|
42251
|
+
const reflogSelectedHash = filteredReflogList[Math.min(state.selectedReflogIndex, Math.max(0, filteredReflogList.length - 1))]?.hash;
|
|
42252
|
+
const submoduleVisibleCount = filteredSubmoduleList.length;
|
|
42253
|
+
filteredSubmoduleList[Math.min(state.selectedSubmoduleIndex, Math.max(0, filteredSubmoduleList.length - 1))]?.path;
|
|
42254
|
+
const issueVisibleCount = filteredIssueList.length;
|
|
42255
|
+
const issueSelectedUrl = filteredIssueList[Math.min(state.selectedIssueIndex, Math.max(0, filteredIssueList.length - 1))]?.url;
|
|
42256
|
+
const pullRequestTriageVisibleCount = filteredPullRequestTriageList.length;
|
|
42257
|
+
const pullRequestTriageSelectedUrl = filteredPullRequestTriageList[Math.min(state.selectedPullRequestTriageIndex, Math.max(0, filteredPullRequestTriageList.length - 1))]?.url;
|
|
42258
|
+
const worktreeVisibleCount = filteredWorktreeList.length;
|
|
42259
|
+
// When the diff view is showing a stash patch, swap the previewLineCount
|
|
42260
|
+
// to the stash diff length so the existing pageDetailPreview path
|
|
42261
|
+
// (j/k, PgUp/PgDn) scrolls through it without a parallel pipeline.
|
|
42262
|
+
const diffPreviewLineCount = state.diffSource === 'stash'
|
|
42263
|
+
? stashDiffLines?.length
|
|
42264
|
+
: filePreview?.hunks.length;
|
|
42265
|
+
// Per-file segmentation for stash diffs reads the LogInkApp-scoped
|
|
42266
|
+
// memo so navigation keys + the input-context derivation share a
|
|
42267
|
+
// single parse pass per stash patch instead of re-walking the
|
|
42268
|
+
// entire patch text on every keystroke.
|
|
42269
|
+
const stashDiffFiles = state.diffSource === 'stash' ? stashDiffParsedFiles : [];
|
|
42270
|
+
const stashDiffFileOffsets = stashDiffFiles.map((file) => file.startLine);
|
|
42271
|
+
const stashDiffSelectedPath = state.diffSource === 'stash'
|
|
42272
|
+
? findStashFileForOffset(stashDiffFiles, state.diffPreviewOffset)?.path
|
|
42273
|
+
: undefined;
|
|
42274
|
+
getLogInkInputEvents(state, inputValue, key, {
|
|
42275
|
+
detailFileCount: detail?.files.length,
|
|
42276
|
+
previewLineCount: diffPreviewLineCount,
|
|
42277
|
+
worktreeDiffLineCount: worktreeDiff?.lines.length,
|
|
42278
|
+
worktreeFileCount: visibleWorktreeFilesGrouped.length,
|
|
42279
|
+
worktreeHunkOffsets: worktreeDiff?.hunkOffsets,
|
|
42280
|
+
commitDiffHunkOffsets,
|
|
42281
|
+
branchCount: branchVisibleCount,
|
|
42282
|
+
branchSelectedShortName,
|
|
42283
|
+
tagCount: tagVisibleCount,
|
|
42284
|
+
tagSelectedName,
|
|
42285
|
+
stashCount: stashVisibleCount,
|
|
42286
|
+
reflogCount: reflogVisibleCount,
|
|
42287
|
+
reflogSelectedHash,
|
|
42288
|
+
submoduleCount: submoduleVisibleCount,
|
|
42289
|
+
issueCount: issueVisibleCount,
|
|
42290
|
+
issueSelectedUrl,
|
|
42291
|
+
pullRequestTriageCount: pullRequestTriageVisibleCount,
|
|
42292
|
+
pullRequestTriageSelectedUrl,
|
|
42293
|
+
stashSelectedRef,
|
|
42294
|
+
stashDiffFileOffsets: stashDiffFileOffsets.length ? stashDiffFileOffsets : undefined,
|
|
42295
|
+
stashDiffSelectedPath,
|
|
42296
|
+
worktreeListCount: worktreeVisibleCount,
|
|
42297
|
+
worktreeSelectedPath: visibleWorktreeFilesGrouped[state.selectedWorktreeFileIndex]?.path,
|
|
42298
|
+
statusGroups: visibleWorktreeGroups.map((group) => ({
|
|
42299
|
+
state: group.state,
|
|
42300
|
+
count: group.files.length,
|
|
42301
|
+
startIndex: group.startIndex,
|
|
42302
|
+
})),
|
|
42303
|
+
inspectorActionCount: getInspectorActionsForState(state).length,
|
|
42304
|
+
commitDiffSelectedPath: state.diffSource === 'commit'
|
|
42305
|
+
? selectedDetailFile?.path
|
|
42306
|
+
: undefined,
|
|
42307
|
+
commitDiffSelectedSha: state.diffSource === 'commit'
|
|
42308
|
+
? selected?.hash
|
|
42309
|
+
: undefined,
|
|
42310
|
+
// #931 PR 3b — Submodule drill-in target for the cursored file
|
|
42311
|
+
// in a commit diff. Resolved per-render so the Enter handler in
|
|
42312
|
+
// `inkInput.ts` doesn't have to re-walk the submodule overview;
|
|
42313
|
+
// undefined whenever the cursored file isn't a registered
|
|
42314
|
+
// submodule (or the overview / repo root haven't loaded yet).
|
|
42315
|
+
commitDiffSubmoduleDrillIn: state.diffSource === 'commit' && selectedDetailFile
|
|
42316
|
+
? resolveCommitDiffDrillInTarget({
|
|
42317
|
+
selectedFile: {
|
|
42318
|
+
path: selectedDetailFile.path,
|
|
42319
|
+
submoduleChange: filePreview?.path === selectedDetailFile.path
|
|
42320
|
+
? filePreview.submoduleChange
|
|
42321
|
+
: undefined,
|
|
42322
|
+
},
|
|
42323
|
+
submodules: context.submodules,
|
|
42324
|
+
activeRepoRoot,
|
|
42325
|
+
})
|
|
42326
|
+
: undefined,
|
|
42327
|
+
// #931 PR 4 / #932 — Submodule drill-in target for the cursored
|
|
42328
|
+
// row in the dedicated submodules view. Resolved per-render so
|
|
42329
|
+
// the Enter handler in `inkInput.ts` doesn't have to re-walk the
|
|
42330
|
+
// submodule overview. Gated on `activeView === 'submodules'` so
|
|
42331
|
+
// a stale resolution from a different view can't accidentally
|
|
42332
|
+
// fire — the runtime only ever populates it when the user is
|
|
42333
|
+
// actually on the view.
|
|
42334
|
+
submoduleViewDrillIn: state.activeView === 'submodules'
|
|
42335
|
+
? resolveSubmoduleViewDrillInTarget({
|
|
42336
|
+
selectedIndex: state.selectedSubmoduleIndex,
|
|
42337
|
+
submodules: context.submodules,
|
|
42338
|
+
activeRepoRoot,
|
|
42339
|
+
})
|
|
42340
|
+
: undefined,
|
|
42341
|
+
worktreeDirty,
|
|
42342
|
+
conflictFileCount: context.operation?.conflictedFiles.length,
|
|
42343
|
+
conflictSelectedPath: (() => {
|
|
42344
|
+
const files = context.operation?.conflictedFiles;
|
|
42345
|
+
if (!files || files.length === 0)
|
|
42346
|
+
return undefined;
|
|
42347
|
+
const clamped = Math.min(state.selectedConflictFileIndex, files.length - 1);
|
|
42348
|
+
return files[clamped]?.path;
|
|
42349
|
+
})(),
|
|
42350
|
+
// H / gH need the actual diff text (not just hunk offsets) to
|
|
42351
|
+
// slice the cursored hunk into a `git apply` patch. Stash uses
|
|
42352
|
+
// the full `git stash show -p` output; commit-diff uses the
|
|
42353
|
+
// per-file `filePreview.hunks` array. Either way, extractDiffHunk
|
|
42354
|
+
// walks `@@` headers and synthesizes a fresh diff --git / --- /
|
|
42355
|
+
// +++ header set using the path the caller already resolved.
|
|
42356
|
+
diffLinesForHunkApply: state.diffSource === 'stash'
|
|
42357
|
+
? stashDiffLines
|
|
42358
|
+
: state.diffSource === 'commit'
|
|
42359
|
+
? filePreview?.hunks
|
|
42360
|
+
: undefined,
|
|
42361
|
+
// Line count of the changelog text, used by the changelog view's
|
|
42362
|
+
// j/k/PgUp/PgDn scroll bindings to clamp `pageChangelog` deltas.
|
|
42363
|
+
// Computed from view state rather than threaded through context
|
|
42364
|
+
// because the surface owns its own content — no external loader.
|
|
42365
|
+
changelogLineCount: state.changelogView.text?.split('\n').length,
|
|
42366
|
+
// Approximate line count for the split-plan overlay. Each group
|
|
42367
|
+
// renders as a header + (body if any) + files block + (rationale
|
|
42368
|
+
// if any) + blank separator. Used by j/k/PgUp/PgDn to clamp the
|
|
42369
|
+
// scroll offset. The exact render math lives in the overlay
|
|
42370
|
+
// module — this is a close-enough heuristic for clamping.
|
|
42371
|
+
// #879 item 3 — short sha of the bisect terminator (if any).
|
|
42372
|
+
// Gates `y`/`Y` yank on the completion panel and lets the
|
|
42373
|
+
// runtime resolve the value without re-parsing the log.
|
|
42374
|
+
bisectCompletionSha: context.bisect?.active
|
|
42375
|
+
? getBisectCompletion(context.bisect.log)?.sha
|
|
42376
|
+
: undefined,
|
|
42377
|
+
// #879 item 4 — disambiguates the bisect view's `s` keystroke
|
|
42378
|
+
// (skip current candidate vs. start the wizard).
|
|
42379
|
+
bisectActive: Boolean(context.bisect?.active),
|
|
42380
|
+
splitPlanLineCount: state.splitPlan?.plan
|
|
42381
|
+
? state.splitPlan.plan.groups.reduce((sum, group) => {
|
|
42382
|
+
let lines = 2; // title + separator
|
|
42383
|
+
if (group.body)
|
|
42384
|
+
lines += group.body.split('\n').length + 1;
|
|
42385
|
+
if (group.rationale)
|
|
42386
|
+
lines += 2;
|
|
42387
|
+
lines += (group.files?.length || 0) + 1;
|
|
42388
|
+
const hunkCount = group.hunks?.length || 0;
|
|
42389
|
+
if (hunkCount > 0)
|
|
42390
|
+
lines += hunkCount + 1;
|
|
42391
|
+
return sum + lines;
|
|
42392
|
+
}, 0)
|
|
42393
|
+
: undefined,
|
|
42394
|
+
}).forEach((event) => {
|
|
42395
|
+
if (event.type === 'exit') {
|
|
42396
|
+
exit();
|
|
42397
|
+
}
|
|
42398
|
+
else if (event.type === 'refreshContext') {
|
|
42399
|
+
void refreshContext();
|
|
42400
|
+
}
|
|
42401
|
+
else if (event.type === 'toggleSelectedFileStage') {
|
|
42402
|
+
void toggleSelectedFileStage();
|
|
42403
|
+
}
|
|
42404
|
+
else if (event.type === 'toggleSelectedHunkStage') {
|
|
42405
|
+
void toggleSelectedHunkStage();
|
|
42406
|
+
}
|
|
42407
|
+
else if (event.type === 'revertSelectedFile') {
|
|
42408
|
+
void revertSelectedFile();
|
|
42409
|
+
}
|
|
42410
|
+
else if (event.type === 'revertSelectedHunk') {
|
|
42411
|
+
void revertSelectedHunk();
|
|
42412
|
+
}
|
|
42413
|
+
else if (event.type === 'createManualCommit') {
|
|
42414
|
+
void createCommitFromCompose();
|
|
42415
|
+
}
|
|
42416
|
+
else if (event.type === 'runAiCommitDraft') {
|
|
42417
|
+
void runAiCommitDraft();
|
|
42418
|
+
}
|
|
42419
|
+
else if (event.type === 'cancelAiCommitDraft') {
|
|
42420
|
+
cancelAiCommitDraft();
|
|
42421
|
+
}
|
|
42422
|
+
else if (event.type === 'startCreatePullRequest') {
|
|
42423
|
+
void startCreatePullRequest();
|
|
42424
|
+
}
|
|
42425
|
+
else if (event.type === 'cancelPullRequestBodyDraft') {
|
|
42426
|
+
cancelPullRequestBodyDraft();
|
|
42427
|
+
}
|
|
42428
|
+
else if (event.type === 'startChangelogView') {
|
|
42429
|
+
void startChangelogView();
|
|
42430
|
+
}
|
|
42431
|
+
else if (event.type === 'regenerateChangelog') {
|
|
42432
|
+
regenerateChangelog();
|
|
42433
|
+
}
|
|
42434
|
+
else if (event.type === 'yankChangelog') {
|
|
42435
|
+
yankChangelog();
|
|
42436
|
+
}
|
|
42437
|
+
else if (event.type === 'openChangelogInEditor') {
|
|
42438
|
+
openChangelogInEditor();
|
|
42439
|
+
}
|
|
42440
|
+
else if (event.type === 'openComposeInEditor') {
|
|
42441
|
+
openComposeInEditor();
|
|
42442
|
+
}
|
|
42443
|
+
else if (event.type === 'startCommitSplit') {
|
|
42444
|
+
void startCommitSplit();
|
|
42445
|
+
}
|
|
42446
|
+
else if (event.type === 'applyCommitSplit') {
|
|
42447
|
+
void applyCommitSplit();
|
|
42448
|
+
}
|
|
42449
|
+
else if (event.type === 'cancelCommitSplit') {
|
|
42450
|
+
cancelCommitSplit();
|
|
42451
|
+
}
|
|
42452
|
+
else if (event.type === 'yankText') {
|
|
42453
|
+
void yankText(event.value, event.label);
|
|
42454
|
+
}
|
|
42455
|
+
else if (event.type === 'runWorkflowAction') {
|
|
42456
|
+
void runWorkflowAction(event.id, event.payload);
|
|
42457
|
+
}
|
|
42458
|
+
else if (event.type === 'openFileInEditor') {
|
|
42459
|
+
openInEditor(event.path);
|
|
42460
|
+
}
|
|
42461
|
+
else if (event.type === 'yankFromActiveView') {
|
|
42462
|
+
void yankFromActiveView(event.short);
|
|
42463
|
+
}
|
|
42464
|
+
else if (event.type === 'applyThemePreset') {
|
|
42465
|
+
// Apply for the session immediately, and best-effort persist to the
|
|
42466
|
+
// global config so it sticks across launches. The picker has already
|
|
42467
|
+
// dispatched `toggleThemePicker` (closing it), which clears the
|
|
42468
|
+
// preview via the sync effect below — the session preset takes over.
|
|
42469
|
+
const preset = event.preset;
|
|
42470
|
+
setThemeSessionPreset(preset);
|
|
42471
|
+
saveThemePreset(preset);
|
|
42472
|
+
}
|
|
42473
|
+
else {
|
|
42474
|
+
// P4.5: enrich filter-mutating actions with a precomputed
|
|
42475
|
+
// selection snapshot so the reducer can preserve the cursor on
|
|
42476
|
+
// the same item when it's still in the filtered result, only
|
|
42477
|
+
// snapping to result[0] when the previously selected item drops
|
|
42478
|
+
// out. The snapshot lives in the action so the reducer never
|
|
42479
|
+
// needs context items.
|
|
42480
|
+
const enriched = enrichFilterActionWithRectification(event.action, state, context);
|
|
42481
|
+
dispatch(enriched);
|
|
42482
|
+
}
|
|
42483
|
+
});
|
|
42484
|
+
});
|
|
42485
|
+
// Layout depends on focus (sidebar grows when focused), so it's
|
|
42486
|
+
// computed here — after state is in scope but before the render path.
|
|
42487
|
+
const layout = getLogInkLayout({
|
|
42488
|
+
columns: windowSize.columns || process.stdout.columns || LOG_INK_DEFAULT_COLUMNS,
|
|
42489
|
+
rows: windowSize.rows || process.stdout.rows || LOG_INK_DEFAULT_ROWS,
|
|
42490
|
+
sidebarFocused: state.focus === 'sidebar',
|
|
42491
|
+
inspectorFocused: state.focus === 'detail',
|
|
42492
|
+
helpOverlayActive: state.showHelp,
|
|
42493
|
+
});
|
|
42494
|
+
if (layout.tooSmall) {
|
|
42495
|
+
return h(Box, {
|
|
42496
|
+
flexDirection: 'column',
|
|
42497
|
+
height: layout.rows,
|
|
42498
|
+
paddingX: 1,
|
|
42499
|
+
paddingY: 1,
|
|
42500
|
+
}, 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.'));
|
|
42501
|
+
}
|
|
42502
|
+
// First-launch onboarding overlay (P1.3) replaces the entire UI for
|
|
42503
|
+
// one render — any keystroke dismisses it and persists the seen-marker.
|
|
42504
|
+
if (showOnboarding) {
|
|
42505
|
+
return renderOnboardingOverlay(h, { Box, Text }, layout.rows, layout.columns, theme, appLabel);
|
|
42506
|
+
}
|
|
42507
|
+
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));
|
|
41782
42508
|
}
|
|
41783
42509
|
|
|
41784
42510
|
/**
|
|
@@ -41956,6 +42682,7 @@ async function startInkInteractiveLog(git, rows, streams = {}, options = {}) {
|
|
|
41956
42682
|
React,
|
|
41957
42683
|
rows,
|
|
41958
42684
|
theme: createLogInkTheme(options.theme),
|
|
42685
|
+
themeConfig: options.theme,
|
|
41959
42686
|
resumeRef,
|
|
41960
42687
|
});
|
|
41961
42688
|
const instance = ink.render(app, getLogInkRenderOptions({ input, output, error }));
|
|
@@ -43401,7 +44128,7 @@ const options$1 = {
|
|
|
43401
44128
|
},
|
|
43402
44129
|
theme: {
|
|
43403
44130
|
description: 'TUI theme preset',
|
|
43404
|
-
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'],
|
|
44131
|
+
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'],
|
|
43405
44132
|
},
|
|
43406
44133
|
};
|
|
43407
44134
|
const builder$1 = (yargs) => {
|
|
@@ -43429,7 +44156,7 @@ const options = {
|
|
|
43429
44156
|
},
|
|
43430
44157
|
theme: {
|
|
43431
44158
|
description: 'TUI theme preset',
|
|
43432
|
-
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'],
|
|
44159
|
+
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'],
|
|
43433
44160
|
},
|
|
43434
44161
|
};
|
|
43435
44162
|
const builder = (yargs) => {
|
|
@@ -44132,6 +44859,9 @@ function createWorkspaceState(init) {
|
|
|
44132
44859
|
roots: init.roots,
|
|
44133
44860
|
showHelp: false,
|
|
44134
44861
|
showOnboarding: Boolean(init.showOnboarding),
|
|
44862
|
+
showThemePicker: false,
|
|
44863
|
+
themePickerFilter: '',
|
|
44864
|
+
themePickerIndex: 0,
|
|
44135
44865
|
knownRepoPaths: init.knownRepoPaths ?? [],
|
|
44136
44866
|
pullRequestFetching: [],
|
|
44137
44867
|
};
|
|
@@ -44302,6 +45032,39 @@ function applyWorkspaceAction(state, action) {
|
|
|
44302
45032
|
case 'close-help': {
|
|
44303
45033
|
return { ...state, showHelp: false };
|
|
44304
45034
|
}
|
|
45035
|
+
case 'toggle-theme-picker': {
|
|
45036
|
+
return {
|
|
45037
|
+
...state,
|
|
45038
|
+
showThemePicker: !state.showThemePicker,
|
|
45039
|
+
showHelp: false,
|
|
45040
|
+
showOnboarding: false,
|
|
45041
|
+
themePickerFilter: '',
|
|
45042
|
+
themePickerIndex: 0,
|
|
45043
|
+
};
|
|
45044
|
+
}
|
|
45045
|
+
case 'move-theme-picker': {
|
|
45046
|
+
return {
|
|
45047
|
+
...state,
|
|
45048
|
+
themePickerIndex: clampCursor(state.themePickerIndex + action.delta, action.presetCount),
|
|
45049
|
+
};
|
|
45050
|
+
}
|
|
45051
|
+
case 'append-theme-picker-filter': {
|
|
45052
|
+
return {
|
|
45053
|
+
...state,
|
|
45054
|
+
themePickerFilter: `${state.themePickerFilter}${action.value}`,
|
|
45055
|
+
themePickerIndex: 0,
|
|
45056
|
+
};
|
|
45057
|
+
}
|
|
45058
|
+
case 'backspace-theme-picker-filter': {
|
|
45059
|
+
return {
|
|
45060
|
+
...state,
|
|
45061
|
+
themePickerFilter: state.themePickerFilter.slice(0, -1),
|
|
45062
|
+
themePickerIndex: 0,
|
|
45063
|
+
};
|
|
45064
|
+
}
|
|
45065
|
+
case 'clear-theme-picker-filter': {
|
|
45066
|
+
return { ...state, themePickerFilter: '', themePickerIndex: 0 };
|
|
45067
|
+
}
|
|
44305
45068
|
case 'dismiss-onboarding': {
|
|
44306
45069
|
return { ...state, showOnboarding: false };
|
|
44307
45070
|
}
|
|
@@ -44713,33 +45476,48 @@ function buildWorkspaceHeaderChips(state, options = { focusLabel: 'List' }) {
|
|
|
44713
45476
|
});
|
|
44714
45477
|
return chips;
|
|
44715
45478
|
}
|
|
44716
|
-
// Footer hints
|
|
44717
|
-
//
|
|
44718
|
-
//
|
|
44719
|
-
//
|
|
44720
|
-
|
|
44721
|
-
|
|
44722
|
-
|
|
44723
|
-
const
|
|
44724
|
-
const
|
|
44725
|
-
|
|
45479
|
+
// Footer hints are split into two slots — same pattern as `coco ui`:
|
|
45480
|
+
// - contextual : per-mode actions (sort, filter, refresh, add/remove)
|
|
45481
|
+
// - global : always-on essentials (help, quit) — never crowded out
|
|
45482
|
+
//
|
|
45483
|
+
// The contextual slot drops bindings users can find via the help
|
|
45484
|
+
// overlay (arrow keys, tab); the global slot is the safety net so
|
|
45485
|
+
// `? help` and `q quit` never disappear.
|
|
45486
|
+
const LIST_CONTEXTUAL = ['s sort', '/ filter', 'r/R refresh', 'a add', 'd remove'];
|
|
45487
|
+
const SIDEBAR_CONTEXTUAL = ['↑/↓ cycle tab', 'enter open'];
|
|
45488
|
+
const FILTER_CONTEXTUAL = ['type to filter', 'enter apply', 'esc cancel'];
|
|
45489
|
+
const ADD_REPO_CONTEXTUAL = ['type path', 'tab to complete', 'enter to add', 'esc to cancel'];
|
|
45490
|
+
const CONFIRM_DELETE_CONTEXTUAL = ['y confirm', 'any other key cancels'];
|
|
45491
|
+
const GLOBAL_HINTS = ['? help', 'q quit'];
|
|
45492
|
+
function contextualHintsFor(focus) {
|
|
44726
45493
|
switch (focus) {
|
|
44727
45494
|
case 'sidebar':
|
|
44728
|
-
return
|
|
45495
|
+
return SIDEBAR_CONTEXTUAL;
|
|
44729
45496
|
case 'filter':
|
|
44730
|
-
return
|
|
45497
|
+
return FILTER_CONTEXTUAL;
|
|
44731
45498
|
case 'add-repo':
|
|
44732
|
-
return
|
|
45499
|
+
return ADD_REPO_CONTEXTUAL;
|
|
44733
45500
|
case 'confirm-delete':
|
|
44734
|
-
return
|
|
45501
|
+
return CONFIRM_DELETE_CONTEXTUAL;
|
|
44735
45502
|
case 'list':
|
|
44736
45503
|
default:
|
|
44737
|
-
return
|
|
45504
|
+
return LIST_CONTEXTUAL;
|
|
44738
45505
|
}
|
|
44739
45506
|
}
|
|
44740
45507
|
function buildWorkspaceFooter(state) {
|
|
45508
|
+
const contextual = contextualHintsFor(state.focus);
|
|
45509
|
+
// Modal modes (filter / add-repo / confirm-delete) suppress the
|
|
45510
|
+
// global hints — those bindings are not reachable while a prompt
|
|
45511
|
+
// is open and showing them would be misleading.
|
|
45512
|
+
const isModal = state.focus === 'filter' ||
|
|
45513
|
+
state.focus === 'add-repo' ||
|
|
45514
|
+
state.focus === 'confirm-delete';
|
|
45515
|
+
const global = isModal ? [] : GLOBAL_HINTS;
|
|
45516
|
+
const allHints = [...contextual, ...global];
|
|
44741
45517
|
return {
|
|
44742
|
-
hint:
|
|
45518
|
+
hint: allHints.join(' · '),
|
|
45519
|
+
contextual,
|
|
45520
|
+
global,
|
|
44743
45521
|
status: state.status,
|
|
44744
45522
|
filterMode: state.focus === 'filter',
|
|
44745
45523
|
};
|
|
@@ -44749,11 +45527,26 @@ function buildWorkspaceFooter(state) {
|
|
|
44749
45527
|
* sections so users can scan by intent ("how do I navigate?" "how
|
|
44750
45528
|
* do I act?") rather than reading a flat alphabetized list.
|
|
44751
45529
|
*
|
|
45530
|
+
* Section order mirrors `coco ui`'s help convention — Essentials
|
|
45531
|
+
* first so newcomers see `?`/`esc`/`q` immediately, then move outward
|
|
45532
|
+
* to navigation, modification, and the destructive verbs last.
|
|
45533
|
+
*
|
|
44752
45534
|
* The view layer composes these into a panel with section titles,
|
|
44753
45535
|
* a leading app/title bar, and a closing hint at the bottom.
|
|
44754
45536
|
*/
|
|
44755
45537
|
function buildWorkspaceHelpSections() {
|
|
44756
45538
|
return [
|
|
45539
|
+
{
|
|
45540
|
+
title: 'Essentials',
|
|
45541
|
+
subtitle: 'The keys you reach for most often.',
|
|
45542
|
+
rows: [
|
|
45543
|
+
{ glyph: '?', keys: '?', description: 'Toggle this help overlay' },
|
|
45544
|
+
{ glyph: '⎋', keys: 'esc', description: 'Clear filter / close overlay / cancel prompt' },
|
|
45545
|
+
{ glyph: '◴', keys: 'q · ctrl+c', description: 'Quit the workspace surface' },
|
|
45546
|
+
{ glyph: '↵', keys: 'enter', description: 'Drill into the cursored repo (coco ui)' },
|
|
45547
|
+
{ glyph: '◧', keys: 'T', description: 'Theme picker — browse, live-preview & apply a color theme' },
|
|
45548
|
+
],
|
|
45549
|
+
},
|
|
44757
45550
|
{
|
|
44758
45551
|
title: 'Navigate',
|
|
44759
45552
|
subtitle: 'Move the cursor and switch focus between panels.',
|
|
@@ -44763,7 +45556,6 @@ function buildWorkspaceHelpSections() {
|
|
|
44763
45556
|
{ glyph: '←', keys: 'h', description: 'Jump focus to the sidebar' },
|
|
44764
45557
|
{ glyph: '→', keys: 'l', description: 'Jump focus to the list' },
|
|
44765
45558
|
{ glyph: '⤒', keys: 'g / G', description: 'Jump to top / bottom of the list' },
|
|
44766
|
-
{ glyph: '↵', keys: 'enter', description: 'Drill into the cursored repo (coco ui)' },
|
|
44767
45559
|
],
|
|
44768
45560
|
},
|
|
44769
45561
|
{
|
|
@@ -44783,14 +45575,6 @@ function buildWorkspaceHelpSections() {
|
|
|
44783
45575
|
{ glyph: '✕', keys: 'd', description: 'Remove the cursored repo from the known-repos store' },
|
|
44784
45576
|
],
|
|
44785
45577
|
},
|
|
44786
|
-
{
|
|
44787
|
-
title: 'General',
|
|
44788
|
-
rows: [
|
|
44789
|
-
{ glyph: '?', keys: '?', description: 'Toggle this help overlay' },
|
|
44790
|
-
{ glyph: '⎋', keys: 'esc', description: 'Clear filter / close overlay / cancel prompt' },
|
|
44791
|
-
{ glyph: '◴', keys: 'q · ctrl+c', description: 'Quit the workspace surface' },
|
|
44792
|
-
],
|
|
44793
|
-
},
|
|
44794
45578
|
];
|
|
44795
45579
|
}
|
|
44796
45580
|
function buildWorkspaceOnboarding(state) {
|
|
@@ -45237,13 +46021,19 @@ function renderFooter(deps) {
|
|
|
45237
46021
|
// height shifted by a row every time a status banner came and went,
|
|
45238
46022
|
// forcing the panel chrome to reflow.
|
|
45239
46023
|
const statusContent = model.status ?? '';
|
|
46024
|
+
const contextualText = model.contextual.join(' ');
|
|
46025
|
+
const globalText = model.global.join(' · ');
|
|
45240
46026
|
return React.createElement(Box, {
|
|
45241
46027
|
borderColor: focusBorderColor(theme, false),
|
|
45242
46028
|
borderStyle: theme.borderStyle,
|
|
45243
46029
|
paddingX: 1,
|
|
45244
46030
|
flexDirection: 'column',
|
|
45245
46031
|
height: FOOTER_HEIGHT,
|
|
45246
|
-
},
|
|
46032
|
+
},
|
|
46033
|
+
// Row 1: contextual ↔ global hints. justifyContent pushes them
|
|
46034
|
+
// to opposite edges so the eye scans each cluster as one block —
|
|
46035
|
+
// same shape as `coco ui`'s footer post-0.54.2 redesign.
|
|
46036
|
+
React.createElement(Box, { flexDirection: 'row', justifyContent: 'space-between' }, React.createElement(Text, { dimColor: true }, contextualText), React.createElement(Text, { dimColor: true }, globalText)), React.createElement(Text, {
|
|
45247
46037
|
color: model.status ? toneColor('warn', theme) : undefined,
|
|
45248
46038
|
dimColor: !model.status,
|
|
45249
46039
|
}, statusContent || ' '));
|
|
@@ -45279,6 +46069,11 @@ function renderWorkspaceApp(deps) {
|
|
|
45279
46069
|
if (deps.state.showHelp) {
|
|
45280
46070
|
return React.createElement(Box, { flexDirection: 'column', height: rootHeight }, renderHeader(deps), renderHelpOverlay(deps), renderFooter(deps));
|
|
45281
46071
|
}
|
|
46072
|
+
// Theme picker is modal too — the chrome live-previews underneath via
|
|
46073
|
+
// the reactive `deps.theme`, while the overlay replaces the body.
|
|
46074
|
+
if (deps.state.showThemePicker) {
|
|
46075
|
+
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));
|
|
46076
|
+
}
|
|
45282
46077
|
const bodyHeight = computeBodyHeight(deps);
|
|
45283
46078
|
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));
|
|
45284
46079
|
}
|
|
@@ -45319,6 +46114,37 @@ function resolveWorkspaceInput(input, key, state) {
|
|
|
45319
46114
|
}
|
|
45320
46115
|
return { kind: 'noop' };
|
|
45321
46116
|
}
|
|
46117
|
+
// Theme picker is modal (like `coco ui`'s gC): type to filter, ↑/↓ to
|
|
46118
|
+
// move, Enter applies + persists, Esc clears the filter then closes.
|
|
46119
|
+
if (state.showThemePicker) {
|
|
46120
|
+
const filtered = filterThemePresets(state.themePickerFilter);
|
|
46121
|
+
if (key.escape) {
|
|
46122
|
+
if (state.themePickerFilter.length > 0) {
|
|
46123
|
+
return { kind: 'action', action: { type: 'clear-theme-picker-filter' } };
|
|
46124
|
+
}
|
|
46125
|
+
return { kind: 'action', action: { type: 'toggle-theme-picker' } };
|
|
46126
|
+
}
|
|
46127
|
+
if (key.return) {
|
|
46128
|
+
const selected = getThemePickerSelectionFor(state.themePickerFilter, state.themePickerIndex);
|
|
46129
|
+
if (!selected) {
|
|
46130
|
+
return { kind: 'action', action: { type: 'toggle-theme-picker' } };
|
|
46131
|
+
}
|
|
46132
|
+
return { kind: 'apply-theme', preset: selected };
|
|
46133
|
+
}
|
|
46134
|
+
if (key.upArrow || (key.ctrl && input === 'p')) {
|
|
46135
|
+
return { kind: 'action', action: { type: 'move-theme-picker', delta: -1, presetCount: filtered.length } };
|
|
46136
|
+
}
|
|
46137
|
+
if (key.downArrow || (key.ctrl && input === 'n')) {
|
|
46138
|
+
return { kind: 'action', action: { type: 'move-theme-picker', delta: 1, presetCount: filtered.length } };
|
|
46139
|
+
}
|
|
46140
|
+
if (key.backspace || key.delete) {
|
|
46141
|
+
return { kind: 'action', action: { type: 'backspace-theme-picker-filter' } };
|
|
46142
|
+
}
|
|
46143
|
+
if (input && !key.ctrl && !key.meta) {
|
|
46144
|
+
return { kind: 'action', action: { type: 'append-theme-picker-filter', value: input } };
|
|
46145
|
+
}
|
|
46146
|
+
return { kind: 'noop' };
|
|
46147
|
+
}
|
|
45322
46148
|
if (state.focus === 'filter') {
|
|
45323
46149
|
if (key.escape) {
|
|
45324
46150
|
return { kind: 'action', action: { type: 'clear-filter' } };
|
|
@@ -45436,6 +46262,12 @@ function resolveWorkspaceInput(input, key, state) {
|
|
|
45436
46262
|
if (input === '?') {
|
|
45437
46263
|
return { kind: 'action', action: { type: 'toggle-help' } };
|
|
45438
46264
|
}
|
|
46265
|
+
// `T` opens the theme picker (browse + live-preview + apply a color
|
|
46266
|
+
// theme). Single key here rather than `coco ui`'s gC chord since the
|
|
46267
|
+
// workspace keymap is flat (no g-prefix continuations).
|
|
46268
|
+
if (input === 'T') {
|
|
46269
|
+
return { kind: 'action', action: { type: 'toggle-theme-picker' } };
|
|
46270
|
+
}
|
|
45439
46271
|
return { kind: 'noop' };
|
|
45440
46272
|
}
|
|
45441
46273
|
|
|
@@ -45817,6 +46649,7 @@ async function startWorkspace(options) {
|
|
|
45817
46649
|
ink,
|
|
45818
46650
|
React,
|
|
45819
46651
|
theme,
|
|
46652
|
+
themeConfig: options.theme,
|
|
45820
46653
|
resumeRef,
|
|
45821
46654
|
});
|
|
45822
46655
|
// Override exitOnCtrlC. Ink's default ctrl+c handler reaches into
|
|
@@ -45982,6 +46815,17 @@ function WorkspaceInkApp(props) {
|
|
|
45982
46815
|
// (see effect below) so idle workspaces don't burn CPU on animation
|
|
45983
46816
|
// frames.
|
|
45984
46817
|
const [spinnerTick, setSpinnerTick] = React.useState(0);
|
|
46818
|
+
// Theme picker (`T`) — reactive theme so the chrome live-previews the
|
|
46819
|
+
// cursored theme. `themePreviewPreset` follows the picker cursor while
|
|
46820
|
+
// open; `themeSessionPreset` is the applied choice. The effective theme
|
|
46821
|
+
// rebuilds from the original config; when neither is set we use the
|
|
46822
|
+
// static `props.theme` unchanged (mirrors `coco ui`).
|
|
46823
|
+
const [themePreviewPreset, setThemePreviewPreset] = React.useState(undefined);
|
|
46824
|
+
const [themeSessionPreset, setThemeSessionPreset] = React.useState(undefined);
|
|
46825
|
+
const effectiveThemePreset = themePreviewPreset ?? themeSessionPreset;
|
|
46826
|
+
const theme = React.useMemo(() => effectiveThemePreset
|
|
46827
|
+
? createLogInkTheme({ ...props.themeConfig, preset: effectiveThemePreset })
|
|
46828
|
+
: props.theme, [effectiveThemePreset, props.themeConfig, props.theme]);
|
|
45985
46829
|
const dispatch = React.useCallback((action) => {
|
|
45986
46830
|
setState((prev) => applyWorkspaceAction(prev, action));
|
|
45987
46831
|
}, []);
|
|
@@ -46356,6 +47200,13 @@ function WorkspaceInkApp(props) {
|
|
|
46356
47200
|
case 'action':
|
|
46357
47201
|
dispatch(intent.action);
|
|
46358
47202
|
break;
|
|
47203
|
+
case 'apply-theme':
|
|
47204
|
+
// Apply for the session + persist to the global config (best-effort),
|
|
47205
|
+
// then close the picker (clearing the preview via the sync effect).
|
|
47206
|
+
setThemeSessionPreset(intent.preset);
|
|
47207
|
+
saveThemePreset(intent.preset);
|
|
47208
|
+
dispatch({ type: 'toggle-theme-picker' });
|
|
47209
|
+
break;
|
|
46359
47210
|
case 'quit':
|
|
46360
47211
|
workspaceDebug('→ exit() called from quit intent');
|
|
46361
47212
|
exitRefHolder.current.current = { kind: 'quit' };
|
|
@@ -46405,11 +47256,20 @@ function WorkspaceInkApp(props) {
|
|
|
46405
47256
|
filter: state.filter,
|
|
46406
47257
|
});
|
|
46407
47258
|
}, [props.roots, state.sortMode, state.tab, state.filter]);
|
|
47259
|
+
// Keep the live preview in sync with the preset under the picker cursor
|
|
47260
|
+
// while the overlay is open; clear it on close so the theme reverts to
|
|
47261
|
+
// the applied session preset (or the original config theme).
|
|
47262
|
+
const themePickerSelection = state.showThemePicker
|
|
47263
|
+
? getThemePickerSelectionFor(state.themePickerFilter, state.themePickerIndex)
|
|
47264
|
+
: undefined;
|
|
47265
|
+
React.useEffect(() => {
|
|
47266
|
+
setThemePreviewPreset(state.showThemePicker ? themePickerSelection : undefined);
|
|
47267
|
+
}, [state.showThemePicker, themePickerSelection]);
|
|
46408
47268
|
return renderWorkspaceApp({
|
|
46409
47269
|
React,
|
|
46410
47270
|
ink: { Box: ink.Box, Text: ink.Text },
|
|
46411
47271
|
state,
|
|
46412
|
-
theme
|
|
47272
|
+
theme,
|
|
46413
47273
|
appLabel: props.appLabel,
|
|
46414
47274
|
filterDraft,
|
|
46415
47275
|
addRepoDraft,
|
|
@@ -46420,8 +47280,20 @@ function WorkspaceInkApp(props) {
|
|
|
46420
47280
|
});
|
|
46421
47281
|
}
|
|
46422
47282
|
|
|
46423
|
-
|
|
46424
|
-
|
|
47283
|
+
/**
|
|
47284
|
+
* Resolve the directories the workspace scans for git repos, in
|
|
47285
|
+
* precedence order:
|
|
47286
|
+
* 1. `--root` CLI flag(s)
|
|
47287
|
+
* 2. `workspace.roots` from config
|
|
47288
|
+
* 3. the current working directory (`cwd`)
|
|
47289
|
+
*
|
|
47290
|
+
* Falling back to `cwd` (rather than a hardcoded `~/code`) means a bare
|
|
47291
|
+
* `coco` / `coco ws` discovers repos wherever you launched it — the
|
|
47292
|
+
* handler chdir's to honor `--repo` / `--cwd` before this runs, so `cwd`
|
|
47293
|
+
* already reflects the targeted directory. `cwd` is a parameter (not a
|
|
47294
|
+
* direct `process.cwd()` call) so the resolver stays a pure unit.
|
|
47295
|
+
*/
|
|
47296
|
+
function resolveWorkspaceRoots(argv, config, cwd = process.cwd()) {
|
|
46425
47297
|
const raw = argv.root;
|
|
46426
47298
|
if (Array.isArray(raw) && raw.length > 0) {
|
|
46427
47299
|
return raw.map((entry) => String(entry));
|
|
@@ -46432,7 +47304,7 @@ function resolveWorkspaceRoots(argv, config) {
|
|
|
46432
47304
|
if (config.workspace?.roots && config.workspace.roots.length > 0) {
|
|
46433
47305
|
return [...config.workspace.roots];
|
|
46434
47306
|
}
|
|
46435
|
-
return [
|
|
47307
|
+
return [cwd];
|
|
46436
47308
|
}
|
|
46437
47309
|
function resolveWorkspaceKnownRepos(config) {
|
|
46438
47310
|
return config.workspace?.knownRepos ? [...config.workspace.knownRepos] : [];
|
|
@@ -46546,6 +47418,148 @@ var workspace = {
|
|
|
46546
47418
|
options,
|
|
46547
47419
|
};
|
|
46548
47420
|
|
|
47421
|
+
/**
|
|
47422
|
+
* Default-command router for `coco` invoked with no positional
|
|
47423
|
+
* arguments. Decides where to send the user based on the state of
|
|
47424
|
+
* their machine:
|
|
47425
|
+
*
|
|
47426
|
+
* ┌─────────────────────────┬─────────────────────┬──────────────┐
|
|
47427
|
+
* │ Config present? │ In a git repo? │ Action │
|
|
47428
|
+
* ├─────────────────────────┼─────────────────────┼──────────────┤
|
|
47429
|
+
* │ No (default-only) │ — │ run `init` │
|
|
47430
|
+
* │ Yes │ Yes (worktree) │ run `ui` │
|
|
47431
|
+
* │ Yes │ No │ run `ws` │
|
|
47432
|
+
* └─────────────────────────┴─────────────────────┴──────────────┘
|
|
47433
|
+
*
|
|
47434
|
+
* The pre-existing default — fall through to `commit` — was hostile
|
|
47435
|
+
* to first-time users: a fresh install with no config landed
|
|
47436
|
+
* straight in the API-key error path, with no hint that `coco init`
|
|
47437
|
+
* was the right next step. Routing fresh installs to `init` and
|
|
47438
|
+
* configured users to the workstation/UI matches what every other
|
|
47439
|
+
* git-aware CLI does (lazygit, tig, gitui all open their TUI on bare
|
|
47440
|
+
* invocation).
|
|
47441
|
+
*
|
|
47442
|
+
* `coco commit` keeps its dedicated subcommand entry so existing
|
|
47443
|
+
* scripts (`git aliases`, hook integrations, CI jobs) that call
|
|
47444
|
+
* `coco commit` continue to work unchanged.
|
|
47445
|
+
*
|
|
47446
|
+
* The router is a thin shim — it forwards to the existing handlers
|
|
47447
|
+
* via their public exports rather than re-implementing the logic.
|
|
47448
|
+
*/
|
|
47449
|
+
/**
|
|
47450
|
+
* Pure decision function — given probed signals (whether config
|
|
47451
|
+
* exists, whether the current directory is a git repo, whether the
|
|
47452
|
+
* user opted into legacy commit-by-default), decides which command
|
|
47453
|
+
* to invoke. Kept pure so unit tests can cover every quadrant of
|
|
47454
|
+
* the router table without spawning processes.
|
|
47455
|
+
*
|
|
47456
|
+
* "Config exists" is defined as: the loader detected at least one
|
|
47457
|
+
* source beyond `default` — i.e., the user has either a project
|
|
47458
|
+
* config, a git config `[coco]` section, an env var, or an XDG
|
|
47459
|
+
* config. A pure-defaults run is treated as "never been configured"
|
|
47460
|
+
* because `coco init` is the only way to populate any of those
|
|
47461
|
+
* sources.
|
|
47462
|
+
*/
|
|
47463
|
+
function decideDefaultRoute(input) {
|
|
47464
|
+
if (input.envOverride === 'commit' || input.explicitCommit) {
|
|
47465
|
+
return {
|
|
47466
|
+
kind: 'commit',
|
|
47467
|
+
reason: input.envOverride === 'commit' ? 'env-override' : 'explicit-flag',
|
|
47468
|
+
};
|
|
47469
|
+
}
|
|
47470
|
+
if (!input.hasConfigSource) {
|
|
47471
|
+
return { kind: 'init', reason: 'no-config' };
|
|
47472
|
+
}
|
|
47473
|
+
if (input.isGitRepo) {
|
|
47474
|
+
return { kind: 'ui', reason: 'config-and-repo' };
|
|
47475
|
+
}
|
|
47476
|
+
return { kind: 'workspace', reason: 'config-no-repo' };
|
|
47477
|
+
}
|
|
47478
|
+
/**
|
|
47479
|
+
* Probe whether the cwd (after `--repo` is honored) is inside a git
|
|
47480
|
+
* worktree. Tolerant of every error class — a thrown simple-git
|
|
47481
|
+
* call should never block the router; it should fall back to
|
|
47482
|
+
* "not a repo" so the user lands somewhere sensible (workspace
|
|
47483
|
+
* surface) rather than crashing on an empty machine.
|
|
47484
|
+
*/
|
|
47485
|
+
async function probeIsGitRepo() {
|
|
47486
|
+
try {
|
|
47487
|
+
// Lazy-import simple-git so the cold-start path stays fast for
|
|
47488
|
+
// users running `coco --help` / `coco doctor` etc.
|
|
47489
|
+
const { default: simpleGit } = await import('simple-git');
|
|
47490
|
+
const git = simpleGit();
|
|
47491
|
+
return await git.checkIsRepo();
|
|
47492
|
+
}
|
|
47493
|
+
catch {
|
|
47494
|
+
return false;
|
|
47495
|
+
}
|
|
47496
|
+
}
|
|
47497
|
+
/**
|
|
47498
|
+
* Build a synthetic argv for one of the targeted handlers. Each
|
|
47499
|
+
* handler reads its own typed argv contract (`CommitArgv`,
|
|
47500
|
+
* `InitArgv`, `UiArgv`, `WorkspaceArgv`) so we can't just spread the
|
|
47501
|
+
* raw default argv — we have to project the shared fields and let
|
|
47502
|
+
* the handler fill in command-specific defaults.
|
|
47503
|
+
*/
|
|
47504
|
+
function buildSyntheticArgv(argv) {
|
|
47505
|
+
return {
|
|
47506
|
+
_: ['$0'],
|
|
47507
|
+
$0: argv.$0,
|
|
47508
|
+
repo: argv.repo,
|
|
47509
|
+
cwd: argv.cwd,
|
|
47510
|
+
verbose: argv.verbose ?? false,
|
|
47511
|
+
interactive: true,
|
|
47512
|
+
version: false,
|
|
47513
|
+
help: false,
|
|
47514
|
+
};
|
|
47515
|
+
}
|
|
47516
|
+
/**
|
|
47517
|
+
* Default-command handler installed under yargs's `$0` slot. Probes
|
|
47518
|
+
* the environment, computes the right route, and forwards to the
|
|
47519
|
+
* matching command handler. Falls through to commit if any
|
|
47520
|
+
* unexpected error blocks routing — preserves backwards-compat
|
|
47521
|
+
* for users on weird setups while still giving newcomers the
|
|
47522
|
+
* onboarding path they deserve.
|
|
47523
|
+
*/
|
|
47524
|
+
const defaultRouteHandler = async (argv, logger) => {
|
|
47525
|
+
// The `--repo` flag has to land before any probe runs — otherwise
|
|
47526
|
+
// we'd sniff the launcher's cwd instead of the targeted repo.
|
|
47527
|
+
applyRepoCwd(argv);
|
|
47528
|
+
// Trigger a config load so `getConfigSources()` returns the active
|
|
47529
|
+
// source list. We discard the config object — the decision only
|
|
47530
|
+
// cares about which sources contributed.
|
|
47531
|
+
void loadConfig(argv);
|
|
47532
|
+
const sources = getConfigSources();
|
|
47533
|
+
const hasConfigSource = sources.some((source) => source.source !== 'default');
|
|
47534
|
+
const isGitRepo = await probeIsGitRepo();
|
|
47535
|
+
const decision = decideDefaultRoute({
|
|
47536
|
+
hasConfigSource,
|
|
47537
|
+
isGitRepo,
|
|
47538
|
+
explicitCommit: Boolean(argv.commit),
|
|
47539
|
+
envOverride: process.env.COCO_DEFAULT,
|
|
47540
|
+
});
|
|
47541
|
+
switch (decision.kind) {
|
|
47542
|
+
case 'init':
|
|
47543
|
+
// Friendly hint before the wizard kicks in — sets expectations
|
|
47544
|
+
// that the user is being walked through setup, not silently
|
|
47545
|
+
// routed to a different command.
|
|
47546
|
+
logger.log('No coco config detected — running `coco init` to set up your provider + key.', { color: 'cyan' });
|
|
47547
|
+
logger.log('');
|
|
47548
|
+
await handler$7(buildSyntheticArgv(argv), logger);
|
|
47549
|
+
return;
|
|
47550
|
+
case 'ui':
|
|
47551
|
+
await handler$5(buildSyntheticArgv(argv));
|
|
47552
|
+
return;
|
|
47553
|
+
case 'workspace':
|
|
47554
|
+
await handler(buildSyntheticArgv(argv));
|
|
47555
|
+
return;
|
|
47556
|
+
case 'commit':
|
|
47557
|
+
default:
|
|
47558
|
+
await handler$9(buildSyntheticArgv(argv), logger);
|
|
47559
|
+
return;
|
|
47560
|
+
}
|
|
47561
|
+
};
|
|
47562
|
+
|
|
46549
47563
|
var types = /*#__PURE__*/Object.freeze({
|
|
46550
47564
|
__proto__: null
|
|
46551
47565
|
});
|
|
@@ -46564,7 +47578,37 @@ y.option('repo', {
|
|
|
46564
47578
|
description: 'Target a specific repository directory instead of the current working directory.',
|
|
46565
47579
|
global: true,
|
|
46566
47580
|
});
|
|
46567
|
-
|
|
47581
|
+
// Global `--verbose` (alias `-v`) — every subcommand inherits it.
|
|
47582
|
+
// Flips `argv.verbose: true` so `commandExecutor` and `Logger` print
|
|
47583
|
+
// stack traces / debug spans. Previously only settable via the
|
|
47584
|
+
// `COCO_VERBOSE=true` env var or `coco.verbose` git/json config —
|
|
47585
|
+
// `BaseArgvOptions.verbose` was typed but never declared as a yargs
|
|
47586
|
+
// option, so passing `--verbose` from the CLI was a silent no-op.
|
|
47587
|
+
y.option('verbose', {
|
|
47588
|
+
type: 'boolean',
|
|
47589
|
+
alias: 'v',
|
|
47590
|
+
description: 'Print verbose diagnostic output (stack traces on errors, debug spans).',
|
|
47591
|
+
default: false,
|
|
47592
|
+
global: true,
|
|
47593
|
+
});
|
|
47594
|
+
// `$0` (no positional args) routes through the smart default router
|
|
47595
|
+
// rather than aliasing directly to `coco commit`. The router probes
|
|
47596
|
+
// the user's environment (config presence, git-repo presence) and
|
|
47597
|
+
// forwards to `init` / `ui` / `workspace` / `commit` based on which
|
|
47598
|
+
// of those is most likely to be helpful. Mirrors what other modern
|
|
47599
|
+
// git-aware CLIs do (lazygit / tig / gitui) — fresh installs land in
|
|
47600
|
+
// a setup wizard, configured users land in the TUI, scripts that
|
|
47601
|
+
// rely on `coco commit` keep their dedicated subcommand entry.
|
|
47602
|
+
y.command('$0', 'Smart entry point — routes to init / ui / workspace / commit based on your environment.', (yargs) => yargs.option('commit', {
|
|
47603
|
+
type: 'boolean',
|
|
47604
|
+
description: 'Force the legacy default — run `coco commit` regardless of routing.',
|
|
47605
|
+
default: false,
|
|
47606
|
+
}),
|
|
47607
|
+
// `commandExecutor` wraps every command with config loading, error
|
|
47608
|
+
// formatting, and exit-code handling. The router is a regular
|
|
47609
|
+
// command so it lights up the same plumbing for free.
|
|
47610
|
+
commandExecutor(defaultRouteHandler));
|
|
47611
|
+
y.command(commit.command, commit.desc, commit.builder, commit.handler);
|
|
46568
47612
|
y.command(changelog.command, changelog.desc, changelog.builder, changelog.handler);
|
|
46569
47613
|
y.command(recap.command, recap.desc, recap.builder, recap.handler);
|
|
46570
47614
|
y.command(review.command, review.desc, review.builder, review.handler);
|