reslop 0.2.0 → 0.2.1
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 +14 -1
- package/README.md +2 -1
- package/lib/ansi.js +144 -3
- package/lib/api.js +2 -0
- package/lib/capabilities.js +6 -0
- package/lib/cli.js +10 -2
- package/lib/git-branches.js +185 -6
- package/lib/git-reword-todo.js +20 -0
- package/lib/git.js +20 -3
- package/lib/render/branches.js +4 -32
- package/lib/render/chrome.js +47 -5
- package/lib/render/commits.js +63 -1
- package/lib/render/diff.js +4 -4
- package/lib/render/frame.js +13 -1
- package/lib/render/notes.js +22 -5
- package/lib/render/primitives.js +54 -11
- package/lib/render/render.js +3 -3
- package/lib/review-merge.js +270 -0
- package/lib/review.js +38 -6
- package/lib/session/actions.js +30 -3
- package/lib/session/commit.js +101 -8
- package/lib/session/compose.js +48 -27
- package/lib/session/dispatch.js +6 -0
- package/lib/session/files-pane.js +17 -3
- package/lib/session/lifecycle.js +113 -16
- package/lib/session/pointer.js +4 -1
- package/lib/session/todos.js +29 -1
- package/lib/session/watch.js +120 -8
- package/lib/session.js +1 -0
- package/package.json +2 -2
package/lib/session/watch.js
CHANGED
|
@@ -72,26 +72,127 @@ const closeWatcher = (watcher) => {
|
|
|
72
72
|
}
|
|
73
73
|
};
|
|
74
74
|
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
if (
|
|
78
|
-
|
|
79
|
-
|
|
75
|
+
const reviewMarkdown = (rel) => {
|
|
76
|
+
const norm = `${rel}`.replaceAll('\\', '/');
|
|
77
|
+
if (!norm.startsWith('.review/')) return false;
|
|
78
|
+
const rest = norm.slice('.review/'.length);
|
|
79
|
+
if (!rest || rest.includes('/')) return false;
|
|
80
|
+
return rest.endsWith('.md');
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const isReviewDir = (root, dir) => {
|
|
84
|
+
const rel = path.relative(root, dir).replaceAll('\\', '/');
|
|
85
|
+
return rel === '.review' || rel.startsWith('.review/');
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const hasFile = (full) => {
|
|
89
|
+
try {
|
|
90
|
+
fs.accessSync(full);
|
|
91
|
+
return true;
|
|
92
|
+
} catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const isBareReviewName = (root, dir, rel) => {
|
|
98
|
+
if (!rel || rel.includes('/') || !rel.endsWith('.md')) return false;
|
|
99
|
+
const base = path.basename(rel);
|
|
100
|
+
if (hasFile(path.join(dir, base))) return false;
|
|
101
|
+
return hasFile(path.join(root, '.review', base));
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const isReviewEvent = (root, dir, name) => {
|
|
105
|
+
if (isReviewDir(root, dir)) return true;
|
|
106
|
+
if (!name) return false;
|
|
107
|
+
const rel = path.relative(root, path.join(dir, name));
|
|
108
|
+
const norm = rel.replaceAll('\\', '/');
|
|
109
|
+
if (reviewMarkdown(norm)) return true;
|
|
110
|
+
return isBareReviewName(root, dir, norm);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const isOwnDirEvent = (dir, name) => {
|
|
114
|
+
if (!name) return false;
|
|
115
|
+
const base = path.basename(dir);
|
|
116
|
+
const slash = `${name}`.replaceAll('\\', '/');
|
|
117
|
+
const bare = slash.startsWith('./') ? slash.slice(2) : slash;
|
|
118
|
+
const trimmed = bare.endsWith('/') ? bare.slice(0, -1) : bare;
|
|
119
|
+
if (trimmed === base) {
|
|
120
|
+
try {
|
|
121
|
+
return !fs.statSync(path.join(dir, trimmed)).isFile();
|
|
122
|
+
} catch {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const full = path.resolve(dir, name);
|
|
127
|
+
if (full === path.resolve(dir)) return true;
|
|
128
|
+
if (path.basename(full) !== base) return false;
|
|
129
|
+
try {
|
|
130
|
+
return fs.realpathSync(full) === fs.realpathSync(dir);
|
|
131
|
+
} catch {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const schedule = (state, timerKey, fire) => {
|
|
137
|
+
if (state.closed || !fire) return;
|
|
138
|
+
if (state[timerKey] !== null) clearTimeout(state[timerKey]);
|
|
139
|
+
state[timerKey] = setTimeout(() => {
|
|
140
|
+
state[timerKey] = null;
|
|
80
141
|
if (state.closed) return;
|
|
81
|
-
|
|
142
|
+
fire();
|
|
82
143
|
}, state.debounceMs);
|
|
83
|
-
|
|
144
|
+
const timer = state[timerKey];
|
|
145
|
+
if (timer && typeof timer.unref === 'function') timer.unref();
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const REVIEW_HOLD_MS = 200;
|
|
149
|
+
|
|
150
|
+
const reviewBurst = (state) => {
|
|
151
|
+
const seen = state.reviewSeenAt;
|
|
152
|
+
if (!seen) return false;
|
|
153
|
+
const hold = Math.max(state.debounceMs * 2, REVIEW_HOLD_MS);
|
|
154
|
+
return Date.now() - seen < hold;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const scheduleChange = (state) => {
|
|
158
|
+
schedule(state, 'timer', () => {
|
|
159
|
+
if (reviewBurst(state)) return;
|
|
160
|
+
if (state.onChange) state.onChange();
|
|
161
|
+
});
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const scheduleUnnamed = (state) => {
|
|
165
|
+
if (reviewBurst(state)) return;
|
|
166
|
+
schedule(state, 'looseTimer', () => {
|
|
167
|
+
if (reviewBurst(state)) return;
|
|
168
|
+
scheduleChange(state);
|
|
169
|
+
});
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const dropChange = (state) => {
|
|
173
|
+
if (state.timer !== null) clearTimeout(state.timer);
|
|
174
|
+
if (state.looseTimer !== null) clearTimeout(state.looseTimer);
|
|
175
|
+
state.timer = null;
|
|
176
|
+
state.looseTimer = null;
|
|
84
177
|
};
|
|
85
178
|
|
|
86
179
|
const handleWatchEvent = (state, dir, eventType, filename) => {
|
|
87
180
|
if (state.closed) return;
|
|
88
181
|
const name = filename ? `${filename}` : '';
|
|
89
182
|
const rel = name ? path.relative(state.root, path.join(dir, name)) : '';
|
|
183
|
+
if (isReviewEvent(state.root, dir, name)) {
|
|
184
|
+
state.reviewSeenAt = Date.now();
|
|
185
|
+
dropChange(state);
|
|
186
|
+
return void schedule(state, 'reviewTimer', state.onReview);
|
|
187
|
+
}
|
|
90
188
|
if (rel && ignoredRel(rel)) return;
|
|
189
|
+
if (reviewBurst(state)) return;
|
|
190
|
+
if (!name) return void scheduleUnnamed(state);
|
|
191
|
+
if (isOwnDirEvent(dir, name)) return;
|
|
91
192
|
if (eventType === 'rename' && name) {
|
|
92
193
|
state.watchNewDir(path.join(dir, name));
|
|
93
194
|
}
|
|
94
|
-
|
|
195
|
+
scheduleChange(state);
|
|
95
196
|
};
|
|
96
197
|
|
|
97
198
|
const watchOne = (state, target, extra = {}) => {
|
|
@@ -141,6 +242,7 @@ const maybeWatchNewDir = (state, full) => {
|
|
|
141
242
|
return;
|
|
142
243
|
}
|
|
143
244
|
if (!stat.isDirectory()) return;
|
|
245
|
+
if (path.basename(full) === '.review') return void watchOne(state, full);
|
|
144
246
|
if (skipDirName(path.basename(full))) return;
|
|
145
247
|
watchTree(state, full);
|
|
146
248
|
};
|
|
@@ -166,7 +268,11 @@ const watchGit = (state) => {
|
|
|
166
268
|
const stopState = (state) => {
|
|
167
269
|
state.closed = true;
|
|
168
270
|
if (state.timer !== null) clearTimeout(state.timer);
|
|
271
|
+
if (state.reviewTimer !== null) clearTimeout(state.reviewTimer);
|
|
272
|
+
if (state.looseTimer !== null) clearTimeout(state.looseTimer);
|
|
169
273
|
state.timer = null;
|
|
274
|
+
state.reviewTimer = null;
|
|
275
|
+
state.looseTimer = null;
|
|
170
276
|
while (state.watchers.length) closeWatcher(state.watchers.pop());
|
|
171
277
|
state.watched.clear();
|
|
172
278
|
};
|
|
@@ -189,13 +295,18 @@ const createDiskWatcher = (options) => {
|
|
|
189
295
|
dirWatchOpts: recursive ? { recursive: true } : {},
|
|
190
296
|
watchers: [],
|
|
191
297
|
watched: new Set(),
|
|
298
|
+
onReview: options.onReview,
|
|
192
299
|
timer: null,
|
|
300
|
+
reviewTimer: null,
|
|
301
|
+
looseTimer: null,
|
|
302
|
+
reviewSeenAt: 0,
|
|
193
303
|
closed: false,
|
|
194
304
|
watchNewDir: (full) => maybeWatchNewDir(state, full),
|
|
195
305
|
};
|
|
196
306
|
const scopes = options.paths && options.paths.length ? options.paths : ['.'];
|
|
197
307
|
for (const spec of scopes) watchScope(state, spec);
|
|
198
308
|
watchGit(state);
|
|
309
|
+
watchOne(state, path.resolve(state.root, '.review'));
|
|
199
310
|
return {
|
|
200
311
|
close: () => stopState(state),
|
|
201
312
|
watchFiles: (rels) => watchFiles(state, rels),
|
|
@@ -207,5 +318,6 @@ module.exports = {
|
|
|
207
318
|
POLL_MS,
|
|
208
319
|
canWatchRecursive,
|
|
209
320
|
ignoredRel,
|
|
321
|
+
isOwnDirEvent,
|
|
210
322
|
createDiskWatcher,
|
|
211
323
|
};
|
package/lib/session.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reslop",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Interactive code review and repair planning for AI-assisted changes",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"code-review",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"eslint": "^9.39.4",
|
|
60
60
|
"eslint-config-metarhia": "^9.1.9",
|
|
61
61
|
"metaskills": "^1.0.5",
|
|
62
|
-
"prettier": "^3.9.
|
|
62
|
+
"prettier": "^3.9.9"
|
|
63
63
|
},
|
|
64
64
|
"dependencies": {
|
|
65
65
|
"metautil": "^5.5.2"
|