riffer-rig 0.4.0 → 0.6.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.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +37 -0
  3. data/README.md +43 -6
  4. data/lib/riffer/rig/cli.rb +51 -47
  5. data/lib/riffer/rig/coding_agent.rb +1 -6
  6. data/lib/riffer/rig/credentials/resolution.rb +16 -0
  7. data/lib/riffer/rig/credentials.rb +151 -60
  8. data/lib/riffer/rig/env.rb +29 -0
  9. data/lib/riffer/rig/events/command_output.rb +27 -0
  10. data/lib/riffer/rig/events/event.rb +30 -0
  11. data/lib/riffer/rig/events/notify.rb +27 -0
  12. data/lib/riffer/rig/events/session_end.rb +24 -0
  13. data/lib/riffer/rig/events/session_start.rb +27 -0
  14. data/lib/riffer/rig/events/skill_activated.rb +24 -0
  15. data/lib/riffer/rig/events/turn_end.rb +32 -0
  16. data/lib/riffer/rig/events.rb +4 -0
  17. data/lib/riffer/rig/extension.rb +27 -0
  18. data/lib/riffer/rig/hosts/base.rb +36 -0
  19. data/lib/riffer/rig/hosts/mirror.rb +60 -0
  20. data/lib/riffer/rig/hosts/null.rb +36 -0
  21. data/lib/riffer/rig/provider_setup/field.rb +24 -0
  22. data/lib/riffer/rig/provider_setup.rb +84 -0
  23. data/lib/riffer/rig/registrar.rb +35 -0
  24. data/lib/riffer/rig/repl.rb +42 -31
  25. data/lib/riffer/rig/runtime/cancel_flag.rb +27 -0
  26. data/lib/riffer/rig/runtime.rb +225 -0
  27. data/lib/riffer/rig/settings/document.rb +28 -0
  28. data/lib/riffer/rig/settings/pricing.rb +11 -1
  29. data/lib/riffer/rig/settings.rb +49 -60
  30. data/lib/riffer/rig/token_tally.rb +1 -21
  31. data/lib/riffer/rig/tools/bash.rb +33 -12
  32. data/lib/riffer/rig/tools/edit.rb +1 -1
  33. data/lib/riffer/rig/tools/read.rb +2 -2
  34. data/lib/riffer/rig/ui/animator.rb +8 -15
  35. data/lib/riffer/rig/ui/cursor.rb +4 -6
  36. data/lib/riffer/rig/ui/renderer.rb +18 -31
  37. data/lib/riffer/rig/ui/smoother.rb +15 -23
  38. data/lib/riffer/rig/ui/theme.rb +4 -6
  39. data/lib/riffer/rig/version.rb +1 -1
  40. data/lib/riffer/rig.rb +26 -0
  41. data/sig/generated/riffer/rig/cli.rbs +27 -19
  42. data/sig/generated/riffer/rig/credentials/resolution.rbs +13 -0
  43. data/sig/generated/riffer/rig/credentials.rbs +71 -41
  44. data/sig/generated/riffer/rig/env.rbs +20 -0
  45. data/sig/generated/riffer/rig/events/command_output.rbs +19 -0
  46. data/sig/generated/riffer/rig/events/event.rbs +20 -0
  47. data/sig/generated/riffer/rig/events/notify.rbs +19 -0
  48. data/sig/generated/riffer/rig/events/session_end.rbs +16 -0
  49. data/sig/generated/riffer/rig/events/session_start.rbs +19 -0
  50. data/sig/generated/riffer/rig/events/skill_activated.rbs +16 -0
  51. data/sig/generated/riffer/rig/events/turn_end.rbs +22 -0
  52. data/sig/generated/riffer/rig/events.rbs +4 -0
  53. data/sig/generated/riffer/rig/extension.rbs +24 -0
  54. data/sig/generated/riffer/rig/hosts/base.rbs +26 -0
  55. data/sig/generated/riffer/rig/hosts/mirror.rbs +41 -0
  56. data/sig/generated/riffer/rig/hosts/null.rbs +26 -0
  57. data/sig/generated/riffer/rig/provider_setup/field.rbs +22 -0
  58. data/sig/generated/riffer/rig/provider_setup.rbs +30 -0
  59. data/sig/generated/riffer/rig/registrar.rbs +25 -0
  60. data/sig/generated/riffer/rig/repl.rbs +17 -16
  61. data/sig/generated/riffer/rig/runtime/cancel_flag.rbs +19 -0
  62. data/sig/generated/riffer/rig/runtime.rbs +120 -0
  63. data/sig/generated/riffer/rig/settings/document.rbs +24 -0
  64. data/sig/generated/riffer/rig/settings/pricing.rbs +4 -1
  65. data/sig/generated/riffer/rig/settings.rbs +28 -49
  66. data/sig/generated/riffer/rig/token_tally.rbs +2 -21
  67. data/sig/generated/riffer/rig/tools/bash.rbs +15 -3
  68. data/sig/generated/riffer/rig/tools/edit.rbs +2 -2
  69. data/sig/generated/riffer/rig/tools/read.rbs +3 -3
  70. data/sig/generated/riffer/rig/ui/animator.rbs +8 -16
  71. data/sig/generated/riffer/rig/ui/cursor.rbs +4 -6
  72. data/sig/generated/riffer/rig/ui/renderer.rbs +13 -35
  73. data/sig/generated/riffer/rig/ui/smoother.rbs +9 -22
  74. data/sig/generated/riffer/rig/ui/theme.rbs +3 -5
  75. data/sig/generated/riffer/rig.rbs +12 -0
  76. data/sig/manual/riffer/rig/cli.rbs +2 -4
  77. data/sig/manual/riffer/rig/credentials.rbs +2 -4
  78. data/sig/manual/riffer/rig/hosts.rbs +5 -0
  79. data/sig/manual/riffer/rig/settings.rbs +2 -4
  80. data/sig/manual/riffer/rig/ui/banner.rbs +2 -4
  81. data/sig/manual/riffer/rig.rbs +11 -0
  82. metadata +45 -3
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Riffer::Rig::Events::SessionEnd < Riffer::Rig::Events::Event
4
+ # @dynamic reason
5
+ attr_reader :reason #: Symbol
6
+
7
+ # @rbs reason: Symbol
8
+ # @rbs return: void
9
+ def initialize(reason)
10
+ super()
11
+ @reason = reason
12
+ freeze
13
+ end
14
+
15
+ # @rbs return: Symbol
16
+ def type
17
+ :session_end
18
+ end
19
+
20
+ # @rbs return: Hash[Symbol, untyped]
21
+ def to_h
22
+ { type: type, reason: reason }
23
+ end
24
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Riffer::Rig::Events::SessionStart < Riffer::Rig::Events::Event
4
+ # @dynamic id, reason
5
+ attr_reader :id #: String
6
+ attr_reader :reason #: Symbol
7
+
8
+ # @rbs id: String
9
+ # @rbs reason: Symbol
10
+ # @rbs return: void
11
+ def initialize(id, reason)
12
+ super()
13
+ @id = id
14
+ @reason = reason
15
+ freeze
16
+ end
17
+
18
+ # @rbs return: Symbol
19
+ def type
20
+ :session_start
21
+ end
22
+
23
+ # @rbs return: Hash[Symbol, untyped]
24
+ def to_h
25
+ { type: type, id: id, reason: reason }
26
+ end
27
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Riffer::Rig::Events::SkillActivated < Riffer::Rig::Events::Event
4
+ # @dynamic name
5
+ attr_reader :name #: String
6
+
7
+ # @rbs name: String
8
+ # @rbs return: void
9
+ def initialize(name)
10
+ super()
11
+ @name = name
12
+ freeze
13
+ end
14
+
15
+ # @rbs return: Symbol
16
+ def type
17
+ :skill_activated
18
+ end
19
+
20
+ # @rbs return: Hash[Symbol, untyped]
21
+ def to_h
22
+ { type: type, name: name }
23
+ end
24
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Riffer::Rig::Events::TurnEnd < Riffer::Rig::Events::Event
4
+ # @dynamic stop_reason, usage
5
+ attr_reader :stop_reason #: Symbol
6
+ attr_reader :usage #: ::Riffer::Providers::TokenUsage?
7
+
8
+ # @rbs stop_reason: Symbol
9
+ # @rbs usage: ::Riffer::Providers::TokenUsage?
10
+ # @rbs return: void
11
+ def initialize(stop_reason, usage)
12
+ super()
13
+ @stop_reason = stop_reason
14
+ @usage = usage
15
+ freeze
16
+ end
17
+
18
+ # @rbs return: Float?
19
+ def cost
20
+ usage&.cost
21
+ end
22
+
23
+ # @rbs return: Symbol
24
+ def type
25
+ :turn_end
26
+ end
27
+
28
+ # @rbs return: Hash[Symbol, untyped]
29
+ def to_h
30
+ { type: type, stop_reason: stop_reason, usage: usage }
31
+ end
32
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Riffer::Rig::Events
4
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Riffer::Rig::Extension
4
+ # @rbs @name: String
5
+ # @rbs @requires: Gem::Requirement?
6
+ # @rbs @block: ^(::Riffer::Rig::Registrar) -> void
7
+
8
+ # @dynamic name, requires
9
+ attr_reader :name #: String
10
+ attr_reader :requires #: Gem::Requirement?
11
+
12
+ # @rbs name: String
13
+ # @rbs requires: String?
14
+ # @rbs &block: (::Riffer::Rig::Registrar) -> void
15
+ # @rbs return: void
16
+ def initialize(name, requires: nil, &block)
17
+ @name = name
18
+ @requires = requires && Gem::Requirement.new(requires)
19
+ @block = block
20
+ end
21
+
22
+ # @rbs registrar: Riffer::Rig::Registrar
23
+ # @rbs return: void
24
+ def run(registrar)
25
+ @block.call(registrar)
26
+ end
27
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Riffer::Rig::Hosts::Base
4
+ # @rbs return: Set[Symbol]
5
+ def capabilities
6
+ raise NotImplementedError, "#{self.class} must implement #{__method__}"
7
+ end
8
+
9
+ # @rbs question: String?
10
+ # @rbs options: Array[String]?
11
+ # @rbs secret: bool
12
+ # @rbs return: String?
13
+ def ask(question = nil, options: nil, secret: false)
14
+ raise NotImplementedError, "#{self.class} must implement #{__method__}"
15
+ end
16
+
17
+ # @rbs question: String?
18
+ # @rbs return: bool
19
+ def confirm(question = nil)
20
+ raise NotImplementedError, "#{self.class} must implement #{__method__}"
21
+ end
22
+
23
+ # @rbs message: String?
24
+ # @rbs level: Symbol
25
+ # @rbs return: void
26
+ def notify(message = nil, level: :info)
27
+ raise NotImplementedError, "#{self.class} must implement #{__method__}"
28
+ end
29
+
30
+ # @rbs label: String?
31
+ # @rbs &: ? () -> void
32
+ # @rbs return: void
33
+ def progress(label = nil, &)
34
+ raise NotImplementedError, "#{self.class} must implement #{__method__}"
35
+ end
36
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Riffer::Rig::Hosts::Mirror < Riffer::Rig::Hosts::Base
4
+ # @rbs @host: Riffer::Rig::Hosts::Base
5
+ # @rbs @queue: Array[Riffer::Rig::Events::Event]
6
+
7
+ # @dynamic capabilities
8
+ attr_reader :capabilities #: Set[Symbol]
9
+
10
+ # @rbs host: Riffer::Rig::Hosts::Base
11
+ # @rbs return: void
12
+ def initialize(host)
13
+ super()
14
+ @host = host
15
+ @capabilities = host.capabilities
16
+ @queue = []
17
+ end
18
+
19
+ # @rbs question: String?
20
+ # @rbs options: Array[String]?
21
+ # @rbs secret: bool
22
+ # @rbs return: String?
23
+ def ask(question = nil, options: nil, secret: false)
24
+ @host.ask(question, options: options, secret: secret)
25
+ end
26
+
27
+ # @rbs question: String?
28
+ # @rbs return: bool
29
+ def confirm(question = nil)
30
+ @host.confirm(question)
31
+ end
32
+
33
+ # @rbs message: String?
34
+ # @rbs level: Symbol
35
+ # @rbs return: void
36
+ def notify(message = nil, level: :info)
37
+ queue(Riffer::Rig::Events::Notify.new(message, level))
38
+ @host.notify(message, level: level)
39
+ end
40
+
41
+ # @rbs label: String?
42
+ # @rbs &block: ? () -> void
43
+ # @rbs return: void
44
+ def progress(label = nil, &block)
45
+ @host.progress(label) { block&.call }
46
+ end
47
+
48
+ # @rbs event: Riffer::Rig::Events::Event
49
+ # @rbs return: void
50
+ def queue(event)
51
+ @queue << event
52
+ end
53
+
54
+ # @rbs return: Array[Riffer::Rig::Events::Event]
55
+ def drain
56
+ queued = @queue
57
+ @queue = []
58
+ queued
59
+ end
60
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Riffer::Rig::Hosts::Null < Riffer::Rig::Hosts::Base
4
+ # @rbs return: Set[Symbol]
5
+ def capabilities
6
+ Set.new.freeze
7
+ end
8
+
9
+ # @rbs question: String?
10
+ # @rbs options: Array[String]?
11
+ # @rbs secret: bool
12
+ # @rbs return: String?
13
+ def ask(question = nil, options: nil, secret: false)
14
+ nil
15
+ end
16
+
17
+ # @rbs question: String?
18
+ # @rbs return: bool
19
+ def confirm(question = nil)
20
+ false
21
+ end
22
+
23
+ # @rbs message: String?
24
+ # @rbs level: Symbol
25
+ # @rbs return: void
26
+ def notify(message = nil, level: :info)
27
+ nil
28
+ end
29
+
30
+ # @rbs label: String?
31
+ # @rbs &block: ? () -> void
32
+ # @rbs return: void
33
+ def progress(label = nil, &block)
34
+ block&.call
35
+ end
36
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Riffer::Rig::ProviderSetup::Field
4
+ # @dynamic name, env, secret, required, fallback
5
+ attr_reader :name #: Symbol
6
+ attr_reader :env #: Array[String]
7
+ attr_reader :secret, :required #: bool
8
+ attr_reader :fallback #: (^() -> String?)?
9
+
10
+ # @rbs name: Symbol
11
+ # @rbs env: Array[String]
12
+ # @rbs secret: bool
13
+ # @rbs required: bool
14
+ # @rbs fallback: (^() -> String?)?
15
+ # @rbs return: void
16
+ def initialize(name:, env:, secret:, required:, fallback: nil)
17
+ @name = name
18
+ @env = env.freeze
19
+ @secret = secret
20
+ @required = required
21
+ @fallback = fallback
22
+ freeze
23
+ end
24
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Riffer::Rig::ProviderSetup
4
+ # @dynamic url, chain, fields
5
+ attr_reader :url #: String?
6
+ attr_reader :chain #: bool
7
+ attr_reader :fields #: Array[Riffer::Rig::ProviderSetup::Field]
8
+
9
+ # @rbs fields: Array[Riffer::Rig::ProviderSetup::Field]
10
+ # @rbs url: String?
11
+ # @rbs chain: bool
12
+ # @rbs return: void
13
+ def initialize(fields:, url: nil, chain: false)
14
+ @url = url
15
+ @chain = chain
16
+ @fields = fields.freeze
17
+ freeze
18
+ end
19
+
20
+ AWS_SHARED_CONFIG_REGION = lambda do
21
+ require 'aws-sdk-core'
22
+ Object.const_get(:Aws).shared_config.region
23
+ rescue LoadError
24
+ nil
25
+ end #: ^() -> String?
26
+
27
+ # Upstream candidate: each riffer provider could declare its own credential
28
+ # fields, and this table would go.
29
+ TABLE = {
30
+ anthropic: new(
31
+ url: 'https://console.anthropic.com/settings/keys',
32
+ fields: [Field.new(name: :api_key, env: ['ANTHROPIC_API_KEY'], secret: true, required: true)]
33
+ ),
34
+ openai: new(
35
+ url: 'https://platform.openai.com/api-keys',
36
+ fields: [
37
+ Field.new(name: :api_key, env: ['OPENAI_API_KEY'], secret: true, required: true),
38
+ Field.new(name: :base_url, env: ['OPENAI_BASE_URL'], secret: false, required: false)
39
+ ]
40
+ ),
41
+ gemini: new(
42
+ url: 'https://aistudio.google.com/app/apikey',
43
+ fields: [Field.new(name: :api_key, env: ['GEMINI_API_KEY'], secret: true, required: true)]
44
+ ),
45
+ openrouter: new(
46
+ url: 'https://openrouter.ai/keys',
47
+ fields: [Field.new(name: :api_key, env: ['OPENROUTER_API_KEY'], secret: true, required: true)]
48
+ ),
49
+ azure_openai: new(
50
+ url: 'https://portal.azure.com',
51
+ fields: [
52
+ Field.new(name: :endpoint, env: ['AZURE_OPENAI_ENDPOINT'], secret: false, required: true),
53
+ Field.new(name: :api_key, env: ['AZURE_OPENAI_API_KEY'], secret: true, required: true)
54
+ ]
55
+ ),
56
+ amazon_bedrock: new(
57
+ url: 'https://console.aws.amazon.com/bedrock',
58
+ chain: true,
59
+ fields: [
60
+ Field.new(
61
+ name: :region,
62
+ env: %w[AWS_REGION AWS_DEFAULT_REGION],
63
+ secret: false,
64
+ required: true,
65
+ fallback: AWS_SHARED_CONFIG_REGION
66
+ ),
67
+ Field.new(name: :api_token, env: ['AWS_BEARER_TOKEN_BEDROCK'], secret: true, required: false)
68
+ ]
69
+ )
70
+ }.freeze #: Hash[Symbol, Riffer::Rig::ProviderSetup]
71
+
72
+ # @rbs identifier: String | Symbol
73
+ # @rbs return: Riffer::Rig::ProviderSetup?
74
+ def self.[](identifier)
75
+ TABLE[identifier.to_sym]
76
+ end
77
+
78
+ # @rbs identifier: String | Symbol
79
+ # @rbs return: Riffer::Rig::ProviderSetup
80
+ def self.for(identifier)
81
+ self[identifier] ||
82
+ new(fields: [Field.new(name: :api_key, env: ["#{identifier.to_s.upcase}_API_KEY"], secret: true, required: true)])
83
+ end
84
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Riffer::Rig::Registrar
4
+ # @rbs @tools: Array[singleton(Riffer::Tool)]
5
+ # @rbs @prompts: Hash[Symbol, ^(Riffer::Rig::Runtime) -> String?]
6
+
7
+ # @rbs return: void
8
+ def initialize
9
+ @tools = []
10
+ @prompts = {}
11
+ end
12
+
13
+ # @rbs klass: singleton(Riffer::Tool)
14
+ # @rbs return: void
15
+ def tool(klass)
16
+ @tools << klass
17
+ end
18
+
19
+ # @rbs name: Symbol
20
+ # @rbs &block: (Riffer::Rig::Runtime) -> String?
21
+ # @rbs return: void
22
+ def prompt(name, &block)
23
+ @prompts[name] = block
24
+ end
25
+
26
+ # @rbs return: Array[singleton(Riffer::Tool)]
27
+ def tools
28
+ @tools.dup
29
+ end
30
+
31
+ # @rbs return: Hash[Symbol, ^(Riffer::Rig::Runtime) -> String?]
32
+ def prompts
33
+ @prompts.dup
34
+ end
35
+ end
@@ -11,28 +11,28 @@ class Riffer::Rig::REPL
11
11
  # @rbs @smoother: Riffer::Rig::UI::Smoother
12
12
  # @rbs @cursor: Riffer::Rig::UI::Cursor
13
13
  # @rbs @theme: Riffer::Rig::UI::Theme
14
- # @rbs @input: untyped
15
- # @rbs @output: untyped
16
-
17
- # @rbs ?agent: Riffer::Agent
18
- # @rbs ?renderer: Riffer::Rig::UI::Renderer
19
- # @rbs input: untyped
20
- # @rbs output: untyped
21
- # @rbs ?theme: Riffer::Rig::UI::Theme
22
- # @rbs ?animator: Riffer::Rig::UI::Animator
23
- # @rbs ?smoother: Riffer::Rig::UI::Smoother
24
- # @rbs ?cursor: Riffer::Rig::UI::Cursor
14
+ # @rbs @input: IO
15
+ # @rbs @output: IO
16
+
17
+ # @rbs agent: Riffer::Agent
18
+ # @rbs renderer: Riffer::Rig::UI::Renderer
19
+ # @rbs input: IO
20
+ # @rbs output: IO
21
+ # @rbs theme: Riffer::Rig::UI::Theme
22
+ # @rbs animator: Riffer::Rig::UI::Animator
23
+ # @rbs smoother: Riffer::Rig::UI::Smoother
24
+ # @rbs cursor: Riffer::Rig::UI::Cursor
25
25
  # @rbs return: void
26
- def initialize(agent:, renderer:, input: $stdin, output: $stdout, theme: Riffer::Rig::UI::Theme.for(output),
27
- animator: Riffer::Rig::UI::Animator.new(
28
- io: output,
29
- theme:
30
- ), smoother: Riffer::Rig::UI::Smoother.new(
31
- io: output,
32
- theme:
33
- ), cursor: Riffer::Rig::UI::Cursor.new(
34
- io: output, theme:
35
- ))
26
+ def initialize(
27
+ agent:,
28
+ renderer:,
29
+ input: $stdin,
30
+ output: $stdout,
31
+ theme: Riffer::Rig::UI::Theme.for(output),
32
+ animator: Riffer::Rig::UI::Animator.new(io: output, theme:),
33
+ smoother: Riffer::Rig::UI::Smoother.new(io: output, theme:),
34
+ cursor: Riffer::Rig::UI::Cursor.new(io: output, theme:)
35
+ )
36
36
  @agent = agent
37
37
  @renderer = renderer
38
38
  @animator = animator
@@ -73,15 +73,17 @@ class Riffer::Rig::REPL
73
73
  # @rbs prompt: String
74
74
  # @rbs return: void
75
75
  def run_turn(prompt)
76
+ # A hidden cursor can't flicker against the animator's erase-and-redraw
77
+ # churn.
76
78
  @cursor.hide
77
- @animator.start
79
+ start_animator
78
80
  @smoother.start
79
81
  @agent.stream(prompt).each do |event|
80
82
  case event
81
83
  when Riffer::StreamEvents::ReasoningDelta
82
- @animator.start(:reasoning)
84
+ start_animator(:reasoning)
83
85
  when Riffer::StreamEvents::ReasoningDone
84
- @animator.start
86
+ start_animator
85
87
  when Riffer::StreamEvents::ToolCallDelta, Riffer::StreamEvents::FinishReasonDone
86
88
  next
87
89
  when Riffer::StreamEvents::SkillActivation
@@ -89,13 +91,13 @@ class Riffer::Rig::REPL
89
91
  # spinner comes straight back on to cover the silent stretch.
90
92
  @animator.stop
91
93
  @renderer.render(event)
92
- @animator.start
94
+ start_animator
93
95
  next
94
96
  when Riffer::StreamEvents::TokenUsageDone
95
97
  # Usage renders nothing inline — it flushes below the turn's output.
96
98
  # Restarting here covers the silent tool-execution stretch that follows.
97
99
  @renderer.render(event)
98
- @animator.start
100
+ start_animator
99
101
  next
100
102
  else
101
103
  @animator.stop
@@ -154,12 +156,11 @@ class Riffer::Rig::REPL
154
156
  "<skill name=\"#{name}\">\n#{body}\n</skill>"
155
157
  end
156
158
 
157
- # Chrome lines that never pass through the renderer (errors, exit line) end
158
- # their line with a newline but leave the renderer's tool-group state alone —
159
- # they can only follow prose, never mid-group.
160
- #
159
+ # @rbs &block: () -> String
161
160
  # @rbs return: void
162
161
  def print_block(&)
162
+ # Bypasses the renderer's tool-group state: these lines (errors, the exit
163
+ # line) can only follow prose, never land mid-group.
163
164
  @output.puts
164
165
  @output.puts(yield)
165
166
  @output.flush
@@ -172,6 +173,16 @@ class Riffer::Rig::REPL
172
173
 
173
174
  @animator.stop
174
175
  @renderer.render_tool_result(message)
175
- @animator.start
176
+ start_animator
177
+ end
178
+
179
+ # @rbs mode: Symbol
180
+ # @rbs return: void
181
+ def start_animator(mode = :neutral)
182
+ # The smoother's backlog may still be trickling out from streamed prose,
183
+ # and spinner frames drawn mid-drain would carve `\r…\e[K` through a
184
+ # half-printed sentence.
185
+ @smoother.drain
186
+ @animator.start(mode)
176
187
  end
177
188
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Riffer::Rig::Runtime::CancelFlag
4
+ # @rbs @mutex: Thread::Mutex
5
+ # @rbs @set: bool
6
+
7
+ # @rbs return: void
8
+ def initialize
9
+ @mutex = Mutex.new
10
+ @set = false
11
+ end
12
+
13
+ # @rbs return: void
14
+ def set
15
+ @mutex.synchronize { @set = true }
16
+ end
17
+
18
+ # @rbs return: void
19
+ def clear
20
+ @mutex.synchronize { @set = false }
21
+ end
22
+
23
+ # @rbs return: bool
24
+ def set?
25
+ @mutex.synchronize { @set }
26
+ end
27
+ end