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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +21 -0
- data/README.md +528 -0
- data/Rakefile +7 -0
- data/app/assets/javascripts/binocs/application.js +105 -0
- data/app/assets/stylesheets/binocs/application.css +67 -0
- data/app/channels/binocs/application_cable/channel.rb +8 -0
- data/app/channels/binocs/application_cable/connection.rb +8 -0
- data/app/channels/binocs/requests_channel.rb +13 -0
- data/app/controllers/binocs/application_controller.rb +62 -0
- data/app/controllers/binocs/requests_controller.rb +69 -0
- data/app/helpers/binocs/application_helper.rb +61 -0
- data/app/models/binocs/application_record.rb +7 -0
- data/app/models/binocs/request.rb +198 -0
- data/app/views/binocs/requests/_empty_list.html.erb +9 -0
- data/app/views/binocs/requests/_request.html.erb +61 -0
- data/app/views/binocs/requests/index.html.erb +115 -0
- data/app/views/binocs/requests/show.html.erb +227 -0
- data/app/views/layouts/binocs/application.html.erb +109 -0
- data/config/importmap.rb +6 -0
- data/config/routes.rb +11 -0
- data/db/migrate/20240101000000_create_binocs_requests.rb +36 -0
- data/exe/binocs +86 -0
- data/lib/binocs/agent.rb +153 -0
- data/lib/binocs/agent_context.rb +165 -0
- data/lib/binocs/agent_manager.rb +302 -0
- data/lib/binocs/configuration.rb +65 -0
- data/lib/binocs/engine.rb +61 -0
- data/lib/binocs/log_subscriber.rb +56 -0
- data/lib/binocs/middleware/request_recorder.rb +264 -0
- data/lib/binocs/swagger/client.rb +100 -0
- data/lib/binocs/swagger/path_matcher.rb +118 -0
- data/lib/binocs/tui/agent_output.rb +163 -0
- data/lib/binocs/tui/agents_list.rb +195 -0
- data/lib/binocs/tui/app.rb +726 -0
- data/lib/binocs/tui/colors.rb +115 -0
- data/lib/binocs/tui/filter_menu.rb +162 -0
- data/lib/binocs/tui/help_screen.rb +93 -0
- data/lib/binocs/tui/request_detail.rb +899 -0
- data/lib/binocs/tui/request_list.rb +268 -0
- data/lib/binocs/tui/spirit_animal.rb +235 -0
- data/lib/binocs/tui/window.rb +98 -0
- data/lib/binocs/tui.rb +24 -0
- data/lib/binocs/version.rb +5 -0
- data/lib/binocs.rb +27 -0
- data/lib/generators/binocs/install/install_generator.rb +61 -0
- data/lib/generators/binocs/install/templates/create_binocs_requests.rb +36 -0
- data/lib/generators/binocs/install/templates/initializer.rb +25 -0
- data/lib/tasks/binocs_tasks.rake +38 -0
- 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
|