sight 0.3.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 +4 -4
- data/CHANGELOG.md +28 -0
- data/CLAUDE.md +10 -3
- data/README.md +33 -0
- data/RELEASING.md +7 -0
- data/lib/sight/annotation_formatter.rb +9 -9
- data/lib/sight/app.rb +70 -14
- data/lib/sight/claude_hook_installer.rb +64 -0
- data/lib/sight/cli.rb +66 -16
- data/lib/sight/diff_parser.rb +14 -2
- data/lib/sight/git.rb +29 -0
- data/lib/sight/version.rb +1 -1
- data/lib/sight.rb +1 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 32b20a4a62fbca9fee67a4354538a8e033dc079b945c7aa5a6f6d23f63c0fac6
|
|
4
|
+
data.tar.gz: 4be26d309ed3cb0649bc093a78326d6f9a4a2ce2f2f3f3ec9943b713bac9f372
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7acf8bc51543269963f35953c5e7ffe7452483e655330bd5d9c314c394a575f6710233fe26b117eaa7ca00491c0d4f561c879e2c7ab160fcb32a6d3db15bf7e4
|
|
7
|
+
data.tar.gz: 696a3493aa44d9e7913913df11f9ef583de057e15bc210811027121a629534d486c7cb7c197180c146644aadba2eea331db8d49315c83da9197d2a4116f472ca
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
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
|
+
|
|
18
|
+
## [0.4.0] - 2026-03-06
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- Untracked file support with file status badges (new, modified, deleted)
|
|
23
|
+
- Color-coded status badges in file header
|
|
24
|
+
- Ctrl-f/b/d/u scroll navigation
|
|
25
|
+
- Scroll percentage indicator replacing line counter
|
|
26
|
+
- Hook system for AI agent integration (`sight install-hook <agent>`)
|
|
27
|
+
- Claude Code hook via `UserPromptSubmit`
|
|
28
|
+
- Cursor hook via `beforeSubmitPrompt`
|
|
29
|
+
- Highlighted commented hunks in gutter and status bar
|
|
30
|
+
|
|
3
31
|
## [0.3.0] - 2026-03-04
|
|
4
32
|
|
|
5
33
|
### Added
|
data/CLAUDE.md
CHANGED
|
@@ -13,14 +13,21 @@ 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
|
-
**Data flow**: `Git.diff` (raw string) → `DiffParser.parse` (returns `DiffFile[]`) → `App` (curses TUI)
|
|
18
|
+
**Data flow**: `Git.diff` (raw string) → `DiffParser.parse` (returns `DiffFile[]`) → `App` (curses TUI).
|
|
19
|
+
Untracked files are added via `Git.untracked_files` → `DiffParser.build_untracked`.
|
|
19
20
|
|
|
20
|
-
**Key structs** (all in `diff_parser.rb`): `DiffFile(path, hunks)`, `Hunk(context, lines)`, `DiffLine(type, content, lineno, old_lineno)`.
|
|
21
|
+
**Key structs** (all in `diff_parser.rb`): `DiffFile(path, hunks, status)`, `Hunk(context, lines)`, `DiffLine(type, content, lineno, old_lineno)`.
|
|
22
|
+
`DisplayLine(type, text, lineno)` is the render-side equivalent in `display_line.rb`.
|
|
23
|
+
`Annotation(file_path, type, hunk, comment)` in `annotation.rb` stores per-hunk comments; `AnnotationFormatter` serializes them for output.
|
|
21
24
|
|
|
22
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).
|
|
23
26
|
|
|
27
|
+
**Hook system**: `sight install-hook claude` / `sight uninstall-hook claude`.
|
|
28
|
+
`ClaudeHookInstaller` manages a Claude Code `UserPromptSubmit` hook in `~/.config/claude/settings.json`.
|
|
29
|
+
Reads `.git/sight/pending-review`, outputs annotations, and deletes the file. Hidden subcommand: `hook-run`.
|
|
30
|
+
|
|
24
31
|
## Conventions
|
|
25
32
|
|
|
26
33
|
- Ruby >= 3.2, uses `frozen_string_literal` in all files
|
data/README.md
CHANGED
|
@@ -24,10 +24,43 @@ sight
|
|
|
24
24
|
| `k` | Previous hunk |
|
|
25
25
|
| `n` | Next file |
|
|
26
26
|
| `p` | Previous file |
|
|
27
|
+
| `Ctrl-F` / `Ctrl-B` | Scroll full page down / up |
|
|
28
|
+
| `Ctrl-D` / `Ctrl-U` | Scroll half page down / up |
|
|
27
29
|
| `?` | Toggle help |
|
|
28
30
|
| `c` | Comment on hunk |
|
|
29
31
|
| `q` / `Esc` | Quit |
|
|
30
32
|
|
|
33
|
+
### Agent Integration
|
|
34
|
+
|
|
35
|
+
Install a hook so annotations are automatically fed as context in your next message:
|
|
36
|
+
|
|
37
|
+
**Claude Code** (~/.config/claude/settings.json):
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
sight install-hook claude
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
When you quit sight after annotating, the next message you send will include your annotations.
|
|
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
|
+
|
|
47
|
+
To remove:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
sight uninstall-hook claude
|
|
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.
|
|
62
|
+
```
|
|
63
|
+
|
|
31
64
|
## Development
|
|
32
65
|
|
|
33
66
|
```bash
|
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,
|
|
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 |
|
|
17
|
-
context =
|
|
18
|
-
out
|
|
19
|
-
out
|
|
20
|
-
|
|
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
|
|
22
|
+
out << "#{line.content}\n"
|
|
23
23
|
end
|
|
24
|
-
out
|
|
25
|
-
out
|
|
24
|
+
out << "```\n"
|
|
25
|
+
out << "> #{annotation.comment}\n\n"
|
|
26
26
|
end
|
|
27
27
|
out
|
|
28
28
|
end
|
data/lib/sight/app.rb
CHANGED
|
@@ -53,6 +53,7 @@ module Sight
|
|
|
53
53
|
Curses.init_pair(3, Curses::COLOR_CYAN, -1)
|
|
54
54
|
Curses.init_pair(4, Curses::COLOR_YELLOW, -1)
|
|
55
55
|
Curses.init_pair(5, 240, -1)
|
|
56
|
+
Curses.init_pair(6, Curses::COLOR_MAGENTA, -1)
|
|
56
57
|
end
|
|
57
58
|
|
|
58
59
|
def lines
|
|
@@ -70,11 +71,19 @@ module Sight
|
|
|
70
71
|
end
|
|
71
72
|
|
|
72
73
|
def render_header(win, width)
|
|
73
|
-
|
|
74
|
+
file = files[file_idx]
|
|
75
|
+
badge = "[#{file.status || :modified}]"
|
|
76
|
+
path = file.path
|
|
77
|
+
gap = width - path.length - badge.length
|
|
74
78
|
win.setpos(0, 0)
|
|
75
|
-
|
|
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
|
|
76
85
|
win.setpos(1, 0)
|
|
77
|
-
win.attron(
|
|
86
|
+
win.attron(Curses.color_pair(0) | Curses::A_BOLD) { win.addstr("\u2500" * width) }
|
|
78
87
|
end
|
|
79
88
|
|
|
80
89
|
def render_content(win, width)
|
|
@@ -83,11 +92,9 @@ module Sight
|
|
|
83
92
|
content_width = width - gutter - 3
|
|
84
93
|
|
|
85
94
|
selected_start = hunk_offsets[hunk_idx]
|
|
86
|
-
selected_end =
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
lines.size
|
|
90
|
-
end
|
|
95
|
+
selected_end = hunk_end_offset(hunk_idx)
|
|
96
|
+
|
|
97
|
+
commented_lines = commented_hunk_lines
|
|
91
98
|
|
|
92
99
|
scroll_height.times do |row|
|
|
93
100
|
idx = offset + row
|
|
@@ -95,7 +102,13 @@ module Sight
|
|
|
95
102
|
line = lines[idx]
|
|
96
103
|
win.setpos(row + 2, 0)
|
|
97
104
|
active = idx >= selected_start && idx < selected_end
|
|
98
|
-
|
|
105
|
+
gutter_str = format_gutter(line.type, line.lineno, gutter)
|
|
106
|
+
commented = commented_lines.include?(idx)
|
|
107
|
+
separator = commented ? "\u2503" : "\u2502"
|
|
108
|
+
sep_attr = commented ? Curses.color_pair(4) : dim
|
|
109
|
+
win.attron(dim) { win.addstr("#{gutter_str} ") }
|
|
110
|
+
win.attron(sep_attr) { win.addstr(separator) }
|
|
111
|
+
win.attron(dim) { win.addstr(" ") }
|
|
99
112
|
attr = active ? color_for(line.type) : Curses.color_pair(5)
|
|
100
113
|
win.attron(attr) { win.addstr(line.text[0, content_width]) }
|
|
101
114
|
end
|
|
@@ -104,7 +117,13 @@ module Sight
|
|
|
104
117
|
def render_status_bar(win, width)
|
|
105
118
|
win.setpos(Curses.lines - 1, 0)
|
|
106
119
|
win.attron(Curses.color_pair(4) | Curses::A_REVERSE) do
|
|
107
|
-
|
|
120
|
+
percent = if lines.size <= scroll_height
|
|
121
|
+
100
|
|
122
|
+
else
|
|
123
|
+
((offset + scroll_height) * 100.0 / lines.size).ceil.clamp(0, 100)
|
|
124
|
+
end
|
|
125
|
+
commented = hunk_commented?(file_idx, hunk_idx) ? " [commented]" : ""
|
|
126
|
+
status = " File #{file_idx + 1}/#{files.size} | Hunk #{hunk_idx + 1}/#{hunk_offsets.size}#{commented} | #{percent}% "
|
|
108
127
|
win.addstr(status.ljust(width))
|
|
109
128
|
end
|
|
110
129
|
end
|
|
@@ -123,11 +142,20 @@ module Sight
|
|
|
123
142
|
case type
|
|
124
143
|
when :add then Curses.color_pair(1)
|
|
125
144
|
when :del then Curses.color_pair(2)
|
|
126
|
-
when :header then Curses.color_pair(
|
|
145
|
+
when :header then Curses.color_pair(0) | Curses::A_BOLD
|
|
127
146
|
else Curses.color_pair(0)
|
|
128
147
|
end
|
|
129
148
|
end
|
|
130
149
|
|
|
150
|
+
def badge_color(status)
|
|
151
|
+
case status
|
|
152
|
+
when :added then Curses.color_pair(1) | Curses::A_BOLD
|
|
153
|
+
when :deleted then Curses.color_pair(2) | Curses::A_BOLD
|
|
154
|
+
when :untracked then Curses.color_pair(6) | Curses::A_BOLD
|
|
155
|
+
else Curses.color_pair(4) | Curses::A_BOLD
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
131
159
|
def scroll_height
|
|
132
160
|
Curses.lines - 3
|
|
133
161
|
end
|
|
@@ -147,6 +175,10 @@ module Sight
|
|
|
147
175
|
when "k" then jump_hunk(-1)
|
|
148
176
|
when "n" then jump_file(1)
|
|
149
177
|
when "p" then jump_file(-1)
|
|
178
|
+
when 6 then scroll(scroll_height)
|
|
179
|
+
when 2 then scroll(-scroll_height)
|
|
180
|
+
when 4 then scroll(scroll_height / 2)
|
|
181
|
+
when 21 then scroll(-scroll_height / 2)
|
|
150
182
|
when "c" then annotate_hunk
|
|
151
183
|
when "?" then show_help
|
|
152
184
|
end
|
|
@@ -156,6 +188,10 @@ module Sight
|
|
|
156
188
|
HELP_KEYS = [
|
|
157
189
|
["j", "Next hunk"],
|
|
158
190
|
["k", "Previous hunk"],
|
|
191
|
+
["C-f", "Page down"],
|
|
192
|
+
["C-b", "Page up"],
|
|
193
|
+
["C-d", "Half page down"],
|
|
194
|
+
["C-u", "Half page up"],
|
|
159
195
|
["n", "Next file"],
|
|
160
196
|
["p", "Previous file"],
|
|
161
197
|
["q / Esc", "Quit"],
|
|
@@ -214,9 +250,7 @@ module Sight
|
|
|
214
250
|
|
|
215
251
|
def prompt_comment(title)
|
|
216
252
|
win = Curses.stdscr
|
|
217
|
-
|
|
218
|
-
width = [Curses.cols * 2 / 3, 50].max
|
|
219
|
-
width = [width, max_width].min
|
|
253
|
+
width = (Curses.cols * 2 / 3).clamp(50, 80)
|
|
220
254
|
height = 5
|
|
221
255
|
top = (Curses.lines - height) / 2
|
|
222
256
|
left = (Curses.cols - width) / 2
|
|
@@ -271,6 +305,28 @@ module Sight
|
|
|
271
305
|
end
|
|
272
306
|
end
|
|
273
307
|
|
|
308
|
+
def hunk_end_offset(idx)
|
|
309
|
+
(idx + 1 < hunk_offsets.size) ? hunk_offsets[idx + 1] : lines.size
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def hunk_commented?(file_index, hunk_index)
|
|
313
|
+
path = files[file_index].path
|
|
314
|
+
hunk = files[file_index].hunks[hunk_index]
|
|
315
|
+
annotations.any? { |a| a.file_path == path && a.hunk.equal?(hunk) }
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def commented_hunk_lines
|
|
319
|
+
hunk_offsets.each_with_index.each_with_object(Set.new) do |(offset, hunk_index), set|
|
|
320
|
+
next unless hunk_commented?(file_idx, hunk_index)
|
|
321
|
+
(offset...hunk_end_offset(hunk_index)).each { |i| set << i }
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def scroll(delta)
|
|
326
|
+
max = [0, lines.size - scroll_height].max
|
|
327
|
+
self.offset = (offset + delta).clamp(0, max)
|
|
328
|
+
end
|
|
329
|
+
|
|
274
330
|
def jump_hunk(delta)
|
|
275
331
|
return if hunk_offsets.empty?
|
|
276
332
|
self.hunk_idx = (hunk_idx + delta).clamp(0, hunk_offsets.size - 1)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
module Sight
|
|
7
|
+
module ClaudeHookInstaller
|
|
8
|
+
HOOK_COMMAND = "sight hook-run"
|
|
9
|
+
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def settings_path
|
|
13
|
+
File.join(Dir.home, ".config", "claude", "settings.json")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def install(path: settings_path)
|
|
17
|
+
settings = if File.exist?(path)
|
|
18
|
+
JSON.parse(File.read(path))
|
|
19
|
+
else
|
|
20
|
+
{}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
hooks = settings["hooks"] ||= {}
|
|
24
|
+
prompt_hooks = hooks["UserPromptSubmit"] ||= []
|
|
25
|
+
|
|
26
|
+
if prompt_hooks.any? { |h| hook_is_sight?(h) }
|
|
27
|
+
puts "sight hook already installed"
|
|
28
|
+
return
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
prompt_hooks << {
|
|
32
|
+
"matcher" => "*",
|
|
33
|
+
"hooks" => [{"type" => "command", "command" => HOOK_COMMAND}]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
37
|
+
File.write(path, JSON.pretty_generate(settings) + "\n")
|
|
38
|
+
puts "Installed sight hook into #{path}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def uninstall(path: settings_path)
|
|
42
|
+
unless File.exist?(path)
|
|
43
|
+
puts "No settings file found"
|
|
44
|
+
return
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
settings = JSON.parse(File.read(path))
|
|
48
|
+
prompt_hooks = settings.dig("hooks", "UserPromptSubmit")
|
|
49
|
+
|
|
50
|
+
unless prompt_hooks&.any? { |h| hook_is_sight?(h) }
|
|
51
|
+
puts "No sight hook found"
|
|
52
|
+
return
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
prompt_hooks.reject! { |h| hook_is_sight?(h) }
|
|
56
|
+
File.write(path, JSON.pretty_generate(settings) + "\n")
|
|
57
|
+
puts "Uninstalled sight hook"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def hook_is_sight?(entry)
|
|
61
|
+
Array(entry["hooks"]).any? { |h| h["command"]&.include?("sight") }
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
data/lib/sight/cli.rb
CHANGED
|
@@ -5,34 +5,84 @@ module Sight
|
|
|
5
5
|
module_function
|
|
6
6
|
|
|
7
7
|
def run(argv)
|
|
8
|
-
if argv.include?("--help") || argv.include?("-h")
|
|
9
|
-
|
|
10
|
-
puts "Interactive git diff viewer (staged + unstaged)"
|
|
11
|
-
puts
|
|
12
|
-
puts "Keys: j/k hunks, n/p files, c comment, ? help, q quit"
|
|
13
|
-
puts
|
|
14
|
-
puts "Annotations are printed to stdout on quit."
|
|
15
|
-
return
|
|
16
|
-
end
|
|
8
|
+
return print_help if argv.include?("--help") || argv.include?("-h")
|
|
9
|
+
return puts("sight #{VERSION}") if argv.include?("--version") || argv.include?("-v")
|
|
17
10
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
21
16
|
end
|
|
17
|
+
end
|
|
18
|
+
|
|
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
|
|
29
|
+
|
|
30
|
+
def open
|
|
31
|
+
Git.clear_pending_review
|
|
22
32
|
|
|
23
33
|
raw = Git.diff
|
|
24
|
-
|
|
25
|
-
|
|
34
|
+
files = DiffParser.parse(raw)
|
|
35
|
+
|
|
36
|
+
Git.untracked_files.each do |path|
|
|
37
|
+
content = Git.file_content(path)
|
|
38
|
+
next unless content.valid_encoding? && !content.include?("\x00")
|
|
39
|
+
files << DiffParser.build_untracked(path, content)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
if files.empty?
|
|
43
|
+
warn "No changes"
|
|
26
44
|
return
|
|
27
45
|
end
|
|
28
46
|
|
|
29
|
-
files = DiffParser.parse(raw)
|
|
30
47
|
app = App.new(files)
|
|
31
48
|
app.run
|
|
32
49
|
|
|
33
50
|
unless app.annotations.empty?
|
|
34
|
-
|
|
51
|
+
formatted = AnnotationFormatter.format(app.annotations)
|
|
52
|
+
puts formatted
|
|
53
|
+
Git.save_pending_review(formatted)
|
|
35
54
|
end
|
|
36
55
|
end
|
|
56
|
+
|
|
57
|
+
AGENTS = {
|
|
58
|
+
"claude" => ClaudeHookInstaller
|
|
59
|
+
}.freeze
|
|
60
|
+
|
|
61
|
+
def install_hook(agent)
|
|
62
|
+
resolve_installer(agent)&.install
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def uninstall_hook(agent)
|
|
66
|
+
resolve_installer(agent)&.uninstall
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def resolve_installer(agent)
|
|
70
|
+
AGENTS.fetch(agent) { warn "Unknown agent: #{agent.inspect}. Use: claude" }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def run_hook
|
|
74
|
+
git_dir = Git.repo_dir
|
|
75
|
+
rescue Error
|
|
76
|
+
nil
|
|
77
|
+
else
|
|
78
|
+
file = File.join(git_dir, "sight", "pending-review")
|
|
79
|
+
return unless File.exist?(file)
|
|
80
|
+
|
|
81
|
+
content = File.read(file)
|
|
82
|
+
puts "The user has just finished reviewing your code changes in sight. Here are their annotations:"
|
|
83
|
+
puts
|
|
84
|
+
puts content
|
|
85
|
+
File.delete(file)
|
|
86
|
+
end
|
|
37
87
|
end
|
|
38
88
|
end
|
data/lib/sight/diff_parser.rb
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module Sight
|
|
4
4
|
DiffLine = Struct.new(:type, :content, :lineno, :old_lineno, keyword_init: true)
|
|
5
5
|
Hunk = Struct.new(:context, :lines, keyword_init: true)
|
|
6
|
-
DiffFile = Struct.new(:path, :hunks, keyword_init: true)
|
|
6
|
+
DiffFile = Struct.new(:path, :hunks, :status, keyword_init: true)
|
|
7
7
|
|
|
8
8
|
module DiffParser
|
|
9
9
|
module_function
|
|
@@ -19,10 +19,14 @@ module Sight
|
|
|
19
19
|
if line.start_with?("diff --git ")
|
|
20
20
|
current_hunk = nil
|
|
21
21
|
path = line.split(" b/", 2).last
|
|
22
|
-
current_file = DiffFile.new(path: path, hunks: [])
|
|
22
|
+
current_file = DiffFile.new(path: path, hunks: [], status: :modified)
|
|
23
23
|
files << current_file
|
|
24
24
|
elsif current_file.nil?
|
|
25
25
|
next
|
|
26
|
+
elsif line.start_with?("new file mode")
|
|
27
|
+
current_file.status = :added
|
|
28
|
+
elsif line.start_with?("deleted file mode")
|
|
29
|
+
current_file.status = :deleted
|
|
26
30
|
elsif line.start_with?("@@ ")
|
|
27
31
|
context, new_start, old_start = parse_hunk_header(line)
|
|
28
32
|
new_lineno = new_start
|
|
@@ -48,6 +52,14 @@ module Sight
|
|
|
48
52
|
files
|
|
49
53
|
end
|
|
50
54
|
|
|
55
|
+
def build_untracked(path, content)
|
|
56
|
+
lines = content.lines(chomp: true).each_with_index.map do |text, i|
|
|
57
|
+
DiffLine.new(type: :add, content: "+#{text}", lineno: i + 1, old_lineno: nil)
|
|
58
|
+
end
|
|
59
|
+
hunk = Hunk.new(context: nil, lines: lines)
|
|
60
|
+
DiffFile.new(path: path, hunks: [hunk], status: :untracked)
|
|
61
|
+
end
|
|
62
|
+
|
|
51
63
|
def parse_hunk_header(line)
|
|
52
64
|
match = line.match(/@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@(.*)/)
|
|
53
65
|
return [nil, 1, 1] unless match
|
data/lib/sight/git.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
3
5
|
module Sight
|
|
4
6
|
module Git
|
|
5
7
|
module_function
|
|
@@ -10,6 +12,33 @@ module Sight
|
|
|
10
12
|
output
|
|
11
13
|
end
|
|
12
14
|
|
|
15
|
+
def untracked_files
|
|
16
|
+
output, success = run_cmd(["git", "ls-files", "--others", "--exclude-standard"])
|
|
17
|
+
return [] unless success
|
|
18
|
+
output.lines(chomp: true).reject(&:empty?)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def file_content(path)
|
|
22
|
+
File.read(path, mode: "rb")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def repo_dir
|
|
26
|
+
output, success = run_cmd(["git", "rev-parse", "--git-dir"])
|
|
27
|
+
raise Error, "not a git repository" unless success
|
|
28
|
+
output.strip
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def save_pending_review(content)
|
|
32
|
+
dir = File.join(repo_dir, "sight")
|
|
33
|
+
FileUtils.mkdir_p(dir)
|
|
34
|
+
File.write(File.join(dir, "pending-review"), content)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def clear_pending_review
|
|
38
|
+
path = File.join(repo_dir, "sight", "pending-review")
|
|
39
|
+
File.delete(path) if File.exist?(path)
|
|
40
|
+
end
|
|
41
|
+
|
|
13
42
|
def run_cmd(cmd, err: IO::NULL)
|
|
14
43
|
output = IO.popen(cmd, err: err, &:read)
|
|
15
44
|
[output, $?.success?]
|
data/lib/sight/version.rb
CHANGED
data/lib/sight.rb
CHANGED
|
@@ -6,6 +6,7 @@ require_relative "sight/diff_parser"
|
|
|
6
6
|
require_relative "sight/git"
|
|
7
7
|
require_relative "sight/annotation"
|
|
8
8
|
require_relative "sight/annotation_formatter"
|
|
9
|
+
require_relative "sight/claude_hook_installer"
|
|
9
10
|
require_relative "sight/cli"
|
|
10
11
|
require_relative "sight/app"
|
|
11
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
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ariel Rzezak
|
|
@@ -37,12 +37,14 @@ 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
|
|
43
44
|
- lib/sight/annotation.rb
|
|
44
45
|
- lib/sight/annotation_formatter.rb
|
|
45
46
|
- lib/sight/app.rb
|
|
47
|
+
- lib/sight/claude_hook_installer.rb
|
|
46
48
|
- lib/sight/cli.rb
|
|
47
49
|
- lib/sight/diff_parser.rb
|
|
48
50
|
- lib/sight/display_line.rb
|