binocs 0.1.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.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +21 -0
  3. data/README.md +528 -0
  4. data/Rakefile +7 -0
  5. data/app/assets/javascripts/binocs/application.js +105 -0
  6. data/app/assets/stylesheets/binocs/application.css +67 -0
  7. data/app/channels/binocs/application_cable/channel.rb +8 -0
  8. data/app/channels/binocs/application_cable/connection.rb +8 -0
  9. data/app/channels/binocs/requests_channel.rb +13 -0
  10. data/app/controllers/binocs/application_controller.rb +62 -0
  11. data/app/controllers/binocs/requests_controller.rb +69 -0
  12. data/app/helpers/binocs/application_helper.rb +61 -0
  13. data/app/models/binocs/application_record.rb +7 -0
  14. data/app/models/binocs/request.rb +198 -0
  15. data/app/views/binocs/requests/_empty_list.html.erb +9 -0
  16. data/app/views/binocs/requests/_request.html.erb +61 -0
  17. data/app/views/binocs/requests/index.html.erb +115 -0
  18. data/app/views/binocs/requests/show.html.erb +227 -0
  19. data/app/views/layouts/binocs/application.html.erb +109 -0
  20. data/config/importmap.rb +6 -0
  21. data/config/routes.rb +11 -0
  22. data/db/migrate/20240101000000_create_binocs_requests.rb +36 -0
  23. data/exe/binocs +86 -0
  24. data/lib/binocs/agent.rb +153 -0
  25. data/lib/binocs/agent_context.rb +165 -0
  26. data/lib/binocs/agent_manager.rb +302 -0
  27. data/lib/binocs/configuration.rb +65 -0
  28. data/lib/binocs/engine.rb +61 -0
  29. data/lib/binocs/log_subscriber.rb +56 -0
  30. data/lib/binocs/middleware/request_recorder.rb +264 -0
  31. data/lib/binocs/swagger/client.rb +100 -0
  32. data/lib/binocs/swagger/path_matcher.rb +118 -0
  33. data/lib/binocs/tui/agent_output.rb +163 -0
  34. data/lib/binocs/tui/agents_list.rb +195 -0
  35. data/lib/binocs/tui/app.rb +726 -0
  36. data/lib/binocs/tui/colors.rb +115 -0
  37. data/lib/binocs/tui/filter_menu.rb +162 -0
  38. data/lib/binocs/tui/help_screen.rb +93 -0
  39. data/lib/binocs/tui/request_detail.rb +899 -0
  40. data/lib/binocs/tui/request_list.rb +268 -0
  41. data/lib/binocs/tui/spirit_animal.rb +235 -0
  42. data/lib/binocs/tui/window.rb +98 -0
  43. data/lib/binocs/tui.rb +24 -0
  44. data/lib/binocs/version.rb +5 -0
  45. data/lib/binocs.rb +27 -0
  46. data/lib/generators/binocs/install/install_generator.rb +61 -0
  47. data/lib/generators/binocs/install/templates/create_binocs_requests.rb +36 -0
  48. data/lib/generators/binocs/install/templates/initializer.rb +25 -0
  49. data/lib/tasks/binocs_tasks.rake +38 -0
  50. metadata +149 -0
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Binocs
4
+ module TUI
5
+ class AgentOutput < Window
6
+ attr_accessor :agent, :scroll_offset, :auto_scroll
7
+
8
+ def initialize(height:, width:, top:, left:)
9
+ super
10
+ @agent = nil
11
+ @scroll_offset = 0
12
+ @auto_scroll = true
13
+ @output_lines = []
14
+ @last_read_size = 0
15
+ end
16
+
17
+ def set_agent(agent)
18
+ @agent = agent
19
+ @scroll_offset = 0
20
+ @auto_scroll = true
21
+ @output_lines = []
22
+ @last_read_size = 0
23
+ load_output
24
+ end
25
+
26
+ def load_output
27
+ return unless @agent
28
+
29
+ output = @agent.output
30
+ @output_lines = output.split("\n")
31
+
32
+ # Auto-scroll to bottom if enabled
33
+ if @auto_scroll
34
+ max_scroll = [@output_lines.length - content_height, 0].max
35
+ @scroll_offset = max_scroll
36
+ end
37
+ end
38
+
39
+ def scroll_up
40
+ @auto_scroll = false
41
+ @scroll_offset = [@scroll_offset - 1, 0].max
42
+ end
43
+
44
+ def scroll_down
45
+ max_scroll = [@output_lines.length - content_height, 0].max
46
+ @scroll_offset = [@scroll_offset + 1, max_scroll].min
47
+ @auto_scroll = @scroll_offset == max_scroll
48
+ end
49
+
50
+ def page_up
51
+ @auto_scroll = false
52
+ @scroll_offset = [@scroll_offset - content_height, 0].max
53
+ end
54
+
55
+ def page_down
56
+ max_scroll = [@output_lines.length - content_height, 0].max
57
+ @scroll_offset = [@scroll_offset + content_height, max_scroll].min
58
+ @auto_scroll = @scroll_offset == max_scroll
59
+ end
60
+
61
+ def go_to_top
62
+ @auto_scroll = false
63
+ @scroll_offset = 0
64
+ end
65
+
66
+ def go_to_bottom
67
+ max_scroll = [@output_lines.length - content_height, 0].max
68
+ @scroll_offset = max_scroll
69
+ @auto_scroll = true
70
+ end
71
+
72
+ def draw
73
+ return unless @agent
74
+
75
+ clear
76
+ draw_box(build_title)
77
+ draw_output
78
+ draw_status_bar
79
+ refresh
80
+ end
81
+
82
+ private
83
+
84
+ def content_height
85
+ @height - 4
86
+ end
87
+
88
+ def content_width
89
+ @width - 4
90
+ end
91
+
92
+ def build_title
93
+ status = @agent.status.to_s.upcase
94
+ tool = @agent.tool.to_s
95
+ "Agent Output [#{tool}] - #{status}"
96
+ end
97
+
98
+ def draw_output
99
+ start_y = 1
100
+ visible_rows = content_height
101
+
102
+ if @output_lines.empty?
103
+ write(start_y + 2, 2, "No output yet...", Colors::MUTED)
104
+ if @agent.running?
105
+ write(start_y + 3, 2, "Agent is running, waiting for output.", Colors::MUTED)
106
+ end
107
+ return
108
+ end
109
+
110
+ visible_rows.times do |i|
111
+ line_index = @scroll_offset + i
112
+ break if line_index >= @output_lines.length
113
+
114
+ line = @output_lines[line_index] || ''
115
+ y = start_y + i
116
+
117
+ # Truncate long lines
118
+ display_line = line.length > content_width ? "#{line[0, content_width - 1]}…" : line
119
+
120
+ # Color code based on content
121
+ color = line_color(line)
122
+ write(y, 2, display_line, color)
123
+ end
124
+ end
125
+
126
+ def draw_status_bar
127
+ y = @height - 2
128
+
129
+ write(y - 1, 1, '─' * (@width - 2), Colors::BORDER)
130
+
131
+ # Left: scroll info
132
+ total = @output_lines.length
133
+ visible_end = [@scroll_offset + content_height, total].min
134
+ scroll_info = "Lines #{@scroll_offset + 1}-#{visible_end} of #{total}"
135
+ scroll_info += " [AUTO-SCROLL]" if @auto_scroll
136
+ write(y, 2, scroll_info, Colors::MUTED, Curses::A_DIM)
137
+
138
+ # Right: hints
139
+ hints = "j/k:scroll g/G:top/bottom r:refresh q/Esc:back"
140
+ write(y, @width - hints.length - 2, hints, Colors::KEY_HINT, Curses::A_DIM)
141
+ end
142
+
143
+ def line_color(line)
144
+ case line
145
+ when /error|Error|ERROR|exception|Exception/i
146
+ Colors::ERROR
147
+ when /warning|Warning|WARN/i
148
+ Colors::STATUS_CLIENT_ERROR
149
+ when /success|Success|completed|Completed/i
150
+ Colors::STATUS_SUCCESS
151
+ when /^[+]/
152
+ Colors::STATUS_SUCCESS # Git diff add
153
+ when /^[-]/
154
+ Colors::STATUS_SERVER_ERROR # Git diff remove
155
+ when /^@@/
156
+ Colors::STATUS_REDIRECT # Git diff hunk header
157
+ else
158
+ Colors::NORMAL
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,195 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Binocs
4
+ module TUI
5
+ class AgentsList < Window
6
+ attr_accessor :selected_index, :scroll_offset
7
+
8
+ def initialize(height:, width:, top:, left:)
9
+ super
10
+ @selected_index = 0
11
+ @scroll_offset = 0
12
+ end
13
+
14
+ def agents
15
+ @agents ||= []
16
+ end
17
+
18
+ def load_agents
19
+ @agents = Binocs::Agent.all.sort_by(&:created_at).reverse
20
+ @selected_index = [@selected_index, @agents.length - 1].min
21
+ @selected_index = 0 if @selected_index < 0
22
+ adjust_scroll
23
+ end
24
+
25
+ def selected_agent
26
+ agents[@selected_index]
27
+ end
28
+
29
+ def move_up
30
+ if @selected_index > 0
31
+ @selected_index -= 1
32
+ adjust_scroll
33
+ end
34
+ end
35
+
36
+ def move_down
37
+ if @selected_index < agents.length - 1
38
+ @selected_index += 1
39
+ adjust_scroll
40
+ end
41
+ end
42
+
43
+ def go_to_top
44
+ @selected_index = 0
45
+ @scroll_offset = 0
46
+ end
47
+
48
+ def go_to_bottom
49
+ @selected_index = [agents.length - 1, 0].max
50
+ adjust_scroll
51
+ end
52
+
53
+ def draw
54
+ clear
55
+ draw_box("Agents (#{agents.length} total, #{Binocs::Agent.running_count} running)")
56
+ draw_header
57
+ draw_agents
58
+ draw_status_bar
59
+ refresh
60
+ end
61
+
62
+ private
63
+
64
+ def content_height
65
+ @height - 5
66
+ end
67
+
68
+ def content_width
69
+ @width - 2
70
+ end
71
+
72
+ def adjust_scroll
73
+ visible_rows = content_height
74
+
75
+ if @selected_index < @scroll_offset
76
+ @scroll_offset = @selected_index
77
+ end
78
+
79
+ if @selected_index >= @scroll_offset + visible_rows
80
+ @scroll_offset = @selected_index - visible_rows + 1
81
+ end
82
+ end
83
+
84
+ def draw_header
85
+ y = 1
86
+ x = 1
87
+
88
+ header = "STATUS TOOL PROMPT DURATION BRANCH"
89
+ write(y, x, header[0, content_width], Colors::HEADER, Curses::A_BOLD)
90
+
91
+ write(2, 1, '─' * content_width, Colors::BORDER)
92
+ end
93
+
94
+ def draw_agents
95
+ visible_rows = content_height
96
+ start_y = 3
97
+
98
+ if agents.empty?
99
+ write(start_y + 2, 2, "No agents running. Press 'Esc' to go back.", Colors::MUTED)
100
+ write(start_y + 4, 2, "To launch an agent:", Colors::MUTED)
101
+ write(start_y + 5, 2, "1. View a request (Enter)", Colors::MUTED)
102
+ write(start_y + 6, 2, "2. Go to Agent tab (9)", Colors::MUTED)
103
+ write(start_y + 7, 2, "3. Press Enter to compose prompt", Colors::MUTED)
104
+ return
105
+ end
106
+
107
+ visible_rows.times do |i|
108
+ agent_index = @scroll_offset + i
109
+ break if agent_index >= agents.length
110
+
111
+ agent = agents[agent_index]
112
+ y = start_y + i
113
+ is_selected = agent_index == @selected_index
114
+
115
+ draw_agent_row(y, agent, is_selected)
116
+ end
117
+ end
118
+
119
+ def draw_agent_row(y, agent, is_selected)
120
+ if is_selected
121
+ write(y, 1, ' ' * content_width, Colors::SELECTED)
122
+ end
123
+
124
+ x = 1
125
+
126
+ # Status
127
+ status_text = format_status(agent.status).ljust(10)
128
+ status_color = status_color_for(agent.status)
129
+ if is_selected
130
+ write(y, x, status_text, Colors::SELECTED, Curses::A_BOLD)
131
+ else
132
+ write(y, x, status_text, status_color, Curses::A_BOLD)
133
+ end
134
+ x += 11
135
+
136
+ # Tool
137
+ tool_text = agent.tool.to_s.ljust(11)
138
+ write(y, x, tool_text, is_selected ? Colors::SELECTED : Colors::MUTED)
139
+ x += 12
140
+
141
+ # Prompt (truncated)
142
+ prompt_width = [content_width - 55, 20].max
143
+ prompt_text = agent.short_prompt(prompt_width).ljust(prompt_width)
144
+ write(y, x, prompt_text, is_selected ? Colors::SELECTED : Colors::NORMAL)
145
+ x += prompt_width + 2
146
+
147
+ # Duration
148
+ duration_text = (agent.duration || '-').ljust(9)
149
+ write(y, x, duration_text, is_selected ? Colors::SELECTED : Colors::MUTED)
150
+ x += 10
151
+
152
+ # Branch (short)
153
+ branch_text = truncate_branch(agent.branch_name)
154
+ write(y, x, branch_text, is_selected ? Colors::SELECTED : Colors::MUTED, Curses::A_DIM)
155
+ end
156
+
157
+ def draw_status_bar
158
+ y = @height - 2
159
+
160
+ write(y - 1, 1, '─' * content_width, Colors::BORDER)
161
+
162
+ hints = "Enter:view output d:delete o:open folder r:refresh Esc:back"
163
+ write(y, content_width - hints.length, hints, Colors::KEY_HINT, Curses::A_DIM)
164
+ end
165
+
166
+ def format_status(status)
167
+ case status
168
+ when :pending then 'PENDING'
169
+ when :running then 'RUNNING'
170
+ when :completed then 'DONE'
171
+ when :failed then 'FAILED'
172
+ when :stopped then 'STOPPED'
173
+ else status.to_s.upcase
174
+ end
175
+ end
176
+
177
+ def status_color_for(status)
178
+ case status
179
+ when :running then Colors::STATUS_REDIRECT # Yellow
180
+ when :completed then Colors::STATUS_SUCCESS # Blue/teal
181
+ when :failed then Colors::STATUS_SERVER_ERROR # Red
182
+ when :stopped then Colors::STATUS_CLIENT_ERROR # Magenta
183
+ else Colors::MUTED
184
+ end
185
+ end
186
+
187
+ def truncate_branch(branch)
188
+ return '-' unless branch
189
+
190
+ short = branch.sub('agent/', '')
191
+ short.length > 20 ? "#{short[0, 17]}..." : short
192
+ end
193
+ end
194
+ end
195
+ end