ruby_rich 0.4.0 → 0.4.2
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/lib/ruby_rich/agent_shell.rb +254 -0
- data/lib/ruby_rich/ansi_code.rb +46 -0
- data/lib/ruby_rich/app_shell.rb +374 -0
- data/lib/ruby_rich/attachment.rb +25 -0
- data/lib/ruby_rich/composer.rb +512 -0
- data/lib/ruby_rich/console.rb +174 -25
- data/lib/ruby_rich/dialog.rb +2 -1
- data/lib/ruby_rich/event.rb +29 -0
- data/lib/ruby_rich/focus_manager.rb +77 -0
- data/lib/ruby_rich/layout.rb +117 -29
- data/lib/ruby_rich/line_editor.rb +325 -0
- data/lib/ruby_rich/live.rb +100 -19
- data/lib/ruby_rich/markdown.rb +100 -230
- data/lib/ruby_rich/panel.rb +1 -1
- data/lib/ruby_rich/print.rb +6 -6
- data/lib/ruby_rich/progress_manager.rb +150 -0
- data/lib/ruby_rich/sidebar.rb +85 -0
- data/lib/ruby_rich/slash_input.rb +197 -0
- data/lib/ruby_rich/table.rb +12 -12
- data/lib/ruby_rich/terminal.rb +510 -0
- data/lib/ruby_rich/text.rb +1 -1
- data/lib/ruby_rich/theme.rb +96 -0
- data/lib/ruby_rich/tool_block.rb +92 -0
- data/lib/ruby_rich/transcript.rb +553 -0
- data/lib/ruby_rich/version.rb +1 -1
- data/lib/ruby_rich/viewport.rb +468 -0
- data/lib/ruby_rich.rb +38 -13
- metadata +32 -25
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a6389b429fa73b0aa3057ed437aa50e051bf1e9b7775a2f5ee4d0860fe1f2dbe
|
|
4
|
+
data.tar.gz: a0a2030db498d207fa27e4ba370da391afe654b2ba8c1998973a644c3ee324f3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bf56a855f9292e8eb03cbe2e5d71b0bef6be0de091b91f070c00a7cee14b989e50fc7ae98455f506e6667b2f3e846dc7e6bd0ff73653fc04f84fcd6fb3039a54
|
|
7
|
+
data.tar.gz: 0566e96e17f22b5499d867eebfb18d5519cda6336c4bc26531c0212eb763dd4df7aa90a362388bc31d837c0fbe2b3243873e341eb76a4b2d63135a936e79cbd6
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thread"
|
|
4
|
+
|
|
5
|
+
module RubyRich
|
|
6
|
+
class AgentShell < AppShell
|
|
7
|
+
attr_reader :mode
|
|
8
|
+
|
|
9
|
+
def initialize(**options)
|
|
10
|
+
@callbacks = {}
|
|
11
|
+
@state_mutex = Mutex.new
|
|
12
|
+
@id_mutex = Mutex.new
|
|
13
|
+
@pending_actions = Queue.new
|
|
14
|
+
@entry_sequence = 0
|
|
15
|
+
@state = :initialized
|
|
16
|
+
@ui_thread = nil
|
|
17
|
+
@mode = :chat
|
|
18
|
+
super
|
|
19
|
+
@stop_on_ctrl_c = false
|
|
20
|
+
@composer.instance_variable_set(:@on_interrupt, method(:handle_interrupt))
|
|
21
|
+
@composer.instance_variable_set(:@on_eof, method(:handle_eof))
|
|
22
|
+
attach_agent_controls
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def on_submit(&block)
|
|
26
|
+
@callbacks[:submit] = block
|
|
27
|
+
self
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def on_interrupt(&block)
|
|
31
|
+
@callbacks[:interrupt] = block
|
|
32
|
+
self
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def on_mode_toggle(&block)
|
|
36
|
+
@callbacks[:mode_toggle] = block
|
|
37
|
+
self
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def on_command(&block)
|
|
41
|
+
@callbacks[:command] = block
|
|
42
|
+
self
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def start(refresh_rate: 24, mouse: true, alt_screen: false)
|
|
46
|
+
@state_mutex.synchronize { @state = :starting }
|
|
47
|
+
Live.start(@layout, refresh_rate: refresh_rate, mouse: mouse, alt_screen: alt_screen, autowrap: false) do |live|
|
|
48
|
+
@state_mutex.synchronize do
|
|
49
|
+
@live = live
|
|
50
|
+
@ui_thread = Thread.current
|
|
51
|
+
@state = :running
|
|
52
|
+
end
|
|
53
|
+
drain_pending_actions(live)
|
|
54
|
+
live.listening = true
|
|
55
|
+
end
|
|
56
|
+
ensure
|
|
57
|
+
@state_mutex.synchronize do
|
|
58
|
+
@live = nil
|
|
59
|
+
@ui_thread = nil
|
|
60
|
+
@state = :stopped
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def stop
|
|
65
|
+
dispatch_after_start_failure = false
|
|
66
|
+
@state_mutex.synchronize do
|
|
67
|
+
return false if @state == :stopped
|
|
68
|
+
|
|
69
|
+
if @live
|
|
70
|
+
return @live.stop if Thread.current == @ui_thread
|
|
71
|
+
|
|
72
|
+
return @live.post { |live| live.stop } || false
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
@state = :stopped
|
|
76
|
+
dispatch_after_start_failure = true
|
|
77
|
+
end
|
|
78
|
+
dispatch_after_start_failure
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def add_user_message(text)
|
|
82
|
+
add_message(:user, text)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def add_assistant_message(text, streaming: false)
|
|
86
|
+
add_message(:assistant, text, streaming: streaming)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def add_markdown(content, streaming: false)
|
|
90
|
+
add_message(:markdown, content, streaming: streaming)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def add_system_message(text)
|
|
94
|
+
add_message(:system, text)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def add_error_message(text)
|
|
98
|
+
add_message(:error, text)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def add_diff(title: nil, content:, language: "diff")
|
|
102
|
+
text = title ? "#{title}\n#{content}" : content
|
|
103
|
+
add_message(:diff, text, language: language)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def append_to_message(id, delta)
|
|
107
|
+
dispatch { @transcript.append_block(id, delta).tap { @viewport.scroll_to_bottom } }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def replace_message(id, text)
|
|
111
|
+
dispatch { @transcript.replace_block(id, text).tap { @viewport.scroll_to_bottom } }
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def remove_entry(id)
|
|
115
|
+
dispatch { @transcript.remove_block(id).tap { @viewport.scroll_to_bottom } }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def start_tool_call(name:, input: nil, status: :running)
|
|
119
|
+
id = reserve_id(:tool)
|
|
120
|
+
return nil unless id
|
|
121
|
+
|
|
122
|
+
ok = dispatch do
|
|
123
|
+
@transcript.add_tool(name, status: status, result: tool_body(input: input), collapsed: false, id: id)
|
|
124
|
+
@viewport.scroll_to_bottom
|
|
125
|
+
id
|
|
126
|
+
end
|
|
127
|
+
ok ? id : nil
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def update_tool_call(id, status: nil, output: nil, input: nil)
|
|
131
|
+
options = {}
|
|
132
|
+
options[:status] = status if status
|
|
133
|
+
text = output.nil? && input.nil? ? nil : tool_body(input: input, output: output)
|
|
134
|
+
dispatch do
|
|
135
|
+
block = @transcript.find_block(id)
|
|
136
|
+
next false unless block
|
|
137
|
+
|
|
138
|
+
text ||= block[:text]
|
|
139
|
+
@transcript.replace_block(id, text, **options).tap { @viewport.scroll_to_bottom }
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def finish_tool_call(id, status: :done, output: nil)
|
|
144
|
+
update_tool_call(id, status: status, output: output)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def update_tasks(tasks)
|
|
148
|
+
dispatch { @sidebar.set_tasks(tasks) }
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def update_status(text)
|
|
152
|
+
dispatch { @status = text.to_s }
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def show_token_usage(input: nil, output: nil, total: nil, **extra)
|
|
156
|
+
dispatch { @token_usage = { input: input, output: output, total: total }.merge(extra).compact }
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def stopped?
|
|
160
|
+
@state_mutex.synchronize { @state == :stopped }
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
private
|
|
164
|
+
|
|
165
|
+
def add_message(type, text, **options)
|
|
166
|
+
id = reserve_id(type)
|
|
167
|
+
return nil unless id
|
|
168
|
+
|
|
169
|
+
ok = dispatch do
|
|
170
|
+
@transcript.add_block(type, text, **options, id: id)
|
|
171
|
+
@viewport.scroll_to_bottom
|
|
172
|
+
id
|
|
173
|
+
end
|
|
174
|
+
ok ? id : nil
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def reserve_id(type)
|
|
178
|
+
@state_mutex.synchronize { return nil if @state == :stopped }
|
|
179
|
+
|
|
180
|
+
@id_mutex.synchronize do
|
|
181
|
+
@entry_sequence += 1
|
|
182
|
+
"#{type}-#{@entry_sequence}"
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def dispatch
|
|
187
|
+
state, live, ui_thread = @state_mutex.synchronize { [@state, @live, @ui_thread] }
|
|
188
|
+
return false if state == :stopped
|
|
189
|
+
|
|
190
|
+
if live
|
|
191
|
+
return yield if Thread.current == ui_thread
|
|
192
|
+
|
|
193
|
+
return live.post { yield }
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
if state == :starting
|
|
197
|
+
@pending_actions << proc { yield }
|
|
198
|
+
return true
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
yield
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def drain_pending_actions(live)
|
|
205
|
+
@pending_actions.pop(true).call(live) until @pending_actions.empty?
|
|
206
|
+
rescue ThreadError
|
|
207
|
+
nil
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def attach_agent_controls
|
|
211
|
+
@layout.key(:ctrl_c, 2_000) do |_event, live|
|
|
212
|
+
handle_interrupt(live, self)
|
|
213
|
+
false
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
@layout.key(:ctrl_m, 2_000) do |_event, _live|
|
|
217
|
+
toggle_mode
|
|
218
|
+
false
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def handle_interrupt(_live = nil, _source = nil)
|
|
223
|
+
@callbacks[:interrupt]&.call(input_was_empty: @composer.value.to_s.empty?)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def handle_eof(live = nil, _source = nil)
|
|
227
|
+
@callbacks[:eof]&.call if @callbacks[:eof]
|
|
228
|
+
live&.stop
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def handle_submit(value, live, attachments = [])
|
|
232
|
+
text = value.to_s
|
|
233
|
+
if text.start_with?("/")
|
|
234
|
+
command = text.split(/\s+/, 2).first
|
|
235
|
+
@callbacks[:command]&.call(command)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
@callbacks[:submit]&.call(text, attachments)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def toggle_mode
|
|
242
|
+
@mode = @mode == :chat ? :agent : :chat
|
|
243
|
+
@callbacks[:mode_toggle]&.call(@mode)
|
|
244
|
+
@status = "mode · #{@mode}"
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def tool_body(input: nil, output: nil)
|
|
248
|
+
parts = []
|
|
249
|
+
parts << "input:\n#{input}" unless input.nil? || input.to_s.empty?
|
|
250
|
+
parts << "output:\n#{output}" unless output.nil? || output.to_s.empty?
|
|
251
|
+
parts.join("\n")
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
data/lib/ruby_rich/ansi_code.rb
CHANGED
|
@@ -62,10 +62,14 @@ module RubyRich
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
def self.reset
|
|
65
|
+
return "" unless color_enabled?
|
|
66
|
+
|
|
65
67
|
ANSI_CODES[:reset]
|
|
66
68
|
end
|
|
67
69
|
|
|
68
70
|
def self.color(color, bright=false)
|
|
71
|
+
return "" unless color_enabled?
|
|
72
|
+
|
|
69
73
|
if bright
|
|
70
74
|
"\e[#{ANSI_CODES[:bright_color][color]}m"
|
|
71
75
|
else
|
|
@@ -74,6 +78,8 @@ module RubyRich
|
|
|
74
78
|
end
|
|
75
79
|
|
|
76
80
|
def self.background(color, bright=false)
|
|
81
|
+
return "" unless color_enabled?
|
|
82
|
+
|
|
77
83
|
if bright
|
|
78
84
|
"\e[#{ANSI_CODES[:bright_background][color]}m"
|
|
79
85
|
else
|
|
@@ -82,14 +88,26 @@ module RubyRich
|
|
|
82
88
|
end
|
|
83
89
|
|
|
84
90
|
def self.bold
|
|
91
|
+
return "" unless color_enabled?
|
|
92
|
+
|
|
85
93
|
"\e[#{ANSI_CODES[:bold]}m"
|
|
86
94
|
end
|
|
87
95
|
|
|
96
|
+
def self.faint
|
|
97
|
+
return "" unless color_enabled?
|
|
98
|
+
|
|
99
|
+
"\e[#{ANSI_CODES[:faint]}m"
|
|
100
|
+
end
|
|
101
|
+
|
|
88
102
|
def self.italic
|
|
103
|
+
return "" unless color_enabled?
|
|
104
|
+
|
|
89
105
|
"\e[#{ANSI_CODES[:italic]}m"
|
|
90
106
|
end
|
|
91
107
|
|
|
92
108
|
def self.underline(style=nil)
|
|
109
|
+
return "" unless color_enabled?
|
|
110
|
+
|
|
93
111
|
case style
|
|
94
112
|
when nil
|
|
95
113
|
return "\e[#{ANSI_CODES[:underline]}m"
|
|
@@ -105,38 +123,56 @@ module RubyRich
|
|
|
105
123
|
end
|
|
106
124
|
|
|
107
125
|
def self.blink
|
|
126
|
+
return "" unless color_enabled?
|
|
127
|
+
|
|
108
128
|
"\e[#{ANSI_CODES[:blink]}m"
|
|
109
129
|
end
|
|
110
130
|
|
|
111
131
|
def self.rapid_blink
|
|
132
|
+
return "" unless color_enabled?
|
|
133
|
+
|
|
112
134
|
"\e[#{ANSI_CODES[:rapid_blink]}m"
|
|
113
135
|
end
|
|
114
136
|
|
|
115
137
|
def self.inverse
|
|
138
|
+
return "" unless color_enabled?
|
|
139
|
+
|
|
116
140
|
"\e[#{ANSI_CODES[:inverse]}m"
|
|
117
141
|
end
|
|
118
142
|
|
|
119
143
|
def self.fraktur
|
|
144
|
+
return "" unless color_enabled?
|
|
145
|
+
|
|
120
146
|
"\e[#{ANSI_CODES[:fraktur]}m"
|
|
121
147
|
end
|
|
122
148
|
|
|
123
149
|
def self.invisible
|
|
150
|
+
return "" unless color_enabled?
|
|
151
|
+
|
|
124
152
|
"\e[#{ANSI_CODES[:invisible]}m"
|
|
125
153
|
end
|
|
126
154
|
|
|
127
155
|
def self.strikethrough
|
|
156
|
+
return "" unless color_enabled?
|
|
157
|
+
|
|
128
158
|
"\e[#{ANSI_CODES[:strikethrough]}m"
|
|
129
159
|
end
|
|
130
160
|
|
|
131
161
|
def self.overline
|
|
162
|
+
return "" unless color_enabled?
|
|
163
|
+
|
|
132
164
|
"\e[#{ANSI_CODES[:overline]}m"
|
|
133
165
|
end
|
|
134
166
|
|
|
135
167
|
def self.no_blink
|
|
168
|
+
return "" unless color_enabled?
|
|
169
|
+
|
|
136
170
|
"\e[#{ANSI_CODES[:no_blink]}m"
|
|
137
171
|
end
|
|
138
172
|
|
|
139
173
|
def self.no_inverse
|
|
174
|
+
return "" unless color_enabled?
|
|
175
|
+
|
|
140
176
|
"\e[#{ANSI_CODES[:no_inverse]}m"
|
|
141
177
|
end
|
|
142
178
|
|
|
@@ -151,6 +187,8 @@ module RubyRich
|
|
|
151
187
|
strikethrough: false,
|
|
152
188
|
overline: false
|
|
153
189
|
)
|
|
190
|
+
return "" unless color_enabled?
|
|
191
|
+
|
|
154
192
|
code = if font_bright
|
|
155
193
|
"\e[#{ANSI_CODES[:bright_color][font_color]}"
|
|
156
194
|
else
|
|
@@ -191,5 +229,13 @@ module RubyRich
|
|
|
191
229
|
end
|
|
192
230
|
return code+"m"
|
|
193
231
|
end
|
|
232
|
+
|
|
233
|
+
def self.color_enabled?
|
|
234
|
+
ENV["NO_COLOR"].nil? && ENV["TERM"] != "dumb" && @color_enabled != false
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def self.color_enabled=(enabled)
|
|
238
|
+
@color_enabled = enabled
|
|
239
|
+
end
|
|
194
240
|
end
|
|
195
241
|
end
|