girb 0.2.0 → 0.3.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.
@@ -4,11 +4,17 @@ require "debug"
4
4
  require_relative "debug_context_builder"
5
5
  require_relative "debug_prompt_builder"
6
6
  require_relative "debug_session_history"
7
+ require_relative "session_persistence"
7
8
 
8
9
  module Girb
9
10
  module DebugIntegration
11
+ # Define at module level so it's accessible as Girb::DebugIntegration::GIRB_DIR
12
+ # Points to lib directory, not gem root, so user's files aren't filtered
13
+ GIRB_DIR = File.expand_path('..', __dir__)
14
+
10
15
  @ai_triggered = false
11
16
  @interrupted = false
17
+ @session_started = false
12
18
 
13
19
  class << self
14
20
  attr_accessor :ai_triggered, :auto_continue, :interrupted, :api_thread
@@ -43,13 +49,57 @@ module Girb
43
49
  @interrupted = false
44
50
  end
45
51
 
52
+ def session_started?
53
+ @session_started
54
+ end
55
+
56
+ def start_session!
57
+ return if @session_started
58
+
59
+ SessionPersistence.start_session
60
+ @session_started = true
61
+ setup_exit_hook
62
+ end
63
+
64
+ def save_session!
65
+ SessionPersistence.save_session if @session_started
66
+ end
67
+
46
68
  def setup
47
69
  return unless defined?(DEBUGGER__::SESSION)
70
+ return if @setup_done
48
71
 
49
72
  register_ai_command
50
73
  register_debug_tools
51
74
  setup_keybinding
52
- puts "[girb] Debug AI assistant loaded. Use 'ai <question>' or Ctrl+Space."
75
+ patch_debugger_frame_filter
76
+ @setup_done = true
77
+ puts "[girb] Debug AI assistant loaded. Use 'qq <question>' or Ctrl+Space."
78
+ end
79
+
80
+ def patch_debugger_frame_filter
81
+ return unless defined?(DEBUGGER__)
82
+ return if @frame_filter_patched
83
+
84
+ # girbのフレームもdebuggerのスタックトレースから除外
85
+ if DEBUGGER__.respond_to?(:capture_frames)
86
+ original_method = DEBUGGER__.method(:capture_frames)
87
+ DEBUGGER__.define_singleton_method(:capture_frames) do |*args|
88
+ frames = original_method.call(*args)
89
+ frames.reject! do |frame|
90
+ frame.realpath&.start_with?(Girb::DebugIntegration::GIRB_DIR)
91
+ end
92
+ frames
93
+ end
94
+ end
95
+
96
+ @frame_filter_patched = true
97
+ end
98
+
99
+ # IRBからdebugモードに入った時に呼ばれる
100
+ def setup_if_needed
101
+ return if @setup_done
102
+ setup
53
103
  end
54
104
 
55
105
  private
@@ -75,12 +125,29 @@ module Girb
75
125
 
76
126
  Reline.core.config.add_default_key_binding_by_keymap(:emacs, [0], :girb_debug_ai_prefix)
77
127
  end
128
+
129
+ def setup_exit_hook
130
+ at_exit do
131
+ Girb::DebugIntegration.save_session!
132
+ end
133
+ end
78
134
  end
79
135
 
80
136
  module GirbDebugCommands
81
137
  MAX_AUTO_CONTINUE = 20
82
138
 
83
139
  def wait_command
140
+ # First, check for any pending commands (e.g., injected qq commands from IRB mode transition)
141
+ # Process these before entering auto_continue or waiting for user input
142
+ pending_cmds = Girb::DebugIntegration.take_pending_debug_commands
143
+ if pending_cmds.any?
144
+ pending_cmds.each do |cmd|
145
+ result = process_command(cmd)
146
+ return result unless result == :retry
147
+ end
148
+ return :retry
149
+ end
150
+
84
151
  if Girb::DebugIntegration.auto_continue?
85
152
  @girb_auto_continue_count ||= 0
86
153
  @girb_auto_continue_count += 1
@@ -133,9 +200,9 @@ module Girb
133
200
  return :retry
134
201
  end
135
202
 
136
- pending_cmds = Girb::DebugIntegration.take_pending_debug_commands
137
- if pending_cmds.any?
138
- pending_cmds.each do |cmd|
203
+ more_cmds = Girb::DebugIntegration.take_pending_debug_commands
204
+ if more_cmds.any?
205
+ more_cmds.each do |cmd|
139
206
  result = process_command(cmd)
140
207
  return result unless result == :retry
141
208
  end
@@ -170,10 +237,16 @@ module Girb
170
237
  return :retry
171
238
  end
172
239
 
173
- if line.start_with?("ai ")
174
- question = line.sub(/^ai\s+/, "").strip
240
+ if line.start_with?("qq ")
241
+ question = line.sub(/^qq\s+/, "").strip
175
242
  return :retry if question.empty?
176
243
 
244
+ # セッション管理コマンド
245
+ if question.start_with?("session ")
246
+ handle_session_command(question.sub(/^session\s+/, ""))
247
+ return :retry
248
+ end
249
+
177
250
  Girb::DebugSessionHistory.record_ai_question(question)
178
251
  handle_ai_question(question)
179
252
  pending_cmds = Girb::DebugIntegration.take_pending_debug_commands
@@ -220,7 +293,10 @@ module Girb
220
293
  context = Girb::DebugContextBuilder.new(current_binding).build
221
294
  client = Girb::AiClient.new
222
295
  continuation = "(auto-continue: The debug command has been executed. Analyze the new state and continue your task.)"
223
- client.ask(continuation, context, binding: current_binding, debug_mode: true)
296
+ # Disable Ruby's Timeout during API call to avoid deadlock with debug gem's threading
297
+ with_timeout_disabled do
298
+ client.ask(continuation, context, binding: current_binding, debug_mode: true)
299
+ end
224
300
  rescue StandardError => e
225
301
  @ui.puts "[girb] Auto-continue error: #{e.message}"
226
302
  Girb::DebugIntegration.auto_continue = false
@@ -234,14 +310,54 @@ module Girb
234
310
  return
235
311
  end
236
312
 
313
+ # 初回のAI質問時にセッションを開始
314
+ Girb::DebugIntegration.start_session!
315
+
316
+ # Ctrl+Cでプロセスがクラッシュするのを防ぐ
317
+ # trapハンドラを設置し、SIGINTをフラグ設定のみに抑える
318
+ original_handler = trap("INT") do
319
+ Girb::DebugIntegration.interrupt!
320
+ end
321
+
237
322
  context = Girb::DebugContextBuilder.new(current_binding).build
238
323
  client = Girb::AiClient.new
239
- client.ask(question, context, binding: current_binding, debug_mode: true)
324
+ # Disable Ruby's Timeout during API call to avoid deadlock with debug gem's threading
325
+ with_timeout_disabled do
326
+ client.ask(question, context, binding: current_binding, debug_mode: true)
327
+ end
328
+
329
+ # API呼び出し後にinterruptチェック
330
+ if Girb::DebugIntegration.interrupted?
331
+ Girb::DebugIntegration.clear_interrupt!
332
+ Girb::DebugIntegration.auto_continue = false
333
+ Girb::DebugIntegration.take_pending_debug_commands
334
+ puts "\n[girb] Interrupted by user (Ctrl+C)"
335
+ end
240
336
  rescue Girb::ConfigurationError => e
241
337
  puts "[girb] #{e.message}"
242
338
  rescue StandardError => e
243
339
  puts "[girb] Error: #{e.message}"
244
340
  puts e.backtrace.first(3).join("\n") if Girb.configuration.debug
341
+ ensure
342
+ if original_handler
343
+ trap("INT", original_handler)
344
+ else
345
+ trap("INT", "DEFAULT")
346
+ end
347
+ end
348
+
349
+ # Temporarily disable Ruby's Timeout module to avoid deadlock with debug gem
350
+ # The underlying socket has its own timeout, so this is safe
351
+ def with_timeout_disabled
352
+ return yield unless defined?(Timeout)
353
+
354
+ original_timeout = Timeout.method(:timeout)
355
+ Timeout.define_singleton_method(:timeout) do |_sec, _klass = nil, _message = nil, &block|
356
+ block.call
357
+ end
358
+ yield
359
+ ensure
360
+ Timeout.define_singleton_method(:timeout, original_timeout) if original_timeout
245
361
  end
246
362
 
247
363
  def handle_ai_turn_limit_reached
@@ -254,7 +370,9 @@ module Girb
254
370
  "Summarize your progress so far and tell the user what was accomplished. " \
255
371
  "If the task is not complete, explain what remains and instruct the user " \
256
372
  "to continue with a follow-up request.)"
257
- client.ask(limit_message, context, binding: current_binding, debug_mode: true)
373
+ with_timeout_disabled do
374
+ client.ask(limit_message, context, binding: current_binding, debug_mode: true)
375
+ end
258
376
  rescue StandardError => e
259
377
  puts "[girb] Auto-continue limit reached (#{MAX_AUTO_CONTINUE})"
260
378
  puts "[girb] Error summarizing: #{e.message}" if Girb.configuration.debug
@@ -270,7 +388,9 @@ module Girb
270
388
  interrupt_message = "(System: User interrupted with Ctrl+C. " \
271
389
  "Briefly summarize your progress so far. " \
272
390
  "Tell the user where you stopped and how to continue if needed.)"
273
- client.ask(interrupt_message, context, binding: current_binding, debug_mode: true)
391
+ with_timeout_disabled do
392
+ client.ask(interrupt_message, context, binding: current_binding, debug_mode: true)
393
+ end
274
394
  rescue StandardError => e
275
395
  puts "[girb] Error summarizing: #{e.message}" if Girb.configuration.debug
276
396
  end
@@ -293,6 +413,34 @@ module Girb
293
413
  trap("INT", "DEFAULT")
294
414
  end
295
415
  end
416
+
417
+ def handle_session_command(cmd)
418
+ case cmd.strip
419
+ when "clear"
420
+ Girb::SessionPersistence.clear_session
421
+ when "list"
422
+ sessions = Girb::SessionPersistence.list_sessions
423
+ if sessions.empty?
424
+ puts "[girb] No saved sessions"
425
+ else
426
+ puts "[girb] Saved sessions:"
427
+ sessions.each do |s|
428
+ puts " - #{s[:id]} (#{s[:message_count]} messages, saved: #{s[:saved_at]})"
429
+ end
430
+ end
431
+ when "status"
432
+ if Girb::SessionPersistence.current_session_id
433
+ puts "[girb] Current session: #{Girb::SessionPersistence.current_session_id}"
434
+ elsif Girb.debug_session
435
+ puts "[girb] Session configured: #{Girb.debug_session} (not started)"
436
+ else
437
+ puts "[girb] No session configured (use Girb.debug_session = 'name' to enable)"
438
+ end
439
+ else
440
+ puts "[girb] Unknown session command: #{cmd}"
441
+ puts "[girb] Available: clear, list, status"
442
+ end
443
+ end
296
444
  end
297
445
  end
298
446
  end
@@ -42,17 +42,77 @@ module Girb
42
42
  - Instance variables: `@x_values = []` then `@x_values << x`
43
43
  - Global variables: `$x_values = []` then `$x_values << x`
44
44
 
45
- Example for tracking a variable's history efficiently:
46
- 1. `evaluate_code("$tracked = []")`
47
- 2. Use a "silent" breakpoint that records but doesn't stop:
48
- `break file.rb:11 if: ($tracked << x; false)`
49
- This appends x to $tracked and returns false, so execution continues without stopping.
50
- 3. Set a normal breakpoint at the END of the script to see results:
51
- `break file.rb:14`
52
- 4. `continue` to run through all iterations
53
- 5. At the final breakpoint: `evaluate_code("$tracked")` to see all values
54
-
55
- This approach is much more efficient than stopping at every iteration!
45
+ ## Efficiency: Prefer Conditional Breakpoints for Loops
46
+ When tracking variables through many iterations (loops, recursion), avoid repeated `next`/`step`
47
+ commands. Each step requires an API call, which is slow. Use conditional breakpoints instead:
48
+
49
+ **CRITICAL: Breakpoint Line Placement Rules**
50
+ Before setting a breakpoint, use `read_file` to verify the target line.
51
+ - NEVER place a breakpoint on a block header line (a line containing `do |...|`, `.each`, `.map`, `.times`, etc.).
52
+ Block header lines execute only ONCE when the method is called, so the breakpoint will only hit once.
53
+ - ALWAYS place breakpoints on a line INSIDE the block body. Block body lines execute on every iteration.
54
+ - Example:
55
+ ```
56
+ 10: data.each_with_index do |val, i| # BAD: this line hits only once
57
+ 11: x = (x * val + i * 3) % 100 # GOOD: this line hits every iteration
58
+ 12: end
59
+ ```
60
+ Use `break file.rb:11` (body line), NOT `break file.rb:10` (header line).
61
+
62
+ **Efficient approach for loops with many iterations:**
63
+ 1. `read_file` to check the source and identify the correct body line (not a block header)
64
+ 2. `evaluate_code("$tracked = []")` - initialize tracking array
65
+ 3. Use a conditional breakpoint on a block BODY line that records AND stops on condition:
66
+ `break file.rb:11 if: ($tracked << x; x == 1)`
67
+ This appends x to $tracked on EVERY iteration, but only stops when x == 1.
68
+ 4. CRITICAL: In the SAME tool call batch, call `run_debug_command("c")` with `auto_continue: true`.
69
+ You MUST continue immediately after setting the breakpoint — do NOT stop and wait for user input.
70
+ When the breakpoint hits, you will be re-invoked with the new context.
71
+ 5. When re-invoked after the breakpoint hits: `evaluate_code("$tracked")` to retrieve results,
72
+ then report the findings to the user.
73
+
74
+ Steps 3 and 4 MUST happen in the same turn. Example tool calls in one response:
75
+ - `run_debug_command("break file.rb:11 if: ($tracked << x; x == 1)")`
76
+ - `run_debug_command("c", auto_continue: true)`
77
+
78
+ This completes in 3-4 API turns instead of many turns with repeated stepping.
79
+
80
+ **Alternative: evaluate_code for pure tracking scenarios**
81
+ When the goal is purely to collect variable values and stop on a condition (without needing
82
+ to interact at the breakpoint), `evaluate_code` can run the loop directly. This is simpler
83
+ and avoids breakpoint line selection issues entirely:
84
+ ```ruby
85
+ evaluate_code <<~RUBY
86
+ $tracked = [x]
87
+ catch(:girb_stop) do
88
+ data.each_with_index do |val, i|
89
+ x = (x * val + i * 3) % 100
90
+ $tracked << x
91
+ throw(:girb_stop) if x == 1
92
+ end
93
+ end
94
+ { tracked_values: $tracked, stopped: (x == 1) }
95
+ RUBY
96
+ ```
97
+ Use this when the user wants to collect values and find when a condition is met,
98
+ and you can reconstruct the loop logic from the source code.
99
+
100
+ **When to use repeated stepping (next/step):**
101
+ - Understanding complex logic flow (few lines)
102
+ - Checking which branch is taken
103
+ - Loops with only 2-3 iterations
104
+ - User explicitly wants to see execution step by step
105
+
106
+ **When to use conditional breakpoints:**
107
+ - Loops with many iterations (5+)
108
+ - "Track variable X until condition Y" requests
109
+ - "Find when X becomes Y" requests
110
+ - Collecting history of values
111
+
112
+ **When to use evaluate_code loop:**
113
+ - Pure value tracking without needing to stop and interact
114
+ - When you can reconstruct the loop logic from source code
115
+ - When breakpoint placement is complex (nested blocks, etc.)
56
116
 
57
117
  ## CRITICAL: Executing Debugger Commands
58
118
  When the user asks you to perform a debugging action (e.g., "go to the next line", "step into",
@@ -35,6 +35,10 @@ module Girb
35
35
  next if tp.path&.include?("readline")
36
36
  next if tp.path&.include?("rdoc")
37
37
  next if tp.path&.include?("/ri/")
38
+ # forwardableは内部でSyntaxErrorを意図的に発生させてrescueする
39
+ next if tp.path&.include?("forwardable")
40
+ # rubygemsのrequireは最初にLoadErrorを発生させてからgemをアクティベートする
41
+ next if tp.path&.include?("rubygems")
38
42
  next if tp.raised_exception.is_a?(SystemExit)
39
43
  next if tp.raised_exception.is_a?(Interrupt)
40
44
  # ErrorHighlight内部の例外を除外
@@ -5,9 +5,42 @@ require "irb/command"
5
5
  require_relative "exception_capture"
6
6
  require_relative "context_builder"
7
7
  require_relative "session_history"
8
+ require_relative "session_persistence"
9
+ require_relative "auto_continue"
8
10
  require_relative "ai_client"
9
11
 
10
12
  module Girb
13
+ # IRB::Debug.setupをフックして、debug gem初期化後にgirb統合をセットアップ
14
+ module IrbDebugHook
15
+ def setup(irb)
16
+ result = super
17
+ if result && defined?(DEBUGGER__::SESSION)
18
+ # DebugIntegrationを動的に読み込む
19
+ require_relative "debug_integration" unless defined?(Girb::DebugIntegration)
20
+ Girb::DebugIntegration.setup_if_needed
21
+
22
+ # Instead of using auto_continue (which causes deadlocks with API calls),
23
+ # inject a qq command to continue the conversation through normal command flow
24
+ if defined?(Girb::AutoContinue) && Girb::AutoContinue.active?
25
+ Girb::AutoContinue.reset!
26
+ # Include original user question so AI remembers the task
27
+ original_question = Girb::IrbIntegration.pending_user_question
28
+ Girb::IrbIntegration.pending_user_question = nil
29
+ if original_question
30
+ continuation = "(auto-continue: デバッグモードに移行しました。最初のデバッグコマンドは既に実行されました。" \
31
+ "同じコマンドを再度発行しないでください。\n" \
32
+ "元の指示: 「#{original_question}」\n" \
33
+ "次のステップに進んでください。例: continueで実行を継続、または結果を確認。)"
34
+ else
35
+ continuation = "(auto-continue: デバッグモードに移行しました。最初のデバッグコマンドは既に実行されました。" \
36
+ "次のステップに進んでください。)"
37
+ end
38
+ Girb::DebugIntegration.add_pending_debug_command("qq #{continuation}")
39
+ end
40
+ end
41
+ result
42
+ end
43
+ end
11
44
  # AI送信フラグ(スレッドローカル)
12
45
  def self.ai_send_pending?
13
46
  Thread.current[:girb_ai_send_pending]
@@ -18,6 +51,83 @@ module Girb
18
51
  end
19
52
 
20
53
  module IrbIntegration
54
+ @session_started = false
55
+ @exit_hook_installed = false
56
+ @pending_irb_commands = []
57
+ @pending_input_commands = []
58
+ @auto_continue = false
59
+
60
+ DEBUG_COMMANDS = %w[next n step s continue c finish break delete backtrace bt info catch debug].freeze
61
+
62
+ class << self
63
+ attr_accessor :auto_continue
64
+
65
+ def pending_irb_commands
66
+ @pending_irb_commands ||= []
67
+ end
68
+
69
+ def add_pending_irb_command(cmd)
70
+ pending_irb_commands << cmd
71
+ end
72
+
73
+ def take_pending_irb_commands
74
+ cmds = @pending_irb_commands || []
75
+ @pending_irb_commands = []
76
+ cmds
77
+ end
78
+
79
+ # Commands to be injected into IRB's input stream
80
+ def pending_input_commands
81
+ @pending_input_commands ||= []
82
+ end
83
+
84
+ def add_pending_input_command(cmd)
85
+ pending_input_commands << cmd
86
+ end
87
+
88
+ def take_next_input_command
89
+ @pending_input_commands ||= []
90
+ @pending_input_commands.shift
91
+ end
92
+
93
+ def has_pending_input?
94
+ @pending_input_commands && !@pending_input_commands.empty?
95
+ end
96
+
97
+ def debug_command?(cmd)
98
+ name = cmd.strip.split(/\s+/, 2).first&.downcase
99
+ DEBUG_COMMANDS.include?(name)
100
+ end
101
+
102
+ def auto_continue?
103
+ @auto_continue
104
+ end
105
+
106
+ # Store the original user question for continuation after debug mode transition
107
+ attr_accessor :pending_user_question
108
+
109
+ def session_started?
110
+ @session_started
111
+ end
112
+
113
+ def start_session!
114
+ return if @session_started
115
+ return unless SessionPersistence.enabled?
116
+
117
+ SessionPersistence.start_session
118
+ @session_started = true
119
+ setup_exit_hook unless @exit_hook_installed
120
+ end
121
+
122
+ def save_session!
123
+ return unless @session_started
124
+ SessionPersistence.save_session
125
+ rescue => e
126
+ # exit時のエラーは静かに無視
127
+ STDERR.puts "[girb] Warning: Failed to save session: #{e.message}" if ENV["GIRB_DEBUG"]
128
+ end
129
+ end
130
+
21
131
  def self.setup
22
132
  # コマンドを登録
23
133
  require_relative "../irb/command/qq"
@@ -30,6 +140,39 @@ module Girb
30
140
 
31
141
  # Ctrl+Space キーバインドをインストール
32
142
  install_ai_keybinding
143
+
144
+ # readmultiline パッチをインストール(コマンド注入用)
145
+ install_readmultiline_patch
146
+
147
+ # セッション永続化が有効なら開始
148
+ start_session! if SessionPersistence.enabled?
149
+
150
+ # IRB::Debugをフックして、debug開始時にgirb統合をセットアップ
151
+ install_debug_hook
152
+ end
153
+
154
+ def self.install_debug_hook
155
+ return if @debug_hook_installed
156
+ return unless defined?(IRB::Debug)
157
+
158
+ IRB::Debug.singleton_class.prepend(Girb::IrbDebugHook)
159
+ @debug_hook_installed = true
160
+ end
161
+
162
+ def self.install_readmultiline_patch
163
+ return if @readmultiline_patch_installed
164
+
165
+ IRB::Irb.prepend(ReadmultilinePatch)
166
+ @readmultiline_patch_installed = true
167
+ end
168
+
169
+ def self.setup_exit_hook
170
+ return if @exit_hook_installed
171
+ @exit_hook_installed = true
172
+
173
+ at_exit do
174
+ Girb::IrbIntegration.save_session!
175
+ end
33
176
  end
34
177
 
35
178
  def self.install_eval_hook
@@ -77,11 +220,100 @@ module Girb
77
220
  private
78
221
 
79
222
  def ask_ai(question, line_no)
223
+ # Store the question for continuation after debug mode transition
224
+ Girb::IrbIntegration.pending_user_question = question
225
+
80
226
  context = ContextBuilder.new(workspace.binding, self).build
81
227
  client = AiClient.new
82
228
  client.ask(question, context, binding: workspace.binding, line_no: line_no, irb_context: self)
229
+
230
+ # Execute any pending IRB commands after AI response
231
+ execute_pending_commands
83
232
  rescue StandardError => e
84
233
  puts "[girb] Error: #{e.message}"
85
234
  end
235
+
236
+ def execute_pending_commands
237
+ commands = Girb::IrbIntegration.take_pending_irb_commands
238
+ return if commands.empty?
239
+
240
+ commands.each do |cmd|
241
+ if Girb::IrbIntegration.debug_command?(cmd)
242
+ # Debug commands need to be processed at IRB's top level
243
+ # Queue them for injection via readmultiline patch
244
+ puts "[girb] Queuing debug command: #{cmd}"
245
+ Girb::IrbIntegration.add_pending_input_command(cmd)
246
+ else
247
+ # Non-debug commands can be executed directly
248
+ puts "[girb] Executing: #{cmd}"
249
+ begin
250
+ execute_irb_command(cmd)
251
+ rescue StandardError => e
252
+ puts "[girb] Command error: #{e.message}"
253
+ end
254
+ end
255
+ end
256
+ end
257
+
258
+ def execute_irb_command(cmd)
259
+ # Parse command and arguments
260
+ parts = cmd.strip.split(/\s+/, 2)
261
+ command_name = parts[0]
262
+ arg = parts[1] || ""
263
+
264
+ # Map command names to IRB command classes
265
+ command_class = find_irb_command_class(command_name)
266
+
267
+ if command_class
268
+ command_class.execute(self, arg)
269
+ else
270
+ # Fall back to evaluating as Ruby code
271
+ evaluate_expression(cmd, 0)
272
+ end
273
+ end
274
+
275
+ def find_irb_command_class(name)
276
+ # Debug-related command mappings
277
+ command_map = {
278
+ "next" => "Next", "n" => "Next",
279
+ "step" => "Step", "s" => "Step",
280
+ "continue" => "Continue", "c" => "Continue",
281
+ "finish" => "Finish",
282
+ "break" => "Break",
283
+ "delete" => "Delete",
284
+ "backtrace" => "Backtrace", "bt" => "Backtrace",
285
+ "info" => "Info",
286
+ "catch" => "Catch",
287
+ "debug" => "Debug"
288
+ }
289
+
290
+ class_name = command_map[name.downcase]
291
+ return nil unless class_name
292
+
293
+ begin
294
+ IRB::Command.const_get(class_name)
295
+ rescue NameError
296
+ nil
297
+ end
298
+ end
299
+ end
300
+
301
+ # Patch to inject pending commands into IRB's input stream
302
+ # This ensures debug commands are processed at the top level of IRB's loop
303
+ module ReadmultilinePatch
304
+ def readmultiline
305
+ # Check for pending commands from girb AI
306
+ if (cmd = Girb::IrbIntegration.take_next_input_command)
307
+ puts "[girb] Injecting command: #{cmd}"
308
+ # Return command with newline so it's processed as complete input
309
+ return cmd.end_with?("\n") ? cmd : "#{cmd}\n"
310
+ end
311
+
312
+ result = super
313
+
314
+ # After debug command executes and we transition to debug mode,
315
+ # the debug_integration auto_continue mechanism takes over
316
+ result
317
+ end
86
318
  end
87
319
  end