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.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +3 -1
- data/CHANGELOG.md +29 -0
- data/README.md +15 -10
- data/lib/ruby_coded/auth/auth_manager.rb +20 -5
- data/lib/ruby_coded/auth/jwt_decoder.rb +29 -0
- data/lib/ruby_coded/auth/providers/openai.rb +19 -5
- data/lib/ruby_coded/chat/app/event_dispatch.rb +23 -3
- data/lib/ruby_coded/chat/app/login_handler.rb +79 -0
- data/lib/ruby_coded/chat/app/oauth_handler.rb +105 -0
- data/lib/ruby_coded/chat/app.rb +65 -6
- data/lib/ruby_coded/chat/codex_bridge/error_handling.rb +93 -0
- data/lib/ruby_coded/chat/codex_bridge/request_builder.rb +104 -0
- data/lib/ruby_coded/chat/codex_bridge/sse_parser.rb +136 -0
- data/lib/ruby_coded/chat/codex_bridge/token_manager.rb +45 -0
- data/lib/ruby_coded/chat/codex_bridge/tool_approval.rb +51 -0
- data/lib/ruby_coded/chat/codex_bridge/tool_handling.rb +128 -0
- data/lib/ruby_coded/chat/codex_bridge.rb +126 -0
- data/lib/ruby_coded/chat/codex_models.rb +41 -0
- data/lib/ruby_coded/chat/command_handler/login_commands.rb +33 -0
- data/lib/ruby_coded/chat/command_handler/model_commands.rb +19 -6
- data/lib/ruby_coded/chat/command_handler.rb +6 -2
- data/lib/ruby_coded/chat/help.txt +2 -0
- data/lib/ruby_coded/chat/input_handler/login_inputs.rb +66 -0
- data/lib/ruby_coded/chat/input_handler.rb +3 -0
- data/lib/ruby_coded/chat/renderer/chat_panel_input.rb +1 -1
- data/lib/ruby_coded/chat/renderer/login_flow.rb +105 -0
- data/lib/ruby_coded/chat/renderer/login_flow_layout.rb +65 -0
- data/lib/ruby_coded/chat/renderer/status_bar.rb +2 -1
- data/lib/ruby_coded/chat/renderer.rb +12 -3
- data/lib/ruby_coded/chat/state/login_flow.rb +117 -0
- data/lib/ruby_coded/chat/state/login_flow_steps.rb +69 -0
- data/lib/ruby_coded/chat/state.rb +25 -2
- data/lib/ruby_coded/initializer.rb +14 -3
- data/lib/ruby_coded/plugins/command_completion/state_extension.rb +1 -0
- data/lib/ruby_coded/strategies/oauth_strategy.rb +4 -3
- data/lib/ruby_coded/version.rb +1 -1
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|