ruby_coded 0.3.1 → 0.4.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +20 -0
  3. data/README.md +113 -1
  4. data/lib/ruby_coded/chat/app.rb +35 -34
  5. data/lib/ruby_coded/chat/bridge_common/auto_switch.rb +45 -0
  6. data/lib/ruby_coded/chat/bridge_common/mode_transitions.rb +70 -0
  7. data/lib/ruby_coded/chat/bridge_common/tool_flow.rb +61 -0
  8. data/lib/ruby_coded/chat/bridge_common.rb +31 -0
  9. data/lib/ruby_coded/chat/bridge_factory.rb +61 -0
  10. data/lib/ruby_coded/chat/codex_bridge/error_handling.rb +1 -20
  11. data/lib/ruby_coded/chat/codex_bridge/request_builder.rb +3 -15
  12. data/lib/ruby_coded/chat/codex_bridge/tool_approval.rb +8 -32
  13. data/lib/ruby_coded/chat/codex_bridge/tool_handling.rb +3 -34
  14. data/lib/ruby_coded/chat/codex_bridge.rb +31 -46
  15. data/lib/ruby_coded/chat/command_handler/agent_commands.rb +3 -3
  16. data/lib/ruby_coded/chat/command_handler/plan_commands.rb +1 -1
  17. data/lib/ruby_coded/chat/command_handler/skill_commands.rb +87 -0
  18. data/lib/ruby_coded/chat/command_handler.rb +3 -0
  19. data/lib/ruby_coded/chat/llm_bridge/chat_configuration.rb +38 -0
  20. data/lib/ruby_coded/chat/llm_bridge/plan_mode.rb +3 -32
  21. data/lib/ruby_coded/chat/llm_bridge/tool_call_handling.rb +9 -50
  22. data/lib/ruby_coded/chat/llm_bridge.rb +31 -65
  23. data/lib/ruby_coded/chat/prompt_builder.rb +66 -0
  24. data/lib/ruby_coded/chat/renderer/chat_panel.rb +48 -62
  25. data/lib/ruby_coded/chat/renderer/chat_panel_cache.rb +35 -0
  26. data/lib/ruby_coded/chat/renderer/chat_panel_formatting.rb +72 -0
  27. data/lib/ruby_coded/chat/renderer/chat_panel_layout.rb +81 -0
  28. data/lib/ruby_coded/chat/renderer/chat_panel_sections.rb +93 -0
  29. data/lib/ruby_coded/chat/renderer/chat_panel_thinking.rb +20 -34
  30. data/lib/ruby_coded/chat/renderer/chat_panel_thinking_render.rb +49 -0
  31. data/lib/ruby_coded/chat/renderer/rich_text.rb +92 -0
  32. data/lib/ruby_coded/chat/renderer/rich_text_inline.rb +81 -0
  33. data/lib/ruby_coded/chat/renderer.rb +12 -0
  34. data/lib/ruby_coded/chat/runtime_mode.rb +97 -0
  35. data/lib/ruby_coded/commands/core_provider.rb +7 -0
  36. data/lib/ruby_coded/commands.rb +11 -0
  37. data/lib/ruby_coded/plugins.rb +10 -1
  38. data/lib/ruby_coded/skills/catalog.rb +103 -0
  39. data/lib/ruby_coded/skills/markdown_loader.rb +112 -0
  40. data/lib/ruby_coded/skills/prompt_formatter.rb +42 -0
  41. data/lib/ruby_coded/skills/skill_definition.rb +29 -0
  42. data/lib/ruby_coded/skills.rb +18 -0
  43. data/lib/ruby_coded/tools/base_tool.rb +14 -0
  44. data/lib/ruby_coded/tools/delete_path_tool.rb +5 -8
  45. data/lib/ruby_coded/tools/edit_file_tool.rb +6 -8
  46. data/lib/ruby_coded/tools/execution_pipeline.rb +60 -0
  47. data/lib/ruby_coded/tools/execution_policy.rb +89 -0
  48. data/lib/ruby_coded/tools/write_file_tool.rb +6 -9
  49. data/lib/ruby_coded/version.rb +1 -1
  50. data/lib/ruby_coded.rb +1 -0
  51. metadata +25 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 45aa4262fb573ca03510db92e2d3bd9795136b935fe7ffe8ba02821a570b107b
4
- data.tar.gz: ed299052fcaa75af5a03951d18fbae6021fccd2463d28f73ea7f57cacdca28d7
3
+ metadata.gz: c0c96bdf1a87ce1f830d0a36b9fb78600f8ee1df2bd81603630f7bf87b0cac08
4
+ data.tar.gz: 3a08a46116afd6c8b1f82ec0c085c4758b65f349631854a47027883578ff3c3c
5
5
  SHA512:
6
- metadata.gz: f06c04df1359024f169e00185535ab2f8be8f24a6eee94f3f231c130dc31d385a48163072a843330ac83a79060e553d94a59dc4224e5a93679066894d5642e81
7
- data.tar.gz: 66e264de72737cbe9b3158267faf941de67156c2e05b855e7cf679ca640ceb3ecaa16c15f694aa5c2e549578004b8851aeb2ed7513a358bcb8396603eb217f34
6
+ metadata.gz: 9a5f8e7e7587b8e87dd830b9a2a20976611ec4d04a2b61f6c805a2877727e4b9299865b947a89929b3e83a71149ac0f5a868de69fbdd02262d00de1b86595c6a
7
+ data.tar.gz: 3739c33f411b92918553b02816218f7ea54a11664802b10a7f15fa37138b9f4631315373e2624fbfd2a4246c44f27455454f0111385ccc855f88b08e3e75e597
data/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.4.0] - 2026-07-04
4
+
5
+ ### Added
6
+
7
+ - **Highlight the users' messages in the chat section**: Now the chat sections highlights and spots the users' mesagges in blocks
8
+ for a easely reading of the chat
9
+ - **Rich text support in the TUI chat renderer**: Added rich text rendering support for chat output in the terminal UI.
10
+ - **Project-local skills system**: RubyCoded can now load Markdown skill files from `.rubycoded/skills/` using YAML frontmatter metadata (`name`, `description`, `modes`, optional `tags`, `trigger`, and `priority`) and expose them as reusable project instructions.
11
+ - **Skill management commands**: Added `/skills reload` and `/skills list` to reload and inspect project-local skills without restarting the application.
12
+ - **Skill validation and cataloging**: Added a dedicated `RubyCoded::Skills` subsystem with a markdown loader, normalized skill definitions, duplicate detection, invalid file reporting, and mode-aware in-memory cataloging.
13
+
14
+ ### Changed
15
+
16
+ - **Refactor chat panel rendering system into multiple modules**
17
+ - **Prompt construction now supports project skills**: Chat, plan, and agent mode instructions now append relevant project-local skills so project-specific guidance is consistently available across runtime modes, including Codex-backed sessions.
18
+ - **Ruby-conventional predicate names on bridges**: `LLMBridge` and `CodexBridge` now expose `agentic_mode?` and `plan_mode?` (previously `agentic_mode` / `plan_mode`). Plugins that read these predicates need to update the call site.
19
+ - **Internal chat bridge refactor**: Extracted `RuntimeMode` as the single source of truth for chat/agent/plan modes, `BridgeFactory` for backend selection, `Tools::ExecutionPolicy` for centralized risk/approval/limits, `PromptBuilder` for mode-aware system instructions, and `BridgeCommon` (`ModeTransitions`, `ToolFlow`, `AutoSwitch`) to share behavior between `LLMBridge` and `CodexBridge`.
20
+ - **Tool execution pipeline**: Added `Tools::ExecutionPipeline` and `BaseTool#run_pipeline` to standardize path validation, project-root guarding and filesystem error normalization for side-effecting tools; adopted in `WriteFileTool`, `EditFileTool` and `DeletePathTool`.
21
+ - **Extension model documented**: The README now describes the boundary between commands, skills and plugins.
22
+
3
23
  ## [0.3.1] - 2026-06-28
4
24
 
5
25
  ### Added
data/README.md CHANGED
@@ -28,7 +28,7 @@ An AI-powered terminal coding assistant built in Ruby. Chat with LLMs, let an ag
28
28
  - **Tool confirmation** — Write and dangerous operations require explicit approval; safe operations (read, list) run automatically
29
29
  - **Token & cost tracking** — Live status bar showing token usage, estimated session cost, and auth mode indicator
30
30
  - **Plugin system** — Extend the chat with custom state, input handlers, renderer overlays, and commands
31
- - **Slash commands** — `/agent`, `/plan`, `/model`, `/login`, `/history`, `/tokens`, `/commands reload`, `/commands list`, `/help`, and more
31
+ - **Slash commands** — `/agent`, `/plan`, `/model`, `/login`, `/history`, `/tokens`, `/commands reload`, `/commands list`, `/skills reload`, `/skills list`, `/help`, and more
32
32
 
33
33
  ## Requirements
34
34
 
@@ -64,6 +64,8 @@ On first launch you'll be asked to authenticate with a provider. After that, you
64
64
  | `/login` | Authenticate with a provider (OpenAI, Anthropic) |
65
65
  | `/commands reload` | Reload custom markdown commands from the current project |
66
66
  | `/commands list` | List currently loaded custom markdown commands |
67
+ | `/skills reload` | Reload project-local skills from the current project |
68
+ | `/skills list` | List currently loaded project-local skills |
67
69
  | `/tokens` | Show detailed token usage breakdown |
68
70
  | `/history` | Show conversation history |
69
71
  | `/clear` | Clear the conversation |
@@ -93,6 +95,22 @@ Safe tools run without asking. Confirm and dangerous tools show the operation de
93
95
 
94
96
  Plan mode restricts the model to read-only tools and a planning-oriented system prompt. The model will analyze your project and propose a structured plan. If the model needs clarification, it presents interactive options you can select or answer with custom text.
95
97
 
98
+ ### Extension model
99
+
100
+ RubyCoded exposes three distinct extension mechanisms. When adding new behavior, pick the one that matches the intent:
101
+
102
+ | Mechanism | What it is | Trigger | Where it lives |
103
+ |---|---|---|---|
104
+ | **Commands** | Explicit user-invoked actions (built-in or Markdown-defined slash commands) | User types `/foo` | `lib/ruby_coded/chat/command_handler/` (built-in) or `.ruby_coded/commands/*.md` (project-local) |
105
+ | **Skills** | Automatic instruction overlays merged into the system prompt when they match the active mode/context | Mode + optional tags/trigger match the current request | `.rubycoded/skills/*.md` (project-local) |
106
+ | **Plugins** | Code-level extensions that hook into state, input, renderer, or add new commands at runtime | Loaded on startup | Ruby classes registered via `RubyCoded::Plugins` |
107
+
108
+ Rule of thumb:
109
+
110
+ - Use a **command** if the user should invoke it explicitly.
111
+ - Use a **skill** if you want extra instructions injected automatically for a mode.
112
+ - Use a **plugin** if you need Ruby code to observe or mutate runtime behavior.
113
+
96
114
  ### Custom commands
97
115
 
98
116
  You can define your own project-specific slash commands using Markdown files inside:
@@ -173,6 +191,100 @@ List currently loaded custom commands:
173
191
  - Plugin commands also take precedence over custom Markdown commands.
174
192
  - Invalid Markdown command files are ignored during reload.
175
193
 
194
+ ### Project skills
195
+
196
+ RubyCoded also supports project-local skills: reusable Markdown instructions that are automatically injected into chat, plan, and agent behavior when they match the active mode.
197
+
198
+ Skills are loaded from:
199
+
200
+ ```bash
201
+ .rubycoded/skills/
202
+ ```
203
+
204
+ This path is intentionally different from custom commands, which use:
205
+
206
+ ```bash
207
+ .ruby_coded/commands/
208
+ ```
209
+
210
+ Each `.md` file defines one skill.
211
+
212
+ #### Skill file format
213
+
214
+ Use YAML frontmatter followed by the skill body:
215
+
216
+ ```md
217
+ ---
218
+ name: Rails Migration Safety
219
+ description: Improve safety when changing schema or migrations
220
+ modes:
221
+ - agent
222
+ - plan
223
+ tags:
224
+ - rails
225
+ - migration
226
+ trigger: schema
227
+ priority: 10
228
+ ---
229
+
230
+ Before proposing or applying migration changes:
231
+ - inspect existing schema and recent migrations
232
+ - prefer additive, reversible changes
233
+ - call out data backfill or locking risks explicitly
234
+ ```
235
+
236
+ #### Required fields
237
+
238
+ - `name` — unique skill name within the project
239
+ - `description` — short explanation shown in `/skills list`
240
+ - `modes` — one or more supported modes: `chat`, `plan`, `agent`
241
+
242
+ #### Optional fields
243
+
244
+ - `tags` — keywords used for simple relevance matching
245
+ - `trigger` — plain-text trigger checked against the current request
246
+ - `priority` — higher-priority skills are listed first and applied first
247
+
248
+ #### How skills are activated
249
+
250
+ Current behavior is intentionally simple:
251
+
252
+ - all skills compatible with the active mode are loaded
253
+ - if a request matches skill `tags` or `trigger`, matching skills are preferred
254
+ - if nothing matches, all mode-compatible skills remain available
255
+
256
+ This means skills are useful immediately without requiring manual activation UI.
257
+
258
+ #### Managing skills
259
+
260
+ Reload skills after adding, editing, or deleting skill files:
261
+
262
+ ```bash
263
+ /skills reload
264
+ ```
265
+
266
+ List the currently loaded skills:
267
+
268
+ ```bash
269
+ /skills list
270
+ ```
271
+
272
+ The reload command reports:
273
+
274
+ - how many skills were added
275
+ - how many were removed
276
+ - how many are currently available
277
+ - how many invalid files were ignored
278
+ - how many duplicate skill names were ignored
279
+
280
+ #### Skill behavior notes
281
+
282
+ - Skills are project-local and loaded relative to the current project root.
283
+ - Skills are injected into system instructions for `chat`, `plan`, and `agent` modes.
284
+ - If a skill conflicts with higher-priority system instructions or the user’s explicit request, the higher-priority instruction should win.
285
+ - Invalid or malformed skill files are ignored during reload.
286
+ - If duplicate skill names exist, the first loaded definition wins and later duplicates are ignored.
287
+
176
288
  ## Keyboard shortcuts
177
289
 
178
290
  | Key | Action |
@@ -10,9 +10,7 @@ require_relative "state"
10
10
  require_relative "input_handler"
11
11
  require_relative "renderer"
12
12
  require_relative "command_handler"
13
- require_relative "llm_bridge"
14
- require_relative "codex_bridge"
15
- require_relative "codex_models"
13
+ require_relative "bridge_factory"
16
14
  require_relative "../commands/catalog"
17
15
  require_relative "../auth/credentials_store"
18
16
  require_relative "../auth/jwt_decoder"
@@ -42,16 +40,31 @@ module RubyCoded
42
40
  end
43
41
 
44
42
  def build_components!
43
+ build_catalogs!
44
+ @state = State.new(model: @model, command_catalog: @command_catalog)
45
+ @credentials_store = Auth::CredentialsStore.new(user_config: @user_config)
46
+ @bridge_factory = build_bridge_factory
47
+ @llm_bridge = @bridge_factory.build
48
+ @input_handler = InputHandler.new(@state)
49
+ @command_handler = build_command_handler
50
+ end
51
+
52
+ def build_catalogs!
45
53
  @command_catalog = RubyCoded::Commands::Catalog.new(
46
54
  project_root: Dir.pwd,
47
55
  plugin_registry: RubyCoded.plugin_registry
48
56
  )
57
+ @skill_catalog = RubyCoded::Skills::Catalog.new(project_root: Dir.pwd)
58
+ end
49
59
 
50
- @state = State.new(model: @model, command_catalog: @command_catalog)
51
- @credentials_store = Auth::CredentialsStore.new(user_config: @user_config)
52
- @llm_bridge = create_bridge
53
- @input_handler = InputHandler.new(@state)
54
- @command_handler = build_command_handler
60
+ def build_bridge_factory
61
+ BridgeFactory.new(
62
+ state: @state,
63
+ credentials_store: @credentials_store,
64
+ auth_manager: @auth_manager,
65
+ skill_catalog: @skill_catalog,
66
+ user_config: @user_config
67
+ )
55
68
  end
56
69
 
57
70
  IDLE_POLL_TIMEOUT = 0.016
@@ -109,7 +122,8 @@ module RubyCoded
109
122
  user_config: @user_config,
110
123
  credentials_store: @credentials_store,
111
124
  auth_manager: @auth_manager,
112
- command_catalog: @command_catalog
125
+ command_catalog: @command_catalog,
126
+ skill_catalog: @skill_catalog
113
127
  )
114
128
  end
115
129
 
@@ -125,7 +139,7 @@ module RubyCoded
125
139
  end
126
140
 
127
141
  def enable_default_agent_mode!
128
- return if @llm_bridge.agentic_mode
142
+ return if @llm_bridge.agentic_mode?
129
143
 
130
144
  @llm_bridge.toggle_agentic_mode!(true)
131
145
  end
@@ -149,34 +163,21 @@ module RubyCoded
149
163
  @state.add_message(:system, "Model switched to #{model_name}.")
150
164
  end
151
165
 
152
- def create_bridge
153
- openai_creds = @credentials_store.retrieve(:openai)
154
- if openai_creds && openai_creds["auth_method"] == "oauth"
155
- @state.codex_mode = true
156
- ensure_valid_codex_model!
157
- CodexBridge.new(@state, credentials_store: @credentials_store, auth_manager: @auth_manager)
158
- else
159
- @state.codex_mode = false
160
- LLMBridge.new(@state)
161
- end
162
- end
163
-
164
- def ensure_valid_codex_model!
165
- return if CodexModels.codex_model?(@state.model)
166
-
167
- @state.model = CodexBridge::DEFAULT_MODEL
168
- @user_config&.set_config("model", CodexBridge::DEFAULT_MODEL)
169
- end
170
-
171
166
  def recreate_bridge!
172
167
  @command_catalog.reload!
173
- agentic = @llm_bridge.agentic_mode
174
- plan = @llm_bridge.plan_mode
175
- @llm_bridge = create_bridge
176
- @llm_bridge.toggle_agentic_mode!(agentic) if agentic
177
- @llm_bridge.toggle_plan_mode!(plan) if plan
168
+ @skill_catalog.reload!
169
+ previous_mode = @llm_bridge.mode
170
+ @llm_bridge = @bridge_factory.build
171
+ restore_bridge_mode!(previous_mode)
178
172
  @command_handler = build_command_handler
179
173
  end
174
+
175
+ def restore_bridge_mode!(mode)
176
+ case mode.name
177
+ when :agent then @llm_bridge.toggle_agentic_mode!(true)
178
+ when :plan then @llm_bridge.toggle_plan_mode!(true)
179
+ end
180
+ end
180
181
  end
181
182
  end
182
183
  end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyCoded
4
+ module Chat
5
+ module BridgeCommon
6
+ # Recognizes "please implement the plan" style messages and
7
+ # switches the runtime from plan mode to agent mode so that
8
+ # write tools become available for execution.
9
+ module AutoSwitch
10
+ IMPLEMENTATION_PATTERNS = [
11
+ /\bimplement/i,
12
+ /\bgo ahead/i,
13
+ /\bproceed/i,
14
+ /\bexecut/i,
15
+ /\bejecutar?/i,
16
+ /\bcomenz/i,
17
+ /\bcomienz/i,
18
+ /\bhazlo/i,
19
+ /\bconstru[iy]/i,
20
+ /\badelante/i,
21
+ /\bdale\b/i,
22
+ /\bdo it/i,
23
+ /\bbuild it/i
24
+ ].freeze
25
+
26
+ private
27
+
28
+ def should_auto_switch_to_agent?(input)
29
+ @mode.plan? && @state.respond_to?(:current_plan) && @state.current_plan &&
30
+ implementation_request?(input)
31
+ end
32
+
33
+ def implementation_request?(input)
34
+ IMPLEMENTATION_PATTERNS.any? { |pattern| input.match?(pattern) }
35
+ end
36
+
37
+ def auto_switch_to_agent!
38
+ toggle_agentic_mode!(true)
39
+ @state.add_message(:system,
40
+ "Plan mode disabled — switching to agent mode to implement the plan.")
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../runtime_mode"
4
+
5
+ module RubyCoded
6
+ module Chat
7
+ module BridgeCommon
8
+ # Encapsulates transitions between chat/agent/plan modes and
9
+ # keeps the associated State flags (agentic_mode, plan tracking,
10
+ # auto-approve) consistent.
11
+ module ModeTransitions
12
+ def agentic_mode?
13
+ @mode.agent?
14
+ end
15
+
16
+ def plan_mode?
17
+ @mode.plan?
18
+ end
19
+
20
+ def toggle_agentic_mode!(enabled)
21
+ if enabled
22
+ transition_to_agent!
23
+ else
24
+ transition_out_of_agent!
25
+ end
26
+ after_mode_change!
27
+ end
28
+
29
+ def toggle_plan_mode!(enabled)
30
+ if enabled
31
+ transition_to_plan!
32
+ elsif @mode.plan?
33
+ @mode = RuntimeMode.chat
34
+ end
35
+ after_mode_change!
36
+ end
37
+
38
+ private
39
+
40
+ # Override in the concrete bridge when reconfiguration is
41
+ # needed after a mode change (e.g. re-registering tools).
42
+ def after_mode_change!
43
+ nil
44
+ end
45
+
46
+ def transition_to_agent!
47
+ was_plan = @mode.plan?
48
+ @mode = RuntimeMode.agent
49
+ @state.agentic_mode = true
50
+ @state.deactivate_plan_mode! if was_plan
51
+ end
52
+
53
+ def transition_out_of_agent!
54
+ @mode = RuntimeMode.chat if @mode.agent?
55
+ @state.agentic_mode = false
56
+ @state.disable_auto_approve!
57
+ end
58
+
59
+ def transition_to_plan!
60
+ was_agent = @mode.agent?
61
+ @mode = RuntimeMode.plan
62
+ return unless was_agent
63
+
64
+ @state.agentic_mode = false
65
+ @state.disable_auto_approve!
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../tools/agent_cancelled_error"
4
+ require_relative "../../tools/tool_rejected_error"
5
+
6
+ module RubyCoded
7
+ module Chat
8
+ module BridgeCommon
9
+ # Shared tool-confirmation flow and cancellation signaling.
10
+ # Uses the State's condition variable to block the streaming
11
+ # thread until the TUI supplies a decision (approve/reject/cancel).
12
+ module ToolFlow
13
+ def cancel!
14
+ @cancel_requested = true
15
+ @state.mutex.synchronize { @state.tool_cv.signal }
16
+ end
17
+
18
+ def approve_tool!
19
+ @state.tool_confirmation_response = :approved
20
+ end
21
+
22
+ def approve_all_tools!
23
+ @state.enable_auto_approve!
24
+ @state.tool_confirmation_response = :approved
25
+ end
26
+
27
+ def reject_tool!
28
+ @state.tool_confirmation_response = :rejected
29
+ end
30
+
31
+ private
32
+
33
+ def poll_tool_decision
34
+ @state.mutex.synchronize do
35
+ loop do
36
+ return :cancelled if @cancel_requested
37
+
38
+ resp = @state.instance_variable_get(:@tool_confirmation_response)
39
+ return resp if %i[approved rejected].include?(resp)
40
+
41
+ @state.tool_cv.wait(@state.mutex, 0.1)
42
+ end
43
+ end
44
+ end
45
+
46
+ def apply_tool_decision(decision, display_name)
47
+ case decision
48
+ when :cancelled
49
+ @state.clear_tool_confirmation!
50
+ raise Tools::AgentCancelledError, "Operation cancelled by user"
51
+ when :approved
52
+ @state.resolve_tool_confirmation!(:approved)
53
+ when :rejected
54
+ @state.resolve_tool_confirmation!(:rejected)
55
+ raise Tools::ToolRejectedError, "User rejected #{display_name}"
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "runtime_mode"
4
+ require_relative "bridge_common/mode_transitions"
5
+ require_relative "bridge_common/tool_flow"
6
+ require_relative "bridge_common/auto_switch"
7
+
8
+ module RubyCoded
9
+ module Chat
10
+ # Shared behavior between LLMBridge and CodexBridge.
11
+ #
12
+ # Composes three concerns:
13
+ #
14
+ # - {ModeTransitions} — chat/agent/plan transitions + State sync,
15
+ # - {ToolFlow} — approve/reject/cancel + poll/apply decision,
16
+ # - {AutoSwitch} — "implement the plan" → agent mode heuristic.
17
+ #
18
+ # Consumers must:
19
+ # - initialize `@mode = RuntimeMode.chat` (or another mode),
20
+ # - initialize `@cancel_requested = false`,
21
+ # - override `after_mode_change!` if the backend needs to
22
+ # reconfigure state (e.g. the RubyLLM chat instance).
23
+ module BridgeCommon
24
+ def self.included(base)
25
+ base.include(ModeTransitions)
26
+ base.include(ToolFlow)
27
+ base.include(AutoSwitch)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "llm_bridge"
4
+ require_relative "codex_bridge"
5
+ require_relative "codex_models"
6
+
7
+ module RubyCoded
8
+ module Chat
9
+ # Decides which bridge (LLMBridge vs CodexBridge) to instantiate
10
+ # based on the stored OpenAI credentials.
11
+ #
12
+ # Extracted from Chat::App so that backend selection remains a
13
+ # single, testable responsibility and App stays focused on
14
+ # orchestration.
15
+ class BridgeFactory
16
+ def initialize(state:, credentials_store:, auth_manager:, skill_catalog:, user_config: nil)
17
+ @state = state
18
+ @credentials_store = credentials_store
19
+ @auth_manager = auth_manager
20
+ @skill_catalog = skill_catalog
21
+ @user_config = user_config
22
+ end
23
+
24
+ def build
25
+ return build_llm_bridge unless codex_backend?
26
+
27
+ build_codex_bridge
28
+ end
29
+
30
+ private
31
+
32
+ def codex_backend?
33
+ credentials = @credentials_store.retrieve(:openai)
34
+ credentials && credentials["auth_method"] == "oauth"
35
+ end
36
+
37
+ def build_codex_bridge
38
+ @state.codex_mode = true
39
+ ensure_valid_codex_model!
40
+ CodexBridge.new(
41
+ @state,
42
+ credentials_store: @credentials_store,
43
+ auth_manager: @auth_manager,
44
+ skill_catalog: @skill_catalog
45
+ )
46
+ end
47
+
48
+ def build_llm_bridge
49
+ @state.codex_mode = false
50
+ LLMBridge.new(@state, skill_catalog: @skill_catalog)
51
+ end
52
+
53
+ def ensure_valid_codex_model!
54
+ return if CodexModels.codex_model?(@state.model)
55
+
56
+ @state.model = CodexBridge::DEFAULT_MODEL
57
+ @user_config&.set_config("model", CodexBridge::DEFAULT_MODEL)
58
+ end
59
+ end
60
+ end
61
+ end
@@ -4,13 +4,7 @@ module RubyCoded
4
4
  module Chat
5
5
  class CodexBridge
6
6
  # Retry logic and error message formatting for the Codex API client.
7
- # rubocop:disable Metrics/ModuleLength
8
7
  module ErrorHandling
9
- AGENT_SWITCH_PATTERN = /
10
- \b(implement|go[ ]ahead|proceed|execut|ejecutar?|comenz|
11
- comienz|hazlo|constru[iy]|adelante|dale|do[ ]it|build[ ]it)\b
12
- /ix
13
-
14
8
  UNSUPPORTED_MODEL_PATTERN = /not supported when using Codex with a ChatGPT account/i
15
9
 
16
10
  private
@@ -23,8 +17,7 @@ module RubyCoded
23
17
  end
24
18
 
25
19
  def reset_call_counts
26
- @tool_call_count = 0
27
- @write_tool_call_count = 0
20
+ @policy.reset_counters!
28
21
  end
29
22
 
30
23
  def prepare_send(input)
@@ -35,17 +28,6 @@ module RubyCoded
35
28
  @state.add_message(:assistant, "")
36
29
  end
37
30
 
38
- def should_auto_switch_to_agent?(input)
39
- @plan_mode && @state.respond_to?(:current_plan) && @state.current_plan &&
40
- input.match?(AGENT_SWITCH_PATTERN)
41
- end
42
-
43
- def auto_switch_to_agent!
44
- toggle_agentic_mode!(true)
45
- @state.add_message(:system,
46
- "Plan mode disabled — switching to agent mode to implement the plan.")
47
- end
48
-
49
31
  def attempt_with_retries(input, retries = 0, fallback_attempted: false)
50
32
  perform_codex_request(input)
51
33
  rescue Tools::AgentCancelledError, Tools::AgentIterationLimitError, Tools::ToolRejectedError => e
@@ -145,7 +127,6 @@ module RubyCoded
145
127
  MSG
146
128
  end
147
129
  end
148
- # rubocop:enable Metrics/ModuleLength
149
130
  end
150
131
  end
151
132
  end
@@ -12,9 +12,6 @@ module RubyCoded
12
12
  "OpenAI-Beta" => "responses=experimental"
13
13
  }.freeze
14
14
 
15
- DEFAULT_INSTRUCTIONS = "You are a helpful coding assistant. " \
16
- "Answer concisely and provide code examples when relevant."
17
-
18
15
  private
19
16
 
20
17
  def codex_headers
@@ -29,7 +26,7 @@ module RubyCoded
29
26
 
30
27
  def build_request_body
31
28
  body = base_request_body
32
- body[:tools] = build_tools_spec if @agentic_mode || @plan_mode
29
+ body[:tools] = build_tools_spec if @mode.allows_tools?
33
30
  body
34
31
  end
35
32
 
@@ -68,20 +65,11 @@ module RubyCoded
68
65
  end
69
66
 
70
67
  def build_instructions
71
- if @agentic_mode
72
- Tools::SystemPrompt.build(
73
- project_root: @project_root, max_write_rounds: MAX_WRITE_TOOL_ROUNDS,
74
- max_total_rounds: MAX_TOTAL_TOOL_ROUNDS
75
- )
76
- elsif @plan_mode
77
- Tools::PlanSystemPrompt.build(project_root: @project_root)
78
- else
79
- DEFAULT_INSTRUCTIONS
80
- end
68
+ @prompt_builder.build(@mode)
81
69
  end
82
70
 
83
71
  def build_tools_spec
84
- tool_instances = if @agentic_mode
72
+ tool_instances = if @mode.allows_mutation?
85
73
  @tool_registry.build_tools
86
74
  else
87
75
  @tool_registry.build_readonly_tools