sight 0.4.0 → 0.5.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: 32b20a4a62fbca9fee67a4354538a8e033dc079b945c7aa5a6f6d23f63c0fac6
4
+ data.tar.gz: 4be26d309ed3cb0649bc093a78326d6f9a4a2ce2f2f3f3ec9943b713bac9f372
5
5
  SHA512:
6
- metadata.gz: c591ba467ae6d6a64d1d54c188513a2afe743c40e767bca6892f9f30e6ff45ecfb1f134fcef62c420b4e7fb8c13a77fcd9c69e8f830c6cf1a7c79f8d4d61b82f
7
- data.tar.gz: 131fd548a595de6b14288b2ae536b2275eb66fb50f6e08182875866bdee44c0acfbb8194a0ee515e510472bb72f38eeae34fa7c64f64e0a97b2e4e4433867775
6
+ metadata.gz: 7acf8bc51543269963f35953c5e7ffe7452483e655330bd5d9c314c394a575f6710233fe26b117eaa7ca00491c0d4f561c879e2c7ab160fcb32a6d3db15bf7e4
7
+ data.tar.gz: 696a3493aa44d9e7913913df11f9ef583de057e15bc210811027121a629534d486c7cb7c197180c146644aadba2eea331db8d49315c83da9197d2a4116f472ca
data/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.5.0] - 2026-03-07
4
+
5
+ ### Removed
6
+
7
+ - Cursor hook support (use Cursor rules instead)
8
+
9
+ ### Changed
10
+
11
+ - Refactored CLI dispatch to use case statement with extracted `open` and `print_help` methods
12
+ - DRYed up install/uninstall hook agent lookup
13
+ - Use `Set` for commented hunk lines lookup
14
+ - Extracted `hunk_end_offset` to deduplicate boundary logic
15
+ - Use `<<` for string building in `AnnotationFormatter`
16
+ - Simplified prompt comment width clamping
17
+
3
18
  ## [0.4.0] - 2026-03-06
4
19
 
5
20
  ### 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,10 +24,9 @@ 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
 
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
@@ -8,21 +8,21 @@ module Sight
8
8
  return "" if annotations.empty?
9
9
 
10
10
  grouped = annotations.group_by(&:file_path)
11
- grouped.map { |path, anns| format_file(path, anns) }.join("\n")
11
+ grouped.map { |path, file_annotations| format_file(path, file_annotations) }.join("\n")
12
12
  end
13
13
 
14
14
  def format_file(path, annotations)
15
15
  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|
16
+ annotations.each do |annotation|
17
+ context = annotation.hunk.context ? " #{annotation.hunk.context}" : ""
18
+ out << "Hunk (@@#{context}):\n"
19
+ out << "```diff\n"
20
+ annotation.hunk.lines.each do |line|
21
21
  next unless %i[add del].include?(line.type)
22
- out += "#{line.content}\n"
22
+ out << "#{line.content}\n"
23
23
  end
24
- out += "```\n"
25
- out += "> #{ann.comment}\n\n"
24
+ out << "```\n"
25
+ out << "> #{annotation.comment}\n\n"
26
26
  end
27
27
  out
28
28
  end
data/lib/sight/app.rb CHANGED
@@ -92,11 +92,7 @@ module Sight
92
92
  content_width = width - gutter - 3
93
93
 
94
94
  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
95
+ selected_end = hunk_end_offset(hunk_idx)
100
96
 
101
97
  commented_lines = commented_hunk_lines
102
98
 
@@ -254,9 +250,7 @@ module Sight
254
250
 
255
251
  def prompt_comment(title)
256
252
  win = Curses.stdscr
257
- max_width = 80
258
- width = [Curses.cols * 2 / 3, 50].max
259
- width = [width, max_width].min
253
+ width = (Curses.cols * 2 / 3).clamp(50, 80)
260
254
  height = 5
261
255
  top = (Curses.lines - height) / 2
262
256
  left = (Curses.cols - width) / 2
@@ -311,6 +305,10 @@ module Sight
311
305
  end
312
306
  end
313
307
 
308
+ def hunk_end_offset(idx)
309
+ (idx + 1 < hunk_offsets.size) ? hunk_offsets[idx + 1] : lines.size
310
+ end
311
+
314
312
  def hunk_commented?(file_index, hunk_index)
315
313
  path = files[file_index].path
316
314
  hunk = files[file_index].hunks[hunk_index]
@@ -318,13 +316,10 @@ module Sight
318
316
  end
319
317
 
320
318
  def commented_hunk_lines
321
- result = []
322
- hunk_offsets.each_with_index do |offset, hunk_index|
319
+ hunk_offsets.each_with_index.each_with_object(Set.new) do |(offset, hunk_index), set|
323
320
  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 }
321
+ (offset...hunk_end_offset(hunk_index)).each { |i| set << i }
326
322
  end
327
- result
328
323
  end
329
324
 
330
325
  def scroll(delta)
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
@@ -71,26 +55,19 @@ module Sight
71
55
  end
72
56
 
73
57
  AGENTS = {
74
- "claude" => ClaudeHookInstaller,
75
- "cursor" => CursorHookInstaller
58
+ "claude" => ClaudeHookInstaller
76
59
  }.freeze
77
60
 
78
61
  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
62
+ resolve_installer(agent)&.install
85
63
  end
86
64
 
87
65
  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
66
+ resolve_installer(agent)&.uninstall
67
+ end
68
+
69
+ def resolve_installer(agent)
70
+ AGENTS.fetch(agent) { warn "Unknown agent: #{agent.inspect}. Use: claude" }
94
71
  end
95
72
 
96
73
  def run_hook
@@ -107,20 +84,5 @@ module Sight
107
84
  puts content
108
85
  File.delete(file)
109
86
  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
87
  end
126
88
  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.5.0"
5
5
  end
data/lib/sight.rb CHANGED
@@ -7,7 +7,6 @@ require_relative "sight/git"
7
7
  require_relative "sight/annotation"
8
8
  require_relative "sight/annotation_formatter"
9
9
  require_relative "sight/claude_hook_installer"
10
- require_relative "sight/cursor_hook_installer"
11
10
  require_relative "sight/cli"
12
11
  require_relative "sight/app"
13
12
 
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.5.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,7 +46,6 @@ 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
@@ -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