riffer-rig 0.2.0 → 0.3.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 +20 -0
- data/README.md +2 -2
- data/lib/riffer/rig/cli.rb +21 -1
- data/lib/riffer/rig/coding_agent.rb +7 -5
- data/lib/riffer/rig/credentials.rb +24 -4
- data/lib/riffer/rig/repl.rb +70 -5
- data/lib/riffer/rig/settings/pricing.rb +19 -0
- data/lib/riffer/rig/settings.rb +33 -8
- data/lib/riffer/rig/token_tally.rb +22 -6
- data/lib/riffer/rig/tools/bash.rb +19 -4
- data/lib/riffer/rig/tools/edit.rb +6 -0
- data/lib/riffer/rig/tools/read.rb +6 -1
- data/lib/riffer/rig/tools/write.rb +4 -0
- data/lib/riffer/rig/ui/animator.rb +51 -11
- data/lib/riffer/rig/ui/banner.rb +28 -4
- data/lib/riffer/rig/ui/cursor.rb +46 -0
- data/lib/riffer/rig/ui/palette.rb +13 -7
- data/lib/riffer/rig/ui/renderer.rb +38 -7
- data/lib/riffer/rig/ui/smoother.rb +31 -5
- data/lib/riffer/rig/ui/theme.rb +38 -7
- data/lib/riffer/rig/version.rb +1 -1
- data/sig/generated/riffer/rig/cli.rbs +41 -0
- data/sig/generated/riffer/rig/coding_agent.rbs +9 -0
- data/sig/generated/riffer/rig/credentials.rbs +55 -0
- data/sig/generated/riffer/rig/repl.rbs +76 -0
- data/sig/generated/riffer/rig/settings/pricing.rbs +23 -0
- data/sig/generated/riffer/rig/settings.rbs +92 -0
- data/sig/generated/riffer/rig/token_tally.rbs +61 -0
- data/sig/generated/riffer/rig/tools/bash.rbs +28 -0
- data/sig/generated/riffer/rig/tools/edit.rbs +11 -0
- data/sig/generated/riffer/rig/tools/read.rbs +12 -0
- data/sig/generated/riffer/rig/tools/write.rbs +9 -0
- data/sig/generated/riffer/rig/ui/animator.rbs +76 -0
- data/sig/generated/riffer/rig/ui/banner.rbs +42 -0
- data/sig/generated/riffer/rig/ui/cursor.rbs +33 -0
- data/sig/generated/riffer/rig/ui/palette.rbs +17 -0
- data/sig/generated/riffer/rig/ui/renderer.rbs +68 -0
- data/sig/generated/riffer/rig/ui/smoother.rbs +67 -0
- data/sig/generated/riffer/rig/ui/theme.rbs +60 -0
- data/sig/generated/riffer/rig/version.rbs +7 -0
- data/sig/generated/riffer/rig.rbs +2 -0
- data/sig/manual/riffer/rig/cli.rbs +8 -0
- data/sig/manual/riffer/rig/credentials.rbs +8 -0
- data/sig/manual/riffer/rig/settings.rbs +8 -0
- data/sig/manual/riffer/rig/tools.rbs +5 -0
- data/sig/manual/riffer/rig/ui/banner.rbs +8 -0
- data/sig/manual/riffer/rig/ui.rbs +5 -0
- metadata +30 -2
|
@@ -3,20 +3,36 @@
|
|
|
3
3
|
# Animated output is gated behind an interactive, colour-enabled TTY, so piped
|
|
4
4
|
# or tested runs stay silent and escape-free.
|
|
5
5
|
class Riffer::Rig::UI::Animator
|
|
6
|
-
REVEAL_FRAME_SECONDS = 0.05
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
REVEAL_FRAME_SECONDS = 0.05 #: Float
|
|
7
|
+
|
|
8
|
+
SPINNER_FRAME_SECONDS = 0.12 #: Float
|
|
9
|
+
|
|
10
|
+
EQ_LEVELS = '▁▂▃▄▅▆▇█'.chars.freeze #: Array[String]
|
|
11
|
+
|
|
12
|
+
EQ_BARS = 7 #: Integer
|
|
13
|
+
|
|
14
|
+
NEUTRAL_LABEL = 'riffing…' #: String
|
|
15
|
+
|
|
11
16
|
REASONING_PHRASES = [
|
|
12
17
|
'contemplating…', 'pondering…', 'mulling it over…', 'reasoning…',
|
|
13
18
|
'connecting the dots…', 'herding thoughts…', 'consulting the muse…',
|
|
14
19
|
'doing some deep listening…', 'warming up…', 'tuning up…',
|
|
15
20
|
'in the woodshed…', 'vamping…', 'finding the key…', 'counting it off…',
|
|
16
21
|
'jamming internally…'
|
|
17
|
-
].freeze
|
|
18
|
-
REASONING_TICK_RANGE = (1..5)
|
|
22
|
+
].freeze #: Array[String]
|
|
19
23
|
|
|
24
|
+
REASONING_TICK_RANGE = (1..5) #: Range[Integer]
|
|
25
|
+
|
|
26
|
+
# @rbs @io: untyped
|
|
27
|
+
# @rbs @theme: Riffer::Rig::UI::Theme
|
|
28
|
+
# @rbs @thread: Thread?
|
|
29
|
+
# @rbs @mode: Symbol
|
|
30
|
+
# @rbs @phrase: String?
|
|
31
|
+
# @rbs @stop: bool
|
|
32
|
+
|
|
33
|
+
# @rbs io: untyped
|
|
34
|
+
# @rbs ?theme: Riffer::Rig::UI::Theme
|
|
35
|
+
# @rbs return: void
|
|
20
36
|
def initialize(io: $stdout, theme: Riffer::Rig::UI::Theme.for(io))
|
|
21
37
|
@io = io
|
|
22
38
|
@theme = theme
|
|
@@ -26,6 +42,9 @@ class Riffer::Rig::UI::Animator
|
|
|
26
42
|
end
|
|
27
43
|
|
|
28
44
|
# When not on a TTY, prints the final frame once instead of animating.
|
|
45
|
+
#
|
|
46
|
+
# @rbs frames: Array[Array[String]]
|
|
47
|
+
# @rbs return: void
|
|
29
48
|
def reveal(frames)
|
|
30
49
|
unless enabled?
|
|
31
50
|
frames.last.each { |line| @io.puts(line) }
|
|
@@ -41,6 +60,8 @@ class Riffer::Rig::UI::Animator
|
|
|
41
60
|
end
|
|
42
61
|
end
|
|
43
62
|
|
|
63
|
+
# @rbs ?mode: Symbol
|
|
64
|
+
# @rbs return: void
|
|
44
65
|
def start(mode = :neutral)
|
|
45
66
|
return unless enabled?
|
|
46
67
|
|
|
@@ -54,16 +75,21 @@ class Riffer::Rig::UI::Animator
|
|
|
54
75
|
@thread = Thread.new { animate }
|
|
55
76
|
end
|
|
56
77
|
|
|
78
|
+
# @rbs return: void
|
|
57
79
|
def stop
|
|
58
|
-
|
|
80
|
+
thread = @thread
|
|
81
|
+
return unless thread
|
|
59
82
|
|
|
60
83
|
@stop = true
|
|
61
|
-
|
|
84
|
+
thread.join
|
|
62
85
|
@thread = nil
|
|
63
86
|
@io.print("\r\e[K")
|
|
64
87
|
@io.flush
|
|
65
88
|
end
|
|
66
89
|
|
|
90
|
+
# @rbs tick: Integer
|
|
91
|
+
# @rbs ?label: String
|
|
92
|
+
# @rbs return: String
|
|
67
93
|
def equalizer(tick, label = NEUTRAL_LABEL)
|
|
68
94
|
bars = Array.new(EQ_BARS) do |i|
|
|
69
95
|
height = (Math.sin((tick + i) * 0.6).abs * (EQ_LEVELS.length - 1)).round
|
|
@@ -75,6 +101,7 @@ class Riffer::Rig::UI::Animator
|
|
|
75
101
|
|
|
76
102
|
private
|
|
77
103
|
|
|
104
|
+
# @rbs return: void
|
|
78
105
|
def animate
|
|
79
106
|
tick = 0
|
|
80
107
|
roll_at = 0.0 # force an immediate phrase roll when entering reasoning mode
|
|
@@ -88,18 +115,31 @@ class Riffer::Rig::UI::Animator
|
|
|
88
115
|
end
|
|
89
116
|
|
|
90
117
|
# Re-rolls both phrase and duration whenever the reasoning tick expires.
|
|
118
|
+
#
|
|
119
|
+
# @rbs roll_at: Float
|
|
120
|
+
# @rbs return: Float
|
|
91
121
|
def roll_phrase(roll_at)
|
|
92
122
|
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
93
123
|
return roll_at unless @mode == :reasoning && now >= roll_at
|
|
94
124
|
|
|
95
125
|
@phrase = REASONING_PHRASES.sample
|
|
96
|
-
now +
|
|
126
|
+
now + roll_rand
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Range rand returns nil for an empty range; this one is a non-empty constant.
|
|
130
|
+
#
|
|
131
|
+
# @rbs return: Integer
|
|
132
|
+
def roll_rand
|
|
133
|
+
x = rand(REASONING_TICK_RANGE)
|
|
134
|
+
x || 0
|
|
97
135
|
end
|
|
98
136
|
|
|
137
|
+
# @rbs return: String
|
|
99
138
|
def label
|
|
100
|
-
@mode == :reasoning ? @phrase : NEUTRAL_LABEL
|
|
139
|
+
@mode == :reasoning ? @phrase || NEUTRAL_LABEL : NEUTRAL_LABEL
|
|
101
140
|
end
|
|
102
141
|
|
|
142
|
+
# @rbs return: bool
|
|
103
143
|
def enabled?
|
|
104
144
|
@theme.enabled && @io.respond_to?(:tty?) && @io.tty?
|
|
105
145
|
end
|
data/lib/riffer/rig/ui/banner.rb
CHANGED
|
@@ -10,7 +10,7 @@ module Riffer::Rig::UI::Banner
|
|
|
10
10
|
'██╔══██╗██║██╔══╝ ██╔══╝ ██╔══╝ ██╔══██╗',
|
|
11
11
|
'██║ ██║██║██║ ██║ ███████╗██║ ██║',
|
|
12
12
|
'╚═╝ ╚═╝╚═╝╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝'
|
|
13
|
-
].freeze
|
|
13
|
+
].freeze #: Array[String]
|
|
14
14
|
|
|
15
15
|
ROW_COLOURS = [
|
|
16
16
|
Riffer::Rig::UI::Palette::PINK,
|
|
@@ -19,25 +19,49 @@ module Riffer::Rig::UI::Banner
|
|
|
19
19
|
Riffer::Rig::UI::Palette::BLUE,
|
|
20
20
|
Riffer::Rig::UI::Palette::CYAN,
|
|
21
21
|
Riffer::Rig::UI::Palette::CYAN
|
|
22
|
-
].freeze
|
|
22
|
+
].freeze #: Array[Array[Integer]]
|
|
23
23
|
|
|
24
|
-
INFO_LABEL_WIDTH = 8
|
|
25
|
-
INDENT = ' '
|
|
24
|
+
INFO_LABEL_WIDTH = 8 #: Integer
|
|
26
25
|
|
|
26
|
+
INDENT = ' ' #: String
|
|
27
|
+
|
|
28
|
+
# @rbs theme: Riffer::Rig::UI::Theme
|
|
29
|
+
# @rbs model: String
|
|
30
|
+
# @rbs cwd: String
|
|
31
|
+
# @rbs context: String
|
|
32
|
+
# @rbs skills: String
|
|
33
|
+
# @rbs version: String
|
|
34
|
+
# @rbs return: String
|
|
27
35
|
def call(theme, model:, cwd:, context:, skills:, version:)
|
|
28
36
|
lines(theme, model: model, cwd: cwd, context: context, skills: skills, version: version).join("\n")
|
|
29
37
|
end
|
|
30
38
|
|
|
39
|
+
# @rbs theme: Riffer::Rig::UI::Theme
|
|
40
|
+
# @rbs model: String
|
|
41
|
+
# @rbs cwd: String
|
|
42
|
+
# @rbs context: String
|
|
43
|
+
# @rbs skills: String
|
|
44
|
+
# @rbs version: String
|
|
45
|
+
# @rbs return: Array[String]
|
|
31
46
|
def lines(theme, model:, cwd:, context:, skills:, version:)
|
|
32
47
|
[''] + art(theme) + [''] +
|
|
33
48
|
info(theme, model: model, cwd: cwd, context: context, skills: skills, version: version) + ['']
|
|
34
49
|
end
|
|
35
50
|
|
|
51
|
+
# @rbs theme: Riffer::Rig::UI::Theme
|
|
52
|
+
# @rbs return: Array[String]
|
|
36
53
|
def art(theme)
|
|
37
54
|
art = WORDMARK.each_index.map { |i| "#{INDENT}#{theme.paint(WORDMARK[i], ROW_COLOURS[i])}" }
|
|
38
55
|
art + ["#{INDENT}#{theme.cyan("♪ let's riff ♪")}"]
|
|
39
56
|
end
|
|
40
57
|
|
|
58
|
+
# @rbs theme: Riffer::Rig::UI::Theme
|
|
59
|
+
# @rbs model: String
|
|
60
|
+
# @rbs cwd: String
|
|
61
|
+
# @rbs context: String
|
|
62
|
+
# @rbs skills: String
|
|
63
|
+
# @rbs version: String
|
|
64
|
+
# @rbs return: Array[String]
|
|
41
65
|
def info(theme, model:, cwd:, context:, skills:, version:)
|
|
42
66
|
rows = { model: model, cwd: cwd, context: context, skills: skills, version: version }.map do |label, value|
|
|
43
67
|
" #{theme.magenta('▸')} #{theme.grey(label.to_s.ljust(INFO_LABEL_WIDTH))}#{value}"
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Hides the terminal cursor while a turn is in flight: a hidden cursor can't
|
|
4
|
+
# flicker against the animator's erase-and-redraw churn.
|
|
5
|
+
class Riffer::Rig::UI::Cursor
|
|
6
|
+
HIDE = "\e[?25l" #: String
|
|
7
|
+
|
|
8
|
+
SHOW = "\e[?25h" #: String
|
|
9
|
+
|
|
10
|
+
# @rbs @io: untyped
|
|
11
|
+
# @rbs @theme: Riffer::Rig::UI::Theme
|
|
12
|
+
|
|
13
|
+
# @rbs io: untyped
|
|
14
|
+
# @rbs ?theme: Riffer::Rig::UI::Theme
|
|
15
|
+
# @rbs return: void
|
|
16
|
+
def initialize(io: $stdout, theme: Riffer::Rig::UI::Theme.for(io))
|
|
17
|
+
@io = io
|
|
18
|
+
@theme = theme
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @rbs return: void
|
|
22
|
+
def hide
|
|
23
|
+
write(HIDE)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @rbs return: void
|
|
27
|
+
def show
|
|
28
|
+
write(SHOW)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
# @rbs sequence: String
|
|
34
|
+
# @rbs return: void
|
|
35
|
+
def write(sequence)
|
|
36
|
+
return unless enabled?
|
|
37
|
+
|
|
38
|
+
@io.print(sequence)
|
|
39
|
+
@io.flush
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @rbs return: bool
|
|
43
|
+
def enabled?
|
|
44
|
+
@theme.enabled && @io.respond_to?(:tty?) && @io.tty?
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Riffer::Rig::UI::Palette
|
|
4
|
-
PINK = [255, 106, 193].freeze
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
4
|
+
PINK = [255, 106, 193].freeze #: Array[Integer]
|
|
5
|
+
|
|
6
|
+
MAGENTA = [192, 76, 253].freeze #: Array[Integer]
|
|
7
|
+
|
|
8
|
+
PURPLE = [122, 92, 255].freeze #: Array[Integer]
|
|
9
|
+
|
|
10
|
+
BLUE = [77, 124, 255].freeze #: Array[Integer]
|
|
11
|
+
|
|
12
|
+
CYAN = [45, 226, 230].freeze #: Array[Integer]
|
|
13
|
+
|
|
14
|
+
GREY = [138, 139, 154].freeze #: Array[Integer]
|
|
15
|
+
|
|
16
|
+
RED = [255, 92, 122].freeze #: Array[Integer]
|
|
11
17
|
end
|
|
@@ -3,8 +3,18 @@
|
|
|
3
3
|
require 'json'
|
|
4
4
|
|
|
5
5
|
class Riffer::Rig::UI::Renderer
|
|
6
|
-
RESULT_PREVIEW_LIMIT = 200
|
|
7
|
-
|
|
6
|
+
RESULT_PREVIEW_LIMIT = 200 #: Integer
|
|
7
|
+
|
|
8
|
+
# @rbs @io: untyped
|
|
9
|
+
# @rbs @theme: Riffer::Rig::UI::Theme
|
|
10
|
+
# @rbs @tally: Riffer::Rig::TokenTally?
|
|
11
|
+
# @rbs @smoother: Riffer::Rig::UI::Smoother | PassThroughSmoother?
|
|
12
|
+
|
|
13
|
+
# @rbs io: untyped
|
|
14
|
+
# @rbs ?theme: Riffer::Rig::UI::Theme
|
|
15
|
+
# @rbs ?tally: Riffer::Rig::TokenTally?
|
|
16
|
+
# @rbs ?smoother: Riffer::Rig::UI::Smoother | PassThroughSmoother?
|
|
17
|
+
# @rbs return: void
|
|
8
18
|
def initialize(io: $stdout, theme: Riffer::Rig::UI::Theme.for(io), tally: nil, smoother: nil)
|
|
9
19
|
@io = io
|
|
10
20
|
@theme = theme
|
|
@@ -12,6 +22,8 @@ class Riffer::Rig::UI::Renderer
|
|
|
12
22
|
@smoother = smoother
|
|
13
23
|
end
|
|
14
24
|
|
|
25
|
+
# @rbs event: Riffer::StreamEvents::Base
|
|
26
|
+
# @rbs return: void
|
|
15
27
|
def render(event)
|
|
16
28
|
case event
|
|
17
29
|
when Riffer::StreamEvents::TextDelta
|
|
@@ -31,6 +43,8 @@ class Riffer::Rig::UI::Renderer
|
|
|
31
43
|
end
|
|
32
44
|
end
|
|
33
45
|
|
|
46
|
+
# @rbs message: Riffer::Messages::Base
|
|
47
|
+
# @rbs return: void
|
|
34
48
|
def render_tool_result(message)
|
|
35
49
|
return unless message.is_a?(Riffer::Messages::Tool)
|
|
36
50
|
|
|
@@ -40,46 +54,61 @@ class Riffer::Rig::UI::Renderer
|
|
|
40
54
|
|
|
41
55
|
private
|
|
42
56
|
|
|
57
|
+
# @rbs return: Riffer::Rig::UI::Smoother | PassThroughSmoother
|
|
43
58
|
def smoother
|
|
44
59
|
@smoother || PassThroughSmoother.new(@io)
|
|
45
60
|
end
|
|
46
61
|
|
|
47
|
-
# Stand-in when no smoother is injected, so a bare Renderer still prints
|
|
62
|
+
# Stand-in when no smoother is injected, so a bare Riffer::Rig::UI::Renderer still prints
|
|
48
63
|
# synchronously.
|
|
49
64
|
class PassThroughSmoother
|
|
65
|
+
# @rbs @io: untyped
|
|
66
|
+
|
|
67
|
+
# @rbs io: untyped
|
|
68
|
+
# @rbs return: void
|
|
50
69
|
def initialize(io) = @io = io
|
|
51
70
|
|
|
71
|
+
# @rbs content: String
|
|
72
|
+
# @rbs return: self
|
|
52
73
|
def <<(content)
|
|
53
74
|
@io.print(content)
|
|
54
75
|
@io.flush
|
|
55
76
|
self
|
|
56
77
|
end
|
|
57
78
|
|
|
79
|
+
# @rbs return: nil
|
|
58
80
|
def drain = nil
|
|
59
81
|
|
|
82
|
+
# @rbs return: nil
|
|
60
83
|
def finish = nil
|
|
61
84
|
end
|
|
62
85
|
|
|
86
|
+
# @rbs return: void
|
|
63
87
|
def drain_smoother
|
|
64
88
|
smoother.drain
|
|
65
89
|
end
|
|
66
90
|
|
|
91
|
+
# @rbs usage: Riffer::Providers::TokenUsage
|
|
92
|
+
# @rbs return: void
|
|
67
93
|
def render_token_usage(usage)
|
|
68
|
-
|
|
94
|
+
tally = @tally
|
|
95
|
+
return unless tally
|
|
69
96
|
|
|
70
|
-
|
|
97
|
+
tally.add(usage)
|
|
71
98
|
|
|
72
99
|
parts = ["↑#{usage.input_tokens}", "↓#{usage.output_tokens}"]
|
|
73
100
|
parts << "cache_write:#{usage.cache_write_tokens}" if usage.cache_write_tokens&.positive?
|
|
74
101
|
parts << "cache_read:#{usage.cache_read_tokens}" if usage.cache_read_tokens&.positive?
|
|
75
102
|
|
|
76
|
-
session_parts = ["session #{
|
|
77
|
-
cost =
|
|
103
|
+
session_parts = ["session #{tally.total_tokens} tok"]
|
|
104
|
+
cost = tally.estimated_cost
|
|
78
105
|
session_parts << format('~$%.4f', cost) if cost
|
|
79
106
|
|
|
80
107
|
@io.puts("\n#{@theme.dim("#{parts.join(' · ')} #{session_parts.join(' · ')}")}")
|
|
81
108
|
end
|
|
82
109
|
|
|
110
|
+
# @rbs arguments: String
|
|
111
|
+
# @rbs return: String
|
|
83
112
|
def format_arguments(arguments)
|
|
84
113
|
parsed = JSON.parse(arguments)
|
|
85
114
|
parsed.map { |key, value| "#{key}: #{value.inspect}" }.join(', ')
|
|
@@ -87,6 +116,8 @@ class Riffer::Rig::UI::Renderer
|
|
|
87
116
|
arguments
|
|
88
117
|
end
|
|
89
118
|
|
|
119
|
+
# @rbs content: String
|
|
120
|
+
# @rbs return: String
|
|
90
121
|
def preview(content)
|
|
91
122
|
first_line = content.to_s.lines.first.to_s.chomp
|
|
92
123
|
return first_line if first_line.length <= RESULT_PREVIEW_LIMIT
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
# typing. On a non-TTY output it is transparent: writes pass through
|
|
5
5
|
# synchronously and no thread ever runs.
|
|
6
6
|
class Riffer::Rig::UI::Smoother
|
|
7
|
-
TICK_SECONDS = 1.0 / 60.0
|
|
7
|
+
TICK_SECONDS = 1.0 / 60.0 #: Float
|
|
8
8
|
|
|
9
9
|
# A frame removes only a sixtieth of the remaining backlog — an exponential
|
|
10
10
|
# decay whose tail keeps text flowing for a second or more, so the reveal
|
|
@@ -12,9 +12,24 @@ class Riffer::Rig::UI::Smoother
|
|
|
12
12
|
# releases so the effective rate can settle at the provider's throughput;
|
|
13
13
|
# the floor is fractional (10 chars/s) so it can't over-drain a backlog
|
|
14
14
|
# that's trickling in slower than 60 chars/s.
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
#
|
|
16
|
+
BACKLOG_FRACTION_PER_TICK = Rational(1, 60) #: Rational
|
|
17
17
|
|
|
18
|
+
MIN_CHARS_PER_TICK = Rational(1, 6) #: Rational
|
|
19
|
+
|
|
20
|
+
# @rbs @io: untyped
|
|
21
|
+
# @rbs @theme: Riffer::Rig::UI::Theme
|
|
22
|
+
# @rbs @clock: singleton(Kernel)
|
|
23
|
+
# @rbs @backlog: String
|
|
24
|
+
# @rbs @carry: Rational
|
|
25
|
+
# @rbs @mutex: Mutex
|
|
26
|
+
# @rbs @thread: Thread?
|
|
27
|
+
# @rbs @stop: bool
|
|
28
|
+
|
|
29
|
+
# @rbs io: untyped
|
|
30
|
+
# @rbs ?theme: Riffer::Rig::UI::Theme
|
|
31
|
+
# @rbs clock: singleton(Kernel)
|
|
32
|
+
# @rbs return: void
|
|
18
33
|
def initialize(io: $stdout, theme: Riffer::Rig::UI::Theme.for(io), clock: Kernel)
|
|
19
34
|
@io = io
|
|
20
35
|
@theme = theme
|
|
@@ -26,6 +41,7 @@ class Riffer::Rig::UI::Smoother
|
|
|
26
41
|
@stop = false
|
|
27
42
|
end
|
|
28
43
|
|
|
44
|
+
# @rbs return: void
|
|
29
45
|
def start
|
|
30
46
|
return unless enabled?
|
|
31
47
|
return if @thread
|
|
@@ -39,6 +55,8 @@ class Riffer::Rig::UI::Smoother
|
|
|
39
55
|
end
|
|
40
56
|
end
|
|
41
57
|
|
|
58
|
+
# @rbs content: String
|
|
59
|
+
# @rbs return: self
|
|
42
60
|
def <<(content)
|
|
43
61
|
if enabled?
|
|
44
62
|
@mutex.synchronize { @backlog << content }
|
|
@@ -50,6 +68,8 @@ class Riffer::Rig::UI::Smoother
|
|
|
50
68
|
end
|
|
51
69
|
|
|
52
70
|
# The write sits inside the mutex so a concurrent tick can't reorder a drain.
|
|
71
|
+
#
|
|
72
|
+
# @rbs return: void
|
|
53
73
|
def tick
|
|
54
74
|
@mutex.synchronize do
|
|
55
75
|
return if @backlog.empty?
|
|
@@ -65,6 +85,8 @@ class Riffer::Rig::UI::Smoother
|
|
|
65
85
|
end
|
|
66
86
|
|
|
67
87
|
# Called before non-delta output so printed order matches stream order.
|
|
88
|
+
#
|
|
89
|
+
# @rbs return: void
|
|
68
90
|
def drain
|
|
69
91
|
@mutex.synchronize do
|
|
70
92
|
return if @backlog.empty?
|
|
@@ -75,10 +97,13 @@ class Riffer::Rig::UI::Smoother
|
|
|
75
97
|
end
|
|
76
98
|
|
|
77
99
|
# Idempotent: the REPL's ensure path runs it even after a happy-path finish.
|
|
100
|
+
#
|
|
101
|
+
# @rbs return: void
|
|
78
102
|
def finish
|
|
79
|
-
|
|
103
|
+
thread = @thread
|
|
104
|
+
if thread
|
|
80
105
|
@stop = true
|
|
81
|
-
|
|
106
|
+
thread.join
|
|
82
107
|
@thread = nil
|
|
83
108
|
end
|
|
84
109
|
drain
|
|
@@ -86,6 +111,7 @@ class Riffer::Rig::UI::Smoother
|
|
|
86
111
|
|
|
87
112
|
private
|
|
88
113
|
|
|
114
|
+
# @rbs return: bool
|
|
89
115
|
def enabled?
|
|
90
116
|
@theme.enabled && @io.respond_to?(:tty?) && @io.tty?
|
|
91
117
|
end
|
data/lib/riffer/rig/ui/theme.rb
CHANGED
|
@@ -2,43 +2,74 @@
|
|
|
2
2
|
|
|
3
3
|
# +enabled+ gates every escape so the same code path emits plain, escape-free
|
|
4
4
|
# text when piped or tested.
|
|
5
|
-
Riffer::Rig::UI::Theme = Data.define(:enabled)
|
|
6
|
-
|
|
7
|
-
# Behaviour lives in a reopened class (rather than the `Data.define` block) so
|
|
8
|
-
# the type checker can analyse the methods.
|
|
9
5
|
class Riffer::Rig::UI::Theme
|
|
6
|
+
# @rbs @enabled: bool
|
|
7
|
+
|
|
8
|
+
# @rbs return: bool
|
|
9
|
+
# @dynamic enabled
|
|
10
|
+
attr_reader :enabled #: bool
|
|
11
|
+
|
|
12
|
+
# @rbs enabled: bool
|
|
13
|
+
# @rbs return: void
|
|
14
|
+
def initialize(enabled:)
|
|
15
|
+
@enabled = enabled
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# @rbs io: untyped
|
|
19
|
+
# @rbs return: Riffer::Rig::UI::Theme
|
|
10
20
|
def self.for(io)
|
|
11
21
|
new(enabled: io.respond_to?(:tty?) && io.tty? && !ENV.key?('NO_COLOR'))
|
|
12
22
|
end
|
|
13
23
|
|
|
24
|
+
# @rbs text: String
|
|
25
|
+
# @rbs rgb: Array[Integer]
|
|
26
|
+
# @rbs return: String
|
|
14
27
|
def paint(text, rgb)
|
|
15
|
-
return text
|
|
28
|
+
return text unless enabled
|
|
16
29
|
|
|
17
30
|
"\e[38;2;#{rgb[0]};#{rgb[1]};#{rgb[2]}m#{text}\e[0m"
|
|
18
31
|
end
|
|
19
32
|
|
|
33
|
+
# @rbs text: String
|
|
34
|
+
# @rbs return: String
|
|
20
35
|
def pink(text) = paint(text, Riffer::Rig::UI::Palette::PINK)
|
|
21
36
|
|
|
37
|
+
# @rbs text: String
|
|
38
|
+
# @rbs return: String
|
|
22
39
|
def magenta(text) = paint(text, Riffer::Rig::UI::Palette::MAGENTA)
|
|
23
40
|
|
|
41
|
+
# @rbs text: String
|
|
42
|
+
# @rbs return: String
|
|
24
43
|
def purple(text) = paint(text, Riffer::Rig::UI::Palette::PURPLE)
|
|
25
44
|
|
|
45
|
+
# @rbs text: String
|
|
46
|
+
# @rbs return: String
|
|
26
47
|
def blue(text) = paint(text, Riffer::Rig::UI::Palette::BLUE)
|
|
27
48
|
|
|
49
|
+
# @rbs text: String
|
|
50
|
+
# @rbs return: String
|
|
28
51
|
def cyan(text) = paint(text, Riffer::Rig::UI::Palette::CYAN)
|
|
29
52
|
|
|
53
|
+
# @rbs text: String
|
|
54
|
+
# @rbs return: String
|
|
30
55
|
def grey(text) = paint(text, Riffer::Rig::UI::Palette::GREY)
|
|
31
56
|
|
|
57
|
+
# @rbs text: String
|
|
58
|
+
# @rbs return: String
|
|
32
59
|
def red(text) = paint(text, Riffer::Rig::UI::Palette::RED)
|
|
33
60
|
|
|
61
|
+
# @rbs text: String
|
|
62
|
+
# @rbs return: String
|
|
34
63
|
def dim(text)
|
|
35
|
-
return text
|
|
64
|
+
return text unless enabled
|
|
36
65
|
|
|
37
66
|
"\e[2m#{text}\e[0m"
|
|
38
67
|
end
|
|
39
68
|
|
|
69
|
+
# @rbs text: String
|
|
70
|
+
# @rbs return: String
|
|
40
71
|
def bold(text)
|
|
41
|
-
return text
|
|
72
|
+
return text unless enabled
|
|
42
73
|
|
|
43
74
|
"\e[1m#{text}\e[0m"
|
|
44
75
|
end
|
data/lib/riffer/rig/version.rb
CHANGED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Generated from lib/riffer/rig/cli.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module Riffer::Rig::CLI
|
|
4
|
+
PROVIDER_URLS: Hash[String, String]
|
|
5
|
+
|
|
6
|
+
# @rbs output: untyped
|
|
7
|
+
# @rbs input: untyped
|
|
8
|
+
# @rbs return: Integer
|
|
9
|
+
def start: (?output: untyped, ?input: untyped) -> Integer
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
# @rbs provider: String?
|
|
14
|
+
# @rbs api_key: String
|
|
15
|
+
# @rbs return: void
|
|
16
|
+
def configure_provider: (String? provider, String api_key) -> void
|
|
17
|
+
|
|
18
|
+
# @rbs provider: String?
|
|
19
|
+
# @rbs theme: Riffer::Rig::UI::Theme
|
|
20
|
+
# @rbs output: untyped
|
|
21
|
+
# @rbs input: untyped
|
|
22
|
+
# @rbs return: String?
|
|
23
|
+
def onboard: (String? provider, Riffer::Rig::UI::Theme theme, output: untyped, input: untyped) -> String?
|
|
24
|
+
|
|
25
|
+
# @rbs provider: String?
|
|
26
|
+
# @rbs return: String
|
|
27
|
+
def env_var_for: (String? provider) -> String
|
|
28
|
+
|
|
29
|
+
# @rbs input: untyped
|
|
30
|
+
# @rbs return: String?
|
|
31
|
+
def read_secret: (untyped input) -> String?
|
|
32
|
+
|
|
33
|
+
# @rbs theme: Riffer::Rig::UI::Theme
|
|
34
|
+
# @rbs animator: Riffer::Rig::UI::Animator
|
|
35
|
+
# @rbs model: String
|
|
36
|
+
# @rbs return: void
|
|
37
|
+
def reveal_banner: (Riffer::Rig::UI::Theme theme, Riffer::Rig::UI::Animator animator, String model) -> void
|
|
38
|
+
|
|
39
|
+
# @rbs return: String
|
|
40
|
+
def count_skills: () -> String
|
|
41
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Generated from lib/riffer/rig/credentials.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
# Reads and writes API keys for each supported provider in
|
|
4
|
+
# <tt>~/.riffer/auth.json</tt>.
|
|
5
|
+
#
|
|
6
|
+
# Keys are stored under the provider name, e.g.:
|
|
7
|
+
#
|
|
8
|
+
# {
|
|
9
|
+
# "anthropic": "sk-ant-...",
|
|
10
|
+
# "openai": "sk-...",
|
|
11
|
+
# "gemini": "...",
|
|
12
|
+
# "openrouter": "sk-or-..."
|
|
13
|
+
# }
|
|
14
|
+
#
|
|
15
|
+
# The environment variable checked per provider:
|
|
16
|
+
#
|
|
17
|
+
# anthropic → ANTHROPIC_API_KEY
|
|
18
|
+
# openai → OPENAI_API_KEY
|
|
19
|
+
# gemini → GEMINI_API_KEY
|
|
20
|
+
# openrouter → OPENROUTER_API_KEY
|
|
21
|
+
module Riffer::Rig::Credentials
|
|
22
|
+
PATH: String
|
|
23
|
+
|
|
24
|
+
PROVIDER_ENV_VARS: Hash[String, String]
|
|
25
|
+
|
|
26
|
+
# Returns the API key for +provider+, checking the environment variable first,
|
|
27
|
+
# then the stored file. Returns +nil+ if no key is available.
|
|
28
|
+
#
|
|
29
|
+
# @rbs provider: String?
|
|
30
|
+
# @rbs path: String
|
|
31
|
+
# @rbs return: String?
|
|
32
|
+
def api_key_for: (String? provider, ?path: String) -> String?
|
|
33
|
+
|
|
34
|
+
# Saves an API key for +provider+ to the auth file with 0600 permissions.
|
|
35
|
+
#
|
|
36
|
+
# @rbs provider: String
|
|
37
|
+
# @rbs key: String
|
|
38
|
+
# @rbs path: String
|
|
39
|
+
# @rbs return: String
|
|
40
|
+
def save_api_key: (String provider, String key, ?path: String) -> String
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
# @rbs provider: String?
|
|
45
|
+
# @rbs path: String
|
|
46
|
+
# @rbs return: String?
|
|
47
|
+
def key_from_file: (String? provider, String path) -> String?
|
|
48
|
+
|
|
49
|
+
# The parsed auth file, or an empty hash when the file is absent or
|
|
50
|
+
# malformed.
|
|
51
|
+
#
|
|
52
|
+
# @rbs path: String
|
|
53
|
+
# @rbs return: Hash[String, untyped]
|
|
54
|
+
def read: (String path) -> Hash[String, untyped]
|
|
55
|
+
end
|