aids 0.1.0 → 1.0.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/ansi.rb +4 -4
- data/lib/app.rb +32 -34
- data/lib/commands.rb +18 -24
- data/lib/line_editor.rb +208 -69
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4602f3e7991c9b5ae3e98e512b153aee77ab8f087d3c825334d3da74fd4f671d
|
|
4
|
+
data.tar.gz: b59f603e9faaf95c9dd6f6cc2a427e86529971f1ebe70bd3128430a2749310df
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d6c5fb9d07be2e3b2c569794f81cf5524fdf2b4bdfb60db2c26e4418b878db3d1be63dbd600625b97a5810653dc68374dc59cac2a6e3bb999078f367d6b0148a
|
|
7
|
+
data.tar.gz: b5eecfa573d111c185fb4e91d1650499b64bd910c8b678af532ad98bcbb9d3d5a8adebc2a2f92154d06b7724e1b142800bc74f8b9a086429a3e3d57cbb618fc5
|
data/lib/ansi.rb
CHANGED
|
@@ -3,10 +3,10 @@ module AI
|
|
|
3
3
|
module Ansi
|
|
4
4
|
module_function
|
|
5
5
|
|
|
6
|
-
def reset
|
|
7
|
-
def bold
|
|
8
|
-
def dim
|
|
9
|
-
def fg(n)
|
|
6
|
+
def reset = "\e[0m"
|
|
7
|
+
def bold = "\e[1m"
|
|
8
|
+
def dim = "\e[2m"
|
|
9
|
+
def fg(n) = "\e[38;5;#{n}m"
|
|
10
10
|
def strip(s) = s.gsub(/\e\[[\d;]*m/, "")
|
|
11
11
|
|
|
12
12
|
def width(s)
|
data/lib/app.rb
CHANGED
|
@@ -4,7 +4,7 @@ module AI
|
|
|
4
4
|
TITLE_PROMPT = "Give a concise title (3-10 words) for this conversation. " \
|
|
5
5
|
"Respond with ONLY the title, nothing else."
|
|
6
6
|
|
|
7
|
-
DISCARD_USAGE = "usage: /discard [
|
|
7
|
+
DISCARD_USAGE = "usage: /discard [* | N | A-B | A- | -B]"
|
|
8
8
|
|
|
9
9
|
attr_reader :session, :attachments
|
|
10
10
|
|
|
@@ -12,8 +12,8 @@ module AI
|
|
|
12
12
|
@key = key; @profile = profile
|
|
13
13
|
@editor = LineEditor.new
|
|
14
14
|
@editor.completer = ->(prefix) { complete(prefix) }
|
|
15
|
-
@hl
|
|
16
|
-
@fresh
|
|
15
|
+
@hl = Highlighter.new
|
|
16
|
+
@fresh = true
|
|
17
17
|
@attachments = []
|
|
18
18
|
@title_thread = nil
|
|
19
19
|
if resume && (st = Session.all.last) && (s = Session.load(st))
|
|
@@ -33,8 +33,7 @@ module AI
|
|
|
33
33
|
|
|
34
34
|
print_attachments(attached_data) if attached_data.any?
|
|
35
35
|
puts "\n#{Ansi.fg(p.color)}#{p.icon} #{p.name}#{Ansi.reset}"
|
|
36
|
-
reply, usage =
|
|
37
|
-
begin
|
|
36
|
+
reply, usage = begin
|
|
38
37
|
AI.stream(pending, p.system, @key) { |chunk| print @hl.paint(chunk) }
|
|
39
38
|
rescue Interrupt
|
|
40
39
|
puts "\n#{Ansi.fg(220)}[interrupted — turn discarded]#{Ansi.reset}"
|
|
@@ -52,11 +51,10 @@ module AI
|
|
|
52
51
|
|
|
53
52
|
def discard(arg)
|
|
54
53
|
arg = arg.to_s.strip
|
|
55
|
-
return discard_session if arg == "
|
|
54
|
+
return discard_session if arg == "*"
|
|
56
55
|
|
|
57
56
|
total = @session.turns
|
|
58
|
-
range =
|
|
59
|
-
if arg.empty?
|
|
57
|
+
range = if arg.empty?
|
|
60
58
|
[total, total]
|
|
61
59
|
else
|
|
62
60
|
parsed = parse_discard_arg(arg, total)
|
|
@@ -66,7 +64,7 @@ module AI
|
|
|
66
64
|
|
|
67
65
|
first, last = range
|
|
68
66
|
first = first.clamp(1, total)
|
|
69
|
-
last
|
|
67
|
+
last = last.clamp(1, total)
|
|
70
68
|
if last < first
|
|
71
69
|
notify "no turns in that range"; return
|
|
72
70
|
end
|
|
@@ -104,12 +102,12 @@ module AI
|
|
|
104
102
|
src = @session
|
|
105
103
|
dup = Session.new
|
|
106
104
|
dup.messages = Marshal.load(Marshal.dump(src.messages))
|
|
107
|
-
dup.usages
|
|
108
|
-
dup.title
|
|
105
|
+
dup.usages = Marshal.load(Marshal.dump(src.usages))
|
|
106
|
+
dup.title = src.title ? "#{src.title} (clone)" : "(clone)"
|
|
109
107
|
dup.save(@profile)
|
|
110
108
|
FileUtils.cp(src.history_file, dup.history_file) if File.exist?(src.history_file)
|
|
111
109
|
@session = dup
|
|
112
|
-
@fresh
|
|
110
|
+
@fresh = false
|
|
113
111
|
load_history
|
|
114
112
|
redraw
|
|
115
113
|
notify "cloned to new session"
|
|
@@ -155,19 +153,19 @@ module AI
|
|
|
155
153
|
end
|
|
156
154
|
expanded = File.expand_path(arg)
|
|
157
155
|
targets = if File.directory?(expanded)
|
|
158
|
-
|
|
159
|
-
else
|
|
160
|
-
exact = @attachments.find { |p| Paths.short(p) == arg }
|
|
161
|
-
if exact
|
|
162
|
-
[exact]
|
|
156
|
+
@attachments.select { |p| File.dirname(p) == expanded }
|
|
163
157
|
else
|
|
164
|
-
@attachments.
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
158
|
+
exact = @attachments.find { |p| Paths.short(p) == arg }
|
|
159
|
+
if exact
|
|
160
|
+
[exact]
|
|
161
|
+
else
|
|
162
|
+
@attachments.select { |p|
|
|
163
|
+
File.fnmatch(arg, File.basename(p)) ||
|
|
164
|
+
File.fnmatch(arg, Paths.short(p)) ||
|
|
165
|
+
File.fnmatch(arg, p)
|
|
166
|
+
}
|
|
167
|
+
end
|
|
169
168
|
end
|
|
170
|
-
end
|
|
171
169
|
if targets.empty?
|
|
172
170
|
notify "no matching attachments: #{arg}"
|
|
173
171
|
return
|
|
@@ -187,12 +185,12 @@ module AI
|
|
|
187
185
|
redraw if @session.messages.any?
|
|
188
186
|
loop do
|
|
189
187
|
puts format_attachment_summary if @attachments.any?
|
|
190
|
-
result = begin; @editor.readline(build_prompt);
|
|
188
|
+
result = begin; @editor.readline(build_prompt); rescue Interrupt; break; end
|
|
191
189
|
case result
|
|
192
|
-
when nil
|
|
193
|
-
when :sess_next
|
|
194
|
-
when :sess_prev
|
|
195
|
-
when :redraw
|
|
190
|
+
when nil then break
|
|
191
|
+
when :sess_next then switch_session(+1)
|
|
192
|
+
when :sess_prev then switch_session(-1)
|
|
193
|
+
when :redraw then redraw
|
|
196
194
|
when :del_session then delete_session
|
|
197
195
|
when String
|
|
198
196
|
t = result.strip
|
|
@@ -251,10 +249,10 @@ module AI
|
|
|
251
249
|
def complete(prefix)
|
|
252
250
|
return nil unless Commands.slash?(prefix)
|
|
253
251
|
if (m = prefix.match(/\A(\/\S+)\s+/))
|
|
254
|
-
name
|
|
252
|
+
name = m[1]
|
|
255
253
|
arg_start = m.end(0)
|
|
256
|
-
arg
|
|
257
|
-
cmd
|
|
254
|
+
arg = prefix[arg_start..]
|
|
255
|
+
cmd = Commands.find(name)
|
|
258
256
|
return nil unless cmd && cmd.available?(self) && cmd.arg_complete
|
|
259
257
|
{ matches: cmd.arg_complete.call(self, arg), start: arg_start }
|
|
260
258
|
else
|
|
@@ -264,7 +262,7 @@ module AI
|
|
|
264
262
|
|
|
265
263
|
def handle_command(input)
|
|
266
264
|
case Commands.dispatch(self, input)
|
|
267
|
-
when :unknown
|
|
265
|
+
when :unknown then notify "unknown command: #{input.split.first}"
|
|
268
266
|
when :unavailable then notify "not available right now: #{input.split.first}"
|
|
269
267
|
end
|
|
270
268
|
end
|
|
@@ -279,7 +277,7 @@ module AI
|
|
|
279
277
|
|
|
280
278
|
def center(str)
|
|
281
279
|
cols = IO.console&.winsize&.last || 80
|
|
282
|
-
pad
|
|
280
|
+
pad = [(cols - Ansi.width(str)) / 2, 0].max
|
|
283
281
|
(" " * pad) + str
|
|
284
282
|
end
|
|
285
283
|
|
|
@@ -368,7 +366,7 @@ module AI
|
|
|
368
366
|
def draw_meta
|
|
369
367
|
all = Session.all
|
|
370
368
|
idx = all.index(@session.stamp)
|
|
371
|
-
n
|
|
369
|
+
n = idx ? idx + 1 : all.length + 1
|
|
372
370
|
total = [all.length, n].max
|
|
373
371
|
puts center("#{CMT_STYLE}session #{n}/#{total} \u00b7 #{@session.human_time}#{Ansi.reset}")
|
|
374
372
|
end
|
data/lib/commands.rb
CHANGED
|
@@ -9,11 +9,11 @@ module AI
|
|
|
9
9
|
|
|
10
10
|
def self.register(name, desc:, available: nil, arg_complete: nil, &run)
|
|
11
11
|
REGISTRY[name] = Command.new(
|
|
12
|
-
name: name, desc: desc, available: available, arg_complete: arg_complete, run: run
|
|
12
|
+
name: name, desc: desc, available: available, arg_complete: arg_complete, run: run,
|
|
13
13
|
)
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
def self.find(name)
|
|
16
|
+
def self.find(name) = REGISTRY[name]
|
|
17
17
|
def self.slash?(input) = input.start_with?("/")
|
|
18
18
|
|
|
19
19
|
def self.available_names(app)
|
|
@@ -33,48 +33,42 @@ module AI
|
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
register("/discard",
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
) do |app, args|
|
|
36
|
+
desc: "Remove turn(s): /discard [*|N|A-B|A-|-B]",
|
|
37
|
+
available: ->(app) { !app.session.empty? },
|
|
38
|
+
arg_complete: ->(_app, arg) { %w[*].select { |x| x.start_with?(arg) } }) do |app, args|
|
|
40
39
|
app.discard(args)
|
|
41
40
|
end
|
|
42
41
|
|
|
43
42
|
register("/title",
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
) do |app, args|
|
|
43
|
+
desc: "Set the session title",
|
|
44
|
+
available: ->(app) { !app.session.empty? }) do |app, args|
|
|
47
45
|
app.set_title(args.strip)
|
|
48
46
|
end
|
|
49
47
|
|
|
50
48
|
register("/clone",
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
) do |app, _|
|
|
49
|
+
desc: "Clone this session into a new active one",
|
|
50
|
+
available: ->(app) { !app.session.empty? }) do |app, _|
|
|
54
51
|
app.clone_session
|
|
55
52
|
end
|
|
56
53
|
|
|
57
54
|
register("/attach",
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
) do |app, args|
|
|
55
|
+
desc: "Attach a file or directory to the next turn",
|
|
56
|
+
arg_complete: ->(_app, arg) { Paths.complete(arg) }) do |app, args|
|
|
61
57
|
app.attach(args.strip)
|
|
62
58
|
end
|
|
63
59
|
|
|
64
60
|
register("/detach",
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
) do |app, args|
|
|
61
|
+
desc: "Remove an attachment",
|
|
62
|
+
available: ->(app) { app.attachments.any? },
|
|
63
|
+
arg_complete: ->(app, arg) {
|
|
64
|
+
app.attachments.map { |p| Paths.short(p) }.select { |s| s.start_with?(arg) }
|
|
65
|
+
}) do |app, args|
|
|
71
66
|
app.detach(args.strip)
|
|
72
67
|
end
|
|
73
68
|
|
|
74
69
|
register("/files",
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
) do |app, _|
|
|
70
|
+
desc: "List currently attached files",
|
|
71
|
+
available: ->(app) { app.attachments.any? }) do |app, _|
|
|
78
72
|
app.list_files
|
|
79
73
|
end
|
|
80
74
|
end
|
data/lib/line_editor.rb
CHANGED
|
@@ -1,64 +1,70 @@
|
|
|
1
1
|
module AI
|
|
2
|
-
# ── Mini line editor with history + Tab completion
|
|
2
|
+
# ── Mini line editor with history + Tab completion ───────────────────
|
|
3
3
|
class LineEditor
|
|
4
4
|
attr_accessor :history, :completer
|
|
5
5
|
|
|
6
6
|
SIGNALS = {
|
|
7
|
-
"\ej"
|
|
8
|
-
"\ek"
|
|
9
|
-
"\el"
|
|
7
|
+
"\ej" => :sess_next,
|
|
8
|
+
"\ek" => :sess_prev,
|
|
9
|
+
"\el" => :redraw, "\eL" => :redraw,
|
|
10
10
|
"\e[3;7~" => :del_session, "\e\e[3;5~" => :del_session,
|
|
11
|
-
"\e[3;5~" => :del_session, "\e\e[3^"
|
|
12
|
-
"\e[3^"
|
|
11
|
+
"\e[3;5~" => :del_session, "\e\e[3^" => :del_session,
|
|
12
|
+
"\e[3^" => :del_session, "\e[3;8~" => :del_session,
|
|
13
13
|
}.freeze
|
|
14
14
|
|
|
15
|
+
PASTE_STYLE = "\e[48;5;18;38;5;231m"
|
|
16
|
+
BPASTE_ON = "\e[?2004h"
|
|
17
|
+
BPASTE_OFF = "\e[?2004l"
|
|
18
|
+
|
|
15
19
|
def initialize; @history = []; @completer = nil; end
|
|
16
20
|
|
|
17
21
|
def readline(prompt)
|
|
18
|
-
@prompt = prompt; @buf = +""; @cur = 0
|
|
22
|
+
@prompt = prompt.to_s; @buf = +""; @cur = 0
|
|
19
23
|
@hpos = @history.length; @stash = nil
|
|
20
|
-
@prev_rows =
|
|
24
|
+
@prev_rows = 1; @prev_cur_row = 0
|
|
21
25
|
@cycle = nil
|
|
26
|
+
@pastes = []
|
|
27
|
+
print BPASTE_ON
|
|
22
28
|
render
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
29
|
+
begin
|
|
30
|
+
$stdin.raw do |io|
|
|
31
|
+
loop do
|
|
32
|
+
k = read_key(io) or next
|
|
33
|
+
if (sig = SIGNALS[k]) then return signal(sig) end
|
|
34
|
+
@cycle = nil unless k == "\t"
|
|
35
|
+
case k
|
|
36
|
+
when "\e[200~" then collect_paste(io)
|
|
37
|
+
when "\r", "\n" then insert("\n")
|
|
38
|
+
when "\e\r", "\e\n" then return submit
|
|
39
|
+
when "\t" then handle_tab
|
|
40
|
+
when "\x03" then end_render; print "\r\n"; raise Interrupt
|
|
41
|
+
when "\e[3~" then forward_delete
|
|
42
|
+
when "\x02", "\e[D" then move(-1)
|
|
43
|
+
when "\x06", "\e[C" then move(+1)
|
|
44
|
+
when "\x01", "\e[H", "\eOH", "\e[1~" then @cur = 0; render
|
|
45
|
+
when "\x05", "\e[F", "\eOF", "\e[4~" then @cur = @buf.length; render
|
|
46
|
+
when "\eb" then @cur = word_back; render
|
|
47
|
+
when "\ef" then @cur = word_fwd; render
|
|
48
|
+
when "\x10", "\e[A" then history_step(-1)
|
|
49
|
+
when "\x0e", "\e[B" then history_step(+1)
|
|
50
|
+
when "\x7f", "\b", "\x08" then backspace
|
|
51
|
+
when "\x04"
|
|
52
|
+
if @buf.empty? then end_render; print "\r\n"; return nil else forward_delete end
|
|
53
|
+
when "\x0b" then del_range(@cur, @buf.length); render
|
|
54
|
+
when "\x15" then del_range(0, @cur); render
|
|
55
|
+
when "\x17" then j = word_back; del_range(j, @cur); render
|
|
56
|
+
when "\ed" then j = word_fwd; del_range(@cur, j); render
|
|
57
|
+
when "\eu" then case_word(:upcase); render
|
|
58
|
+
when "\eU" then case_word(:downcase); render
|
|
59
|
+
when "\x14" then transpose; render
|
|
60
|
+
when "\x0c" then print "\e[H\e[2J"; @prev_rows = 0; @prev_cur_row = 0; render
|
|
61
|
+
when "\e" then nil
|
|
62
|
+
else insert(k) if printable?(k)
|
|
47
63
|
end
|
|
48
|
-
when "\x0b" then @buf.slice!(@cur..); render
|
|
49
|
-
when "\x15" then @buf.slice!(0...@cur); @cur = 0; render
|
|
50
|
-
when "\x17"
|
|
51
|
-
j = word_back; @buf.slice!(j...@cur); @cur = j; render
|
|
52
|
-
when "\ed"
|
|
53
|
-
j = word_fwd; @buf.slice!(@cur...j); render
|
|
54
|
-
when "\eu" then case_word(:upcase); render
|
|
55
|
-
when "\eU" then case_word(:downcase); render
|
|
56
|
-
when "\x14" then transpose; render
|
|
57
|
-
when "\x0c" then print "\e[H\e[2J"; @prev_rows = 0; @prev_cur_row = 0; render
|
|
58
|
-
when "\e" then nil
|
|
59
|
-
else insert(k) if printable?(k)
|
|
60
64
|
end
|
|
61
65
|
end
|
|
66
|
+
ensure
|
|
67
|
+
print BPASTE_OFF
|
|
62
68
|
end
|
|
63
69
|
end
|
|
64
70
|
|
|
@@ -69,12 +75,13 @@ module AI
|
|
|
69
75
|
def submit
|
|
70
76
|
end_render
|
|
71
77
|
print "\r\n"
|
|
72
|
-
|
|
73
|
-
@
|
|
78
|
+
out = @buf.dup
|
|
79
|
+
@history << out unless out.empty? || out == @history.last
|
|
80
|
+
out
|
|
74
81
|
end
|
|
75
82
|
|
|
76
83
|
def end_render
|
|
77
|
-
down = @prev_rows - 1 - @prev_cur_row
|
|
84
|
+
down = (@prev_rows - 1) - @prev_cur_row
|
|
78
85
|
print "\e[#{down}B" if down > 0
|
|
79
86
|
print "\r"
|
|
80
87
|
end
|
|
@@ -87,38 +94,78 @@ module AI
|
|
|
87
94
|
|
|
88
95
|
def printable?(k) = !k.start_with?("\e") && k.ord >= 32
|
|
89
96
|
|
|
90
|
-
|
|
97
|
+
# ── Buffer mutations (paste-aware) ───────────────────────────────
|
|
98
|
+
|
|
99
|
+
def insert(k)
|
|
100
|
+
@pastes.each { |p| p[:start] += k.length if p[:start] >= @cur }
|
|
101
|
+
@buf.insert(@cur, k)
|
|
102
|
+
@cur += k.length
|
|
103
|
+
render
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def del_range(from, to)
|
|
107
|
+
from = [from, 0].max
|
|
108
|
+
to = [to, @buf.length].min
|
|
109
|
+
return if to <= from
|
|
110
|
+
len = to - from
|
|
111
|
+
@pastes.reject! { |p| p[:start] >= from && p[:start] + p[:length] <= to }
|
|
112
|
+
@pastes.each { |p| p[:start] -= len if p[:start] >= to }
|
|
113
|
+
@buf.slice!(from, len)
|
|
114
|
+
@cur = from if @cur > from && @cur < to
|
|
115
|
+
@cur -= len if @cur >= to
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def backspace
|
|
119
|
+
return if @cur == 0
|
|
120
|
+
paste = @pastes.find { |p| @cur == p[:start] + p[:length] }
|
|
121
|
+
if paste
|
|
122
|
+
del_range(paste[:start], paste[:start] + paste[:length])
|
|
123
|
+
else
|
|
124
|
+
del_range(@cur - 1, @cur)
|
|
125
|
+
end
|
|
126
|
+
render
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def forward_delete
|
|
130
|
+
return if @cur >= @buf.length
|
|
131
|
+
paste = @pastes.find { |p| @cur == p[:start] }
|
|
132
|
+
if paste
|
|
133
|
+
del_range(paste[:start], paste[:start] + paste[:length])
|
|
134
|
+
else
|
|
135
|
+
del_range(@cur, @cur + 1)
|
|
136
|
+
end
|
|
137
|
+
render
|
|
138
|
+
end
|
|
91
139
|
|
|
92
140
|
def move(d)
|
|
93
141
|
nc = @cur + d
|
|
94
|
-
|
|
142
|
+
return unless nc.between?(0, @buf.length)
|
|
143
|
+
paste = @pastes.find { |p| nc > p[:start] && nc < p[:start] + p[:length] }
|
|
144
|
+
nc = (d > 0 ? paste[:start] + paste[:length] : paste[:start]) if paste
|
|
145
|
+
@cur = nc; render
|
|
95
146
|
end
|
|
96
147
|
|
|
148
|
+
# ── Tab completion ───────────────────────────────────────────────
|
|
149
|
+
|
|
97
150
|
def handle_tab
|
|
98
151
|
if @cycle
|
|
99
152
|
@cycle[:index] = (@cycle[:index] + 1) % @cycle[:matches].length
|
|
100
|
-
apply_cycle
|
|
101
|
-
return
|
|
153
|
+
apply_cycle; return
|
|
102
154
|
end
|
|
103
|
-
|
|
104
155
|
return unless @completer
|
|
105
156
|
prefix = @buf[0...@cur]
|
|
106
157
|
result = @completer.call(prefix) or return
|
|
107
158
|
matches, start = result[:matches], result[:start]
|
|
108
159
|
return if matches.nil? || matches.empty?
|
|
109
|
-
|
|
110
160
|
if matches.length == 1
|
|
111
|
-
replace_arg(start, matches[0])
|
|
112
|
-
return
|
|
161
|
+
replace_arg(start, matches[0]); return
|
|
113
162
|
end
|
|
114
|
-
|
|
115
163
|
common = matches.reduce do |a, b|
|
|
116
164
|
i = 0
|
|
117
165
|
i += 1 while i < a.length && i < b.length && a[i] == b[i]
|
|
118
166
|
a[0...i]
|
|
119
167
|
end
|
|
120
168
|
current = prefix[start..]
|
|
121
|
-
|
|
122
169
|
if common.length > current.length
|
|
123
170
|
replace_arg(start, common)
|
|
124
171
|
else
|
|
@@ -142,6 +189,8 @@ module AI
|
|
|
142
189
|
render
|
|
143
190
|
end
|
|
144
191
|
|
|
192
|
+
# ── Input reading ────────────────────────────────────────────────
|
|
193
|
+
|
|
145
194
|
def read_key(io)
|
|
146
195
|
b = io.getbyte or return nil
|
|
147
196
|
return read_escape(io) if b == 27
|
|
@@ -162,12 +211,13 @@ module AI
|
|
|
162
211
|
return "\e\e" unless IO.select([io], nil, nil, 0.05)
|
|
163
212
|
c2 = io.getbyte or return "\e\e"
|
|
164
213
|
c2 == 91 ? read_csi(io, +"\e\e[") : "\e\e#{c2.chr}"
|
|
214
|
+
when 13, 10 then "\e\r"
|
|
165
215
|
else "\e#{c.chr}"
|
|
166
216
|
end
|
|
167
217
|
end
|
|
168
218
|
|
|
169
219
|
def read_csi(io, buf)
|
|
170
|
-
|
|
220
|
+
24.times do
|
|
171
221
|
nb = io.getbyte or break
|
|
172
222
|
buf << nb.chr
|
|
173
223
|
break if nb.between?(64, 126)
|
|
@@ -175,32 +225,120 @@ module AI
|
|
|
175
225
|
buf
|
|
176
226
|
end
|
|
177
227
|
|
|
228
|
+
# ── Bracketed paste collection ───────────────────────────────────
|
|
229
|
+
|
|
230
|
+
def collect_paste(io)
|
|
231
|
+
terminator = "\e[201~".bytes
|
|
232
|
+
raw = +"".b
|
|
233
|
+
window = []
|
|
234
|
+
loop do
|
|
235
|
+
b = io.getbyte or break
|
|
236
|
+
window << b; window.shift if window.length > terminator.length
|
|
237
|
+
raw << b.chr
|
|
238
|
+
break if window == terminator
|
|
239
|
+
end
|
|
240
|
+
raw.slice!(-terminator.length, terminator.length) if raw.bytes.last(terminator.length) == terminator
|
|
241
|
+
text = raw.force_encoding("UTF-8").scrub
|
|
242
|
+
return if text.empty?
|
|
243
|
+
text = text.gsub("\r\n", "\n").tr("\r", "\n")
|
|
244
|
+
start = @cur
|
|
245
|
+
lines = text.count("\n") + 1
|
|
246
|
+
@pastes.each { |p| p[:start] += text.length if p[:start] >= @cur }
|
|
247
|
+
@buf.insert(@cur, text)
|
|
248
|
+
@pastes << { start: start, length: text.length, lines: lines }
|
|
249
|
+
@pastes.sort_by! { |p| p[:start] }
|
|
250
|
+
@cur = start + text.length
|
|
251
|
+
render
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# ── Rendering ────────────────────────────────────────────────────
|
|
255
|
+
|
|
256
|
+
def paste_label_for(p)
|
|
257
|
+
n = p[:lines]
|
|
258
|
+
"#{PASTE_STYLE} #{n} #{n == 1 ? "line" : "lines"} pasted #{Ansi.reset}"
|
|
259
|
+
end
|
|
260
|
+
|
|
178
261
|
def render
|
|
179
262
|
cols = (IO.console&.winsize&.last || 80)
|
|
180
263
|
cols = 1 if cols < 1
|
|
264
|
+
|
|
181
265
|
print "\e[#{@prev_cur_row}A" if @prev_cur_row > 0
|
|
182
266
|
print "\r\e[J"
|
|
183
|
-
print "#{@prompt}#{@buf}"
|
|
184
267
|
|
|
185
268
|
prompt_w = Ansi.width(@prompt)
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
269
|
+
indent = " " * prompt_w
|
|
270
|
+
|
|
271
|
+
out = +""
|
|
272
|
+
out << @prompt
|
|
273
|
+
|
|
274
|
+
row = 0
|
|
275
|
+
col = prompt_w
|
|
276
|
+
cur_row = 0
|
|
277
|
+
cur_col = prompt_w
|
|
278
|
+
cur_recorded = false
|
|
279
|
+
|
|
280
|
+
advance = lambda do |w|
|
|
281
|
+
col += w
|
|
282
|
+
while col >= cols
|
|
283
|
+
row += 1
|
|
284
|
+
col -= cols
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
newline_to_indent = lambda do
|
|
289
|
+
out << "\r\n" << indent
|
|
290
|
+
row += 1
|
|
291
|
+
col = prompt_w
|
|
194
292
|
end
|
|
195
293
|
|
|
294
|
+
i = 0
|
|
295
|
+
while i <= @buf.length
|
|
296
|
+
if !cur_recorded && i == @cur
|
|
297
|
+
cur_row = row; cur_col = col; cur_recorded = true
|
|
298
|
+
end
|
|
299
|
+
break if i == @buf.length
|
|
300
|
+
|
|
301
|
+
paste = @pastes.find { |p| p[:start] == i }
|
|
302
|
+
if paste
|
|
303
|
+
newline_to_indent.call if col != prompt_w
|
|
304
|
+
label = paste_label_for(paste)
|
|
305
|
+
out << label
|
|
306
|
+
advance.call(Ansi.width(label))
|
|
307
|
+
i += paste[:length]
|
|
308
|
+
newline_to_indent.call
|
|
309
|
+
next
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
c = @buf[i]
|
|
313
|
+
if c == "\n"
|
|
314
|
+
newline_to_indent.call
|
|
315
|
+
i += 1
|
|
316
|
+
next
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
out << c
|
|
320
|
+
advance.call(Ansi.width(c))
|
|
321
|
+
i += 1
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
unless cur_recorded
|
|
325
|
+
cur_row = row; cur_col = col
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
print out
|
|
329
|
+
|
|
330
|
+
end_row = row
|
|
196
331
|
print "\r"
|
|
197
332
|
up = end_row - cur_row
|
|
198
|
-
print "\e[#{up}A"
|
|
333
|
+
print "\e[#{up}A" if up > 0
|
|
199
334
|
print "\e[#{cur_col}C" if cur_col > 0
|
|
200
|
-
|
|
335
|
+
|
|
336
|
+
@prev_rows = end_row + 1
|
|
201
337
|
@prev_cur_row = cur_row
|
|
202
338
|
end
|
|
203
339
|
|
|
340
|
+
# ── History / word ops ───────────────────────────────────────────
|
|
341
|
+
|
|
204
342
|
def history_step(d)
|
|
205
343
|
return if @history.empty?
|
|
206
344
|
@stash = @buf.dup if @hpos == @history.length
|
|
@@ -208,6 +346,7 @@ module AI
|
|
|
208
346
|
return if np == @hpos
|
|
209
347
|
@hpos = np
|
|
210
348
|
@buf = (@hpos == @history.length ? (@stash || +"") : @history[@hpos]).dup
|
|
349
|
+
@pastes = []
|
|
211
350
|
@cur = @buf.length; render
|
|
212
351
|
end
|
|
213
352
|
|