pi-diff-review 0.1.16 → 0.1.17

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 CHANGED
@@ -62,6 +62,7 @@ Review a branch or commit range by passing any `git diff` arguments after `/diff
62
62
  - `/diff --cached` reviews staged changes
63
63
  - `/diff main...HEAD` reviews changes on the current branch relative to `main`
64
64
  - `/diff <git-diff-args>` passes arguments through to `git diff`
65
+ - `h` toggles the command help modal
65
66
  - `j/k` or arrow keys to move
66
67
  - `g/G` to jump to the top or bottom of the diff
67
68
  - `ctrl-u` / `ctrl-d` to move up/down by half a page
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-diff-review",
3
- "version": "0.1.16",
3
+ "version": "0.1.17",
4
4
  "description": "Local diff review TUI extension for pi",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -51,11 +51,32 @@ type AnnotatedDiffRow =
51
51
  | { kind: "split"; splitRowIndex: number }
52
52
  | InlineBoxRow;
53
53
 
54
+ const HELP_COMMANDS = [
55
+ ["h", "show or hide this help"],
56
+ ["j/k or arrows", "move selection"],
57
+ ["ctrl-u / ctrl-d", "move up or down half a page"],
58
+ ["g / G", "jump to top or bottom"],
59
+ ["n / p", "jump to next or previous hunk"],
60
+ ["/", "search diff lines"],
61
+ ["n / N", "jump between search matches"],
62
+ ["J / K", "extend highlighted selection"],
63
+ ["c", "add or edit a line or range comment"],
64
+ ["C", "add or edit an overall diff comment"],
65
+ ["x", "delete the current line or range comment"],
66
+ ["t", "toggle inline comments and explanations"],
67
+ ["v", "toggle unified or split rendering"],
68
+ ["?", "toggle AI explanation for current hunk"],
69
+ ["Enter", "submit comments, save edits, or jump to search result"],
70
+ ["Esc", "close help, cancel search/edit, clear selection, or exit"],
71
+ ["q", "exit review"],
72
+ ] as const;
73
+
54
74
  export class ReviewComponent {
55
75
  private navigation: ReviewNavigationState;
56
76
  private editMode = false;
57
77
  private editingCommentKey?: string;
58
78
  private search: ReviewSearchState;
79
+ private helpVisible = false;
59
80
 
60
81
  private inlineAnnotationsVisible = true;
61
82
  private visibleExplanationKeys = new Set<string>();
@@ -171,6 +192,13 @@ export class ReviewComponent {
171
192
  }
172
193
 
173
194
  handleInput(data: string): void {
195
+ if (this.helpVisible) {
196
+ if (data === "h" || matchesKey(data, "escape")) {
197
+ this.toggleHelp();
198
+ }
199
+ return;
200
+ }
201
+
174
202
  if (this.editMode) {
175
203
  if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
176
204
  this.exitEditMode();
@@ -201,6 +229,10 @@ export class ReviewComponent {
201
229
  this.done({ action: "cancel" });
202
230
  return;
203
231
  }
232
+ if (data === "h") {
233
+ this.toggleHelp();
234
+ return;
235
+ }
204
236
  if (data === "t") {
205
237
  this.toggleInlineAnnotations();
206
238
  return;
@@ -292,15 +324,9 @@ export class ReviewComponent {
292
324
  const output: string[] = [];
293
325
 
294
326
  output.push(
295
- truncateToWidth(
296
- this.theme.fg(
297
- "dim",
298
- this.editMode
299
- ? `${this.title} • ${this.lines.length} lines • ${this.comments.size} comments • editing inline comment • Enter save • Esc/Ctrl+C cancel`
300
- : this.hasSelection()
301
- ? `${this.title} • ${this.lines.length} lines • ${this.comments.size} comments • J/K extend • Esc clear selection • c comment range • C overall comment • Enter submit`
302
- : `${this.title} • ${this.lines.length} lines • ${this.comments.size} comments • ${this.getPositionText(selectedLine)} • inline ${this.inlineAnnotationsVisible ? "shown" : "hidden"} • j/k move • g/G top/bottom • ctrl-u/d page • / search • t annotations • v unified/split • ? explain • J/K extend • c comment • C overall • x delete • ${this.search.query ? "n/N search" : "n/p hunk"} • Enter submit • q quit`,
303
- ),
327
+ this.renderStatusLine(
328
+ this.getHeaderText(selectedLine),
329
+ "Press h for help",
304
330
  width,
305
331
  ),
306
332
  );
@@ -314,9 +340,132 @@ export class ReviewComponent {
314
340
  width,
315
341
  ),
316
342
  );
343
+ return this.helpVisible ? this.renderHelpModal(output, width) : output;
344
+ }
345
+
346
+ private getHeaderText(selectedLine?: ReviewLine): string {
347
+ const base = `${this.title} • ${this.lines.length} lines • ${this.comments.size} comments`;
348
+
349
+ if (this.editMode) {
350
+ return `${base} • editing ${this.editingCommentKey === GLOBAL_COMMENT_KEY ? "overall comment" : "inline comment"}`;
351
+ }
352
+
353
+ if (this.hasSelection()) {
354
+ return `${base} • selection active`;
355
+ }
356
+
357
+ return `${base} • ${this.getPositionText(selectedLine)} • inline ${this.inlineAnnotationsVisible ? "shown" : "hidden"} • ${this.diffRenderMode}`;
358
+ }
359
+
360
+ private renderStatusLine(left: string, right: string, width: number): string {
361
+ const styledLeft = this.theme.fg("dim", left);
362
+ const styledRight = this.theme.fg("muted", right);
363
+ const rightWidth = visibleWidth(styledRight);
364
+ const leftWidth = Math.max(0, width - rightWidth - 1);
365
+ const truncatedLeft = truncateToWidth(styledLeft, leftWidth);
366
+ const spacer = Math.max(
367
+ 1,
368
+ width - visibleWidth(truncatedLeft) - rightWidth,
369
+ );
370
+ return truncateToWidth(
371
+ `${truncatedLeft}${" ".repeat(spacer)}${styledRight}`,
372
+ width,
373
+ );
374
+ }
375
+
376
+ private renderHelpModal(rows: string[], width: number): string[] {
377
+ if (rows.length === 0 || width < 8) return rows;
378
+
379
+ const modalWidth = Math.max(8, Math.min(72, width - 2));
380
+ const contentWidth = Math.max(4, modalWidth - 4);
381
+ const keyWidth = Math.min(16, Math.max(8, Math.floor(contentWidth * 0.35)));
382
+ const modalRows = this.buildHelpModalRows(
383
+ modalWidth,
384
+ contentWidth,
385
+ keyWidth,
386
+ );
387
+ const maxRows = Math.max(1, rows.length - 2);
388
+ const visibleModalRows =
389
+ modalRows.length > maxRows
390
+ ? [
391
+ ...modalRows.slice(0, maxRows - 1),
392
+ this.renderModalRow(
393
+ this.theme.fg("dim", "More commands available on taller screens"),
394
+ contentWidth,
395
+ ),
396
+ ]
397
+ : modalRows;
398
+ const top = Math.max(
399
+ 1,
400
+ Math.floor((rows.length - visibleModalRows.length) / 2),
401
+ );
402
+ const left = Math.max(0, Math.floor((width - modalWidth) / 2));
403
+
404
+ const output = [...rows];
405
+ for (let index = 0; index < visibleModalRows.length; index++) {
406
+ const row = visibleModalRows[index]!;
407
+ output[top + index] = padToWidth(
408
+ truncateToWidth(`${" ".repeat(left)}${row}`, width),
409
+ width,
410
+ );
411
+ }
317
412
  return output;
318
413
  }
319
414
 
415
+ private buildHelpModalRows(
416
+ modalWidth: number,
417
+ contentWidth: number,
418
+ keyWidth: number,
419
+ ): string[] {
420
+ const rows = [
421
+ this.renderModalHorizontal(" Help ", modalWidth, "top"),
422
+ this.renderModalRow(
423
+ this.theme.fg("dim", "Press h or Esc to close."),
424
+ contentWidth,
425
+ ),
426
+ this.renderModalRow("", contentWidth),
427
+ ];
428
+
429
+ for (const [keys, description] of HELP_COMMANDS) {
430
+ const keyCell = padToWidth(
431
+ truncateToWidth(this.theme.fg("accent", keys), keyWidth),
432
+ keyWidth,
433
+ );
434
+ rows.push(
435
+ this.renderModalRow(
436
+ `${keyCell} ${this.theme.fg("text", description)}`,
437
+ contentWidth,
438
+ ),
439
+ );
440
+ }
441
+
442
+ rows.push(this.renderModalHorizontal("", modalWidth, "bottom"));
443
+ return rows;
444
+ }
445
+
446
+ private renderModalRow(text: string, contentWidth: number): string {
447
+ return `${this.theme.fg("borderMuted", "│")} ${padToWidth(
448
+ truncateToWidth(text, contentWidth),
449
+ contentWidth,
450
+ )} ${this.theme.fg("borderMuted", "│")}`;
451
+ }
452
+
453
+ private renderModalHorizontal(
454
+ title: string,
455
+ width: number,
456
+ part: "top" | "bottom",
457
+ ): string {
458
+ const contentWidth = Math.max(0, width - 2);
459
+ const visibleTitle = truncateToWidth(title, contentWidth);
460
+ const remaining = Math.max(0, contentWidth - visibleWidth(visibleTitle));
461
+ const left = part === "top" ? "╭" : "╰";
462
+ const right = part === "top" ? "╮" : "╯";
463
+ return `${this.theme.fg("borderMuted", left)}${visibleTitle}${this.theme.fg(
464
+ "borderMuted",
465
+ "─".repeat(remaining),
466
+ )}${this.theme.fg("borderMuted", right)}`;
467
+ }
468
+
320
469
  private renderAnnotatedDiffRows(width: number, height: number): string[] {
321
470
  const rows = this.getAnnotatedRows(width);
322
471
  const output: string[] = [];
@@ -766,6 +915,11 @@ export class ReviewComponent {
766
915
  this.tui.requestRender(true);
767
916
  }
768
917
 
918
+ private toggleHelp(): void {
919
+ this.helpVisible = !this.helpVisible;
920
+ this.tui.requestRender(true);
921
+ }
922
+
769
923
  private hideInlineExplanation(): void {
770
924
  this.visibleExplanationKeys.clear();
771
925
  }