reslop 0.1.6 → 0.1.7
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/CHANGELOG.md +11 -1
- package/README.md +33 -19
- package/lib/ansi.js +1 -0
- package/lib/files.js +33 -4
- package/lib/git.js +41 -0
- package/lib/keys.js +35 -6
- package/lib/render.js +147 -24
- package/lib/review.js +6 -63
- package/lib/session.js +306 -160
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased][unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.1.7][] - 2026-09-16
|
|
6
|
+
|
|
7
|
+
- Make TODOs repo-wide, not per-file todos
|
|
8
|
+
- Improve TODO screen and files screen TUI/TUX including hotkeys
|
|
9
|
+
- Implement branch rebase and drop
|
|
10
|
+
- Improve branch screen TUI/TUX
|
|
11
|
+
- Fix TODOs autosave bug with todo duplication
|
|
12
|
+
|
|
5
13
|
## [0.1.6][] - 2026-09-15
|
|
6
14
|
|
|
7
15
|
- Auto updates for patch/minor, y/n per new major
|
|
@@ -58,7 +66,9 @@
|
|
|
58
66
|
- Feedback and todos that produce a repair plan for an agent
|
|
59
67
|
- Intra-line highlighting in unified, mixed, and side-by-side layouts
|
|
60
68
|
|
|
61
|
-
[unreleased]: https://github.com/tshemsedinov/reslop/compare/v0.1.
|
|
69
|
+
[unreleased]: https://github.com/tshemsedinov/reslop/compare/v0.1.7...HEAD
|
|
70
|
+
[0.1.7]: https://github.com/tshemsedinov/reslop/compare/v0.1.6...v0.1.7
|
|
71
|
+
[0.1.6]: https://github.com/tshemsedinov/reslop/compare/v0.1.5...v0.1.6
|
|
62
72
|
[0.1.5]: https://github.com/tshemsedinov/reslop/compare/v0.1.4...v0.1.5
|
|
63
73
|
[0.1.4]: https://github.com/tshemsedinov/reslop/compare/v0.1.3...v0.1.4
|
|
64
74
|
[0.1.3]: https://github.com/tshemsedinov/reslop/compare/v0.1.2...v0.1.3
|
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# 👁️ reslop — Review
|
|
1
|
+
# 👁️ reslop — Review AI generated code and plan changes
|
|
2
2
|
|
|
3
3
|
[](https://github.com/tshemsedinov/reslop/actions?query=workflow%3A%22Testing+CI%22+branch%3Amain)
|
|
4
4
|
[](https://snyk.io/test/github/tshemsedinov/reslop)
|
|
@@ -16,12 +16,15 @@ for the agent.
|
|
|
16
16
|
Review → Plan → Repair → Verify
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
- Leave feedback, issues/todos, and in-place code proposals as a repair plan.
|
|
19
20
|
- Review uncommitted git diffs, a given commit, a GitHub PR, or a GitLab MR.
|
|
20
21
|
- Stage, unstage, or revert each contiguous block of diff lines.
|
|
21
|
-
- Leave feedback, todos, and in-place code proposals that become a repair plan.
|
|
22
22
|
- Import GitHub PR and GitLab MR review comments into the local plan for AI.
|
|
23
|
+
- Commit, amend, or fixup staged changes from the file list.
|
|
24
|
+
- Branches: checkout, create, rebase, drop, pull and push.
|
|
23
25
|
- Review each npm dependency once across `package.json` and the lockfile.
|
|
24
26
|
- Propose unused removals, npm audit fixes, and outdated updates as diffs.
|
|
27
|
+
- Code highlighting, diff compare: unified, mixed, and side-by-side layouts.
|
|
25
28
|
|
|
26
29
|
## Install
|
|
27
30
|
|
|
@@ -49,23 +52,34 @@ Reviews go in `.review/YYYY-MM-DD-NN.md` with frontmatter `status`:
|
|
|
49
52
|
|
|
50
53
|
## Hotkeys
|
|
51
54
|
|
|
52
|
-
| Key | Action
|
|
53
|
-
| ------------------- |
|
|
54
|
-
| `a` / `u` / `d` | Stage / unstage / drop this diff
|
|
55
|
-
| `c` |
|
|
56
|
-
| `b` | List branches; `
|
|
57
|
-
| `p` / `s` | git pull/push
|
|
58
|
-
| `f` / `t` / `e` |
|
|
59
|
-
| `r` | Reload diffs from disk
|
|
60
|
-
| `m` | Mode: unified / mixed / side-by-side
|
|
61
|
-
| `⏎` | Open the selected file
|
|
62
|
-
| `→` / `j` | Next remaining diff /
|
|
63
|
-
| `←` / `k` | Previous remaining diff /
|
|
64
|
-
| `Ctrl-e` / `Ctrl-y` | Scroll one line down / up
|
|
65
|
-
| `Ctrl-f` / `Ctrl-b` | Scroll page down / up
|
|
66
|
-
| `Ctrl-d` / `Ctrl-u` | Scroll half-page down / up
|
|
67
|
-
| `⌫` / `Del` | Remove the selected todo
|
|
68
|
-
| `Esc` / `q` | Quit
|
|
55
|
+
| Key | Action |
|
|
56
|
+
| ------------------- | --------------------------------------------- |
|
|
57
|
+
| `a` / `u` / `d` | Stage / unstage / drop this diff |
|
|
58
|
+
| `c` | File list: `c` commit / `a` amend / `f` fixup |
|
|
59
|
+
| `b` | List branches; `n` new; `r` rebase; `d` drop |
|
|
60
|
+
| `p` / `s` | git pull/push |
|
|
61
|
+
| `f` / `t` / `e` | Feedback / repo todo / edit added lines |
|
|
62
|
+
| `r` | Reload diffs from disk |
|
|
63
|
+
| `m` | Mode: unified / mixed / side-by-side |
|
|
64
|
+
| `⏎` | Open the selected file |
|
|
65
|
+
| `→` / `j` | Next remaining diff / file |
|
|
66
|
+
| `←` / `k` | Previous remaining diff / file |
|
|
67
|
+
| `Ctrl-e` / `Ctrl-y` | Scroll one line down / up |
|
|
68
|
+
| `Ctrl-f` / `Ctrl-b` | Scroll page down / up |
|
|
69
|
+
| `Ctrl-d` / `Ctrl-u` | Scroll half-page down / up |
|
|
70
|
+
| `⌫` / `Del` | Remove the selected todo |
|
|
71
|
+
| `Esc` / `q` | Quit |
|
|
72
|
+
|
|
73
|
+
## Future
|
|
74
|
+
|
|
75
|
+
- Import issues from GitHub and GitLab.
|
|
76
|
+
- Security and code-quality audit; propose a repair plan.
|
|
77
|
+
- Send anonymized code blocks for expert review.
|
|
78
|
+
- Send questions and the repair plan to experts for approval.
|
|
79
|
+
- Ask experts.
|
|
80
|
+
- Verify the codebase.
|
|
81
|
+
- Apply refactoring skills.
|
|
82
|
+
- Call agents, harnesses, and IDEs to execute prepared plans.
|
|
69
83
|
|
|
70
84
|
## License
|
|
71
85
|
|
package/lib/ansi.js
CHANGED
package/lib/files.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
const REVIEW_DIR = '.review';
|
|
4
4
|
const ORIGIN_ORDER = ['staged', 'unstaged', 'untracked', 'commit', 'pr'];
|
|
5
|
+
const TODO_FILE = 'TODOs';
|
|
6
|
+
const REPO_TODOS_LABEL = 'Repository TODOs and Issues';
|
|
5
7
|
|
|
6
8
|
const normalizePathSpec = (spec) =>
|
|
7
9
|
`${spec ?? ''}`.replaceAll('\\', '/').replace(/\/+$/, '');
|
|
@@ -21,14 +23,34 @@ const itemPath = (item) => {
|
|
|
21
23
|
return item.file.newPath || item.file.oldPath || '';
|
|
22
24
|
};
|
|
23
25
|
|
|
26
|
+
const isTodoItem = (item) => !!(item && item.origin === 'todo');
|
|
27
|
+
|
|
28
|
+
const isTodosEntry = (entry) => !!(entry && entry.kind === 'todos');
|
|
29
|
+
|
|
30
|
+
const repoTodosLabel = () => REPO_TODOS_LABEL;
|
|
31
|
+
|
|
32
|
+
const makeTodoItem = () => ({
|
|
33
|
+
origin: 'todo',
|
|
34
|
+
file: {
|
|
35
|
+
oldPath: TODO_FILE,
|
|
36
|
+
newPath: TODO_FILE,
|
|
37
|
+
isNew: false,
|
|
38
|
+
isDeleted: false,
|
|
39
|
+
isBinary: false,
|
|
40
|
+
preamble: [],
|
|
41
|
+
hunks: [],
|
|
42
|
+
},
|
|
43
|
+
hunk: null,
|
|
44
|
+
blockId: 'todo',
|
|
45
|
+
patchAdd: '',
|
|
46
|
+
patchRevert: '',
|
|
47
|
+
});
|
|
48
|
+
|
|
24
49
|
const fileStatus = (origins) => {
|
|
25
50
|
const present = new Set(origins);
|
|
26
51
|
const seen = ORIGIN_ORDER.filter((origin) => present.has(origin));
|
|
27
52
|
const key = seen.join('+');
|
|
28
|
-
if (!key)
|
|
29
|
-
if (present.has('todo')) return 'todo';
|
|
30
|
-
return '';
|
|
31
|
-
}
|
|
53
|
+
if (!key) return '';
|
|
32
54
|
if (key === 'staged+unstaged') return 'partial';
|
|
33
55
|
return key;
|
|
34
56
|
};
|
|
@@ -51,6 +73,7 @@ const fileEntries = (items) => {
|
|
|
51
73
|
const groups = new Map();
|
|
52
74
|
for (let i = 0; i < items.length; i++) {
|
|
53
75
|
const item = items[i];
|
|
76
|
+
if (isTodoItem(item)) continue;
|
|
54
77
|
const rel = itemPath(item);
|
|
55
78
|
if (!groups.has(rel)) {
|
|
56
79
|
groups.set(rel, {
|
|
@@ -87,8 +110,14 @@ const fileEntries = (items) => {
|
|
|
87
110
|
|
|
88
111
|
module.exports = {
|
|
89
112
|
REVIEW_DIR,
|
|
113
|
+
TODO_FILE,
|
|
114
|
+
REPO_TODOS_LABEL,
|
|
90
115
|
isPathInScope,
|
|
91
116
|
itemPath,
|
|
117
|
+
isTodoItem,
|
|
118
|
+
isTodosEntry,
|
|
119
|
+
repoTodosLabel,
|
|
120
|
+
makeTodoItem,
|
|
92
121
|
fileStatus,
|
|
93
122
|
fileEntries,
|
|
94
123
|
};
|
package/lib/git.js
CHANGED
|
@@ -1032,6 +1032,39 @@ const createBranchAsync = async (top, name) => {
|
|
|
1032
1032
|
requireOk(await runGitAsync(args, top), 'checkout failed');
|
|
1033
1033
|
};
|
|
1034
1034
|
|
|
1035
|
+
const rebaseFailed = (result) => {
|
|
1036
|
+
if (result.status === 0) return '';
|
|
1037
|
+
return result.stderr || result.stdout || 'rebase failed';
|
|
1038
|
+
};
|
|
1039
|
+
|
|
1040
|
+
const rebaseBranch = (top, onto) => {
|
|
1041
|
+
const branch = namedBranch(onto);
|
|
1042
|
+
const result = runGit(['rebase', branch], top);
|
|
1043
|
+
const failed = rebaseFailed(result);
|
|
1044
|
+
if (!failed) return;
|
|
1045
|
+
runGit(['rebase', '--abort'], top);
|
|
1046
|
+
throw new Error(oneLine(failed));
|
|
1047
|
+
};
|
|
1048
|
+
|
|
1049
|
+
const rebaseBranchAsync = async (top, onto) => {
|
|
1050
|
+
const branch = namedBranch(onto);
|
|
1051
|
+
const result = await runGitAsync(['rebase', branch], top);
|
|
1052
|
+
const failed = rebaseFailed(result);
|
|
1053
|
+
if (!failed) return;
|
|
1054
|
+
await runGitAsync(['rebase', '--abort'], top);
|
|
1055
|
+
throw new Error(oneLine(failed));
|
|
1056
|
+
};
|
|
1057
|
+
|
|
1058
|
+
const dropBranch = (top, name) => {
|
|
1059
|
+
const branch = namedBranch(name);
|
|
1060
|
+
requireOk(runGit(['branch', '-D', branch], top), 'drop failed');
|
|
1061
|
+
};
|
|
1062
|
+
|
|
1063
|
+
const dropBranchAsync = async (top, name) => {
|
|
1064
|
+
const branch = namedBranch(name);
|
|
1065
|
+
requireOk(await runGitAsync(['branch', '-D', branch], top), 'drop failed');
|
|
1066
|
+
};
|
|
1067
|
+
|
|
1035
1068
|
const pullChanges = (top) => {
|
|
1036
1069
|
requireOk(runGit(['pull'], top), 'pull failed');
|
|
1037
1070
|
};
|
|
@@ -1159,6 +1192,10 @@ const createGitRepo = () => ({
|
|
|
1159
1192
|
checkoutAsync: checkoutBranchAsync,
|
|
1160
1193
|
createBranch,
|
|
1161
1194
|
createBranchAsync,
|
|
1195
|
+
rebase: rebaseBranch,
|
|
1196
|
+
rebaseAsync: rebaseBranchAsync,
|
|
1197
|
+
drop: dropBranch,
|
|
1198
|
+
dropAsync: dropBranchAsync,
|
|
1162
1199
|
pull: pullChanges,
|
|
1163
1200
|
pullAsync: pullChangesAsync,
|
|
1164
1201
|
push: pushChanges,
|
|
@@ -1182,6 +1219,10 @@ module.exports = {
|
|
|
1182
1219
|
checkoutBranchAsync,
|
|
1183
1220
|
createBranch,
|
|
1184
1221
|
createBranchAsync,
|
|
1222
|
+
rebaseBranch,
|
|
1223
|
+
rebaseBranchAsync,
|
|
1224
|
+
dropBranch,
|
|
1225
|
+
dropBranchAsync,
|
|
1185
1226
|
pullChanges,
|
|
1186
1227
|
pullChangesAsync,
|
|
1187
1228
|
pushChanges,
|
package/lib/keys.js
CHANGED
|
@@ -21,6 +21,8 @@ const ACTION_IDS = Object.keys(ACTIONS);
|
|
|
21
21
|
|
|
22
22
|
const BRANCH_ACTIONS = {
|
|
23
23
|
newBranch: ['n'],
|
|
24
|
+
rebase: ['r'],
|
|
25
|
+
drop: ['d'],
|
|
24
26
|
};
|
|
25
27
|
|
|
26
28
|
const ACTION_HOT = {
|
|
@@ -34,16 +36,22 @@ const ACTION_LABEL = {
|
|
|
34
36
|
code: 'edit',
|
|
35
37
|
newBranch: 'new',
|
|
36
38
|
quit: 'q',
|
|
39
|
+
prev: '←',
|
|
40
|
+
next: '→',
|
|
37
41
|
};
|
|
38
42
|
|
|
39
|
-
const DIFF_DISABLED = ['reload', 'branch', 'pull', 'push'];
|
|
43
|
+
const DIFF_DISABLED = ['commit', 'reload', 'branch', 'pull', 'push'];
|
|
40
44
|
const FILES_DISABLED = ['layout', 'feedback', 'code'];
|
|
45
|
+
const FILES_GIT_DISABLED = ['add', 'unstage', 'revert'];
|
|
46
|
+
const FILES_TODO_DISABLED = [...FILES_GIT_DISABLED, ...FILES_DISABLED];
|
|
41
47
|
const TODO_DISABLED = [
|
|
42
48
|
'add',
|
|
43
49
|
'unstage',
|
|
44
50
|
'revert',
|
|
51
|
+
'commit',
|
|
45
52
|
'layout',
|
|
46
53
|
'feedback',
|
|
54
|
+
'todo',
|
|
47
55
|
'code',
|
|
48
56
|
'reload',
|
|
49
57
|
'branch',
|
|
@@ -65,11 +73,28 @@ const BRANCHES_DISABLED = [
|
|
|
65
73
|
'push',
|
|
66
74
|
];
|
|
67
75
|
|
|
76
|
+
const isTodosTarget = (item) =>
|
|
77
|
+
!!(item && (item.kind === 'todos' || item.origin === 'todo'));
|
|
78
|
+
|
|
79
|
+
const diffGitDisabled = (item) => {
|
|
80
|
+
if (!item || isTodosTarget(item)) return [];
|
|
81
|
+
if (item.origin === 'staged') return ['add'];
|
|
82
|
+
return ['unstage'];
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const branchGitDisabled = (item) => {
|
|
86
|
+
if (item && !item.current) return [];
|
|
87
|
+
return ['rebase', 'drop'];
|
|
88
|
+
};
|
|
89
|
+
|
|
68
90
|
const disabledActions = (pane, item) => {
|
|
69
|
-
|
|
70
|
-
if (pane === '
|
|
71
|
-
if (
|
|
72
|
-
|
|
91
|
+
const onTodos = isTodosTarget(item);
|
|
92
|
+
if (pane === 'files') return onTodos ? FILES_TODO_DISABLED : FILES_DISABLED;
|
|
93
|
+
if (pane === 'branches') {
|
|
94
|
+
return [...BRANCHES_DISABLED, ...branchGitDisabled(item)];
|
|
95
|
+
}
|
|
96
|
+
if (onTodos) return TODO_DISABLED;
|
|
97
|
+
return [...DIFF_DISABLED, ...diffGitDisabled(item)];
|
|
73
98
|
};
|
|
74
99
|
|
|
75
100
|
const SCROLL = {
|
|
@@ -243,7 +268,7 @@ const actionKeys = (id) => ACTIONS[id] ?? BRANCH_ACTIONS[id] ?? [];
|
|
|
243
268
|
|
|
244
269
|
const actionHot = (id) => ACTION_HOT[id] ?? actionKeys(id)[0];
|
|
245
270
|
|
|
246
|
-
const actionLabel = (id) =>
|
|
271
|
+
const actionLabel = (id) => ACTION_LABEL[id] ?? ACTION_HOT[id] ?? id;
|
|
247
272
|
|
|
248
273
|
const actionLetter = actionHot;
|
|
249
274
|
|
|
@@ -267,9 +292,13 @@ module.exports = {
|
|
|
267
292
|
BRANCH_ACTIONS,
|
|
268
293
|
DIFF_DISABLED,
|
|
269
294
|
FILES_DISABLED,
|
|
295
|
+
FILES_GIT_DISABLED,
|
|
296
|
+
FILES_TODO_DISABLED,
|
|
270
297
|
TODO_DISABLED,
|
|
271
298
|
BRANCHES_DISABLED,
|
|
272
299
|
disabledActions,
|
|
300
|
+
diffGitDisabled,
|
|
301
|
+
branchGitDisabled,
|
|
273
302
|
decodeChunk,
|
|
274
303
|
actionFromKey,
|
|
275
304
|
actionHot,
|
package/lib/render.js
CHANGED
|
@@ -6,15 +6,18 @@ const highlight = require('./highlight.js');
|
|
|
6
6
|
const { detectLang } = require('./detect.js');
|
|
7
7
|
const keys = require('./keys.js');
|
|
8
8
|
const { overlayRows } = require('./select.js');
|
|
9
|
-
const
|
|
9
|
+
const files = require('./files.js');
|
|
10
|
+
const { itemPath, TODO_FILE, isTodosEntry, REPO_TODOS_LABEL } = files;
|
|
10
11
|
const wrap = require('./wrap.js');
|
|
11
12
|
|
|
12
13
|
const { tokenize, overlayTokens } = highlight;
|
|
13
14
|
const { ACTION_IDS, BRANCH_ACTIONS, actionLetter, buttonWord } = keys;
|
|
14
|
-
const {
|
|
15
|
+
const { FILES_DISABLED, FILES_GIT_DISABLED, BRANCHES_DISABLED } = keys;
|
|
16
|
+
const { TODO_DISABLED, DIFF_DISABLED, diffGitDisabled } = keys;
|
|
17
|
+
const { branchGitDisabled } = keys;
|
|
15
18
|
const { wrapPlain, wrapMultiline, cursorInWrap } = wrap;
|
|
16
19
|
const { THEME, CODE_FG, codeFg, paint, visibleWidth } = ansi;
|
|
17
|
-
const { truncateVisible, fg, RESET, ESC, EL, seq } = ansi;
|
|
20
|
+
const { truncateVisible, fg, RESET, ESC, EL, seq, BOLD } = ansi;
|
|
18
21
|
|
|
19
22
|
const STYLES = {
|
|
20
23
|
del: {
|
|
@@ -124,7 +127,7 @@ const BRANCH_COL_ALIGN = {
|
|
|
124
127
|
};
|
|
125
128
|
|
|
126
129
|
const BRANCH_COL_FG = {
|
|
127
|
-
sha: THEME.
|
|
130
|
+
sha: THEME.shaFg,
|
|
128
131
|
ahead: THEME.addLineFg,
|
|
129
132
|
behind: THEME.delLineFg,
|
|
130
133
|
gone: THEME.delLineFg,
|
|
@@ -145,6 +148,8 @@ const BUSY_PROGRESS = [
|
|
|
145
148
|
'npm audit fix',
|
|
146
149
|
'checking out',
|
|
147
150
|
'creating branch',
|
|
151
|
+
'rebasing',
|
|
152
|
+
'dropping',
|
|
148
153
|
'committing',
|
|
149
154
|
'force pushing',
|
|
150
155
|
];
|
|
@@ -190,17 +195,18 @@ const INFO_STATUS = [
|
|
|
190
195
|
'force pushed',
|
|
191
196
|
'checking out',
|
|
192
197
|
'creating branch',
|
|
198
|
+
'rebasing',
|
|
199
|
+
'dropping',
|
|
193
200
|
'committing',
|
|
194
201
|
'checked out',
|
|
195
202
|
'created',
|
|
196
203
|
'empty branch name',
|
|
197
204
|
];
|
|
198
|
-
const INFO_PREFIX = ['checked out ', 'created '];
|
|
205
|
+
const INFO_PREFIX = ['checked out ', 'created ', 'rebased onto ', 'dropped '];
|
|
199
206
|
|
|
200
207
|
const QUIT_CHOICES = [
|
|
201
208
|
{ letter: 'f', label: 'finish as ready', lead: ' ' },
|
|
202
209
|
{ letter: 'c', label: 'continue next time', lead: ' ' },
|
|
203
|
-
{ letter: 'd', label: 'discard', lead: ' ' },
|
|
204
210
|
];
|
|
205
211
|
const QUIT_PROMPT = QUIT_CHOICES.map(
|
|
206
212
|
(choice) => `${choice.lead}${choice.label}`,
|
|
@@ -215,9 +221,10 @@ const COMMIT_PROMPT = COMMIT_CHOICES.map(
|
|
|
215
221
|
).join('');
|
|
216
222
|
const UPDATE_CHOICES = [
|
|
217
223
|
{ letter: 'y', label: 'y', lead: ' ' },
|
|
218
|
-
{ letter: 'n', label: 'n', lead: '
|
|
224
|
+
{ letter: 'n', label: 'n', lead: '/' },
|
|
219
225
|
];
|
|
220
|
-
const updatePrompt = (from, to) => ` update reslop ${from} → ${to}? y
|
|
226
|
+
const updatePrompt = (from, to) => ` update reslop ${from} → ${to}? y/n`;
|
|
227
|
+
const dropPrompt = (name) => ` drop ${name}? y/n`;
|
|
221
228
|
const PUSH_CHOICES = [{ letter: 'f', label: 'force push', lead: ' ' }];
|
|
222
229
|
const PUSH_REJECT_PREFIX = ' Need force-push?';
|
|
223
230
|
const PUSH_PROMPT = `${PUSH_REJECT_PREFIX} force push`;
|
|
@@ -230,7 +237,6 @@ const LOGO = '👁️';
|
|
|
230
237
|
const FILE_MARK = '▶';
|
|
231
238
|
const FILE_LIST_PAD = 1;
|
|
232
239
|
const CHECK_DONE = 'xX';
|
|
233
|
-
const TODO_CAPTION = 'TODO:';
|
|
234
240
|
const INPUT_MAX = 4;
|
|
235
241
|
const NOTE_MAX = 4;
|
|
236
242
|
const NOTE_PAD = 1;
|
|
@@ -423,6 +429,9 @@ const headerTarget = (view) => {
|
|
|
423
429
|
const total = files.length;
|
|
424
430
|
const n = total ? cursor + 1 : 0;
|
|
425
431
|
if (!entry) return { path: '', origin: '', n, total };
|
|
432
|
+
if (isTodosEntry(entry)) {
|
|
433
|
+
return { path: TODO_FILE, origin: 'todo', n, total };
|
|
434
|
+
}
|
|
426
435
|
return { path: entry.path, origin: entry.status, n, total };
|
|
427
436
|
}
|
|
428
437
|
if (pane === 'branches') {
|
|
@@ -441,6 +450,9 @@ const headerTarget = (view) => {
|
|
|
441
450
|
return { path: entry.name, origin, n, total };
|
|
442
451
|
}
|
|
443
452
|
if (!view.item) return { path: '', origin: '', n: 0, total: view.total ?? 0 };
|
|
453
|
+
if (view.item.origin === 'todo') {
|
|
454
|
+
return { path: TODO_FILE, origin: 'todo', n: 1, total: 1 };
|
|
455
|
+
}
|
|
444
456
|
return {
|
|
445
457
|
path: itemPath(view.item),
|
|
446
458
|
origin: originLabel(view.item.origin, view),
|
|
@@ -574,7 +586,13 @@ const paintMarkedWord = (label, letter, color, mark) => {
|
|
|
574
586
|
return out;
|
|
575
587
|
};
|
|
576
588
|
|
|
577
|
-
const layoutButtons = (
|
|
589
|
+
const layoutButtons = (
|
|
590
|
+
width,
|
|
591
|
+
compact = false,
|
|
592
|
+
hidden = [],
|
|
593
|
+
extra = [],
|
|
594
|
+
disabled = [],
|
|
595
|
+
) => {
|
|
578
596
|
const ids = [];
|
|
579
597
|
for (const id of ACTION_IDS) {
|
|
580
598
|
if (id === 'quit') {
|
|
@@ -596,8 +614,9 @@ const layoutButtons = (width, compact = false, hidden = [], extra = []) => {
|
|
|
596
614
|
const piece = `${BUTTON_GAP}${label}`;
|
|
597
615
|
const w = visibleWidth(piece);
|
|
598
616
|
if (x + w > width && parts.length) break;
|
|
599
|
-
|
|
600
|
-
|
|
617
|
+
const off = disabled.includes(id);
|
|
618
|
+
if (!off) hits.push({ id, x0: x, x1: x + w });
|
|
619
|
+
parts.push({ action: { id }, piece, letter, label, disabled: off });
|
|
601
620
|
x += w;
|
|
602
621
|
}
|
|
603
622
|
return { hits, parts, width: x };
|
|
@@ -611,9 +630,12 @@ const layoutButtons = (width, compact = false, hidden = [], extra = []) => {
|
|
|
611
630
|
const paintButtons = (layout, width, color) => {
|
|
612
631
|
let out = '';
|
|
613
632
|
let used = 0;
|
|
633
|
+
const fgRgb = THEME.buttonFg;
|
|
634
|
+
const bgRgb = THEME.buttonBg;
|
|
614
635
|
for (const part of layout.parts) {
|
|
615
|
-
out += paint(BUTTON_GAP,
|
|
616
|
-
out +=
|
|
636
|
+
out += paint(BUTTON_GAP, fgRgb, bgRgb, color);
|
|
637
|
+
if (part.disabled) out += paint(part.label, fgRgb, bgRgb, color);
|
|
638
|
+
else out += paintMarkedWord(part.label, part.letter, color, BUTTON_MARK);
|
|
617
639
|
used += visibleWidth(part.piece);
|
|
618
640
|
}
|
|
619
641
|
if (used < width) {
|
|
@@ -628,6 +650,31 @@ const branchLabel = (name) => {
|
|
|
628
650
|
return `[${branch}]`;
|
|
629
651
|
};
|
|
630
652
|
|
|
653
|
+
const branchNameChip = (entry, rowBg) => {
|
|
654
|
+
if (entry.current) {
|
|
655
|
+
return {
|
|
656
|
+
nameFg: THEME.headerFg,
|
|
657
|
+
wrapFg: THEME.mutedFg,
|
|
658
|
+
nameBg: THEME.buttonHotFg,
|
|
659
|
+
nameBold: false,
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
if (entry.isDefault) {
|
|
663
|
+
return {
|
|
664
|
+
nameFg: THEME.warnFg,
|
|
665
|
+
wrapFg: THEME.mutedFg,
|
|
666
|
+
nameBg: rowBg,
|
|
667
|
+
nameBold: true,
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
return {
|
|
671
|
+
nameFg: THEME.buttonHotFg,
|
|
672
|
+
wrapFg: THEME.mutedFg,
|
|
673
|
+
nameBg: rowBg,
|
|
674
|
+
nameBold: false,
|
|
675
|
+
};
|
|
676
|
+
};
|
|
677
|
+
|
|
631
678
|
const splitBracketName = (label) => {
|
|
632
679
|
const text = `${label ?? ''}`;
|
|
633
680
|
const open = text.startsWith('[') ? '[' : '';
|
|
@@ -637,11 +684,11 @@ const splitBracketName = (label) => {
|
|
|
637
684
|
return { open, name, close };
|
|
638
685
|
};
|
|
639
686
|
|
|
640
|
-
const paintBracketName = (label, nameFg, wrapFg, bgRgb, color) => {
|
|
687
|
+
const paintBracketName = (label, nameFg, wrapFg, bgRgb, color, nameBold) => {
|
|
641
688
|
const { open, name, close } = splitBracketName(label);
|
|
642
689
|
return (
|
|
643
690
|
paint(open, wrapFg, bgRgb, color, true) +
|
|
644
|
-
paint(name, nameFg, bgRgb, color) +
|
|
691
|
+
paint(name, nameFg, bgRgb, color, nameBold) +
|
|
645
692
|
paint(close, wrapFg, bgRgb, color, true)
|
|
646
693
|
);
|
|
647
694
|
};
|
|
@@ -746,6 +793,11 @@ const paintStatusLine = (view, status, width, color) => {
|
|
|
746
793
|
const prefix = ` update reslop ${from} → ${to}?`;
|
|
747
794
|
return paintChoicePrompt(UPDATE_CHOICES, width, color, prefix);
|
|
748
795
|
}
|
|
796
|
+
if (mode === 'confirmDrop') {
|
|
797
|
+
const name = view.dropName ?? '';
|
|
798
|
+
const prefix = ` drop ${name}?`;
|
|
799
|
+
return paintChoicePrompt(UPDATE_CHOICES, width, color, prefix);
|
|
800
|
+
}
|
|
749
801
|
if (mode === 'confirmPush') {
|
|
750
802
|
return paintChoicePrompt(PUSH_CHOICES, width, color, PUSH_REJECT_PREFIX);
|
|
751
803
|
}
|
|
@@ -782,6 +834,7 @@ const statusBranchHit = (view, width, y) => {
|
|
|
782
834
|
if (mode === 'confirmQuit') return null;
|
|
783
835
|
if (mode === 'confirmCommit') return null;
|
|
784
836
|
if (mode === 'confirmUpdate') return null;
|
|
837
|
+
if (mode === 'confirmDrop') return null;
|
|
785
838
|
if (mode === 'confirmPush') return null;
|
|
786
839
|
const branch = view.branch ?? '';
|
|
787
840
|
if (!branchLabel(branch)) return null;
|
|
@@ -807,6 +860,11 @@ const statusBranchHit = (view, width, y) => {
|
|
|
807
860
|
const signedCount = (sign, n) => `${sign}${n ?? 0}`;
|
|
808
861
|
|
|
809
862
|
const fileRowFields = (entry) => {
|
|
863
|
+
if (isTodosEntry(entry)) {
|
|
864
|
+
const n = `${entry.staged ?? 0}`;
|
|
865
|
+
const m = `${entry.remaining ?? 0}`;
|
|
866
|
+
return { add: '', del: '', status: '', n, m };
|
|
867
|
+
}
|
|
810
868
|
const { added, removed, staged, remaining } = entry;
|
|
811
869
|
const add = signedCount('+', added);
|
|
812
870
|
const del = signedCount('-', removed);
|
|
@@ -873,7 +931,30 @@ const paintFileSuffix = (parts, fgRgb, bgRgb, color) => {
|
|
|
873
931
|
return gap + counts + gap + meta;
|
|
874
932
|
};
|
|
875
933
|
|
|
934
|
+
const paintTodosRow = (entry, width, color, selected, cols) => {
|
|
935
|
+
const inner = Math.max(1, width - 1);
|
|
936
|
+
const mark = selected ? FILE_MARK : ' ';
|
|
937
|
+
const prefix = ` ${mark} `;
|
|
938
|
+
const parts = fileRowParts(entry, cols);
|
|
939
|
+
const suffix = joinFileSuffix(parts);
|
|
940
|
+
const pathW = Math.max(1, inner - visibleWidth(prefix + suffix));
|
|
941
|
+
const clipped = truncateVisible(entry.path, pathW);
|
|
942
|
+
const pad = Math.max(0, pathW - visibleWidth(clipped));
|
|
943
|
+
const left = `${prefix}${clipped}${' '.repeat(pad)}`;
|
|
944
|
+
const fgRgb = THEME.buttonHotFg;
|
|
945
|
+
const bgRgb = selected ? THEME.buttonBg : THEME.ctxBg;
|
|
946
|
+
const edge = fill(1, fgRgb, bgRgb, color);
|
|
947
|
+
if (!color) return `${left}${suffix}${edge}`;
|
|
948
|
+
let out = `${seq(fgRgb, bgRgb)}${EL}`;
|
|
949
|
+
out += paint(left, fgRgb, bgRgb, color, true);
|
|
950
|
+
out += paint(suffix, fgRgb, bgRgb, color, true);
|
|
951
|
+
return out + edge;
|
|
952
|
+
};
|
|
953
|
+
|
|
876
954
|
const paintFileRow = (entry, width, color, selected, cols) => {
|
|
955
|
+
if (isTodosEntry(entry)) {
|
|
956
|
+
return paintTodosRow(entry, width, color, selected, cols);
|
|
957
|
+
}
|
|
877
958
|
const inner = Math.max(1, width - 1);
|
|
878
959
|
const mark = selected ? FILE_MARK : ' ';
|
|
879
960
|
const prefix = ` ${mark} `;
|
|
@@ -1164,13 +1245,19 @@ const paintBranchRow = (entry, width, color, selected, cols) => {
|
|
|
1164
1245
|
const left = `${prefix}${label}${after}`;
|
|
1165
1246
|
const fgRgb = branchRowFg(entry, selected);
|
|
1166
1247
|
const bgRgb = selected ? THEME.buttonBg : THEME.ctxBg;
|
|
1167
|
-
const
|
|
1168
|
-
const nameBg = entry.isDefault ? THEME.buttonHotFg : bgRgb;
|
|
1248
|
+
const chip = branchNameChip(entry, bgRgb);
|
|
1169
1249
|
const edge = fill(1, fgRgb, bgRgb, color);
|
|
1170
1250
|
if (!color) return `${left}${suffix}${edge}`;
|
|
1171
1251
|
let out = `${seq(fgRgb, bgRgb)}${EL}`;
|
|
1172
1252
|
out += paint(prefix, fgRgb, bgRgb, color);
|
|
1173
|
-
out += paintBracketName(
|
|
1253
|
+
out += paintBracketName(
|
|
1254
|
+
label,
|
|
1255
|
+
chip.nameFg,
|
|
1256
|
+
chip.wrapFg,
|
|
1257
|
+
chip.nameBg,
|
|
1258
|
+
color,
|
|
1259
|
+
chip.nameBold,
|
|
1260
|
+
);
|
|
1174
1261
|
out += paint(after, fgRgb, bgRgb, color);
|
|
1175
1262
|
out += paintBranchSuffix(parts, cols, fgRgb, bgRgb, color);
|
|
1176
1263
|
return out + edge;
|
|
@@ -1229,8 +1316,16 @@ const paintBodyBranches = (view, width, color, bodyH, headerLines) => {
|
|
|
1229
1316
|
);
|
|
1230
1317
|
};
|
|
1231
1318
|
|
|
1232
|
-
const paintTodoCaption = (width, color) =>
|
|
1233
|
-
|
|
1319
|
+
const paintTodoCaption = (width, color) => {
|
|
1320
|
+
const text = ` ${REPO_TODOS_LABEL}`;
|
|
1321
|
+
const clipped = truncateVisible(text, width);
|
|
1322
|
+
const pad = Math.max(0, width - visibleWidth(clipped));
|
|
1323
|
+
const filled = clipped + ' '.repeat(pad);
|
|
1324
|
+
if (!color) return filled;
|
|
1325
|
+
const fgRgb = THEME.buttonHotFg;
|
|
1326
|
+
const bgRgb = THEME.ctxBg;
|
|
1327
|
+
return `${BOLD}${seq(fgRgb, bgRgb)}${EL}${filled}${RESET}`;
|
|
1328
|
+
};
|
|
1234
1329
|
|
|
1235
1330
|
const paintBodyTodo = (view, width, color) => {
|
|
1236
1331
|
const texts = view.todos ?? [];
|
|
@@ -1510,7 +1605,32 @@ const frameResult = (
|
|
|
1510
1605
|
|
|
1511
1606
|
const hiddenActions = (view) => {
|
|
1512
1607
|
const pane = view.pane ?? 'diff';
|
|
1513
|
-
|
|
1608
|
+
if (pane === 'files') return FILES_DISABLED;
|
|
1609
|
+
if (pane === 'branches') return BRANCHES_DISABLED;
|
|
1610
|
+
if (view.item && view.item.origin === 'todo') return TODO_DISABLED;
|
|
1611
|
+
return DIFF_DISABLED;
|
|
1612
|
+
};
|
|
1613
|
+
|
|
1614
|
+
const dimFooterActions = (view) => {
|
|
1615
|
+
const pane = view.pane ?? 'diff';
|
|
1616
|
+
if (pane === 'files') {
|
|
1617
|
+
const files = view.files ?? [];
|
|
1618
|
+
const entry = files[view.fileCursor ?? 0];
|
|
1619
|
+
if (!entry || entry.kind !== 'todos') return [];
|
|
1620
|
+
return FILES_GIT_DISABLED;
|
|
1621
|
+
}
|
|
1622
|
+
if (pane === 'diff') return diffGitDisabled(view.item);
|
|
1623
|
+
if (pane === 'branches') {
|
|
1624
|
+
const branches = view.branches ?? [];
|
|
1625
|
+
const entry = branches[view.branchCursor ?? 0];
|
|
1626
|
+
return branchGitDisabled(entry);
|
|
1627
|
+
}
|
|
1628
|
+
return [];
|
|
1629
|
+
};
|
|
1630
|
+
|
|
1631
|
+
const extraFooterActions = (view) => {
|
|
1632
|
+
if ((view.pane ?? 'diff') === 'branches') return Object.keys(BRANCH_ACTIONS);
|
|
1633
|
+
return [];
|
|
1514
1634
|
};
|
|
1515
1635
|
|
|
1516
1636
|
const renderFrame = (view, options = {}) => {
|
|
@@ -1518,8 +1638,10 @@ const renderFrame = (view, options = {}) => {
|
|
|
1518
1638
|
const height = Math.max(6, options.height ?? 24);
|
|
1519
1639
|
const color = options.color ?? true;
|
|
1520
1640
|
const rows = [paintHeader(view, width, color)];
|
|
1521
|
-
const extra = view
|
|
1522
|
-
const
|
|
1641
|
+
const extra = extraFooterActions(view);
|
|
1642
|
+
const hidden = hiddenActions(view);
|
|
1643
|
+
const dimmed = dimFooterActions(view);
|
|
1644
|
+
const layout = layoutButtons(width, false, hidden, extra, dimmed);
|
|
1523
1645
|
const notes = frameNotes(view, width, height, color);
|
|
1524
1646
|
const headerLines = rows.length;
|
|
1525
1647
|
const bodyH = notes.bodyH;
|
|
@@ -1571,6 +1693,7 @@ module.exports = {
|
|
|
1571
1693
|
COMMIT_PROMPT,
|
|
1572
1694
|
PUSH_PROMPT,
|
|
1573
1695
|
updatePrompt,
|
|
1696
|
+
dropPrompt,
|
|
1574
1697
|
DEL_CHAR_BG,
|
|
1575
1698
|
ADD_CHAR_BG,
|
|
1576
1699
|
paintDiffLine,
|