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 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.5...HEAD
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 diff and plan changes
1
+ # 👁️ reslop — Review AI generated code and plan changes
2
2
 
3
3
  [![ci status](https://github.com/tshemsedinov/reslop/workflows/Testing%20CI/badge.svg)](https://github.com/tshemsedinov/reslop/actions?query=workflow%3A%22Testing+CI%22+branch%3Amain)
4
4
  [![snyk](https://snyk.io/test/github/tshemsedinov/reslop/badge.svg)](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` | Commit: then `c` commit / `a` amend / `f` fixup |
56
- | `b` | List branches; `b, n` new branch |
57
- | `p` / `s` | git pull/push |
58
- | `f` / `t` / `e` | Add feedback / todo / edit diff text |
59
- | `r` | Reload diffs from disk |
60
- | `m` | Mode: unified / mixed / side-by-side |
61
- | `⏎` | Open the selected file |
62
- | `→` / `j` | Next remaining diff / next file |
63
- | `←` / `k` | Previous remaining diff / previous file |
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
@@ -74,6 +74,7 @@ const THEME = {
74
74
  errorFg: [0xff, 0x80, 0x80],
75
75
  warnFg: [0xe8, 0xc8, 0x4a],
76
76
  mutedFg: [0x7a, 0x7a, 0x8a],
77
+ shaFg: [0x56, 0x9c, 0xd6],
77
78
  stagedDelLineFg: [0xa0, 0xa0, 0xa8],
78
79
  stagedDelLineBg: [0x20, 0x20, 0x26],
79
80
  stagedDelCharFg: [0xf0, 0xf0, 0xf4],
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
- if (pane === 'files') return FILES_DISABLED;
70
- if (pane === 'branches') return BRANCHES_DISABLED;
71
- if (item && item.origin === 'todo') return TODO_DISABLED;
72
- return DIFF_DISABLED;
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) => ACTION_HOT[id] ?? ACTION_LABEL[id] ?? 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 { itemPath } = require('./files.js');
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 { disabledActions } = keys;
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.mutedFg,
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 n`;
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 = (width, compact = false, hidden = [], extra = []) => {
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
- hits.push({ id, x0: x, x1: x + w });
600
- parts.push({ action: { id }, piece, letter, label });
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, THEME.buttonFg, THEME.buttonBg, color);
616
- out += paintMarkedWord(part.label, part.letter, color, BUTTON_MARK);
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 nameFg = entry.isDefault ? THEME.headerFg : THEME.buttonHotFg;
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(label, nameFg, THEME.mutedFg, nameBg, color);
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
- paintBar(` ${TODO_CAPTION}`, width, THEME.chromeFg, THEME.ctxBg, color);
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
- return disabledActions(pane, view.item);
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.pane === 'branches' ? Object.keys(BRANCH_ACTIONS) : [];
1522
- const layout = layoutButtons(width, false, hiddenActions(view), extra);
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,