sight 0.4.0 → 0.6.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +23 -0
- data/CLAUDE.md +4 -4
- data/README.md +16 -3
- data/RELEASING.md +7 -0
- data/lib/sight/annotation_formatter.rb +23 -14
- data/lib/sight/app.rb +80 -83
- data/lib/sight/cli.rb +29 -68
- data/lib/sight/summary.rb +29 -0
- data/lib/sight/version.rb +1 -1
- data/lib/sight.rb +6 -6
- metadata +3 -2
- data/lib/sight/cursor_hook_installer.rb +0 -62
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 461c502e3de33a457cdd4a084d414fe8b978f7761c09fe795eb3047b0bc6a7e0
|
|
4
|
+
data.tar.gz: f116d0821923e83ec1ff9d392da0b9cb88350f3803cf314f7c863c88b1e24c91
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cd7524e81ade2d5bfaa961c45f3e2b5e701ddb1d943e68746a7be83badb2fe3f4037afac9725c45151ca151bde3a03189cf7cdac48372b620056d21af16940d4
|
|
7
|
+
data.tar.gz: f8c76b45f49a4e7995d89283b7546a64e31c7b62a150477ffff6a1b6f503ba075e4ebfc48f66f479bdb648257b389e95f43d0f1e20fcb40a589eb204d0a56011
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.6.0] - 2026-03-26
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- Show annotation summary instead of full dump on quit
|
|
8
|
+
- Extracted `Summary` class from `AnnotationFormatter`
|
|
9
|
+
- Converted `AnnotationFormatter` from module to class
|
|
10
|
+
|
|
11
|
+
## [0.5.0] - 2026-03-07
|
|
12
|
+
|
|
13
|
+
### Removed
|
|
14
|
+
|
|
15
|
+
- Cursor hook support (use Cursor rules instead)
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Refactored CLI dispatch to use case statement with extracted `open` and `print_help` methods
|
|
20
|
+
- DRYed up install/uninstall hook agent lookup
|
|
21
|
+
- Use `Set` for commented hunk lines lookup
|
|
22
|
+
- Extracted `hunk_end_offset` to deduplicate boundary logic
|
|
23
|
+
- Use `<<` for string building in `AnnotationFormatter`
|
|
24
|
+
- Simplified prompt comment width clamping
|
|
25
|
+
|
|
3
26
|
## [0.4.0] - 2026-03-06
|
|
4
27
|
|
|
5
28
|
### Added
|
data/CLAUDE.md
CHANGED
|
@@ -13,7 +13,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|
|
13
13
|
|
|
14
14
|
## Architecture
|
|
15
15
|
|
|
16
|
-
TUI for closing the loop on AI-generated code changes — browse diffs, jump between hunks, and annotate them. Entry point: `exe/sight` → `CLI.run` → `App.new(files).run`.
|
|
16
|
+
TUI for closing the loop on AI-generated code changes — browse diffs, jump between hunks, and annotate them. Entry point: `exe/sight` → `CLI.run` → `CLI.open` → `App.new(files).run`.
|
|
17
17
|
|
|
18
18
|
**Data flow**: `Git.diff` (raw string) → `DiffParser.parse` (returns `DiffFile[]`) → `App` (curses TUI).
|
|
19
19
|
Untracked files are added via `Git.untracked_files` → `DiffParser.build_untracked`.
|
|
@@ -24,14 +24,14 @@ Untracked files are added via `Git.untracked_files` → `DiffParser.build_untrac
|
|
|
24
24
|
|
|
25
25
|
**App** renders per-file views with hunk-based navigation (j/k). Active hunk is highlighted; inactive hunks render in dark gray (color pair 5, color 240).
|
|
26
26
|
|
|
27
|
-
**Hook system**:
|
|
27
|
+
**Hook system**: `sight install-hook claude` / `sight uninstall-hook claude`.
|
|
28
28
|
`ClaudeHookInstaller` manages a Claude Code `UserPromptSubmit` hook in `~/.config/claude/settings.json`.
|
|
29
|
-
|
|
30
|
-
Both read `.git/sight/pending-review`, output annotations, and delete the file. Hidden subcommands: `hook-run`, `cursor-hook-run`.
|
|
29
|
+
Reads `.git/sight/pending-review`, outputs annotations, and deletes the file. Hidden subcommand: `hook-run`.
|
|
31
30
|
|
|
32
31
|
## Conventions
|
|
33
32
|
|
|
34
33
|
- Ruby >= 3.2, uses `frozen_string_literal` in all files
|
|
35
34
|
- Linter: StandardRB (ruby_version: 3.4 in `.standard.yml`)
|
|
36
35
|
- Tests: Minitest with stubs/mocks, no test framework beyond minitest
|
|
36
|
+
- In tests, assign the subject under test to a local before asserting on it
|
|
37
37
|
- Single runtime dependency: `curses ~1.4`
|
data/README.md
CHANGED
|
@@ -34,18 +34,31 @@ sight
|
|
|
34
34
|
|
|
35
35
|
Install a hook so annotations are automatically fed as context in your next message:
|
|
36
36
|
|
|
37
|
+
**Claude Code** (~/.config/claude/settings.json):
|
|
38
|
+
|
|
37
39
|
```bash
|
|
38
|
-
sight install-hook claude
|
|
39
|
-
sight install-hook cursor # Cursor (~/.cursor/hooks.json)
|
|
40
|
+
sight install-hook claude
|
|
40
41
|
```
|
|
41
42
|
|
|
42
43
|
When you quit sight after annotating, the next message you send will include your annotations.
|
|
43
44
|
|
|
45
|
+
The hook runs on every prompt but is a no-op when there's no pending review — it produces no output and adds nothing to the agent's context.
|
|
46
|
+
|
|
44
47
|
To remove:
|
|
45
48
|
|
|
46
49
|
```bash
|
|
47
50
|
sight uninstall-hook claude
|
|
48
|
-
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Cursor** — Create a rule in `.cursor/rules/sight.mdc`:
|
|
54
|
+
|
|
55
|
+
```markdown
|
|
56
|
+
---
|
|
57
|
+
description: When the user asks to review their sight annotations or code review comments
|
|
58
|
+
alwaysApply: false
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
Read `.git/sight/pending-review`, address the annotations, then delete the file.
|
|
49
62
|
```
|
|
50
63
|
|
|
51
64
|
## Development
|
data/RELEASING.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Releasing
|
|
2
|
+
|
|
3
|
+
1. Update `lib/sight/version.rb` and `CHANGELOG.md`
|
|
4
|
+
2. Run `bundle install` to update `Gemfile.lock`
|
|
5
|
+
3. Run `bundle exec rake` to verify tests and lint pass
|
|
6
|
+
4. Commit: `git commit -am "Release vX.Y.Z"`
|
|
7
|
+
5. Run `bundle exec rake release` — tags, builds, and pushes to RubyGems
|
|
@@ -1,29 +1,38 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Sight
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
class AnnotationFormatter
|
|
5
|
+
def initialize(annotations)
|
|
6
|
+
@annotations = annotations
|
|
7
|
+
end
|
|
6
8
|
|
|
7
|
-
def format
|
|
9
|
+
def format
|
|
8
10
|
return "" if annotations.empty?
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
annotations.group_by(&:file_path).map do |path, file_annotations|
|
|
13
|
+
format_file(path, file_annotations)
|
|
14
|
+
end.join("\n")
|
|
12
15
|
end
|
|
13
16
|
|
|
14
|
-
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
attr_reader :annotations
|
|
20
|
+
|
|
21
|
+
def format_file(path, file_annotations)
|
|
15
22
|
out = "## File: #{path}\n\n"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
out
|
|
20
|
-
|
|
23
|
+
|
|
24
|
+
file_annotations.each do |file_annotation|
|
|
25
|
+
context = file_annotation.hunk.context ? " #{file_annotation.hunk.context}" : ""
|
|
26
|
+
out << "Hunk (@@#{context}):\n"
|
|
27
|
+
out << "```diff\n"
|
|
28
|
+
file_annotation.hunk.lines.each do |line|
|
|
21
29
|
next unless %i[add del].include?(line.type)
|
|
22
|
-
out
|
|
30
|
+
out << "#{line.content}\n"
|
|
23
31
|
end
|
|
24
|
-
out
|
|
25
|
-
out
|
|
32
|
+
out << "```\n"
|
|
33
|
+
out << "> #{file_annotation.comment}\n\n"
|
|
26
34
|
end
|
|
35
|
+
|
|
27
36
|
out
|
|
28
37
|
end
|
|
29
38
|
end
|
data/lib/sight/app.rb
CHANGED
|
@@ -4,17 +4,15 @@ require "curses"
|
|
|
4
4
|
|
|
5
5
|
module Sight
|
|
6
6
|
class App
|
|
7
|
-
attr_reader :files, :
|
|
8
|
-
attr_accessor :
|
|
7
|
+
attr_reader :files, :lines, :annotations
|
|
8
|
+
attr_accessor :offset, :hunk_idx
|
|
9
9
|
|
|
10
10
|
def initialize(files)
|
|
11
11
|
@files = files
|
|
12
|
-
@file_lines = files.map { build_file_lines(it) }
|
|
13
|
-
@file_idx = 0
|
|
14
12
|
@offset = 0
|
|
15
13
|
@hunk_idx = 0
|
|
16
|
-
@hunk_offsets_cache = {}
|
|
17
14
|
@annotations = []
|
|
15
|
+
build_flat_view
|
|
18
16
|
end
|
|
19
17
|
|
|
20
18
|
def run
|
|
@@ -31,11 +29,24 @@ module Sight
|
|
|
31
29
|
|
|
32
30
|
private
|
|
33
31
|
|
|
34
|
-
def
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
32
|
+
def build_flat_view
|
|
33
|
+
@lines = []
|
|
34
|
+
@hunk_offsets = []
|
|
35
|
+
@hunk_entries = []
|
|
36
|
+
|
|
37
|
+
@files.each_with_index do |file, file_idx|
|
|
38
|
+
if file_idx > 0
|
|
39
|
+
@lines << DisplayLine.new(type: :file_separator, text: nil, lineno: nil)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
file.hunks.each do |hunk|
|
|
43
|
+
@hunk_offsets << @lines.size
|
|
44
|
+
@hunk_entries << [file, hunk]
|
|
45
|
+
|
|
46
|
+
hunk.lines.each do |diff_line|
|
|
47
|
+
text = (diff_line.type == :meta) ? diff_line.content : diff_line.content[1..]
|
|
48
|
+
@lines << DisplayLine.new(type: diff_line.type, text: text, lineno: diff_line.lineno)
|
|
49
|
+
end
|
|
39
50
|
end
|
|
40
51
|
end
|
|
41
52
|
end
|
|
@@ -56,10 +67,6 @@ module Sight
|
|
|
56
67
|
Curses.init_pair(6, Curses::COLOR_MAGENTA, -1)
|
|
57
68
|
end
|
|
58
69
|
|
|
59
|
-
def lines
|
|
60
|
-
file_lines[file_idx]
|
|
61
|
-
end
|
|
62
|
-
|
|
63
70
|
def render
|
|
64
71
|
win = Curses.stdscr
|
|
65
72
|
win.clear
|
|
@@ -71,19 +78,15 @@ module Sight
|
|
|
71
78
|
end
|
|
72
79
|
|
|
73
80
|
def render_header(win, width)
|
|
74
|
-
file =
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
81
|
+
file, hunk = current_hunk_entry
|
|
82
|
+
return unless file
|
|
83
|
+
badge = " [#{file.status || :modified}]"
|
|
84
|
+
left = "\u2500\u2500 #{file.path} "
|
|
85
|
+
left += "\u2500\u2500 #{hunk.context} " if hunk.context && !hunk.context.empty?
|
|
86
|
+
fill_len = [width - left.length - badge.length, 0].max
|
|
78
87
|
win.setpos(0, 0)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
win.attron(badge_color(file.status)) { win.addstr(badge) }
|
|
82
|
-
else
|
|
83
|
-
win.attron(color_for(:header)) { win.addstr(path[0, width]) }
|
|
84
|
-
end
|
|
85
|
-
win.setpos(1, 0)
|
|
86
|
-
win.attron(Curses.color_pair(0) | Curses::A_BOLD) { win.addstr("\u2500" * width) }
|
|
88
|
+
win.attron(color_for(:file_header)) { win.addstr("#{left}#{"\u2500" * fill_len}"[0, width - badge.length]) }
|
|
89
|
+
win.attron(badge_color(file.status)) { win.addstr(badge) }
|
|
87
90
|
end
|
|
88
91
|
|
|
89
92
|
def render_content(win, width)
|
|
@@ -92,11 +95,7 @@ module Sight
|
|
|
92
95
|
content_width = width - gutter - 3
|
|
93
96
|
|
|
94
97
|
selected_start = hunk_offsets[hunk_idx]
|
|
95
|
-
selected_end =
|
|
96
|
-
hunk_offsets[hunk_idx + 1]
|
|
97
|
-
else
|
|
98
|
-
lines.size
|
|
99
|
-
end
|
|
98
|
+
selected_end = hunk_end_offset(hunk_idx)
|
|
100
99
|
|
|
101
100
|
commented_lines = commented_hunk_lines
|
|
102
101
|
|
|
@@ -104,12 +103,18 @@ module Sight
|
|
|
104
103
|
idx = offset + row
|
|
105
104
|
break if idx >= lines.size
|
|
106
105
|
line = lines[idx]
|
|
107
|
-
win.setpos(row +
|
|
106
|
+
win.setpos(row + 1, 0)
|
|
107
|
+
|
|
108
|
+
if line.type == :file_separator
|
|
109
|
+
win.attron(color_for(:file_header)) { win.addstr("\u2500" * width) }
|
|
110
|
+
next
|
|
111
|
+
end
|
|
112
|
+
|
|
108
113
|
active = idx >= selected_start && idx < selected_end
|
|
109
114
|
gutter_str = format_gutter(line.type, line.lineno, gutter)
|
|
110
115
|
commented = commented_lines.include?(idx)
|
|
111
116
|
separator = commented ? "\u2503" : "\u2502"
|
|
112
|
-
sep_attr = commented ? Curses.color_pair(4) :
|
|
117
|
+
sep_attr = commented ? Curses.color_pair(4) : color_for(:file_header)
|
|
113
118
|
win.attron(dim) { win.addstr("#{gutter_str} ") }
|
|
114
119
|
win.attron(sep_attr) { win.addstr(separator) }
|
|
115
120
|
win.attron(dim) { win.addstr(" ") }
|
|
@@ -126,8 +131,8 @@ module Sight
|
|
|
126
131
|
else
|
|
127
132
|
((offset + scroll_height) * 100.0 / lines.size).ceil.clamp(0, 100)
|
|
128
133
|
end
|
|
129
|
-
commented = hunk_commented?(
|
|
130
|
-
status = "
|
|
134
|
+
commented = hunk_commented?(hunk_idx) ? " [commented]" : ""
|
|
135
|
+
status = " Hunk #{hunk_idx + 1}/#{hunk_offsets.size}#{commented} | #{percent}% "
|
|
131
136
|
win.addstr(status.ljust(width))
|
|
132
137
|
end
|
|
133
138
|
end
|
|
@@ -147,6 +152,7 @@ module Sight
|
|
|
147
152
|
when :add then Curses.color_pair(1)
|
|
148
153
|
when :del then Curses.color_pair(2)
|
|
149
154
|
when :header then Curses.color_pair(0) | Curses::A_BOLD
|
|
155
|
+
when :file_header then Curses.color_pair(3) | Curses::A_BOLD
|
|
150
156
|
else Curses.color_pair(0)
|
|
151
157
|
end
|
|
152
158
|
end
|
|
@@ -161,12 +167,12 @@ module Sight
|
|
|
161
167
|
end
|
|
162
168
|
|
|
163
169
|
def scroll_height
|
|
164
|
-
Curses.lines -
|
|
170
|
+
Curses.lines - 2
|
|
165
171
|
end
|
|
166
172
|
|
|
167
173
|
def gutter_width
|
|
168
174
|
@gutter_width ||= begin
|
|
169
|
-
max =
|
|
175
|
+
max = @lines.filter_map(&:lineno).max || 1
|
|
170
176
|
max.to_s.length
|
|
171
177
|
end
|
|
172
178
|
end
|
|
@@ -175,14 +181,14 @@ module Sight
|
|
|
175
181
|
key = Curses.getch
|
|
176
182
|
case key
|
|
177
183
|
when "q", 27 then return false
|
|
178
|
-
when "j" then
|
|
179
|
-
when "k" then
|
|
180
|
-
when "n" then jump_file(1)
|
|
181
|
-
when "p" then jump_file(-1)
|
|
184
|
+
when "j" then scroll(1)
|
|
185
|
+
when "k" then scroll(-1)
|
|
182
186
|
when 6 then scroll(scroll_height)
|
|
183
187
|
when 2 then scroll(-scroll_height)
|
|
184
188
|
when 4 then scroll(scroll_height / 2)
|
|
185
189
|
when 21 then scroll(-scroll_height / 2)
|
|
190
|
+
when "n", 14 then jump_hunk(1)
|
|
191
|
+
when "p", 16 then jump_hunk(-1)
|
|
186
192
|
when "c" then annotate_hunk
|
|
187
193
|
when "?" then show_help
|
|
188
194
|
end
|
|
@@ -190,14 +196,14 @@ module Sight
|
|
|
190
196
|
end
|
|
191
197
|
|
|
192
198
|
HELP_KEYS = [
|
|
193
|
-
["j", "
|
|
194
|
-
["k", "
|
|
199
|
+
["j", "Scroll down"],
|
|
200
|
+
["k", "Scroll up"],
|
|
195
201
|
["C-f", "Page down"],
|
|
196
202
|
["C-b", "Page up"],
|
|
197
203
|
["C-d", "Half page down"],
|
|
198
204
|
["C-u", "Half page up"],
|
|
199
|
-
["n", "Next
|
|
200
|
-
["p", "Previous
|
|
205
|
+
["n / C-n", "Next hunk"],
|
|
206
|
+
["p / C-p", "Previous hunk"],
|
|
201
207
|
["q / Esc", "Quit"],
|
|
202
208
|
["c", "Comment on hunk"],
|
|
203
209
|
["?", "Toggle this help"]
|
|
@@ -220,32 +226,32 @@ module Sight
|
|
|
220
226
|
def draw_box(win, top, left, width, height, title, content_lines)
|
|
221
227
|
win.attron(Curses.color_pair(0)) do
|
|
222
228
|
win.setpos(top, left)
|
|
223
|
-
win.addstr("
|
|
229
|
+
win.addstr("\u250C#{"\u2500" * (width - 2)}\u2510")
|
|
224
230
|
|
|
225
231
|
win.setpos(top + 1, left)
|
|
226
232
|
pad = width - 2 - title.length
|
|
227
|
-
win.addstr("
|
|
233
|
+
win.addstr("\u2502#{" " * (pad / 2)}#{title}#{" " * (pad - pad / 2)}\u2502")
|
|
228
234
|
|
|
229
235
|
win.setpos(top + 2, left)
|
|
230
|
-
win.addstr("
|
|
236
|
+
win.addstr("\u251C#{"\u2500" * (width - 2)}\u2524")
|
|
231
237
|
|
|
232
238
|
content_lines.each_with_index do |line, i|
|
|
233
239
|
win.setpos(top + 3 + i, left)
|
|
234
|
-
win.addstr("
|
|
240
|
+
win.addstr("\u2502 #{line.ljust(width - 5)} \u2502")
|
|
235
241
|
end
|
|
236
242
|
|
|
237
243
|
win.setpos(top + height - 1, left)
|
|
238
|
-
win.addstr("
|
|
244
|
+
win.addstr("\u2514#{"\u2500" * (width - 2)}\u2518")
|
|
239
245
|
end
|
|
240
246
|
end
|
|
241
247
|
|
|
242
248
|
def annotate_hunk
|
|
243
|
-
hunk =
|
|
249
|
+
file, hunk = current_hunk_entry
|
|
244
250
|
return unless hunk
|
|
245
251
|
comment = prompt_comment("Comment on hunk")
|
|
246
252
|
return unless comment
|
|
247
253
|
@annotations << Annotation.new(
|
|
248
|
-
file_path:
|
|
254
|
+
file_path: file.path,
|
|
249
255
|
type: :hunk,
|
|
250
256
|
hunk: hunk,
|
|
251
257
|
comment: comment
|
|
@@ -254,9 +260,7 @@ module Sight
|
|
|
254
260
|
|
|
255
261
|
def prompt_comment(title)
|
|
256
262
|
win = Curses.stdscr
|
|
257
|
-
|
|
258
|
-
width = [Curses.cols * 2 / 3, 50].max
|
|
259
|
-
width = [width, max_width].min
|
|
263
|
+
width = (Curses.cols * 2 / 3).clamp(50, 80)
|
|
260
264
|
height = 5
|
|
261
265
|
top = (Curses.lines - height) / 2
|
|
262
266
|
left = (Curses.cols - width) / 2
|
|
@@ -299,37 +303,38 @@ module Sight
|
|
|
299
303
|
text&.strip&.empty? ? nil : text&.strip
|
|
300
304
|
end
|
|
301
305
|
|
|
302
|
-
def
|
|
303
|
-
@
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
offsets
|
|
311
|
-
end
|
|
306
|
+
def current_hunk_entry
|
|
307
|
+
@hunk_entries[hunk_idx] || [nil, nil]
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
attr_reader :hunk_offsets
|
|
311
|
+
|
|
312
|
+
def hunk_end_offset(idx)
|
|
313
|
+
(idx + 1 < hunk_offsets.size) ? hunk_offsets[idx + 1] : lines.size
|
|
312
314
|
end
|
|
313
315
|
|
|
314
|
-
def hunk_commented?(
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
annotations.any? { |a| a.file_path == path && a.hunk.equal?(hunk) }
|
|
316
|
+
def hunk_commented?(hunk_index)
|
|
317
|
+
file, hunk = @hunk_entries[hunk_index]
|
|
318
|
+
return false unless file
|
|
319
|
+
annotations.any? { |a| a.file_path == file.path && a.hunk.equal?(hunk) }
|
|
318
320
|
end
|
|
319
321
|
|
|
320
322
|
def commented_hunk_lines
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
hunk_end = (hunk_index + 1 < hunk_offsets.size) ? hunk_offsets[hunk_index + 1] : lines.size
|
|
325
|
-
(offset...hunk_end).each { |i| result << i }
|
|
323
|
+
hunk_offsets.each_with_index.each_with_object(Set.new) do |(offset, hunk_index), set|
|
|
324
|
+
next unless hunk_commented?(hunk_index)
|
|
325
|
+
(offset...hunk_end_offset(hunk_index)).each { |i| set << i }
|
|
326
326
|
end
|
|
327
|
-
result
|
|
328
327
|
end
|
|
329
328
|
|
|
330
329
|
def scroll(delta)
|
|
331
330
|
max = [0, lines.size - scroll_height].max
|
|
332
331
|
self.offset = (offset + delta).clamp(0, max)
|
|
332
|
+
sync_hunk_to_offset
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def sync_hunk_to_offset
|
|
336
|
+
return if hunk_offsets.empty?
|
|
337
|
+
self.hunk_idx = hunk_offsets.rindex { |o| o <= offset } || 0
|
|
333
338
|
end
|
|
334
339
|
|
|
335
340
|
def jump_hunk(delta)
|
|
@@ -340,13 +345,5 @@ module Sight
|
|
|
340
345
|
max = [0, lines.size - scroll_height].max
|
|
341
346
|
self.offset = [target - margin, 0].max.clamp(0, max)
|
|
342
347
|
end
|
|
343
|
-
|
|
344
|
-
def jump_file(direction)
|
|
345
|
-
new_idx = (file_idx + direction).clamp(0, files.size - 1)
|
|
346
|
-
return if new_idx == file_idx
|
|
347
|
-
self.file_idx = new_idx
|
|
348
|
-
self.offset = 0
|
|
349
|
-
self.hunk_idx = 0
|
|
350
|
-
end
|
|
351
348
|
end
|
|
352
349
|
end
|
data/lib/sight/cli.rb
CHANGED
|
@@ -1,49 +1,33 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "json"
|
|
4
|
-
|
|
5
3
|
module Sight
|
|
6
4
|
module CLI
|
|
7
5
|
module_function
|
|
8
6
|
|
|
9
7
|
def run(argv)
|
|
10
|
-
if argv.include?("--help") || argv.include?("-h")
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
puts " uninstall-hook <agent> Remove hook (claude, cursor)"
|
|
19
|
-
return
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
if argv.include?("--version") || argv.include?("-v")
|
|
23
|
-
puts "sight #{VERSION}"
|
|
24
|
-
return
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
if argv[0] == "install-hook"
|
|
28
|
-
install_hook(argv[1])
|
|
29
|
-
return
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
if argv[0] == "uninstall-hook"
|
|
33
|
-
uninstall_hook(argv[1])
|
|
34
|
-
return
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
if argv.include?("hook-run")
|
|
38
|
-
run_hook
|
|
39
|
-
return
|
|
8
|
+
return print_help if argv.include?("--help") || argv.include?("-h")
|
|
9
|
+
return puts("sight #{VERSION}") if argv.include?("--version") || argv.include?("-v")
|
|
10
|
+
|
|
11
|
+
case argv[0]
|
|
12
|
+
when "install-hook" then install_hook(argv[1])
|
|
13
|
+
when "uninstall-hook" then uninstall_hook(argv[1])
|
|
14
|
+
when "hook-run" then run_hook
|
|
15
|
+
else open
|
|
40
16
|
end
|
|
17
|
+
end
|
|
41
18
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
19
|
+
def print_help
|
|
20
|
+
puts "Usage: sight"
|
|
21
|
+
puts "Interactive git diff viewer (staged + unstaged)"
|
|
22
|
+
puts
|
|
23
|
+
puts "Keys: j/k hunks, n/p files, c comment, ? help, q quit"
|
|
24
|
+
puts
|
|
25
|
+
puts "Subcommands:"
|
|
26
|
+
puts " install-hook <agent> Install hook (claude)"
|
|
27
|
+
puts " uninstall-hook <agent> Remove hook (claude)"
|
|
28
|
+
end
|
|
46
29
|
|
|
30
|
+
def open
|
|
47
31
|
Git.clear_pending_review
|
|
48
32
|
|
|
49
33
|
raw = Git.diff
|
|
@@ -64,33 +48,25 @@ module Sight
|
|
|
64
48
|
app.run
|
|
65
49
|
|
|
66
50
|
unless app.annotations.empty?
|
|
67
|
-
|
|
68
|
-
puts
|
|
69
|
-
Git.save_pending_review(formatted)
|
|
51
|
+
Git.save_pending_review(AnnotationFormatter.new(app.annotations).format)
|
|
52
|
+
puts Summary.new(app.annotations)
|
|
70
53
|
end
|
|
71
54
|
end
|
|
72
55
|
|
|
73
56
|
AGENTS = {
|
|
74
|
-
"claude" => ClaudeHookInstaller
|
|
75
|
-
"cursor" => CursorHookInstaller
|
|
57
|
+
"claude" => ClaudeHookInstaller
|
|
76
58
|
}.freeze
|
|
77
59
|
|
|
78
60
|
def install_hook(agent)
|
|
79
|
-
|
|
80
|
-
unless installer
|
|
81
|
-
warn "Unknown agent: #{agent.inspect}. Use: claude, cursor"
|
|
82
|
-
return
|
|
83
|
-
end
|
|
84
|
-
installer.install
|
|
61
|
+
resolve_installer(agent)&.install
|
|
85
62
|
end
|
|
86
63
|
|
|
87
64
|
def uninstall_hook(agent)
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
installer.uninstall
|
|
65
|
+
resolve_installer(agent)&.uninstall
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def resolve_installer(agent)
|
|
69
|
+
AGENTS.fetch(agent) { warn "Unknown agent: #{agent.inspect}. Use: claude" }
|
|
94
70
|
end
|
|
95
71
|
|
|
96
72
|
def run_hook
|
|
@@ -107,20 +83,5 @@ module Sight
|
|
|
107
83
|
puts content
|
|
108
84
|
File.delete(file)
|
|
109
85
|
end
|
|
110
|
-
|
|
111
|
-
def run_cursor_hook
|
|
112
|
-
$stdin.read
|
|
113
|
-
git_dir = Git.repo_dir
|
|
114
|
-
rescue Error
|
|
115
|
-
nil
|
|
116
|
-
else
|
|
117
|
-
file = File.join(git_dir, "sight", "pending-review")
|
|
118
|
-
return unless File.exist?(file)
|
|
119
|
-
|
|
120
|
-
content = File.read(file)
|
|
121
|
-
message = "The user has just finished reviewing your code changes in sight. Here are their annotations:\n\n#{content}"
|
|
122
|
-
puts JSON.generate(user_message: message)
|
|
123
|
-
File.delete(file)
|
|
124
|
-
end
|
|
125
86
|
end
|
|
126
87
|
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sight
|
|
4
|
+
class Summary
|
|
5
|
+
def initialize(annotations)
|
|
6
|
+
@annotations = annotations
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def to_s
|
|
10
|
+
"#{pluralize_annotations} on #{pluralize_files}"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
attr_reader :annotations
|
|
16
|
+
|
|
17
|
+
def pluralize_annotations
|
|
18
|
+
"#{annotations.size} #{(annotations.size == 1) ? "annotation" : "annotations"}"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def pluralize_files
|
|
22
|
+
"#{file_count} #{(file_count == 1) ? "file" : "files"}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def file_count
|
|
26
|
+
annotations.map(&:file_path).uniq.size
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
data/lib/sight/version.rb
CHANGED
data/lib/sight.rb
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "sight/version"
|
|
4
|
-
require_relative "sight/display_line"
|
|
5
|
-
require_relative "sight/diff_parser"
|
|
6
|
-
require_relative "sight/git"
|
|
7
3
|
require_relative "sight/annotation"
|
|
8
4
|
require_relative "sight/annotation_formatter"
|
|
5
|
+
require_relative "sight/app"
|
|
9
6
|
require_relative "sight/claude_hook_installer"
|
|
10
|
-
require_relative "sight/cursor_hook_installer"
|
|
11
7
|
require_relative "sight/cli"
|
|
12
|
-
require_relative "sight/
|
|
8
|
+
require_relative "sight/diff_parser"
|
|
9
|
+
require_relative "sight/display_line"
|
|
10
|
+
require_relative "sight/git"
|
|
11
|
+
require_relative "sight/summary"
|
|
12
|
+
require_relative "sight/version"
|
|
13
13
|
|
|
14
14
|
module Sight
|
|
15
15
|
class Error < StandardError; end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sight
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ariel Rzezak
|
|
@@ -37,6 +37,7 @@ files:
|
|
|
37
37
|
- CODE_OF_CONDUCT.md
|
|
38
38
|
- LICENSE.txt
|
|
39
39
|
- README.md
|
|
40
|
+
- RELEASING.md
|
|
40
41
|
- Rakefile
|
|
41
42
|
- exe/sight
|
|
42
43
|
- lib/sight.rb
|
|
@@ -45,10 +46,10 @@ files:
|
|
|
45
46
|
- lib/sight/app.rb
|
|
46
47
|
- lib/sight/claude_hook_installer.rb
|
|
47
48
|
- lib/sight/cli.rb
|
|
48
|
-
- lib/sight/cursor_hook_installer.rb
|
|
49
49
|
- lib/sight/diff_parser.rb
|
|
50
50
|
- lib/sight/display_line.rb
|
|
51
51
|
- lib/sight/git.rb
|
|
52
|
+
- lib/sight/summary.rb
|
|
52
53
|
- lib/sight/version.rb
|
|
53
54
|
- sig/sight.rbs
|
|
54
55
|
homepage: https://github.com/arzezak/sight
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "json"
|
|
4
|
-
require "fileutils"
|
|
5
|
-
|
|
6
|
-
module Sight
|
|
7
|
-
module CursorHookInstaller
|
|
8
|
-
HOOK_COMMAND = "sight cursor-hook-run"
|
|
9
|
-
|
|
10
|
-
module_function
|
|
11
|
-
|
|
12
|
-
def hooks_path
|
|
13
|
-
File.join(Dir.home, ".cursor", "hooks.json")
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def install(path: hooks_path)
|
|
17
|
-
config = if File.exist?(path)
|
|
18
|
-
JSON.parse(File.read(path))
|
|
19
|
-
else
|
|
20
|
-
{"version" => 1, "hooks" => {}}
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
config["version"] ||= 1
|
|
24
|
-
hooks = config["hooks"] ||= {}
|
|
25
|
-
prompt_hooks = hooks["beforeSubmitPrompt"] ||= []
|
|
26
|
-
|
|
27
|
-
if prompt_hooks.any? { |h| hook_is_sight?(h) }
|
|
28
|
-
puts "sight hook already installed"
|
|
29
|
-
return
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
prompt_hooks << {"command" => HOOK_COMMAND}
|
|
33
|
-
|
|
34
|
-
FileUtils.mkdir_p(File.dirname(path))
|
|
35
|
-
File.write(path, JSON.pretty_generate(config) + "\n")
|
|
36
|
-
puts "Installed sight hook into #{path}"
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def uninstall(path: hooks_path)
|
|
40
|
-
unless File.exist?(path)
|
|
41
|
-
puts "No hooks file found"
|
|
42
|
-
return
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
config = JSON.parse(File.read(path))
|
|
46
|
-
prompt_hooks = config.dig("hooks", "beforeSubmitPrompt")
|
|
47
|
-
|
|
48
|
-
unless prompt_hooks&.any? { |h| hook_is_sight?(h) }
|
|
49
|
-
puts "No sight hook found"
|
|
50
|
-
return
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
prompt_hooks.reject! { |h| hook_is_sight?(h) }
|
|
54
|
-
File.write(path, JSON.pretty_generate(config) + "\n")
|
|
55
|
-
puts "Uninstalled sight hook"
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def hook_is_sight?(entry)
|
|
59
|
-
entry["command"]&.include?("sight")
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
end
|