ruby_rich 0.3.1 → 0.4.1
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/columns.rb +244 -0
- data/lib/ruby_rich/composer.rb +512 -0
- data/lib/ruby_rich/console.rb +191 -29
- 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 +213 -0
- data/lib/ruby_rich/panel.rb +1 -1
- data/lib/ruby_rich/print.rb +6 -6
- data/lib/ruby_rich/progress_bar.rb +229 -11
- 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/status.rb +246 -0
- data/lib/ruby_rich/syntax.rb +171 -0
- data/lib/ruby_rich/table.rb +167 -21
- data/lib/ruby_rich/terminal.rb +510 -0
- data/lib/ruby_rich/text.rb +112 -2
- 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/tree.rb +200 -0
- data/lib/ruby_rich/version.rb +1 -1
- data/lib/ruby_rich/viewport.rb +468 -0
- data/lib/ruby_rich.rb +65 -10
- metadata +93 -3
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyRich
|
|
4
|
+
class Transcript
|
|
5
|
+
ENTRY_TYPES = [
|
|
6
|
+
:user,
|
|
7
|
+
:assistant,
|
|
8
|
+
:thinking,
|
|
9
|
+
:tool,
|
|
10
|
+
:tool_result,
|
|
11
|
+
:system,
|
|
12
|
+
:error,
|
|
13
|
+
:markdown,
|
|
14
|
+
:diff,
|
|
15
|
+
:separator,
|
|
16
|
+
:progress
|
|
17
|
+
].freeze
|
|
18
|
+
|
|
19
|
+
class Entry
|
|
20
|
+
attr_accessor :id, :type, :content, :metadata, :status, :collapsed, :name
|
|
21
|
+
attr_reader :version
|
|
22
|
+
|
|
23
|
+
def initialize(id:, type:, content: "", metadata: {}, status: nil, collapsed: false, name: nil)
|
|
24
|
+
@id = id
|
|
25
|
+
@type = type
|
|
26
|
+
@content = +content.to_s
|
|
27
|
+
@metadata = metadata.dup
|
|
28
|
+
@status = status
|
|
29
|
+
@collapsed = collapsed
|
|
30
|
+
@name = name
|
|
31
|
+
@version = 0
|
|
32
|
+
@render_cache = {}
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def text
|
|
36
|
+
@content
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def text=(value)
|
|
40
|
+
replace(value)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def content=(value)
|
|
44
|
+
replace(value)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def metadata=(value)
|
|
48
|
+
@metadata = (value || {}).dup
|
|
49
|
+
touch
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def status=(value)
|
|
53
|
+
@status = value
|
|
54
|
+
touch
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def collapsed=(value)
|
|
58
|
+
@collapsed = value
|
|
59
|
+
touch
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def name=(value)
|
|
63
|
+
@name = value
|
|
64
|
+
touch
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def append(delta)
|
|
68
|
+
@content << delta.to_s
|
|
69
|
+
touch
|
|
70
|
+
self
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def replace(new_content)
|
|
74
|
+
@content = +new_content.to_s
|
|
75
|
+
touch
|
|
76
|
+
self
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def update
|
|
80
|
+
yield self
|
|
81
|
+
touch
|
|
82
|
+
self
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def [](key)
|
|
86
|
+
case key.to_sym
|
|
87
|
+
when :text, :content
|
|
88
|
+
@content
|
|
89
|
+
when :id
|
|
90
|
+
@id
|
|
91
|
+
when :type
|
|
92
|
+
@type
|
|
93
|
+
when :metadata
|
|
94
|
+
@metadata
|
|
95
|
+
when :status
|
|
96
|
+
@status
|
|
97
|
+
when :collapsed
|
|
98
|
+
@collapsed
|
|
99
|
+
when :name
|
|
100
|
+
@name
|
|
101
|
+
else
|
|
102
|
+
@metadata[key] || @metadata[key.to_sym] || @metadata[key.to_s]
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def []=(key, value)
|
|
107
|
+
case key.to_sym
|
|
108
|
+
when :text, :content
|
|
109
|
+
replace(value)
|
|
110
|
+
when :type
|
|
111
|
+
@type = value
|
|
112
|
+
touch
|
|
113
|
+
when :metadata
|
|
114
|
+
@metadata = value || {}
|
|
115
|
+
touch
|
|
116
|
+
when :status
|
|
117
|
+
@status = value
|
|
118
|
+
touch
|
|
119
|
+
when :collapsed
|
|
120
|
+
@collapsed = value
|
|
121
|
+
touch
|
|
122
|
+
when :name
|
|
123
|
+
@name = value
|
|
124
|
+
touch
|
|
125
|
+
else
|
|
126
|
+
@metadata[key] = value
|
|
127
|
+
touch
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def cache_fetch(cache_key)
|
|
132
|
+
versioned_key = [@version, cache_key]
|
|
133
|
+
return @render_cache[versioned_key] if @render_cache.key?(versioned_key)
|
|
134
|
+
|
|
135
|
+
@render_cache.clear
|
|
136
|
+
@render_cache[versioned_key] = yield
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def to_h
|
|
140
|
+
{
|
|
141
|
+
id: @id,
|
|
142
|
+
type: @type,
|
|
143
|
+
text: @content,
|
|
144
|
+
content: @content,
|
|
145
|
+
metadata: @metadata,
|
|
146
|
+
status: @status,
|
|
147
|
+
collapsed: @collapsed,
|
|
148
|
+
name: @name
|
|
149
|
+
}.compact
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
private
|
|
153
|
+
|
|
154
|
+
def touch
|
|
155
|
+
@version += 1
|
|
156
|
+
@render_cache.clear
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
class Store
|
|
161
|
+
include Enumerable
|
|
162
|
+
|
|
163
|
+
attr_reader :entries
|
|
164
|
+
|
|
165
|
+
def initialize
|
|
166
|
+
@entries = []
|
|
167
|
+
@sequence = 0
|
|
168
|
+
@mutex = Mutex.new
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def add(type:, content: "", metadata: {}, status: nil, collapsed: nil, id: nil, name: nil)
|
|
172
|
+
normalized_type = normalize_type(type)
|
|
173
|
+
collapsed = default_collapsed(normalized_type) if collapsed.nil?
|
|
174
|
+
|
|
175
|
+
@mutex.synchronize do
|
|
176
|
+
entry = Entry.new(
|
|
177
|
+
id: id || next_id(normalized_type),
|
|
178
|
+
type: normalized_type,
|
|
179
|
+
content: content,
|
|
180
|
+
metadata: metadata,
|
|
181
|
+
status: status,
|
|
182
|
+
collapsed: collapsed,
|
|
183
|
+
name: name
|
|
184
|
+
)
|
|
185
|
+
@entries << entry
|
|
186
|
+
entry
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def append(id, delta)
|
|
191
|
+
mutate(id) { |entry| entry.append(delta) }
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def replace(id, new_content)
|
|
195
|
+
mutate(id) { |entry| entry.replace(new_content) }
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def remove(id)
|
|
199
|
+
@mutex.synchronize do
|
|
200
|
+
index = @entries.index { |entry| entry.id == id }
|
|
201
|
+
return false unless index
|
|
202
|
+
|
|
203
|
+
@entries.delete_at(index)
|
|
204
|
+
true
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def update(id)
|
|
209
|
+
mutate(id) { |entry| entry.update { |target| yield target } }
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def find(id)
|
|
213
|
+
@mutex.synchronize { @entries.find { |entry| entry.id == id } }
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def index(id)
|
|
217
|
+
@mutex.synchronize { @entries.index { |entry| entry.id == id } }
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def expand(id)
|
|
221
|
+
update(id) { |entry| entry.collapsed = false }
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def collapse(id)
|
|
225
|
+
update(id) { |entry| entry.collapsed = true }
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def toggle(id)
|
|
229
|
+
update(id) { |entry| entry.collapsed = !entry.collapsed }
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def each(&block)
|
|
233
|
+
@entries.each(&block)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
private
|
|
237
|
+
|
|
238
|
+
def mutate(id)
|
|
239
|
+
@mutex.synchronize do
|
|
240
|
+
entry = @entries.find { |item| item.id == id }
|
|
241
|
+
return false unless entry
|
|
242
|
+
|
|
243
|
+
yield entry
|
|
244
|
+
true
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def normalize_type(type)
|
|
249
|
+
normalized = type.to_sym
|
|
250
|
+
normalized = :tool if normalized == :tool_call
|
|
251
|
+
return normalized if ENTRY_TYPES.include?(normalized)
|
|
252
|
+
|
|
253
|
+
:system
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def default_collapsed(type)
|
|
257
|
+
[:thinking, :tool, :tool_result].include?(type)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def next_id(type)
|
|
261
|
+
@sequence += 1
|
|
262
|
+
"#{type}-#{@sequence}"
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
attr_accessor :width, :height
|
|
267
|
+
attr_reader :store
|
|
268
|
+
|
|
269
|
+
def initialize(store: Store.new)
|
|
270
|
+
@store = store
|
|
271
|
+
@width = 0
|
|
272
|
+
@height = 0
|
|
273
|
+
@selected_collapsible_id = nil
|
|
274
|
+
@focused = true
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def blocks
|
|
278
|
+
@store.entries
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def focus
|
|
282
|
+
@focused = true
|
|
283
|
+
self
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def blur
|
|
287
|
+
@focused = false
|
|
288
|
+
self
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def attach(layout, priority: 150)
|
|
292
|
+
[:ctrl_o, :alt_v].each do |event_name|
|
|
293
|
+
layout.key(event_name, priority) do |event_data, _live|
|
|
294
|
+
handle_event(event_data)
|
|
295
|
+
false
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
self
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def handle_event(event_data)
|
|
303
|
+
return false unless @focused
|
|
304
|
+
|
|
305
|
+
case event_data[:name]
|
|
306
|
+
when :ctrl_o
|
|
307
|
+
toggle_next_collapsible
|
|
308
|
+
when :alt_v
|
|
309
|
+
toggle_next(:tool)
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def add_user(text, **options)
|
|
314
|
+
add_block(:user, text, **options)
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def add_assistant(text, **options)
|
|
318
|
+
add_block(:assistant, text, **options)
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def add_thinking(text, status: "idle", collapsed: true, **options)
|
|
322
|
+
add_block(:thinking, text, status: status, collapsed: collapsed, **options)
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def add_tool(name, status: :running, result: nil, collapsed: false, **options)
|
|
326
|
+
metadata = (options.delete(:metadata) || {}).merge(name: name)
|
|
327
|
+
add_block(:tool, result.to_s, name: name, status: status, collapsed: collapsed, metadata: metadata, **options)
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def add_separator(label = nil, **options)
|
|
331
|
+
add_block(:separator, label.to_s, **options)
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def add_markdown(text, **options)
|
|
335
|
+
add_block(:markdown, text, **options)
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def add_block(type, text = "", **options)
|
|
339
|
+
id = options.delete(:id)
|
|
340
|
+
status = options.delete(:status)
|
|
341
|
+
collapsed = options.delete(:collapsed)
|
|
342
|
+
name = options.delete(:name)
|
|
343
|
+
metadata = options.delete(:metadata) || options
|
|
344
|
+
@store.add(
|
|
345
|
+
type: type,
|
|
346
|
+
content: text,
|
|
347
|
+
id: id,
|
|
348
|
+
status: status,
|
|
349
|
+
collapsed: collapsed,
|
|
350
|
+
metadata: metadata,
|
|
351
|
+
name: name
|
|
352
|
+
).id
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
def append_block(id, delta)
|
|
356
|
+
@store.append(id, delta)
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def replace_block(id, text, **options)
|
|
360
|
+
return false unless @store.replace(id, text)
|
|
361
|
+
return true if options.empty?
|
|
362
|
+
|
|
363
|
+
@store.update(id) do |entry|
|
|
364
|
+
options.each do |key, value|
|
|
365
|
+
entry[key] = value
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def remove_block(id)
|
|
371
|
+
removed = @store.remove(id)
|
|
372
|
+
@selected_collapsible_id = nil if removed && @selected_collapsible_id == id
|
|
373
|
+
removed
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def find_block(id)
|
|
377
|
+
@store.find(id)
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
def expand_entry(id)
|
|
381
|
+
@store.expand(id)
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def collapse_entry(id)
|
|
385
|
+
@store.collapse(id)
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def toggle_entry(id)
|
|
389
|
+
@store.toggle(id)
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
def render
|
|
393
|
+
lines = []
|
|
394
|
+
@store.entries.each_with_index do |entry, index|
|
|
395
|
+
lines.concat(render_entry(entry, index))
|
|
396
|
+
end
|
|
397
|
+
lines
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
private
|
|
401
|
+
|
|
402
|
+
def render_entry(entry, index)
|
|
403
|
+
case entry.type
|
|
404
|
+
when :user
|
|
405
|
+
render_plain_message(entry.content, first_prefix: "#{AnsiCode.color(:blue, true)}●#{AnsiCode.reset} ", rest_prefix: " ")
|
|
406
|
+
when :assistant
|
|
407
|
+
render_plain_message(entry.content, first_prefix: " ", rest_prefix: " ")
|
|
408
|
+
when :thinking
|
|
409
|
+
render_thinking(entry)
|
|
410
|
+
when :tool
|
|
411
|
+
render_tool(entry)
|
|
412
|
+
when :tool_result
|
|
413
|
+
render_tool_result(entry)
|
|
414
|
+
when :system
|
|
415
|
+
wrap_with_prefix(entry.content, "#{AnsiCode.color(:black, true)}system#{AnsiCode.reset} ")
|
|
416
|
+
when :error
|
|
417
|
+
wrap_with_prefix(entry.content, "#{AnsiCode.color(:red, true)}error#{AnsiCode.reset} ")
|
|
418
|
+
when :separator
|
|
419
|
+
[separator_line(entry.content)]
|
|
420
|
+
when :markdown
|
|
421
|
+
render_markdown(entry)
|
|
422
|
+
when :diff
|
|
423
|
+
render_diff(entry)
|
|
424
|
+
when :progress
|
|
425
|
+
render_progress(entry)
|
|
426
|
+
else
|
|
427
|
+
entry.content.to_s.split("\n", -1)
|
|
428
|
+
end
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
def render_plain_message(content, first_prefix:, rest_prefix:)
|
|
432
|
+
lines = content.to_s.split("\n", -1)
|
|
433
|
+
lines = [""] if lines.empty?
|
|
434
|
+
lines.each_with_index.flat_map do |line, index|
|
|
435
|
+
prefix = index.zero? ? first_prefix : rest_prefix
|
|
436
|
+
wrap_line(line, [@width - visible_width(prefix), 20].max).map { |part| prefix + part }
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def render_thinking(entry)
|
|
441
|
+
status = entry.status || "idle"
|
|
442
|
+
header = "#{AnsiCode.color(:white, true)}... thinking #{status}#{AnsiCode.reset}"
|
|
443
|
+
return [header, "#{AnsiCode.italic}thinking collapsed; press Ctrl+O for full text#{AnsiCode.reset}"] if entry.collapsed
|
|
444
|
+
|
|
445
|
+
[header] + wrap_with_prefix(entry.content, "#{AnsiCode.color(:black, true)}│#{AnsiCode.reset} ")
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
def render_tool(entry)
|
|
449
|
+
ToolBlock.new(entry, width: @width).render
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
def render_tool_result(entry)
|
|
453
|
+
status = entry.status || :done
|
|
454
|
+
header = "#{AnsiCode.color(:cyan, true)}• result #{status}#{AnsiCode.reset}"
|
|
455
|
+
return [header, " result collapsed; press Ctrl+O for full output"] if entry.collapsed
|
|
456
|
+
|
|
457
|
+
[header] + wrap_with_prefix(entry.content, " ")
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def render_markdown(entry)
|
|
461
|
+
cache_key = [:markdown, @width]
|
|
462
|
+
entry.cache_fetch(cache_key) do
|
|
463
|
+
rendered = Markdown.render(entry.content, width: [@width, 20].max, table_border_style: :full)
|
|
464
|
+
rendered.split("\n")
|
|
465
|
+
end
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
def render_diff(entry)
|
|
469
|
+
cache_key = [:diff, @width]
|
|
470
|
+
entry.cache_fetch(cache_key) do
|
|
471
|
+
entry.content.split("\n").flat_map do |line|
|
|
472
|
+
color = if line.start_with?("+")
|
|
473
|
+
:green
|
|
474
|
+
elsif line.start_with?("-")
|
|
475
|
+
:red
|
|
476
|
+
elsif line.start_with?("@@")
|
|
477
|
+
:cyan
|
|
478
|
+
else
|
|
479
|
+
:white
|
|
480
|
+
end
|
|
481
|
+
wrap_line(line, [@width, 20].max).map { |part| "#{AnsiCode.color(color, true)}#{part}#{AnsiCode.reset}" }
|
|
482
|
+
end
|
|
483
|
+
end
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
def render_progress(entry)
|
|
487
|
+
total = entry.metadata[:total].to_f
|
|
488
|
+
current = entry.metadata[:current].to_f
|
|
489
|
+
percent = total.positive? ? [[current / total, 0.0].max, 1.0].min : 0.0
|
|
490
|
+
width = [[@width - 20, 10].max, 40].min
|
|
491
|
+
filled = (width * percent).round
|
|
492
|
+
bar = ("█" * filled) + ("░" * (width - filled))
|
|
493
|
+
label = entry.metadata[:label] || entry.content
|
|
494
|
+
["#{label} #{AnsiCode.color(:blue, true)}#{bar}#{AnsiCode.reset} #{(percent * 100).round}%"]
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
def wrap_with_prefix(text, prefix)
|
|
498
|
+
text.split("\n").flat_map do |line|
|
|
499
|
+
wrap_line(line, [@width - visible_width(prefix), 20].max).map { |part| prefix + part }
|
|
500
|
+
end
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
def wrap_line(text, max_width)
|
|
504
|
+
return [""] if text.empty?
|
|
505
|
+
|
|
506
|
+
result = []
|
|
507
|
+
current = +""
|
|
508
|
+
current_width = 0
|
|
509
|
+
text.each_char do |char|
|
|
510
|
+
char_width = Unicode::DisplayWidth.of(char)
|
|
511
|
+
if current_width + char_width > max_width
|
|
512
|
+
result << current
|
|
513
|
+
current = +""
|
|
514
|
+
current_width = 0
|
|
515
|
+
end
|
|
516
|
+
current << char
|
|
517
|
+
current_width += char_width
|
|
518
|
+
end
|
|
519
|
+
result << current unless current.empty?
|
|
520
|
+
result
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
def separator_line(label)
|
|
524
|
+
width = [@width, 20].max
|
|
525
|
+
return "─" * width if label.empty?
|
|
526
|
+
|
|
527
|
+
" #{label} ".center(width, "─")
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
def visible_width(text)
|
|
531
|
+
text.to_s.gsub(/\e\[[0-9;:]*m/, "").display_width
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
def toggle_next_collapsible
|
|
535
|
+
ids = @store.entries.select { |entry| [:thinking, :tool, :tool_result].include?(entry.type) }.map(&:id)
|
|
536
|
+
toggle_next_id(ids)
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
def toggle_next(type)
|
|
540
|
+
ids = @store.entries.select { |entry| entry.type == type }.map(&:id)
|
|
541
|
+
toggle_next_id(ids)
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
def toggle_next_id(ids)
|
|
545
|
+
return false if ids.empty?
|
|
546
|
+
|
|
547
|
+
current_position = ids.index(@selected_collapsible_id)
|
|
548
|
+
next_id = ids[current_position ? (current_position + 1) % ids.length : 0]
|
|
549
|
+
@selected_collapsible_id = next_id
|
|
550
|
+
@store.toggle(next_id)
|
|
551
|
+
end
|
|
552
|
+
end
|
|
553
|
+
end
|