girb 0.1.2 → 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 69a7d9cee0ae606d21d75c1fc94c1aef3665c8e38e4f704c9e8f899d7e70b943
4
- data.tar.gz: 898823b1d088a67f8adb5e93705bb547d623eb0564d1831fe39cfa98757e233e
3
+ metadata.gz: '048b4dc3910c2afaa9bb1bd2a1f2f33f51b631b74405368daabec4cbf1ef1e2a'
4
+ data.tar.gz: d5b46f5c7c0f809053c8b38fd15f9af6709338c62cb2374f0a64b25f283b1807
5
5
  SHA512:
6
- metadata.gz: 93e107025ea0480db5b0403531a3af424836dfbef23a7f7f174e8c89036b1b3d140571ba0334a3c2f1ff7fda89ab91df2c5478f18f945474fd1ca836eb3ae5c3
7
- data.tar.gz: bd5097cc780074724642e3c2932d23178eea0140e6bdd9460489abb97a0ac3f567577d1ecd6153d29ef896ed6803ed9c1988995d64ffd47db3c08dac702f6b21
6
+ metadata.gz: 3e064f6e9a5dcd1ea6af090ccfc9aff2acc476fcafff772aed594ff15ecb95cfe6513e8e7facccbea490d82d691b9e495d4134fe418f8fb2d0b7bfab0e40fe25
7
+ data.tar.gz: 3287871b7937ea1234fdb18c6e551b96da944b12a57305b3e6c5fe5e63d2d602afa04aedc88192331176b70062ad4efc1bcfeb5c7bf3285ce603a3e090a1f005
data/CHANGELOG.md CHANGED
@@ -1,5 +1,43 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.2.0] - 2026-02-05
4
+
5
+ ### Added
6
+
7
+ - **Debug gem (rdbg) integration**: AI assistant for step-through debugging
8
+ - `ai <question>` command in debugger
9
+ - Ctrl+Space to send input to AI
10
+ - Auto-routing of non-ASCII (Japanese) input to AI
11
+ - `run_debug_command` tool for AI to execute debugger commands (step, next, continue, break, etc.)
12
+ - **Auto-continue for autonomous AI investigation**
13
+ - `continue_analysis` tool for IRB mode context refresh
14
+ - AI can loop through investigate-execute-analyze cycles
15
+ - Configurable iteration limits (MAX_ITERATIONS = 20)
16
+ - **Ctrl+C interrupt support** for both IRB and debug modes
17
+ - Graceful interruption of long-running AI operations
18
+ - AI summarizes progress when interrupted
19
+ - **Debug session history tracking**
20
+ - Track debugger commands and AI conversations
21
+ - `get_session_history` tool for debug mode
22
+ - **Efficient variable tracking** with silent breakpoints
23
+ - `break file:line if: ($var << x; false)` pattern for recording without stopping
24
+
25
+ ### Changed
26
+
27
+ - Separate tool sets for IRB and debug modes
28
+ - SHARED_TOOLS: Common tools for both modes
29
+ - IRB_TOOLS: SessionHistoryTool, ContinueAnalysis
30
+ - DEBUG_TOOLS: DebugSessionHistoryTool, RunDebugCommand
31
+ - Improved prompts for debug mode
32
+ - Guidance on variable persistence across frames
33
+ - Instructions for efficient breakpoint usage
34
+ - Context-aware investigation (don't use tools for greetings)
35
+
36
+ ### Fixed
37
+
38
+ - Tool calls now include IDs for proper conversation history
39
+ - Auto-continue loop properly exits when debug commands are queued
40
+
3
41
  ## [0.1.2] - 2026-02-03
4
42
 
5
43
  ### Added
@@ -7,6 +45,7 @@
7
45
  - `.girbrc` configuration file support with directory traversal
8
46
  - Railtie for automatic Rails console integration
9
47
  - GirbrcLoader utility for finding and loading `.girbrc` files
48
+ - `get_current_directory` tool for non-Rails environments
10
49
 
11
50
  ### Changed
12
51
 
data/README.md CHANGED
@@ -10,6 +10,8 @@ An AI assistant embedded in your IRB session. It understands your runtime contex
10
10
  - **Exception Capture**: Automatically captures recent exceptions - just ask "why did this fail?" after an error
11
11
  - **Session History Understanding**: Tracks IRB input history and understands conversation flow
12
12
  - **Tool Execution**: AI autonomously executes code, inspects objects, and retrieves source code
13
+ - **Autonomous Investigation**: AI can loop through investigate-execute-analyze cycles using `continue_analysis`
14
+ - **Debug Gem Integration**: Use with Ruby's debug gem for step-through debugging with AI assistance
13
15
  - **Multi-language Support**: Detects user's language and responds in the same language
14
16
  - **Customizable**: Add custom prompts for project-specific instructions
15
17
  - **Provider Agnostic**: Use any LLM provider or implement your own
@@ -116,6 +118,34 @@ def problematic_method
116
118
  end
117
119
  ```
118
120
 
121
+ ### Debug with debug gem (rdbg)
122
+
123
+ For step-through debugging with AI assistance, add `require "girb"` to your script:
124
+
125
+ ```ruby
126
+ require "girb"
127
+
128
+ def problematic_method
129
+ result = some_calculation
130
+ result
131
+ end
132
+
133
+ problematic_method
134
+ ```
135
+
136
+ Then run with rdbg:
137
+
138
+ ```bash
139
+ rdbg your_script.rb
140
+ ```
141
+
142
+ In the debugger, use:
143
+ - `ai <question>` - Ask AI a question
144
+ - `Ctrl+Space` - Send current input to AI
145
+ - Natural language (non-ASCII) input is automatically routed to AI
146
+
147
+ The AI can execute debugger commands like `step`, `next`, `continue`, and set breakpoints for you.
148
+
119
149
  ### How to Ask AI
120
150
 
121
151
  #### Method 1: Ctrl+Space
@@ -179,6 +209,7 @@ For `girb` command, you can also configure via environment variables (used when
179
209
  | `find_file` | Search for files in the project |
180
210
  | `read_file` | Read file contents |
181
211
  | `session_history` | Get IRB session history |
212
+ | `continue_analysis` | Request context refresh for autonomous investigation |
182
213
 
183
214
  ### Additional Tools in Rails Environment
184
215
 
@@ -187,6 +218,12 @@ For `girb` command, you can also configure via environment variables (used when
187
218
  | `query_model` | Execute queries on ActiveRecord models |
188
219
  | `model_info` | Get model schema information |
189
220
 
221
+ ### Additional Tools in Debug Mode (rdbg)
222
+
223
+ | Tool | Description |
224
+ |------|-------------|
225
+ | `run_debug_command` | Execute debugger commands (step, next, continue, break, etc.) |
226
+
190
227
  ## Custom Providers
191
228
 
192
229
  Implement your own LLM provider:
data/README_ja.md CHANGED
@@ -8,6 +8,8 @@ IRBセッションに組み込まれたAIアシスタント。実行中のコン
8
8
  - **例外キャプチャ**: 直前の例外を自動キャプチャ - エラー後に「なぜ失敗した?」と聞くだけでOK
9
9
  - **セッション履歴の理解**: IRBでの入力履歴を追跡し、会話の流れを理解
10
10
  - **ツール実行**: コードの実行、オブジェクトの検査、ソースコードの取得などをAIが自律的に実行
11
+ - **自律的な調査**: `continue_analysis`を使って、調査→実行→分析のサイクルをAIが自律的にループ可能
12
+ - **debug gem統合**: Rubyのdebug gemと連携し、AIアシスタント付きのステップ実行デバッグが可能
11
13
  - **多言語対応**: ユーザーの言語を検出し、同じ言語で応答
12
14
  - **カスタマイズ可能**: 独自のプロンプトを追加して、プロジェクト固有の指示を設定可能
13
15
  - **プロバイダー非依存**: 任意のLLMプロバイダーを使用、または独自実装が可能
@@ -114,6 +116,34 @@ def problematic_method
114
116
  end
115
117
  ```
116
118
 
119
+ ### debug gem (rdbg) でデバッグ
120
+
121
+ AIアシスタント付きのステップ実行デバッグを行うには、スクリプトに `require "girb"` を追加:
122
+
123
+ ```ruby
124
+ require "girb"
125
+
126
+ def problematic_method
127
+ result = some_calculation
128
+ result
129
+ end
130
+
131
+ problematic_method
132
+ ```
133
+
134
+ rdbgで起動:
135
+
136
+ ```bash
137
+ rdbg your_script.rb
138
+ ```
139
+
140
+ デバッガ内では以下の方法でAIに質問できます:
141
+ - `ai <質問>` - AIに質問
142
+ - `Ctrl+Space` - 入力内容をAIに送信
143
+ - 日本語(非ASCII文字)の入力は自動的にAIにルーティング
144
+
145
+ AIは `step`、`next`、`continue` などのデバッガコマンドを実行したり、ブレークポイントを設定することもできます。
146
+
117
147
  ### AIへの質問方法
118
148
 
119
149
  #### 方法1: Ctrl+Space
@@ -177,6 +207,7 @@ girb --help # ヘルプを表示
177
207
  | `find_file` | プロジェクト内のファイルを検索 |
178
208
  | `read_file` | ファイルの内容を読み取り |
179
209
  | `session_history` | IRBセッションの履歴を取得 |
210
+ | `continue_analysis` | 自律調査のためのコンテキスト更新をリクエスト |
180
211
 
181
212
  ### Rails環境での追加ツール
182
213
 
@@ -185,6 +216,12 @@ girb --help # ヘルプを表示
185
216
  | `query_model` | ActiveRecordモデルへのクエリ実行 |
186
217
  | `model_info` | モデルのスキーマ情報を取得 |
187
218
 
219
+ ### デバッグモード (rdbg) での追加ツール
220
+
221
+ | ツール | 説明 |
222
+ |--------|------|
223
+ | `run_debug_command` | デバッガコマンドを実行(step、next、continue、breakなど) |
224
+
188
225
  ## カスタムプロバイダー
189
226
 
190
227
  独自のLLMプロバイダーを実装:
@@ -1,7 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "auto_continue"
3
4
  require_relative "conversation_history"
4
5
  require_relative "providers/base"
6
+ require_relative "debug_context_builder"
7
+ require_relative "debug_prompt_builder"
5
8
 
6
9
  module Girb
7
10
  class AiClient
@@ -11,23 +14,89 @@ module Girb
11
14
  @provider = Girb.configuration.provider!
12
15
  end
13
16
 
14
- def ask(question, context, binding: nil, line_no: nil)
17
+ def ask(question, context, binding: nil, line_no: nil, irb_context: nil, debug_mode: false)
15
18
  @current_binding = binding
16
19
  @current_line_no = line_no
20
+ @irb_context = irb_context
21
+ @debug_mode = debug_mode
17
22
  @reasoning_log = []
18
23
 
19
- prompt_builder = PromptBuilder.new(question, context)
24
+ prompt_builder = create_prompt_builder(question, context)
20
25
  @system_prompt = prompt_builder.system_prompt
21
26
  user_message = prompt_builder.user_message
22
27
 
23
28
  ConversationHistory.add_user_message(user_message)
24
29
 
25
30
  tools = build_tools
26
- process_with_tools(tools)
31
+
32
+ # In debug mode, auto-continue is handled by DebugIntegration, not here
33
+ if @debug_mode
34
+ process_with_tools(tools)
35
+ else
36
+ auto_continue_count = 0
37
+ original_int_handler = setup_interrupt_handler
38
+
39
+ begin
40
+ loop do
41
+ # Check for interrupt at start of loop
42
+ if Girb::AutoContinue.interrupted?
43
+ Girb::AutoContinue.clear_interrupt!
44
+ handle_irb_interrupted
45
+ break
46
+ end
47
+
48
+ process_with_tools(tools)
49
+
50
+ # Check for interrupt after API call (Ctrl+C during request)
51
+ if Girb::AutoContinue.interrupted?
52
+ Girb::AutoContinue.clear_interrupt!
53
+ handle_irb_interrupted
54
+ break
55
+ end
56
+
57
+ break unless Girb::AutoContinue.active?
58
+
59
+ auto_continue_count += 1
60
+ if auto_continue_count >= Girb::AutoContinue::MAX_ITERATIONS
61
+ handle_irb_limit_reached
62
+ break
63
+ end
64
+
65
+ Girb::AutoContinue.reset!
66
+
67
+ # Rebuild context with current binding state
68
+ new_context = create_context_builder(@current_binding, @irb_context).build
69
+ continuation = "(auto-continue: Your previous action has been completed. " \
70
+ "Here is the updated context. Continue your investigation.)"
71
+ continuation_builder = create_prompt_builder(continuation, new_context)
72
+ ConversationHistory.add_user_message(continuation_builder.user_message)
73
+ end
74
+ ensure
75
+ restore_interrupt_handler(original_int_handler)
76
+ Girb::AutoContinue.reset!
77
+ Girb::AutoContinue.clear_interrupt!
78
+ end
79
+ end
27
80
  end
28
81
 
29
82
  private
30
83
 
84
+ def create_prompt_builder(question, context)
85
+ if @debug_mode
86
+ DebugPromptBuilder.new(question, context)
87
+ else
88
+ PromptBuilder.new(question, context)
89
+ end
90
+ end
91
+
92
+ def create_context_builder(binding, irb_context)
93
+ if @debug_mode
94
+ DebugContextBuilder.new(binding)
95
+ else
96
+ ContextBuilder.new(binding, irb_context)
97
+ end
98
+ end
99
+
31
100
  def build_tools
32
101
  Tools.available_tools.map do |tool_class|
33
102
  {
@@ -43,6 +112,12 @@ module Girb
43
112
  accumulated_text = []
44
113
 
45
114
  loop do
115
+ # Check for interrupt at start of each iteration
116
+ if check_interrupted?
117
+ puts "\n[girb] Interrupted by user (Ctrl+C)"
118
+ break
119
+ end
120
+
46
121
  iterations += 1
47
122
  if iterations > MAX_TOOL_ITERATIONS
48
123
  puts "\n[girb] Tool iteration limit reached"
@@ -50,12 +125,29 @@ module Girb
50
125
  end
51
126
 
52
127
  messages = ConversationHistory.to_normalized
53
- response = @provider.chat(
54
- messages: messages,
55
- system_prompt: @system_prompt,
56
- tools: tools,
57
- binding: @current_binding
58
- )
128
+ begin
129
+ response = @provider.chat(
130
+ messages: messages,
131
+ system_prompt: @system_prompt,
132
+ tools: tools,
133
+ binding: @current_binding
134
+ )
135
+ rescue Interrupt => e
136
+ puts "\n[girb] Interrupted by user (Ctrl+C)"
137
+ Girb::AutoContinue.interrupt! unless @debug_mode
138
+ Girb::DebugIntegration.interrupt! if @debug_mode && defined?(Girb::DebugIntegration)
139
+ break
140
+ rescue Exception => e
141
+ # IRB::Abort and similar exceptions
142
+ if e.class.name.include?("Abort") || e.class.name.include?("Interrupt")
143
+ puts "\n[girb] Interrupted by user (Ctrl+C)"
144
+ Girb::AutoContinue.interrupt! unless @debug_mode
145
+ Girb::DebugIntegration.interrupt! if @debug_mode && defined?(Girb::DebugIntegration)
146
+ break
147
+ else
148
+ raise
149
+ end
150
+ end
59
151
 
60
152
  if Girb.configuration.debug
61
153
  puts "[girb] function_calls: #{response.function_calls.inspect}"
@@ -79,27 +171,46 @@ module Girb
79
171
  accumulated_text << response.text
80
172
  end
81
173
 
82
- function_call = response.function_calls.first
83
- tool_name = function_call[:name]
84
- tool_args = function_call[:args] || {}
174
+ debug_command_called = false
85
175
 
86
- if Girb.configuration.debug
87
- puts "[girb] Tool: #{tool_name}(#{tool_args.map { |k, v| "#{k}: #{v.inspect}" }.join(', ')})"
88
- end
176
+ response.function_calls.each do |function_call|
177
+ tool_name = function_call[:name]
178
+ tool_args = function_call[:args] || {}
179
+ tool_id = function_call[:id]
180
+
181
+ if Girb.configuration.debug
182
+ puts "[girb] Tool: #{tool_name}(#{tool_args.map { |k, v| "#{k}: #{v.inspect}" }.join(', ')})"
183
+ end
89
184
 
90
- result = execute_tool(tool_name, tool_args)
185
+ result = execute_tool(tool_name, tool_args)
91
186
 
92
- @reasoning_log << {
93
- tool: tool_name,
94
- args: tool_args,
95
- result: result
96
- }
187
+ @reasoning_log << {
188
+ tool: tool_name,
189
+ args: tool_args,
190
+ result: result
191
+ }
97
192
 
98
- # Record tool call and result in conversation history
99
- ConversationHistory.add_tool_call(tool_name, tool_args, result)
193
+ ConversationHistory.add_tool_call(tool_name, tool_args, result, id: tool_id)
100
194
 
101
- if Girb.configuration.debug && result.is_a?(Hash) && result[:error]
102
- puts "[girb] Tool error: #{result[:error]}"
195
+ if Girb.configuration.debug && result.is_a?(Hash) && result[:error]
196
+ puts "[girb] Tool error: #{result[:error]}"
197
+ end
198
+
199
+ # In debug mode, if run_debug_command was called, we need to exit
200
+ # the tool loop so the debugger can execute the pending commands
201
+ if @debug_mode && tool_name == "run_debug_command"
202
+ debug_command_called = true
203
+ end
204
+ end
205
+
206
+ # Exit tool loop if debug command was called - let debugger take over
207
+ if debug_command_called
208
+ # Save accumulated text and pending tool calls as assistant message
209
+ text = accumulated_text.any? ? accumulated_text.join("\n") : ""
210
+ ConversationHistory.add_assistant_message(text)
211
+ record_ai_response(text) unless text.empty?
212
+ puts text unless text.empty?
213
+ break
103
214
  end
104
215
  else
105
216
  # Text response
@@ -140,10 +251,13 @@ module Girb
140
251
  end
141
252
 
142
253
  def record_ai_response(response)
143
- return unless @current_line_no
144
-
145
- reasoning = @reasoning_log.empty? ? nil : format_reasoning
146
- SessionHistory.record_ai_response(@current_line_no, response, reasoning)
254
+ if @debug_mode
255
+ require_relative "debug_session_history"
256
+ DebugSessionHistory.record_ai_response(response)
257
+ elsif @current_line_no
258
+ reasoning = @reasoning_log.empty? ? nil : format_reasoning
259
+ SessionHistory.record_ai_response(@current_line_no, response, reasoning)
260
+ end
147
261
  end
148
262
 
149
263
  def format_reasoning
@@ -154,5 +268,57 @@ module Girb
154
268
  "Tool: #{log[:tool]}(#{args_str})\nResult: #{result_str}"
155
269
  end.join("\n\n")
156
270
  end
271
+
272
+ def setup_interrupt_handler
273
+ trap("INT") do
274
+ Girb::AutoContinue.interrupt!
275
+ end
276
+ end
277
+
278
+ def check_interrupted?
279
+ if @debug_mode
280
+ defined?(Girb::DebugIntegration) && Girb::DebugIntegration.interrupted?
281
+ else
282
+ Girb::AutoContinue.interrupted?
283
+ end
284
+ end
285
+
286
+ def restore_interrupt_handler(original_handler)
287
+ if original_handler
288
+ trap("INT", original_handler)
289
+ else
290
+ trap("INT", "DEFAULT")
291
+ end
292
+ end
293
+
294
+ def handle_irb_interrupted
295
+ return unless @current_binding
296
+
297
+ new_context = create_context_builder(@current_binding, @irb_context).build
298
+ interrupt_message = "(System: User interrupted with Ctrl+C. " \
299
+ "Briefly summarize your progress so far. " \
300
+ "Tell the user where you stopped and how to continue if needed.)"
301
+ continuation_builder = create_prompt_builder(interrupt_message, new_context)
302
+ ConversationHistory.add_user_message(continuation_builder.user_message)
303
+ process_with_tools(build_tools)
304
+ rescue StandardError => e
305
+ puts "[girb] Error summarizing: #{e.message}" if Girb.configuration.debug
306
+ end
307
+
308
+ def handle_irb_limit_reached
309
+ puts "\n[girb] Auto-continue limit reached (#{Girb::AutoContinue::MAX_ITERATIONS})"
310
+ return unless @current_binding
311
+
312
+ new_context = create_context_builder(@current_binding, @irb_context).build
313
+ limit_message = "(System: Auto-continue limit (#{Girb::AutoContinue::MAX_ITERATIONS}) reached. " \
314
+ "Summarize your progress so far and tell the user what was accomplished. " \
315
+ "If the task is not complete, explain what remains and instruct the user " \
316
+ "to continue with a follow-up request.)"
317
+ continuation_builder = create_prompt_builder(limit_message, new_context)
318
+ ConversationHistory.add_user_message(continuation_builder.user_message)
319
+ process_with_tools(build_tools)
320
+ rescue StandardError => e
321
+ puts "[girb] Error summarizing: #{e.message}" if Girb.configuration.debug
322
+ end
157
323
  end
158
324
  end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Girb
4
+ module AutoContinue
5
+ MAX_ITERATIONS = 2
6
+
7
+ class << self
8
+ def active?
9
+ @active || false
10
+ end
11
+
12
+ def request!
13
+ @active = true
14
+ end
15
+
16
+ def reset!
17
+ @active = false
18
+ end
19
+
20
+ def interrupted?
21
+ @interrupted || false
22
+ end
23
+
24
+ def interrupt!
25
+ @interrupted = true
26
+ end
27
+
28
+ def clear_interrupt!
29
+ @interrupted = false
30
+ end
31
+ end
32
+ end
33
+ end
@@ -22,8 +22,8 @@ module Girb
22
22
  instance.add_assistant_message(content)
23
23
  end
24
24
 
25
- def add_tool_call(tool_name, args, result)
26
- instance.add_tool_call(tool_name, args, result)
25
+ def add_tool_call(tool_name, args, result, id: nil)
26
+ instance.add_tool_call(tool_name, args, result, id: id)
27
27
  end
28
28
 
29
29
  def to_contents
@@ -72,8 +72,9 @@ module Girb
72
72
  end
73
73
  end
74
74
 
75
- def add_tool_call(tool_name, args, result)
75
+ def add_tool_call(tool_name, args, result, id: nil)
76
76
  @pending_tool_calls << {
77
+ id: id || "call_#{SecureRandom.hex(12)}",
77
78
  name: tool_name,
78
79
  args: args,
79
80
  result: result
@@ -105,15 +106,15 @@ module Girb
105
106
 
106
107
  # Add tool calls and results if present
107
108
  msg.tool_calls&.each do |tc|
108
- result << { role: :tool_call, name: tc[:name], args: tc[:args] }
109
- result << { role: :tool_result, name: tc[:name], result: tc[:result] }
109
+ result << { role: :tool_call, id: tc[:id], name: tc[:name], args: tc[:args] }
110
+ result << { role: :tool_result, id: tc[:id], name: tc[:name], result: tc[:result] }
110
111
  end
111
112
  end
112
113
 
113
114
  # Add pending tool calls
114
115
  @pending_tool_calls.each do |tc|
115
- result << { role: :tool_call, name: tc[:name], args: tc[:args] }
116
- result << { role: :tool_result, name: tc[:name], result: tc[:result] }
116
+ result << { role: :tool_call, id: tc[:id], name: tc[:name], args: tc[:args] }
117
+ result << { role: :tool_result, id: tc[:id], name: tc[:name], result: tc[:result] }
117
118
  end
118
119
 
119
120
  result
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "debug_session_history"
4
+
5
+ module Girb
6
+ class DebugContextBuilder
7
+ MAX_INSPECT_LENGTH = 500
8
+
9
+ def initialize(binding, thread_client: nil)
10
+ @binding = binding
11
+ @thread_client = thread_client
12
+ end
13
+
14
+ def build
15
+ {
16
+ source_location: capture_source_location,
17
+ local_variables: capture_locals,
18
+ instance_variables: capture_instance_variables,
19
+ self_info: capture_self,
20
+ backtrace: capture_backtrace,
21
+ breakpoint_info: capture_breakpoint_info,
22
+ session_history: capture_session_history
23
+ }
24
+ end
25
+
26
+ private
27
+
28
+ def capture_source_location
29
+ loc = @binding.source_location
30
+ return nil unless loc
31
+
32
+ file, line = loc
33
+ {
34
+ file: file,
35
+ line: line
36
+ }
37
+ rescue StandardError
38
+ nil
39
+ end
40
+
41
+ def capture_locals
42
+ @binding.local_variables.to_h do |var|
43
+ value = @binding.local_variable_get(var)
44
+ [var, safe_inspect(value)]
45
+ end
46
+ end
47
+
48
+ def capture_instance_variables
49
+ obj = @binding.receiver
50
+ obj.instance_variables.to_h do |var|
51
+ value = obj.instance_variable_get(var)
52
+ [var, safe_inspect(value)]
53
+ end
54
+ rescue StandardError
55
+ {}
56
+ end
57
+
58
+ def capture_self
59
+ obj = @binding.receiver
60
+ {
61
+ class: obj.class.name,
62
+ inspect: safe_inspect(obj),
63
+ methods: obj.methods(false).first(20)
64
+ }
65
+ end
66
+
67
+ def capture_backtrace
68
+ return nil unless @thread_client
69
+
70
+ @thread_client.current_frame&.location&.to_s
71
+ rescue StandardError
72
+ nil
73
+ end
74
+
75
+ def capture_breakpoint_info
76
+ return nil unless defined?(DEBUGGER__) && DEBUGGER__.respond_to?(:breakpoints)
77
+
78
+ DEBUGGER__.breakpoints.map do |bp|
79
+ { type: bp.class.name, location: bp.to_s }
80
+ end
81
+ rescue StandardError
82
+ nil
83
+ end
84
+
85
+ def capture_session_history
86
+ DebugSessionHistory.format_history(20)
87
+ rescue StandardError
88
+ nil
89
+ end
90
+
91
+ def safe_inspect(obj, max_length: MAX_INSPECT_LENGTH)
92
+ if defined?(ActiveRecord::Base) && obj.is_a?(ActiveRecord::Base)
93
+ return inspect_active_record(obj)
94
+ end
95
+
96
+ result = obj.inspect
97
+ result.length > max_length ? "#{result[0, max_length]}..." : result
98
+ rescue StandardError => e
99
+ "#<#{obj.class} (inspect failed: #{e.message})>"
100
+ end
101
+
102
+ def inspect_active_record(obj)
103
+ {
104
+ class: obj.class.name,
105
+ id: obj.try(:id),
106
+ attributes: obj.attributes,
107
+ new_record: obj.new_record?,
108
+ changed: obj.changed?,
109
+ errors: obj.errors.full_messages
110
+ }
111
+ end
112
+ end
113
+ end