llm.rb 12.6.0 → 13.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 +4 -4
- data/CHANGELOG.md +571 -13
- data/LICENSE +21 -93
- data/README.md +183 -167
- data/bin/llm.rb +124 -0
- data/data/deepinfra.json +3 -0
- data/data/xai.json +1 -1
- data/lib/llm/a2a.rb +1 -1
- data/lib/llm/active_record/acts_as_llm.rb +6 -6
- data/lib/llm/agent.rb +136 -27
- data/lib/llm/buffer.rb +85 -3
- data/lib/llm/compactor/null.rb +19 -0
- data/lib/llm/compactor/truncate.rb +80 -0
- data/lib/llm/compactor.rb +42 -124
- data/lib/llm/context.rb +31 -37
- data/lib/llm/contract.rb +4 -25
- data/lib/llm/function/array.rb +18 -17
- data/lib/llm/function/async/group.rb +54 -0
- data/lib/llm/function/async/reactor.rb +48 -0
- data/lib/llm/function/async/task.rb +83 -0
- data/lib/llm/function/fiber/group.rb +46 -0
- data/lib/llm/function/fiber/task.rb +62 -0
- data/lib/llm/function/{fork_group.rb → fork/group.rb} +14 -5
- data/lib/llm/function/fork/job.rb +2 -2
- data/lib/llm/function/fork/task.rb +19 -10
- data/lib/llm/function/group.rb +40 -0
- data/lib/llm/function/{ractor_group.rb → ractor/group.rb} +13 -5
- data/lib/llm/function/ractor/job.rb +9 -3
- data/lib/llm/function/ractor/mailbox.rb +2 -0
- data/lib/llm/function/ractor/task.rb +23 -15
- data/lib/llm/function/{call_group.rb → sequential/group.rb} +12 -8
- data/lib/llm/function/sequential/task.rb +49 -0
- data/lib/llm/function/task.rb +25 -48
- data/lib/llm/function/thread/group.rb +46 -0
- data/lib/llm/function/thread/task.rb +60 -0
- data/lib/llm/function.rb +54 -65
- data/lib/llm/loop_guard.rb +1 -2
- data/lib/llm/mcp.rb +22 -0
- data/lib/llm/object.rb +2 -1
- data/lib/llm/provider.rb +6 -3
- data/lib/llm/providers/anthropic.rb +1 -1
- data/lib/llm/providers/bedrock/request_adapter.rb +1 -1
- data/lib/llm/providers/google.rb +2 -2
- data/lib/llm/providers/mistral.rb +1 -1
- data/lib/llm/providers/ollama.rb +1 -1
- data/lib/llm/providers/openai/responses.rb +1 -1
- data/lib/llm/providers/openai.rb +1 -1
- data/lib/llm/repl/{transcript.rb → buffer.rb} +34 -21
- data/lib/llm/repl/command.rb +47 -13
- data/lib/llm/repl/commands/compact.rb +33 -0
- data/lib/llm/repl/commands/help.rb +3 -5
- data/lib/llm/repl/input.rb +80 -15
- data/lib/llm/repl/markdown/table.rb +80 -0
- data/lib/llm/repl/markdown.rb +33 -3
- data/lib/llm/repl/node.rb +37 -0
- data/lib/llm/repl/status.rb +4 -4
- data/lib/llm/repl/stream.rb +12 -5
- data/lib/llm/repl/walker.rb +46 -0
- data/lib/llm/repl/window.rb +31 -32
- data/lib/llm/repl.rb +70 -38
- data/lib/llm/response.rb +10 -0
- data/lib/llm/schema/leaf.rb +5 -0
- data/lib/llm/schema/object.rb +11 -5
- data/lib/llm/sequel/plugin.rb +6 -6
- data/lib/llm/skill.rb +20 -4
- data/lib/llm/stream.rb +24 -17
- data/lib/llm/tool.rb +20 -4
- data/lib/llm/tools/chdir.rb +0 -2
- data/lib/llm/tools/{swap_text.rb → edit-file.rb} +3 -3
- data/lib/llm/tools/git.rb +11 -4
- data/lib/llm/tools/mkdir.rb +4 -1
- data/lib/llm/tools/pwd.rb +0 -2
- data/lib/llm/tools/read_file.rb +0 -2
- data/lib/llm/tools/rg.rb +11 -4
- data/lib/llm/tools/ruby.rb +46 -0
- data/lib/llm/tools/shell.rb +11 -4
- data/lib/llm/tools/utils.rb +31 -0
- data/lib/llm/tracer/pretty_logger.rb +127 -0
- data/lib/llm/tracer.rb +1 -0
- data/lib/llm/version.rb +1 -1
- data/lib/llm.rb +25 -5
- data/llm.gemspec +11 -5
- data/resources/deepdive.md +45 -1198
- metadata +39 -17
- data/lib/llm/function/call_task.rb +0 -46
- data/lib/llm/function/fiber_group.rb +0 -105
- data/lib/llm/function/task_group.rb +0 -97
- data/lib/llm/function/thread_group.rb +0 -102
data/lib/llm/repl/input.rb
CHANGED
|
@@ -12,16 +12,30 @@ class LLM::Repl
|
|
|
12
12
|
F: Curses::KEY_CTRL_F,
|
|
13
13
|
K: Curses::KEY_CTRL_K,
|
|
14
14
|
Y: Curses::KEY_CTRL_Y,
|
|
15
|
-
D: Curses::KEY_CTRL_D
|
|
15
|
+
D: Curses::KEY_CTRL_D,
|
|
16
|
+
P: Curses::KEY_CTRL_P,
|
|
17
|
+
N: Curses::KEY_CTRL_N
|
|
16
18
|
}
|
|
17
19
|
|
|
20
|
+
##
|
|
21
|
+
# This hash tracks how many times a given key
|
|
22
|
+
# was pressed repeatedly without being
|
|
23
|
+
# interrupted by another key. The previous key
|
|
24
|
+
# is reset to 0 when a different key is pressed.
|
|
25
|
+
REPEATS = {}
|
|
26
|
+
REPEATS.default = 0
|
|
27
|
+
|
|
18
28
|
UP = Curses::Key::UP
|
|
19
29
|
DOWN = Curses::Key::DOWN
|
|
20
30
|
LEFT = Curses::Key::LEFT
|
|
21
31
|
RIGHT = Curses::Key::RIGHT
|
|
32
|
+
PGUP = Curses::KEY_PPAGE
|
|
33
|
+
PGDOWN = Curses::KEY_NPAGE
|
|
34
|
+
|
|
35
|
+
TAB = 9
|
|
22
36
|
ESC = 27
|
|
23
|
-
ENTER =
|
|
24
|
-
BACKSPACE =
|
|
37
|
+
ENTER = 10
|
|
38
|
+
BACKSPACE = 127
|
|
25
39
|
|
|
26
40
|
##
|
|
27
41
|
# Threshold in seconds. If characters arrive faster than
|
|
@@ -35,21 +49,24 @@ class LLM::Repl
|
|
|
35
49
|
attr_reader :buffer
|
|
36
50
|
|
|
37
51
|
##
|
|
38
|
-
# @param [Boolean]
|
|
52
|
+
# @param [Boolean] value
|
|
39
53
|
# @return [void]
|
|
40
54
|
attr_writer :paste
|
|
41
55
|
|
|
42
56
|
##
|
|
43
|
-
# @param [LLM::
|
|
57
|
+
# @param [LLM::Repl] repl
|
|
44
58
|
# @return [LLM::Repl::Input]
|
|
45
|
-
def initialize(
|
|
46
|
-
@
|
|
47
|
-
@
|
|
59
|
+
def initialize(repl, options = {})
|
|
60
|
+
@name = repl.name
|
|
61
|
+
@agent = repl.agent
|
|
62
|
+
@provider = @agent.llm.name
|
|
48
63
|
@buffer = +""
|
|
49
64
|
@cursor = 0
|
|
50
65
|
@scroll = 0
|
|
51
66
|
@height = options.fetch(:height, 3)
|
|
52
67
|
@last_char_at = nil
|
|
68
|
+
@memory = @agent.messages.select(&:user?).map(&:content)
|
|
69
|
+
@walker = Walker.new(@memory)
|
|
53
70
|
@paste = false
|
|
54
71
|
end
|
|
55
72
|
|
|
@@ -59,8 +76,28 @@ class LLM::Repl
|
|
|
59
76
|
# @return [Symbol, nil]
|
|
60
77
|
def on_char(window, char, now)
|
|
61
78
|
is_paste = lambda { @last_char_at and (now - @last_char_at) < PASTE_THRESHOLD }
|
|
62
|
-
if
|
|
79
|
+
if char and @char != char
|
|
80
|
+
REPEATS[@char] = 0
|
|
81
|
+
end
|
|
82
|
+
if PGUP == char
|
|
83
|
+
(window.rows - 3).times { window.scroll_up }
|
|
84
|
+
:pageup
|
|
85
|
+
elsif PGDOWN == char
|
|
86
|
+
(window.rows - 3).times { window.scroll_down }
|
|
87
|
+
:pagedown
|
|
88
|
+
elsif TAB == char
|
|
89
|
+
autocomplete
|
|
90
|
+
:tab
|
|
91
|
+
elsif ESC == char
|
|
63
92
|
@agent.cancel!
|
|
93
|
+
elsif CTRL[:P] == char
|
|
94
|
+
@buffer = @walker.prev.dup
|
|
95
|
+
@cursor = @buffer.size
|
|
96
|
+
:ctrl_p
|
|
97
|
+
elsif CTRL[:N] == char
|
|
98
|
+
@buffer = @walker.next.dup
|
|
99
|
+
@cursor = @buffer.size
|
|
100
|
+
:ctrl_n
|
|
64
101
|
elsif CTRL[:D] == char
|
|
65
102
|
delete
|
|
66
103
|
:ctrl_d
|
|
@@ -85,14 +122,16 @@ class LLM::Repl
|
|
|
85
122
|
elsif char == RIGHT
|
|
86
123
|
move_right
|
|
87
124
|
:right
|
|
88
|
-
elsif BACKSPACE
|
|
125
|
+
elsif BACKSPACE == char
|
|
89
126
|
backspace
|
|
90
127
|
:backspace
|
|
91
|
-
elsif ENTER
|
|
128
|
+
elsif ENTER == char
|
|
92
129
|
if @paste = is_paste.()
|
|
93
130
|
insert("\n")
|
|
94
131
|
:char
|
|
95
132
|
else
|
|
133
|
+
@memory.push(@buffer.dup)
|
|
134
|
+
@walker.cursor = @memory.size
|
|
96
135
|
:submit
|
|
97
136
|
end
|
|
98
137
|
elsif char == UP
|
|
@@ -108,19 +147,23 @@ class LLM::Repl
|
|
|
108
147
|
nil
|
|
109
148
|
end
|
|
110
149
|
ensure
|
|
111
|
-
|
|
150
|
+
if char
|
|
151
|
+
REPEATS[char] += 1
|
|
152
|
+
@last_char_at = now
|
|
153
|
+
@char = char
|
|
154
|
+
end
|
|
112
155
|
end
|
|
113
156
|
|
|
114
157
|
##
|
|
115
158
|
# @return [String]
|
|
116
159
|
def to_s
|
|
117
|
-
"#{
|
|
160
|
+
"#{prompt}#{@buffer}"
|
|
118
161
|
end
|
|
119
162
|
|
|
120
163
|
##
|
|
121
164
|
# @return [Integer]
|
|
122
165
|
def cursor
|
|
123
|
-
prompt.
|
|
166
|
+
prompt.size + @cursor
|
|
124
167
|
end
|
|
125
168
|
|
|
126
169
|
##
|
|
@@ -182,6 +225,28 @@ class LLM::Repl
|
|
|
182
225
|
@cursor = [0, @cursor + 1].max
|
|
183
226
|
end
|
|
184
227
|
|
|
228
|
+
##
|
|
229
|
+
# @return [void]
|
|
230
|
+
def autocomplete
|
|
231
|
+
return unless @buffer[0] == "/"
|
|
232
|
+
##
|
|
233
|
+
# This method implements a simple autocomplete
|
|
234
|
+
# that supports cycling through all known
|
|
235
|
+
# commands. When given tab in quick succession,
|
|
236
|
+
# we cycle to the nearest neighbour for the last
|
|
237
|
+
# full match. However, it's not based on similarity,
|
|
238
|
+
# it's just the next element in the array.
|
|
239
|
+
keys = LLM::Command.registry.keys
|
|
240
|
+
candidates = LLM::Command.complete(@buffer)
|
|
241
|
+
if REPEATS[TAB] >= 1
|
|
242
|
+
candidate = keys[keys.index(candidates[0]) + 1] || keys[0]
|
|
243
|
+
else
|
|
244
|
+
candidate = candidates[0]
|
|
245
|
+
end
|
|
246
|
+
@buffer = "/#{candidate}"
|
|
247
|
+
@cursor = @buffer.size
|
|
248
|
+
end
|
|
249
|
+
|
|
185
250
|
##
|
|
186
251
|
# @return [void]
|
|
187
252
|
def kill
|
|
@@ -238,7 +303,7 @@ class LLM::Repl
|
|
|
238
303
|
end
|
|
239
304
|
|
|
240
305
|
def prompt
|
|
241
|
-
"#{@provider}> "
|
|
306
|
+
"#{@provider}(#{@name})> "
|
|
242
307
|
end
|
|
243
308
|
|
|
244
309
|
def insert(char)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::Repl::Markdown
|
|
4
|
+
##
|
|
5
|
+
# Renders Kramdown `:table` nodes as aligned columns.
|
|
6
|
+
module Table
|
|
7
|
+
##
|
|
8
|
+
# @api private
|
|
9
|
+
Node = LLM::Repl::Node
|
|
10
|
+
private_constant :Node
|
|
11
|
+
|
|
12
|
+
##
|
|
13
|
+
# Renders a table node by collecting all cells first to
|
|
14
|
+
# compute column widths, then emitting each row with
|
|
15
|
+
# padded text.
|
|
16
|
+
def walk_table(node, attrs)
|
|
17
|
+
rows = collect_rows(node, attrs)
|
|
18
|
+
return if rows.empty?
|
|
19
|
+
widths = column_widths(rows)
|
|
20
|
+
rows.each do |row|
|
|
21
|
+
emit("| ", attrs)
|
|
22
|
+
row.each_with_index do |chunks, i|
|
|
23
|
+
width = widths[i]
|
|
24
|
+
chunks.each { |c| emit(c[:text].ljust(width), c[:attrs]) }
|
|
25
|
+
emit(" | ", attrs) unless i == row.size - 1
|
|
26
|
+
end
|
|
27
|
+
emit(" |", attrs)
|
|
28
|
+
emit("\n", attrs)
|
|
29
|
+
end
|
|
30
|
+
emit("\n", attrs)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def collect_rows(node, attrs)
|
|
36
|
+
node.children.each_with_object([]) do |section, rows|
|
|
37
|
+
next unless [:thead, :tbody].include?(section.type)
|
|
38
|
+
section.children.each do |tr|
|
|
39
|
+
next unless tr.type == :tr
|
|
40
|
+
cells = tr.children.filter_map do |td|
|
|
41
|
+
next unless [:td, :th].include?(td.type)
|
|
42
|
+
collect_chunks(td, attrs)
|
|
43
|
+
end
|
|
44
|
+
rows << cells
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def collect_chunks(node, attrs)
|
|
50
|
+
[].tap do |chunks|
|
|
51
|
+
walk_collect(node, attrs, chunks)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def walk_collect(node, attrs, chunks)
|
|
56
|
+
case node.type
|
|
57
|
+
when :text
|
|
58
|
+
chunks << Node.new(node.value.to_s, attrs)
|
|
59
|
+
when :strong
|
|
60
|
+
node.children.each { walk_collect(_1, Curses::A_BOLD, chunks) }
|
|
61
|
+
when :em
|
|
62
|
+
node.children.each { walk_collect(_1, Curses::A_UNDERLINE, chunks) }
|
|
63
|
+
when :codespan
|
|
64
|
+
chunks << Node.new(node.value, Curses::A_REVERSE)
|
|
65
|
+
when :a
|
|
66
|
+
node.children.each { walk_collect(_1, Curses::A_UNDERLINE, chunks) }
|
|
67
|
+
else
|
|
68
|
+
node.children.each { walk_collect(_1, attrs, chunks) }
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def column_widths(rows)
|
|
73
|
+
return [] if rows.empty?
|
|
74
|
+
cols = rows.first.size
|
|
75
|
+
(0...cols).map do |i|
|
|
76
|
+
rows.map { |r| r[i].map { _1[:text] }.join.length }.max
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
data/lib/llm/repl/markdown.rb
CHANGED
|
@@ -8,16 +8,21 @@ class LLM::Repl
|
|
|
8
8
|
# of text, and also optional style information for
|
|
9
9
|
# that text (eg bold, underscore, ...)
|
|
10
10
|
class Markdown
|
|
11
|
+
require_relative "markdown/table"
|
|
12
|
+
include Table
|
|
13
|
+
|
|
11
14
|
##
|
|
12
15
|
# @param [String] text
|
|
16
|
+
# @param [Integer] width
|
|
13
17
|
# @return [LLM::Repl::Markdown]
|
|
14
|
-
def initialize(text)
|
|
18
|
+
def initialize(text, width)
|
|
15
19
|
@doc = Kramdown::Document.new(text)
|
|
20
|
+
@width = width
|
|
16
21
|
@ast = []
|
|
17
22
|
end
|
|
18
23
|
|
|
19
24
|
##
|
|
20
|
-
# @return [Array<
|
|
25
|
+
# @return [Array<Node>]
|
|
21
26
|
def ast
|
|
22
27
|
@ast.tap do
|
|
23
28
|
##
|
|
@@ -73,13 +78,38 @@ class LLM::Repl
|
|
|
73
78
|
emit("\n\n", attrs)
|
|
74
79
|
when :br
|
|
75
80
|
emit("\n", attrs)
|
|
81
|
+
when :ul, :ol
|
|
82
|
+
node.children.each { walk(_1, attrs) }
|
|
83
|
+
when :li
|
|
84
|
+
node.children.each { walk(_1, attrs) }
|
|
85
|
+
when :table
|
|
86
|
+
walk_table(node, attrs)
|
|
87
|
+
when :thead, :tbody
|
|
88
|
+
node.children.each { walk(_1, attrs) }
|
|
89
|
+
when :tr
|
|
90
|
+
emit("| ", attrs)
|
|
91
|
+
node.children.each { walk(_1, attrs) }
|
|
92
|
+
emit("\n", attrs)
|
|
93
|
+
when :td, :th
|
|
94
|
+
node.children.each { walk(_1, attrs) }
|
|
95
|
+
emit(" | ", attrs)
|
|
96
|
+
when :blockquote
|
|
97
|
+
emit("> ", attrs)
|
|
98
|
+
node.children.each { walk(_1, attrs) }
|
|
99
|
+
when :hr
|
|
100
|
+
emit("─" * @width, attrs)
|
|
101
|
+
emit("\n\n", attrs)
|
|
102
|
+
when :a
|
|
103
|
+
node.children.each { walk(_1, Curses::A_UNDERLINE) }
|
|
104
|
+
when :img
|
|
105
|
+
emit("[image: #{node.attr["alt"]}]", attrs)
|
|
76
106
|
else
|
|
77
107
|
node.children.each { walk(_1, attrs) }
|
|
78
108
|
end
|
|
79
109
|
end
|
|
80
110
|
|
|
81
111
|
def emit(text, attrs)
|
|
82
|
-
@ast.push(
|
|
112
|
+
@ast.push(Node.new(text.to_s, attrs))
|
|
83
113
|
end
|
|
84
114
|
end
|
|
85
115
|
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::Repl
|
|
4
|
+
##
|
|
5
|
+
# The {LLM::Repl::Node LLM::Repl::Node} class wraps a piece
|
|
6
|
+
# of text and optional curses attributes.
|
|
7
|
+
# @api private
|
|
8
|
+
class Node
|
|
9
|
+
##
|
|
10
|
+
# @return [String]
|
|
11
|
+
attr_reader :text
|
|
12
|
+
|
|
13
|
+
##
|
|
14
|
+
# @return [Integer, nil]
|
|
15
|
+
attr_reader :attrs
|
|
16
|
+
|
|
17
|
+
##
|
|
18
|
+
# @param [String] text
|
|
19
|
+
# @param [Integer, nil] attrs
|
|
20
|
+
# @return [LLM::Repl::Node]
|
|
21
|
+
def initialize(text, attrs = nil)
|
|
22
|
+
@text = text.to_s
|
|
23
|
+
@attrs = attrs
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
##
|
|
27
|
+
# Hash-like lookup.
|
|
28
|
+
# @param [Symbol] key
|
|
29
|
+
# @return [String, Integer, nil]
|
|
30
|
+
def [](key)
|
|
31
|
+
case key
|
|
32
|
+
when :text then @text
|
|
33
|
+
when :attrs then @attrs
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
data/lib/llm/repl/status.rb
CHANGED
|
@@ -7,11 +7,11 @@ class LLM::Repl
|
|
|
7
7
|
# @api private
|
|
8
8
|
class Status
|
|
9
9
|
##
|
|
10
|
-
# @param [
|
|
10
|
+
# @param [LLM::Repl] repl
|
|
11
11
|
# @return [LLM::Repl::Status]
|
|
12
|
-
def initialize(
|
|
13
|
-
@agent = agent
|
|
14
|
-
@provider = agent.llm.name
|
|
12
|
+
def initialize(repl)
|
|
13
|
+
@agent = repl.agent
|
|
14
|
+
@provider = @agent.llm.name
|
|
15
15
|
@text = "idle"
|
|
16
16
|
end
|
|
17
17
|
|
data/lib/llm/repl/stream.rb
CHANGED
|
@@ -42,9 +42,9 @@ class LLM::Repl
|
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
##
|
|
45
|
-
#
|
|
45
|
+
# Clear the accumulated buffer
|
|
46
46
|
# @return [void]
|
|
47
|
-
def
|
|
47
|
+
def clear
|
|
48
48
|
@buffer.clear
|
|
49
49
|
end
|
|
50
50
|
|
|
@@ -60,9 +60,16 @@ class LLM::Repl
|
|
|
60
60
|
# @param [Integer] max
|
|
61
61
|
# @return [String]
|
|
62
62
|
def format_args(tool, max: 50)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
##
|
|
64
|
+
# 'tool.arguments' might be returned
|
|
65
|
+
# (by the model) in a different order
|
|
66
|
+
# than the tool definition - this code
|
|
67
|
+
# handles re-sorting.
|
|
68
|
+
args = tool.arguments
|
|
69
|
+
props = tool.params.properties.keys
|
|
70
|
+
props = props.sort_by { tool.params.properties[_1].index }
|
|
71
|
+
props = props.filter_map { args[_1] ? "#{_1}: #{format_value(args[_1])}" : nil }
|
|
72
|
+
result = props.join(", ")
|
|
66
73
|
result.size > max ? "#{result[0...max - 1]}…" : result
|
|
67
74
|
end
|
|
68
75
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::Repl
|
|
4
|
+
##
|
|
5
|
+
# Walks through an array from the last element backward,
|
|
6
|
+
# one step at a time. {#next} moves forward toward the end.
|
|
7
|
+
# Both methods clamp at the array boundaries.
|
|
8
|
+
class Walker
|
|
9
|
+
##
|
|
10
|
+
# Sets the cursor position
|
|
11
|
+
attr_writer :cursor
|
|
12
|
+
|
|
13
|
+
##
|
|
14
|
+
# @param [Array] items
|
|
15
|
+
def initialize(items)
|
|
16
|
+
@items = items
|
|
17
|
+
@cursor = items.size
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
##
|
|
21
|
+
# @return [Object, nil]
|
|
22
|
+
def next
|
|
23
|
+
if @items.empty?
|
|
24
|
+
nil
|
|
25
|
+
elsif @cursor >= @items.size - 1
|
|
26
|
+
@items[@cursor]
|
|
27
|
+
else
|
|
28
|
+
@cursor += 1
|
|
29
|
+
@items[@cursor]
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
##
|
|
34
|
+
# @return [Object, nil]
|
|
35
|
+
def prev
|
|
36
|
+
if @items.empty?
|
|
37
|
+
nil
|
|
38
|
+
elsif @cursor <= 0
|
|
39
|
+
@items[@cursor]
|
|
40
|
+
else
|
|
41
|
+
@cursor -= 1
|
|
42
|
+
@items[@cursor]
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
data/lib/llm/repl/window.rb
CHANGED
|
@@ -11,22 +11,21 @@ class LLM::Repl
|
|
|
11
11
|
attr_reader :status
|
|
12
12
|
|
|
13
13
|
##
|
|
14
|
-
# @return [LLM::Repl::
|
|
15
|
-
attr_reader :
|
|
14
|
+
# @return [LLM::Repl::Buffer]
|
|
15
|
+
attr_reader :buffer
|
|
16
16
|
|
|
17
17
|
##
|
|
18
18
|
# @return [LLM::Repl::Input]
|
|
19
19
|
attr_reader :input
|
|
20
20
|
|
|
21
21
|
##
|
|
22
|
-
# @param [LLM::Repl
|
|
23
|
-
#
|
|
24
|
-
# @param [LLM::Repl::Input] input
|
|
22
|
+
# @param [LLM::Repl] repl
|
|
23
|
+
# A read-eval-print loop.
|
|
25
24
|
# @return [LLM::Repl::Window]
|
|
26
|
-
def initialize(
|
|
27
|
-
@status = status
|
|
28
|
-
@
|
|
29
|
-
@input
|
|
25
|
+
def initialize(repl)
|
|
26
|
+
@status = repl.status
|
|
27
|
+
@buffer = repl.buffer
|
|
28
|
+
@input = repl.input
|
|
30
29
|
end
|
|
31
30
|
|
|
32
31
|
##
|
|
@@ -48,7 +47,7 @@ class LLM::Repl
|
|
|
48
47
|
def redraw
|
|
49
48
|
draw_status(offset: input.height + 1)
|
|
50
49
|
draw_divider(offset: 5)
|
|
51
|
-
|
|
50
|
+
draw_buffer(offset: 0)
|
|
52
51
|
draw_input
|
|
53
52
|
Curses.refresh
|
|
54
53
|
end
|
|
@@ -86,23 +85,42 @@ class LLM::Repl
|
|
|
86
85
|
##
|
|
87
86
|
# @return [void]
|
|
88
87
|
def scroll_up
|
|
89
|
-
|
|
88
|
+
buffer.scroll_up(rows)
|
|
90
89
|
end
|
|
91
90
|
|
|
92
91
|
##
|
|
93
92
|
# @return [void]
|
|
94
93
|
def scroll_down
|
|
95
|
-
|
|
94
|
+
buffer.scroll_down
|
|
96
95
|
end
|
|
97
96
|
|
|
98
97
|
##
|
|
99
98
|
# @return [void]
|
|
100
99
|
def scroll_to_bottom
|
|
101
|
-
|
|
100
|
+
buffer.scroll_to_bottom
|
|
102
101
|
end
|
|
103
102
|
|
|
104
103
|
private
|
|
105
104
|
|
|
105
|
+
def draw_buffer(offset:)
|
|
106
|
+
rows = buffer.visible(self.rows)
|
|
107
|
+
rows.each.with_index(offset) do |row, index|
|
|
108
|
+
Curses.setpos(index, 0)
|
|
109
|
+
Curses.clrtoeol
|
|
110
|
+
row.each do |chunk|
|
|
111
|
+
text, attrs = chunk.text, chunk.attrs
|
|
112
|
+
Curses.attron(attrs) if attrs
|
|
113
|
+
Curses.addstr(text)
|
|
114
|
+
Curses.attroff(attrs) if attrs
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
last_drawn = offset + rows.size
|
|
118
|
+
(last_drawn...self.rows).each do |line|
|
|
119
|
+
Curses.setpos(line, 0)
|
|
120
|
+
Curses.clrtoeol
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
106
124
|
def draw_status(offset:)
|
|
107
125
|
Curses.setpos(Curses.lines - offset, 0)
|
|
108
126
|
Curses.clrtoeol
|
|
@@ -132,25 +150,6 @@ class LLM::Repl
|
|
|
132
150
|
Curses.setpos((Curses.lines - input.height) + line, col)
|
|
133
151
|
end
|
|
134
152
|
|
|
135
|
-
def draw_transcript(offset:)
|
|
136
|
-
rows = transcript.visible(self.rows)
|
|
137
|
-
rows.each.with_index(offset) do |row, index|
|
|
138
|
-
Curses.setpos(index, 0)
|
|
139
|
-
Curses.clrtoeol
|
|
140
|
-
row.each do |chunk|
|
|
141
|
-
text, attrs = chunk.values_at(:text, :attrs)
|
|
142
|
-
Curses.attron(attrs) if attrs
|
|
143
|
-
Curses.addstr(text)
|
|
144
|
-
Curses.attroff(attrs) if attrs
|
|
145
|
-
end
|
|
146
|
-
end
|
|
147
|
-
last_drawn = offset + rows.size
|
|
148
|
-
(last_drawn...self.rows).each do |line|
|
|
149
|
-
Curses.setpos(line, 0)
|
|
150
|
-
Curses.clrtoeol
|
|
151
|
-
end
|
|
152
|
-
end
|
|
153
|
-
|
|
154
153
|
##
|
|
155
154
|
# @return [Integer]
|
|
156
155
|
def columns
|