ruby_coded 0.1.1 → 0.2.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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +3 -1
  3. data/CHANGELOG.md +29 -0
  4. data/README.md +15 -10
  5. data/lib/ruby_coded/auth/auth_manager.rb +20 -5
  6. data/lib/ruby_coded/auth/jwt_decoder.rb +29 -0
  7. data/lib/ruby_coded/auth/providers/openai.rb +19 -5
  8. data/lib/ruby_coded/chat/app/event_dispatch.rb +23 -3
  9. data/lib/ruby_coded/chat/app/login_handler.rb +79 -0
  10. data/lib/ruby_coded/chat/app/oauth_handler.rb +105 -0
  11. data/lib/ruby_coded/chat/app.rb +65 -6
  12. data/lib/ruby_coded/chat/codex_bridge/error_handling.rb +93 -0
  13. data/lib/ruby_coded/chat/codex_bridge/request_builder.rb +104 -0
  14. data/lib/ruby_coded/chat/codex_bridge/sse_parser.rb +136 -0
  15. data/lib/ruby_coded/chat/codex_bridge/token_manager.rb +45 -0
  16. data/lib/ruby_coded/chat/codex_bridge/tool_approval.rb +51 -0
  17. data/lib/ruby_coded/chat/codex_bridge/tool_handling.rb +128 -0
  18. data/lib/ruby_coded/chat/codex_bridge.rb +126 -0
  19. data/lib/ruby_coded/chat/codex_models.rb +41 -0
  20. data/lib/ruby_coded/chat/command_handler/login_commands.rb +33 -0
  21. data/lib/ruby_coded/chat/command_handler/model_commands.rb +19 -6
  22. data/lib/ruby_coded/chat/command_handler.rb +6 -2
  23. data/lib/ruby_coded/chat/help.txt +2 -0
  24. data/lib/ruby_coded/chat/input_handler/login_inputs.rb +66 -0
  25. data/lib/ruby_coded/chat/input_handler.rb +3 -0
  26. data/lib/ruby_coded/chat/renderer/chat_panel_input.rb +1 -1
  27. data/lib/ruby_coded/chat/renderer/login_flow.rb +105 -0
  28. data/lib/ruby_coded/chat/renderer/login_flow_layout.rb +65 -0
  29. data/lib/ruby_coded/chat/renderer/status_bar.rb +2 -1
  30. data/lib/ruby_coded/chat/renderer.rb +12 -3
  31. data/lib/ruby_coded/chat/state/login_flow.rb +117 -0
  32. data/lib/ruby_coded/chat/state/login_flow_steps.rb +69 -0
  33. data/lib/ruby_coded/chat/state.rb +25 -2
  34. data/lib/ruby_coded/initializer.rb +14 -3
  35. data/lib/ruby_coded/plugins/command_completion/state_extension.rb +1 -0
  36. data/lib/ruby_coded/strategies/oauth_strategy.rb +4 -3
  37. data/lib/ruby_coded/version.rb +1 -1
  38. metadata +33 -2
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyCoded
4
+ module Chat
5
+ # Local catalog of models available through the ChatGPT Codex backend.
6
+ # These models are not listed in RubyLLM.models because they use a
7
+ # different API (Responses API via chatgpt.com/backend-api).
8
+ module CodexModels
9
+ CodexModel = Struct.new(:id, :display_name, :context_window, :max_output, keyword_init: true) do
10
+ def to_s
11
+ id
12
+ end
13
+ end
14
+
15
+ MODELS = [
16
+ CodexModel.new(id: "gpt-5.4", display_name: "GPT 5.4 (Recommended)",
17
+ context_window: 272_000, max_output: 128_000),
18
+ CodexModel.new(id: "gpt-5.4-mini", display_name: "GPT 5.4 Mini",
19
+ context_window: 272_000, max_output: 128_000),
20
+ CodexModel.new(id: "gpt-5.3-codex-spark", display_name: "GPT 5.3 Codex Spark (Pro only)",
21
+ context_window: 272_000, max_output: 128_000),
22
+ CodexModel.new(id: "gpt-5.2-codex", display_name: "GPT 5.2 Codex",
23
+ context_window: 272_000, max_output: 128_000),
24
+ CodexModel.new(id: "gpt-5.2", display_name: "GPT 5.2",
25
+ context_window: 272_000, max_output: 128_000)
26
+ ].freeze
27
+
28
+ def self.all
29
+ MODELS
30
+ end
31
+
32
+ def self.find(id)
33
+ MODELS.find { |m| m.id == id }
34
+ end
35
+
36
+ def self.codex_model?(id)
37
+ MODELS.any? { |m| m.id == id }
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyCoded
4
+ module Chat
5
+ class CommandHandler
6
+ # Slash command for authenticating with AI providers from within the TUI.
7
+ module LoginCommands
8
+ private
9
+
10
+ def cmd_login(rest)
11
+ provider_name = rest&.strip&.downcase
12
+
13
+ if provider_name && !provider_name.empty?
14
+ return show_login_usage unless valid_providers.include?(provider_name)
15
+
16
+ @state.enter_login_flow!(provider: provider_name.to_sym)
17
+ else
18
+ @state.enter_login_flow!
19
+ end
20
+ end
21
+
22
+ def valid_providers
23
+ RubyCoded::Auth::AuthManager::PROVIDERS.keys.map(&:to_s)
24
+ end
25
+
26
+ def show_login_usage
27
+ providers = valid_providers.join(", ")
28
+ @state.add_message(:system, "Usage: /login [#{providers}]")
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../model_filter"
4
+ require_relative "../codex_models"
4
5
 
5
6
  module RubyCoded
6
7
  module Chat
@@ -46,7 +47,7 @@ module RubyCoded
46
47
 
47
48
  def open_model_selector(show_all: false)
48
49
  models = fetch_models_for_authenticated_providers
49
- models = ModelFilter.filter(models) unless show_all
50
+ models = ModelFilter.filter(models) unless show_all || codex_oauth_active?
50
51
 
51
52
  if models.empty?
52
53
  @state.add_message(:system,
@@ -62,18 +63,23 @@ module RubyCoded
62
63
  return fetch_chat_models unless @credentials_store
63
64
 
64
65
  models = []
65
-
66
66
  Auth::AuthManager::PROVIDERS.each_key do |name|
67
- next unless @credentials_store.retrieve(name)
68
-
69
- provider_models = RubyLLM.models.by_provider(name).chat_models.to_a
70
- models.concat(provider_models)
67
+ creds = @credentials_store.retrieve(name)
68
+ models.concat(models_for_provider(name, creds)) if creds
71
69
  end
72
70
  models
73
71
  rescue StandardError
74
72
  fetch_chat_models
75
73
  end
76
74
 
75
+ def models_for_provider(name, creds)
76
+ if name == :openai && creds["auth_method"] == "oauth"
77
+ CodexModels.all
78
+ else
79
+ RubyLLM.models.by_provider(name).chat_models.to_a
80
+ end
81
+ end
82
+
77
83
  def fetch_chat_models
78
84
  RubyLLM.models.chat_models.to_a
79
85
  rescue StandardError
@@ -85,6 +91,13 @@ module RubyCoded
85
91
 
86
92
  model.to_s
87
93
  end
94
+
95
+ def codex_oauth_active?
96
+ return false unless @credentials_store
97
+
98
+ creds = @credentials_store.retrieve(:openai)
99
+ creds && creds["auth_method"] == "oauth"
100
+ end
88
101
  end
89
102
  end
90
103
  end
@@ -8,6 +8,7 @@ require_relative "command_handler/token_formatting"
8
8
  require_relative "command_handler/token_commands"
9
9
  require_relative "command_handler/agent_commands"
10
10
  require_relative "command_handler/plan_commands"
11
+ require_relative "command_handler/login_commands"
11
12
 
12
13
  module RubyCoded
13
14
  module Chat
@@ -21,6 +22,7 @@ module RubyCoded
21
22
  include TokenCommands
22
23
  include AgentCommands
23
24
  include PlanCommands
25
+ include LoginCommands
24
26
 
25
27
  BASE_COMMANDS = {
26
28
  "/help" => :cmd_help,
@@ -31,16 +33,18 @@ module RubyCoded
31
33
  "/history" => :cmd_history,
32
34
  "/tokens" => :cmd_tokens,
33
35
  "/agent" => :cmd_agent,
34
- "/plan" => :cmd_plan
36
+ "/plan" => :cmd_plan,
37
+ "/login" => :cmd_login
35
38
  }.freeze
36
39
 
37
40
  HELP_TEXT = File.read(File.join(__dir__, "help.txt")).freeze
38
41
 
39
- def initialize(state, llm_bridge:, user_config: nil, credentials_store: nil)
42
+ def initialize(state, llm_bridge:, user_config: nil, credentials_store: nil, auth_manager: nil)
40
43
  @state = state
41
44
  @llm_bridge = llm_bridge
42
45
  @user_config = user_config
43
46
  @credentials_store = credentials_store
47
+ @auth_manager = auth_manager
44
48
  @commands = build_command_map
45
49
  end
46
50
 
@@ -14,6 +14,8 @@ Available commands:
14
14
  /plan off Disable plan mode (warns if unsaved plan)
15
15
  /plan off --force Disable plan mode discarding unsaved plan
16
16
  /plan save [file] Save current plan to a markdown file
17
+ /login Authenticate with an AI provider
18
+ /login <provider> Authenticate directly with a specific provider (openai, anthropic)
17
19
  /exit, /quit Exit the chat
18
20
 
19
21
  Tool confirmations (agent mode):
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyCoded
4
+ module Chat
5
+ class InputHandler
6
+ # Handles input events for the multi-step login wizard:
7
+ # provider selection, auth method selection, API key entry, and OAuth waiting.
8
+ module LoginInputs
9
+ private
10
+
11
+ def handle_login_mode(event)
12
+ return :quit if event.ctrl_c?
13
+
14
+ case @state.login_step
15
+ when :provider_select, :auth_method_select
16
+ handle_login_select(event)
17
+ when :api_key_input
18
+ handle_login_key_input(event)
19
+ when :oauth_waiting
20
+ handle_login_oauth_waiting(event)
21
+ end
22
+ end
23
+
24
+ def handle_login_select(event)
25
+ return :login_cancel if event.esc?
26
+ return login_select_confirmed if event.enter?
27
+
28
+ if event.up?
29
+ @state.login_select_up
30
+ elsif event.down?
31
+ @state.login_select_down
32
+ end
33
+ nil
34
+ end
35
+
36
+ def login_select_confirmed
37
+ @state.login_step == :provider_select ? :login_provider_selected : :login_method_selected
38
+ end
39
+
40
+ def handle_login_key_input(event)
41
+ return :login_cancel if event.esc?
42
+ return handle_login_key_enter if event.enter?
43
+ return @state.delete_last_login_key_char if event.backspace?
44
+
45
+ append_login_key_char(event)
46
+ end
47
+
48
+ def handle_login_key_enter
49
+ @state.login_key_buffer.strip.empty? ? nil : :login_key_submitted
50
+ end
51
+
52
+ def append_login_key_char(event)
53
+ char = event.to_s
54
+ @state.append_to_login_key(char) unless char.empty? || event.ctrl? || event.alt?
55
+ nil
56
+ end
57
+
58
+ def handle_login_oauth_waiting(event)
59
+ return :login_oauth_cancel if event.esc?
60
+
61
+ nil
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -3,6 +3,7 @@
3
3
  require "ratatui_ruby"
4
4
 
5
5
  require_relative "input_handler/modal_inputs"
6
+ require_relative "input_handler/login_inputs"
6
7
  require_relative "input_handler/normal_mode_input"
7
8
 
8
9
  module RubyCoded
@@ -10,6 +11,7 @@ module RubyCoded
10
11
  # This class is used to handle the input events for the chat
11
12
  class InputHandler
12
13
  include ModalInputs
14
+ include LoginInputs
13
15
  include NormalModeInput
14
16
 
15
17
  def initialize(state)
@@ -30,6 +32,7 @@ module RubyCoded
30
32
  return handle_tool_confirmation_mode(event) if @state.awaiting_tool_confirmation?
31
33
  return handle_plan_clarification_mode(event) if @state.plan_clarification?
32
34
  return handle_model_select_mode(event) if @state.model_select?
35
+ return handle_login_mode(event) if @state.login_active?
33
36
  return handle_streaming_mode(event) if @state.streaming?
34
37
 
35
38
  handle_normal_mode(event)
@@ -38,7 +38,7 @@ module RubyCoded
38
38
  end
39
39
 
40
40
  def input_locked?
41
- @state.streaming? || @state.model_select? || @state.plan_clarification?
41
+ @state.streaming? || @state.model_select? || @state.plan_clarification? || @state.login_active?
42
42
  end
43
43
 
44
44
  def render_input_cursor(frame, area, prefix_len, scroll_offset)
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyCoded
4
+ module Chat
5
+ class Renderer
6
+ # Renders a centered popup for the multi-step login wizard.
7
+ module LoginFlow
8
+ private
9
+
10
+ def render_login_flow(frame, area)
11
+ popup_area = login_centered_popup(area)
12
+ frame.render_widget(@tui.clear, popup_area)
13
+
14
+ case @state.login_step
15
+ when :provider_select then render_login_select(frame, popup_area, title: "Select AI Provider")
16
+ when :auth_method_select then render_login_select(frame, popup_area, title: "Select Auth Method")
17
+ when :api_key_input then render_login_api_key(frame, popup_area)
18
+ when :oauth_waiting then render_login_oauth_waiting(frame, popup_area)
19
+ end
20
+ end
21
+
22
+ def render_login_select(frame, popup_area, title:)
23
+ hint_area, list_area = login_select_layout(popup_area)
24
+ render_login_hint(frame, hint_area)
25
+ render_login_list(frame, list_area, title)
26
+ end
27
+
28
+ def render_login_hint(frame, area)
29
+ widget = @tui.paragraph(
30
+ text: "↑↓ navigate, Enter select, Esc cancel",
31
+ block: @tui.block(title: "Login", borders: [:all])
32
+ )
33
+ frame.render_widget(widget, area)
34
+ end
35
+
36
+ def render_login_list(frame, area, title)
37
+ items = @state.login_items.map { |item| item[:label] }
38
+ widget = @tui.list(
39
+ items: items,
40
+ selected_index: @state.login_select_index,
41
+ highlight_style: @tui.style(bg: :blue, fg: :white, modifiers: [:bold]),
42
+ highlight_symbol: "> ",
43
+ scroll_padding: 2,
44
+ block: @tui.block(title: title, borders: [:all])
45
+ )
46
+ frame.render_widget(widget, area)
47
+ end
48
+
49
+ def render_login_api_key(frame, popup_area)
50
+ provider = @state.login_provider_module
51
+ info_area, input_area, error_area = login_api_key_layout(popup_area)
52
+
53
+ render_login_api_key_info(frame, info_area, provider)
54
+ render_login_api_key_input(frame, input_area)
55
+ render_login_api_key_error(frame, error_area)
56
+ end
57
+
58
+ def render_login_api_key_info(frame, area, provider)
59
+ console = provider.respond_to?(:console_url) ? provider.console_url : ""
60
+ text = "Generate your API key at:\n#{console}"
61
+ widget = @tui.paragraph(
62
+ text: text,
63
+ wrap: true,
64
+ block: @tui.block(title: "#{provider.display_name} API Key", borders: [:all])
65
+ )
66
+ frame.render_widget(widget, area)
67
+ end
68
+
69
+ def render_login_api_key_input(frame, area)
70
+ masked = "*" * @state.login_key_buffer.length
71
+ widget = @tui.paragraph(
72
+ text: "Key: #{masked}",
73
+ block: @tui.block(title: "Enter submit, Esc cancel", borders: [:all])
74
+ )
75
+ frame.render_widget(widget, area)
76
+ end
77
+
78
+ def render_login_api_key_error(frame, area)
79
+ error = @state.login_error
80
+ text = error || ""
81
+ style = error ? @tui.style(fg: :red) : @tui.style(fg: :dark_gray)
82
+ widget = @tui.paragraph(
83
+ text: text,
84
+ style: style,
85
+ wrap: true,
86
+ block: @tui.block(borders: [:all])
87
+ )
88
+ frame.render_widget(widget, area)
89
+ end
90
+
91
+ def render_login_oauth_waiting(frame, popup_area)
92
+ provider = @state.login_provider_module
93
+ widget = @tui.paragraph(
94
+ text: "Your browser has been opened for authentication.\n\n" \
95
+ "Waiting for #{provider.display_name} callback...\n\n" \
96
+ "Press Esc to cancel.",
97
+ wrap: true,
98
+ block: @tui.block(title: "Authenticating with #{provider.display_name}", borders: [:all])
99
+ )
100
+ frame.render_widget(widget, popup_area)
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyCoded
4
+ module Chat
5
+ class Renderer
6
+ # Layout helpers for centering the login flow popup.
7
+ module LoginFlowLayout
8
+ private
9
+
10
+ def login_centered_popup(area)
11
+ vertical = login_centered_vertical(area)
12
+ login_centered_horizontal(vertical[1])
13
+ end
14
+
15
+ def login_centered_vertical(area)
16
+ @tui.layout_split(
17
+ area,
18
+ direction: :vertical,
19
+ constraints: [
20
+ @tui.constraint_percentage(15),
21
+ @tui.constraint_percentage(70),
22
+ @tui.constraint_percentage(15)
23
+ ]
24
+ )
25
+ end
26
+
27
+ def login_centered_horizontal(area)
28
+ horizontal = @tui.layout_split(
29
+ area,
30
+ direction: :horizontal,
31
+ constraints: [
32
+ @tui.constraint_percentage(20),
33
+ @tui.constraint_percentage(60),
34
+ @tui.constraint_percentage(20)
35
+ ]
36
+ )
37
+ horizontal[1]
38
+ end
39
+
40
+ def login_select_layout(popup_area)
41
+ @tui.layout_split(
42
+ popup_area,
43
+ direction: :vertical,
44
+ constraints: [
45
+ @tui.constraint_length(3),
46
+ @tui.constraint_fill(1)
47
+ ]
48
+ )
49
+ end
50
+
51
+ def login_api_key_layout(popup_area)
52
+ @tui.layout_split(
53
+ popup_area,
54
+ direction: :vertical,
55
+ constraints: [
56
+ @tui.constraint_length(5),
57
+ @tui.constraint_length(3),
58
+ @tui.constraint_fill(1)
59
+ ]
60
+ )
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -15,7 +15,8 @@ module RubyCoded
15
15
 
16
16
  def status_bar_text(width)
17
17
  left = status_bar_left
18
- right = "#{@state.model} | #{format_cost(@state.total_session_cost)} "
18
+ model_label = @state.codex_mode ? "#{@state.model} (ChatGPT)" : "#{@state.model} (API)"
19
+ right = "#{model_label} | #{format_cost(@state.total_session_cost)} "
19
20
  center_pad = [width - left.length - right.length, 1].max
20
21
  "#{left}#{" " * center_pad}#{right}"
21
22
  end
@@ -6,6 +6,8 @@ require_relative "renderer/chat_panel_input"
6
6
  require_relative "renderer/model_selector"
7
7
  require_relative "renderer/plan_clarifier_layout"
8
8
  require_relative "renderer/plan_clarifier"
9
+ require_relative "renderer/login_flow_layout"
10
+ require_relative "renderer/login_flow"
9
11
  require_relative "renderer/status_bar"
10
12
 
11
13
  module RubyCoded
@@ -18,6 +20,8 @@ module RubyCoded
18
20
  include ModelSelector
19
21
  include PlanClarifierLayout
20
22
  include PlanClarifier
23
+ include LoginFlowLayout
24
+ include LoginFlow
21
25
  include StatusBar
22
26
 
23
27
  def initialize(tui, state)
@@ -33,14 +37,19 @@ module RubyCoded
33
37
  render_chat_panel(frame, chat_area)
34
38
  render_status_bar(frame, status_area)
35
39
  render_input_panel(frame, input_area)
36
- render_model_selector(frame, chat_area) if @state.model_select?
37
- render_plan_clarifier(frame, chat_area) if @state.plan_clarification?
38
- render_plugin_overlays(frame, chat_area, input_area)
40
+ render_overlays(frame, chat_area, input_area)
39
41
  end
40
42
  end
41
43
 
42
44
  private
43
45
 
46
+ def render_overlays(frame, chat_area, input_area)
47
+ render_model_selector(frame, chat_area) if @state.model_select?
48
+ render_plan_clarifier(frame, chat_area) if @state.plan_clarification?
49
+ render_login_flow(frame, chat_area) if @state.login_active?
50
+ render_plugin_overlays(frame, chat_area, input_area)
51
+ end
52
+
44
53
  # Calls each plugin's render method in registration order.
45
54
  def render_plugin_overlays(frame, chat_area, input_area)
46
55
  RubyCoded.plugin_registry.render_configs.each do |config|
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "login_flow_steps"
4
+
5
+ module RubyCoded
6
+ module Chat
7
+ class State
8
+ # Manages the multi-step login wizard state within the TUI.
9
+ # Steps: :provider_select -> :auth_method_select -> :api_key_input / :oauth_waiting
10
+ module LoginFlow
11
+ include LoginFlowSteps
12
+
13
+ attr_reader :login_step, :login_provider, :login_auth_method,
14
+ :login_items, :login_select_index,
15
+ :login_key_buffer, :login_key_cursor,
16
+ :login_error, :login_oauth_result
17
+
18
+ def init_login_flow
19
+ reset_login_state
20
+ end
21
+
22
+ def login_active?
23
+ @mode == :login
24
+ end
25
+
26
+ def enter_login_flow!(provider: nil)
27
+ reset_login_state
28
+ return enter_login_step_provider_select! unless provider
29
+
30
+ enter_login_for_provider!(provider)
31
+ end
32
+
33
+ def login_select_up
34
+ return if @login_items.empty?
35
+
36
+ @login_select_index = (@login_select_index - 1) % @login_items.size
37
+ mark_dirty!
38
+ end
39
+
40
+ def login_select_down
41
+ return if @login_items.empty?
42
+
43
+ @login_select_index = (@login_select_index + 1) % @login_items.size
44
+ mark_dirty!
45
+ end
46
+
47
+ def login_selected_item
48
+ @login_items[@login_select_index]
49
+ end
50
+
51
+ def login_advance_to_auth_method!(provider_name)
52
+ @login_provider = provider_name
53
+ enter_login_step_auth_method!(provider_name)
54
+ end
55
+
56
+ def login_advance_to_api_key!(provider_name, auth_method = :api_key)
57
+ @login_provider = provider_name
58
+ @login_auth_method = auth_method
59
+ enter_login_step_api_key!(provider_name)
60
+ end
61
+
62
+ def login_advance_to_oauth!(provider_name)
63
+ @login_provider = provider_name
64
+ @login_auth_method = :oauth
65
+ @login_step = :oauth_waiting
66
+ @login_items = []
67
+ @login_select_index = 0
68
+ @login_error = nil
69
+ @mode = :login
70
+ mark_dirty!
71
+ end
72
+
73
+ def append_to_login_key(text)
74
+ @login_key_buffer.insert(@login_key_cursor, text)
75
+ @login_key_cursor += text.length
76
+ @login_error = nil
77
+ mark_dirty!
78
+ end
79
+
80
+ def delete_last_login_key_char
81
+ return if @login_key_cursor <= 0
82
+
83
+ @login_key_buffer.slice!(@login_key_cursor - 1)
84
+ @login_key_cursor -= 1
85
+ @login_error = nil
86
+ mark_dirty!
87
+ end
88
+
89
+ def login_set_oauth_result!(result)
90
+ @mutex.synchronize do
91
+ @login_oauth_result = result
92
+ @dirty = true
93
+ end
94
+ end
95
+
96
+ def login_clear_oauth_result!
97
+ @login_oauth_result = nil
98
+ end
99
+
100
+ def login_set_error!(msg)
101
+ @login_error = msg
102
+ mark_dirty!
103
+ end
104
+
105
+ def exit_login_flow!
106
+ @mode = :chat
107
+ reset_login_state
108
+ mark_dirty!
109
+ end
110
+
111
+ def login_provider_module
112
+ providers_map[@login_provider]
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyCoded
4
+ module Chat
5
+ class State
6
+ # Private helpers for LoginFlow: state reset, provider lookup,
7
+ # and step-specific initialization.
8
+ module LoginFlowSteps
9
+ private
10
+
11
+ def reset_login_state
12
+ @login_step = nil
13
+ @login_provider = nil
14
+ @login_auth_method = nil
15
+ @login_items = []
16
+ @login_select_index = 0
17
+ @login_key_buffer = String.new
18
+ @login_key_cursor = 0
19
+ @login_error = nil
20
+ @login_oauth_result = nil
21
+ end
22
+
23
+ def providers_map
24
+ RubyCoded::Auth::AuthManager::PROVIDERS
25
+ end
26
+
27
+ def enter_login_for_provider!(provider)
28
+ methods = providers_map[provider].auth_methods
29
+ if methods.size == 1
30
+ enter_login_step_api_key!(provider)
31
+ else
32
+ enter_login_step_auth_method!(provider)
33
+ end
34
+ end
35
+
36
+ def enter_login_step_provider_select!
37
+ @login_step = :provider_select
38
+ @login_items = providers_map.map { |key, prov| { key: key, label: prov.display_name } }
39
+ @login_select_index = 0
40
+ @mode = :login
41
+ mark_dirty!
42
+ end
43
+
44
+ def enter_login_step_auth_method!(provider_name)
45
+ @login_provider = provider_name
46
+ provider = providers_map[provider_name]
47
+ @login_step = :auth_method_select
48
+ @login_items = provider.auth_methods
49
+ @login_select_index = 0
50
+ @mode = :login
51
+ mark_dirty!
52
+ end
53
+
54
+ def enter_login_step_api_key!(provider_name)
55
+ @login_provider = provider_name
56
+ @login_auth_method = :api_key
57
+ @login_step = :api_key_input
58
+ @login_items = []
59
+ @login_select_index = 0
60
+ @login_key_buffer = String.new
61
+ @login_key_cursor = 0
62
+ @login_error = nil
63
+ @mode = :login
64
+ mark_dirty!
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end