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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ff89f4bccb463b21835b299946bcf7a54b43f9792eb0b262954812c79633da19
4
- data.tar.gz: 7dab4e4911a9bf145a5b0542ac993c6b5d3ce25af54b345e537291c5567db681
3
+ metadata.gz: 461c502e3de33a457cdd4a084d414fe8b978f7761c09fe795eb3047b0bc6a7e0
4
+ data.tar.gz: f116d0821923e83ec1ff9d392da0b9cb88350f3803cf314f7c863c88b1e24c91
5
5
  SHA512:
6
- metadata.gz: c591ba467ae6d6a64d1d54c188513a2afe743c40e767bca6892f9f30e6ff45ecfb1f134fcef62c420b4e7fb8c13a77fcd9c69e8f830c6cf1a7c79f8d4d61b82f
7
- data.tar.gz: 131fd548a595de6b14288b2ae536b2275eb66fb50f6e08182875866bdee44c0acfbb8194a0ee515e510472bb72f38eeae34fa7c64f64e0a97b2e4e4433867775
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**: Unified via `sight install-hook <agent>` / `sight uninstall-hook <agent>` (agent: `claude` or `cursor`).
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
- `CursorHookInstaller` manages a Cursor `beforeSubmitPrompt` hook in `~/.cursor/hooks.json`.
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 # Claude Code (~/.config/claude/settings.json)
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
- sight uninstall-hook cursor
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
- module AnnotationFormatter
5
- module_function
4
+ class AnnotationFormatter
5
+ def initialize(annotations)
6
+ @annotations = annotations
7
+ end
6
8
 
7
- def format(annotations)
9
+ def format
8
10
  return "" if annotations.empty?
9
11
 
10
- grouped = annotations.group_by(&:file_path)
11
- grouped.map { |path, anns| format_file(path, anns) }.join("\n")
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
- def format_file(path, annotations)
17
+ private
18
+
19
+ attr_reader :annotations
20
+
21
+ def format_file(path, file_annotations)
15
22
  out = "## File: #{path}\n\n"
16
- annotations.each do |ann|
17
- context = ann.hunk.context ? " #{ann.hunk.context}" : ""
18
- out += "Hunk (@@#{context}):\n"
19
- out += "```diff\n"
20
- ann.hunk.lines.each do |line|
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 += "#{line.content}\n"
30
+ out << "#{line.content}\n"
23
31
  end
24
- out += "```\n"
25
- out += "> #{ann.comment}\n\n"
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, :file_lines, :annotations
8
- attr_accessor :file_idx, :offset, :hunk_idx
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 build_file_lines(file)
35
- file.hunks.flat_map do |hunk|
36
- hunk.lines.map do |diff_line|
37
- text = (diff_line.type == :meta) ? diff_line.content : diff_line.content[1..]
38
- DisplayLine.new(type: diff_line.type, text: text, lineno: diff_line.lineno)
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 = files[file_idx]
75
- badge = "[#{file.status || :modified}]"
76
- path = file.path
77
- gap = width - path.length - badge.length
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
- if gap >= 1
80
- win.attron(color_for(:header)) { win.addstr("#{path}#{" " * gap}") }
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 = if hunk_idx + 1 < hunk_offsets.size
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 + 2, 0)
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) : dim
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?(file_idx, hunk_idx) ? " [commented]" : ""
130
- status = " File #{file_idx + 1}/#{files.size} | Hunk #{hunk_idx + 1}/#{hunk_offsets.size}#{commented} | #{percent}% "
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 - 3
170
+ Curses.lines - 2
165
171
  end
166
172
 
167
173
  def gutter_width
168
174
  @gutter_width ||= begin
169
- max = file_lines.flat_map { |file| file.filter_map { |line| line.lineno } }.max || 1
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 jump_hunk(1)
179
- when "k" then jump_hunk(-1)
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", "Next hunk"],
194
- ["k", "Previous hunk"],
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 file"],
200
- ["p", "Previous file"],
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("┌#{"" * (width - 2)}")
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("│#{" " * (pad / 2)}#{title}#{" " * (pad - pad / 2)}")
233
+ win.addstr("\u2502#{" " * (pad / 2)}#{title}#{" " * (pad - pad / 2)}\u2502")
228
234
 
229
235
  win.setpos(top + 2, left)
230
- win.addstr("├#{"" * (width - 2)}")
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(" #{line.ljust(width - 5)} ")
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("└#{"" * (width - 2)}")
244
+ win.addstr("\u2514#{"\u2500" * (width - 2)}\u2518")
239
245
  end
240
246
  end
241
247
 
242
248
  def annotate_hunk
243
- hunk = files[file_idx].hunks[hunk_idx]
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: files[file_idx].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
- max_width = 80
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 hunk_offsets
303
- @hunk_offsets_cache[file_idx] ||= begin
304
- offsets = []
305
- line_idx = 0
306
- files[file_idx].hunks.each do |hunk|
307
- offsets << line_idx
308
- line_idx += hunk.lines.size
309
- end
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?(file_index, hunk_index)
315
- path = files[file_index].path
316
- hunk = files[file_index].hunks[hunk_index]
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
- result = []
322
- hunk_offsets.each_with_index do |offset, hunk_index|
323
- next unless hunk_commented?(file_idx, hunk_index)
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
- puts "Usage: sight"
12
- puts "Interactive git diff viewer (staged + unstaged)"
13
- puts
14
- puts "Keys: j/k hunks, n/p files, c comment, ? help, q quit"
15
- puts
16
- puts "Subcommands:"
17
- puts " install-hook <agent> Install hook (claude, cursor)"
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
- if argv.include?("cursor-hook-run")
43
- run_cursor_hook
44
- return
45
- end
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
- formatted = AnnotationFormatter.format(app.annotations)
68
- puts formatted
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
- installer = AGENTS[agent]
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
- installer = AGENTS[agent]
89
- unless installer
90
- warn "Unknown agent: #{agent.inspect}. Use: claude, cursor"
91
- return
92
- end
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sight
4
- VERSION = "0.4.0"
4
+ VERSION = "0.6.0"
5
5
  end
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/app"
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.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