revspec 0.2.0 → 0.2.2

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/bin/revspec.ts CHANGED
@@ -38,7 +38,8 @@ if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
38
38
  }
39
39
 
40
40
  if (args.includes("--version") || args.includes("-v")) {
41
- console.log("revspec 0.1.0");
41
+ const pkg = await Bun.file(new URL("../package.json", import.meta.url)).json();
42
+ console.log(`revspec ${pkg.version}`);
42
43
  process.exit(0);
43
44
  }
44
45
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "revspec",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "revspec": "./bin/revspec.ts"
package/src/cli/reply.ts CHANGED
@@ -42,12 +42,15 @@ export function runReply(
42
42
  process.exit(1);
43
43
  }
44
44
 
45
+ // Clean up shell escaping artifacts (e.g., \! from bash history expansion)
46
+ const cleanText = text.replace(/\\!/g, "!");
47
+
45
48
  // Append reply event
46
49
  appendEvent(jsonlPath, {
47
50
  type: "reply",
48
51
  threadId,
49
52
  author: "owner",
50
- text,
53
+ text: cleanText,
51
54
  ts: Date.now(),
52
55
  });
53
56
  }
package/src/cli/watch.ts CHANGED
@@ -193,6 +193,12 @@ function processNewEvents(
193
193
  return { approved: true, output: "", newOffset };
194
194
  }
195
195
 
196
+ // Check for session-end — TUI exited, break the loop
197
+ const hasSessionEnd = events.some((e) => e.type === "session-end");
198
+ if (hasSessionEnd) {
199
+ return { approved: false, output: "Session ended. Reviewer exited revspec.\n", newOffset };
200
+ }
201
+
196
202
  // Only return actionable events — comments and replies that need an LLM response.
197
203
  // Resolves, unresolves, and deletes are informational — no reply needed.
198
204
  const actionableEvents = events.filter(
@@ -8,7 +8,8 @@ export type LiveEventType =
8
8
  | "unresolve"
9
9
  | "approve"
10
10
  | "delete"
11
- | "round";
11
+ | "round"
12
+ | "session-end";
12
13
 
13
14
  export interface LiveEvent {
14
15
  type: LiveEventType;
package/src/tui/app.ts CHANGED
@@ -200,6 +200,8 @@ export async function runTui(
200
200
 
201
201
  function mergeAndExit(resolve: () => void): void {
202
202
  doMerge();
203
+ // Signal to watch process that session has ended
204
+ appendEvent(jsonlPath, { type: "session-end", author: "reviewer", ts: Date.now() });
203
205
  liveWatcher.stop();
204
206
  renderer.destroy();
205
207
  resolve();
@@ -249,11 +251,13 @@ export async function runTui(
249
251
  setTimeout(() => { refreshPager(); }, 2000);
250
252
  return "stay";
251
253
  }
254
+ appendEvent(jsonlPath, { type: "session-end", author: "reviewer", ts: Date.now() });
252
255
  liveWatcher.stop();
253
256
  return "exit";
254
257
  }
255
258
  if (cmd === "q!") {
256
259
  // Exit without merging
260
+ appendEvent(jsonlPath, { type: "session-end", author: "reviewer", ts: Date.now() });
257
261
  liveWatcher.stop();
258
262
  return "exit";
259
263
  }
@@ -263,7 +267,7 @@ export async function runTui(
263
267
  // --- Overlay launchers ---
264
268
 
265
269
  function showCommentInput(): void {
266
- const existingThread = state.threadAtLine(state.cursorLine);
270
+ let existingThread = state.threadAtLine(state.cursorLine);
267
271
 
268
272
  const overlay = createCommentInput({
269
273
  renderer,
@@ -278,13 +282,19 @@ export async function runTui(
278
282
  refreshPager();
279
283
  // Don't dismiss — overlay stays open, message appended by comment-input
280
284
  } else {
281
- // New comment — close overlay
285
+ // New comment — create thread, stay open
282
286
  state.addComment(state.cursorLine, text);
283
287
  const newThread = state.threadAtLine(state.cursorLine);
284
288
  if (newThread) {
285
289
  appendEvent(jsonlPath, { type: "comment", threadId: newThread.id, line: state.cursorLine, author: "reviewer", text, ts: Date.now() });
290
+ // Update overlay to reference the new thread
291
+ if (activeOverlay) {
292
+ activeOverlay.threadId = newThread.id;
293
+ activeOverlay.container.title = ` Thread #${newThread.id} (line ${state.cursorLine}) `;
294
+ }
295
+ existingThread = newThread;
286
296
  }
287
- dismissOverlay();
297
+ refreshPager();
288
298
  }
289
299
  },
290
300
  onResolve: () => {
@@ -28,89 +28,12 @@ export interface CommentInputOverlay {
28
28
 
29
29
  export function createCommentInput(opts: CommentInputOptions): CommentInputOverlay {
30
30
  const { renderer, line, existingThread, onSubmit, onResolve, onCancel } = opts;
31
- const hasThread = existingThread && existingThread.messages.length > 0;
32
-
33
- if (!hasThread) {
34
- return createNewComment(renderer, line, onSubmit, onCancel);
35
- }
36
- return createThreadView(renderer, line, existingThread!, onSubmit, onResolve, onCancel);
37
- }
38
-
39
- // --- New comment: insert-only buffer, Tab submits and closes ---
40
- function createNewComment(
41
- renderer: CliRenderer,
42
- line: number,
43
- onSubmit: (text: string) => void,
44
- onCancel: () => void,
45
- ): CommentInputOverlay {
46
- const container = new BoxRenderable(renderer, {
47
- position: "absolute",
48
- top: "30%",
49
- left: "10%",
50
- width: "80%",
51
- height: 10,
52
- zIndex: 100,
53
- backgroundColor: theme.base,
54
- border: true,
55
- borderStyle: "single",
56
- borderColor: theme.borderComment,
57
- title: ` New comment on line ${line} `,
58
- flexDirection: "column",
59
- padding: 1,
60
- });
61
-
62
- const textarea = new TextareaRenderable(renderer, {
63
- width: "100%",
64
- flexGrow: 1,
65
- backgroundColor: theme.surface0,
66
- textColor: theme.text,
67
- focusedBackgroundColor: theme.surface0,
68
- focusedTextColor: theme.text,
69
- wrapMode: "word",
70
- placeholder: "Type your comment...",
71
- placeholderColor: theme.overlay,
72
- initialValue: "",
73
- });
74
-
75
- const hint = new TextRenderable(renderer, {
76
- content: " [Tab] submit [Esc] cancel",
77
- width: "100%",
78
- height: 1,
79
- fg: theme.hintFg,
80
- bg: theme.hintBg,
81
- wrapMode: "none",
82
- truncate: true,
83
- });
84
-
85
- container.add(textarea);
86
- container.add(hint);
87
- setTimeout(() => { textarea.focus(); renderer.requestRender(); }, 0);
88
-
89
- let submitted = false;
90
- const keyHandler = (key: KeyEvent) => {
91
- if (key.name === "escape") {
92
- key.preventDefault(); key.stopPropagation(); onCancel(); return;
93
- }
94
- if (key.name === "tab") {
95
- key.preventDefault(); key.stopPropagation();
96
- if (submitted) return;
97
- submitted = true;
98
- const text = textarea.plainText.trim();
99
- if (text.length > 0) onSubmit(text); else onCancel();
100
- return;
101
- }
102
- };
103
- renderer.keyInput.on("keypress", keyHandler);
104
-
105
- return {
106
- container,
107
- cleanup() { renderer.keyInput.off("keypress", keyHandler); textarea.destroy(); },
108
- addMessage() {},
109
- threadId: null,
110
- };
31
+ // Always use thread view even for new comments (empty history, just the input)
32
+ const thread = existingThread ?? { id: "", line, status: "open" as const, messages: [] };
33
+ return createThreadView(renderer, line, thread, onSubmit, onResolve, onCancel);
111
34
  }
112
35
 
113
- // --- Thread view: two modes (normal/insert), unified buffer ---
36
+ // --- Unified thread view: works for both new comments and existing threads ---
114
37
  function createThreadView(
115
38
  renderer: CliRenderer,
116
39
  line: number,
@@ -130,7 +53,7 @@ function createThreadView(
130
53
  border: true,
131
54
  borderStyle: "single",
132
55
  borderColor: theme.borderComment,
133
- title: ` Thread #${thread.id} (line ${line}) `,
56
+ title: thread.id ? ` Thread #${thread.id} (line ${line}) ` : ` New comment on line ${line} `,
134
57
  flexDirection: "column",
135
58
  padding: 1,
136
59
  });