girb 0.1.2 → 0.3.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.
@@ -0,0 +1,426 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "debug"
4
+ require_relative "debug_context_builder"
5
+ require_relative "debug_prompt_builder"
6
+ require_relative "debug_session_history"
7
+ require_relative "session_persistence"
8
+
9
+ module Girb
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
+
15
+ @ai_triggered = false
16
+ @interrupted = false
17
+ @session_started = false
18
+
19
+ class << self
20
+ attr_accessor :ai_triggered, :auto_continue, :interrupted, :api_thread
21
+
22
+ def pending_debug_commands
23
+ @pending_debug_commands ||= []
24
+ end
25
+
26
+ def add_pending_debug_command(cmd)
27
+ pending_debug_commands << cmd
28
+ end
29
+
30
+ def take_pending_debug_commands
31
+ cmds = @pending_debug_commands || []
32
+ @pending_debug_commands = []
33
+ cmds
34
+ end
35
+
36
+ def auto_continue?
37
+ @auto_continue
38
+ end
39
+
40
+ def interrupted?
41
+ @interrupted
42
+ end
43
+
44
+ def interrupt!
45
+ @interrupted = true
46
+ end
47
+
48
+ def clear_interrupt!
49
+ @interrupted = false
50
+ end
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
+
68
+ def setup
69
+ return unless defined?(DEBUGGER__::SESSION)
70
+ return if @setup_done
71
+
72
+ register_ai_command
73
+ register_debug_tools
74
+ setup_keybinding
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
103
+ end
104
+
105
+ private
106
+
107
+ def register_ai_command
108
+ DEBUGGER__::SESSION.class.prepend(GirbDebugCommands)
109
+ end
110
+
111
+ def register_debug_tools
112
+ require_relative "tools/run_debug_command"
113
+ Girb::Tools.register(Girb::Tools::RunDebugCommand)
114
+ end
115
+
116
+ def setup_keybinding
117
+ return unless defined?(Reline::LineEditor)
118
+
119
+ Reline::LineEditor.prepend(Module.new do
120
+ private def girb_debug_ai_prefix(key)
121
+ Girb::DebugIntegration.ai_triggered = true
122
+ finish
123
+ end
124
+ end)
125
+
126
+ Reline.core.config.add_default_key_binding_by_keymap(:emacs, [0], :girb_debug_ai_prefix)
127
+ end
128
+
129
+ def setup_exit_hook
130
+ at_exit do
131
+ Girb::DebugIntegration.save_session!
132
+ end
133
+ end
134
+ end
135
+
136
+ module GirbDebugCommands
137
+ MAX_AUTO_CONTINUE = 20
138
+
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
+
151
+ if Girb::DebugIntegration.auto_continue?
152
+ @girb_auto_continue_count ||= 0
153
+ @girb_auto_continue_count += 1
154
+
155
+ # Set up interrupt handler on first iteration
156
+ if @girb_auto_continue_count == 1
157
+ setup_interrupt_handler
158
+ end
159
+
160
+ # Check for interrupt (Ctrl+C)
161
+ if Girb::DebugIntegration.interrupted?
162
+ Girb::DebugIntegration.auto_continue = false
163
+ Girb::DebugIntegration.clear_interrupt!
164
+ @girb_auto_continue_count = 0
165
+ restore_interrupt_handler
166
+ handle_ai_interrupted
167
+ return :retry
168
+ end
169
+
170
+ if @girb_auto_continue_count > MAX_AUTO_CONTINUE
171
+ Girb::DebugIntegration.auto_continue = false
172
+ @girb_auto_continue_count = 0
173
+ restore_interrupt_handler
174
+ handle_ai_turn_limit_reached
175
+ return :retry
176
+ end
177
+
178
+ begin
179
+ handle_ai_continuation
180
+ rescue Exception => e
181
+ if e.is_a?(Interrupt) || e.class.name.include?("Interrupt") || e.class.name.include?("Abort")
182
+ Girb::DebugIntegration.auto_continue = false
183
+ Girb::DebugIntegration.clear_interrupt!
184
+ @girb_auto_continue_count = 0
185
+ restore_interrupt_handler
186
+ handle_ai_interrupted
187
+ return :retry
188
+ else
189
+ raise
190
+ end
191
+ end
192
+
193
+ # Check for interrupt after API call (Ctrl+C during request)
194
+ if Girb::DebugIntegration.interrupted?
195
+ Girb::DebugIntegration.auto_continue = false
196
+ Girb::DebugIntegration.clear_interrupt!
197
+ @girb_auto_continue_count = 0
198
+ restore_interrupt_handler
199
+ handle_ai_interrupted
200
+ return :retry
201
+ end
202
+
203
+ more_cmds = Girb::DebugIntegration.take_pending_debug_commands
204
+ if more_cmds.any?
205
+ more_cmds.each do |cmd|
206
+ result = process_command(cmd)
207
+ return result unless result == :retry
208
+ end
209
+ else
210
+ Girb::DebugIntegration.auto_continue = false
211
+ restore_interrupt_handler
212
+ end
213
+ return :retry
214
+ else
215
+ @girb_auto_continue_count = 0
216
+ Girb::DebugIntegration.clear_interrupt!
217
+ end
218
+
219
+ super
220
+ end
221
+
222
+ def process_command(line)
223
+ if Girb::DebugIntegration.ai_triggered
224
+ Girb::DebugIntegration.ai_triggered = false
225
+ question = line.strip
226
+ return :retry if question.empty?
227
+
228
+ Girb::DebugSessionHistory.record_ai_question(question)
229
+ handle_ai_question(question)
230
+ pending_cmds = Girb::DebugIntegration.take_pending_debug_commands
231
+ if pending_cmds.any?
232
+ pending_cmds.each do |cmd|
233
+ result = super(cmd)
234
+ return result unless result == :retry
235
+ end
236
+ end
237
+ return :retry
238
+ end
239
+
240
+ if line.start_with?("qq ")
241
+ question = line.sub(/^qq\s+/, "").strip
242
+ return :retry if question.empty?
243
+
244
+ # セッション管理コマンド
245
+ if question.start_with?("session ")
246
+ handle_session_command(question.sub(/^session\s+/, ""))
247
+ return :retry
248
+ end
249
+
250
+ Girb::DebugSessionHistory.record_ai_question(question)
251
+ handle_ai_question(question)
252
+ pending_cmds = Girb::DebugIntegration.take_pending_debug_commands
253
+ if pending_cmds.any?
254
+ pending_cmds.each do |cmd|
255
+ result = super(cmd)
256
+ return result unless result == :retry
257
+ end
258
+ end
259
+ return :retry
260
+ end
261
+
262
+ # Auto-detect natural language (non-ASCII input) and route to AI
263
+ if line.match?(/[^\x00-\x7F]/)
264
+ question = line.strip
265
+ return :retry if question.empty?
266
+
267
+ Girb::DebugSessionHistory.record_ai_question(question)
268
+ handle_ai_question(question)
269
+ pending_cmds = Girb::DebugIntegration.take_pending_debug_commands
270
+ if pending_cmds.any?
271
+ pending_cmds.each do |cmd|
272
+ result = super(cmd)
273
+ return result unless result == :retry
274
+ end
275
+ end
276
+ return :retry
277
+ end
278
+
279
+ # Record regular debugger commands
280
+ Girb::DebugSessionHistory.record_command(line)
281
+ super
282
+ end
283
+
284
+ private
285
+
286
+ def handle_ai_continuation
287
+ current_binding = @tc&.current_frame&.eval_binding
288
+ unless current_binding
289
+ @ui.puts "[girb] Error: No current frame available"
290
+ return
291
+ end
292
+
293
+ context = Girb::DebugContextBuilder.new(current_binding).build
294
+ client = Girb::AiClient.new
295
+ continuation = "(auto-continue: The debug command has been executed. Analyze the new state and continue your task.)"
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
300
+ rescue StandardError => e
301
+ @ui.puts "[girb] Auto-continue error: #{e.message}"
302
+ Girb::DebugIntegration.auto_continue = false
303
+ end
304
+
305
+ def handle_ai_question(question)
306
+ current_binding = @tc&.current_frame&.eval_binding
307
+
308
+ unless current_binding
309
+ puts "[girb] Error: No current frame available"
310
+ return
311
+ end
312
+
313
+ # 初回のAI質問時にセッションを開始
314
+ Girb::DebugIntegration.start_session!
315
+
316
+ context = Girb::DebugContextBuilder.new(current_binding).build
317
+ client = Girb::AiClient.new
318
+ # Disable Ruby's Timeout during API call to avoid deadlock with debug gem's threading
319
+ with_timeout_disabled do
320
+ client.ask(question, context, binding: current_binding, debug_mode: true)
321
+ end
322
+ rescue Girb::ConfigurationError => e
323
+ puts "[girb] #{e.message}"
324
+ rescue StandardError => e
325
+ puts "[girb] Error: #{e.message}"
326
+ puts e.backtrace.first(3).join("\n") if Girb.configuration.debug
327
+ end
328
+
329
+ # Temporarily disable Ruby's Timeout module to avoid deadlock with debug gem
330
+ # The underlying socket has its own timeout, so this is safe
331
+ def with_timeout_disabled
332
+ return yield unless defined?(Timeout)
333
+
334
+ original_timeout = Timeout.method(:timeout)
335
+ Timeout.define_singleton_method(:timeout) do |_sec, _klass = nil, _message = nil, &block|
336
+ block.call
337
+ end
338
+ yield
339
+ ensure
340
+ Timeout.define_singleton_method(:timeout, original_timeout) if original_timeout
341
+ end
342
+
343
+ def handle_ai_turn_limit_reached
344
+ current_binding = @tc&.current_frame&.eval_binding
345
+ return unless current_binding
346
+
347
+ context = Girb::DebugContextBuilder.new(current_binding).build
348
+ client = Girb::AiClient.new
349
+ limit_message = "(System: Auto-continue turn limit (#{MAX_AUTO_CONTINUE}) reached. " \
350
+ "Summarize your progress so far and tell the user what was accomplished. " \
351
+ "If the task is not complete, explain what remains and instruct the user " \
352
+ "to continue with a follow-up request.)"
353
+ with_timeout_disabled do
354
+ client.ask(limit_message, context, binding: current_binding, debug_mode: true)
355
+ end
356
+ rescue StandardError => e
357
+ puts "[girb] Auto-continue limit reached (#{MAX_AUTO_CONTINUE})"
358
+ puts "[girb] Error summarizing: #{e.message}" if Girb.configuration.debug
359
+ end
360
+
361
+ def handle_ai_interrupted
362
+ puts "\n[girb] Interrupted by user (Ctrl+C)"
363
+ current_binding = @tc&.current_frame&.eval_binding
364
+ return unless current_binding
365
+
366
+ context = Girb::DebugContextBuilder.new(current_binding).build
367
+ client = Girb::AiClient.new
368
+ interrupt_message = "(System: User interrupted with Ctrl+C. " \
369
+ "Briefly summarize your progress so far. " \
370
+ "Tell the user where you stopped and how to continue if needed.)"
371
+ with_timeout_disabled do
372
+ client.ask(interrupt_message, context, binding: current_binding, debug_mode: true)
373
+ end
374
+ rescue StandardError => e
375
+ puts "[girb] Error summarizing: #{e.message}" if Girb.configuration.debug
376
+ end
377
+
378
+ def setup_interrupt_handler
379
+ Girb::DebugIntegration.api_thread = Thread.current
380
+ @original_int_handler = trap("INT") do
381
+ Girb::DebugIntegration.interrupt!
382
+ # Raise Interrupt to break out of blocking IO operations
383
+ thread = Girb::DebugIntegration.api_thread
384
+ thread&.raise(Interrupt) if thread&.alive?
385
+ end
386
+ end
387
+
388
+ def restore_interrupt_handler
389
+ if @original_int_handler
390
+ trap("INT", @original_int_handler)
391
+ @original_int_handler = nil
392
+ else
393
+ trap("INT", "DEFAULT")
394
+ end
395
+ end
396
+
397
+ def handle_session_command(cmd)
398
+ case cmd.strip
399
+ when "clear"
400
+ Girb::SessionPersistence.clear_session
401
+ when "list"
402
+ sessions = Girb::SessionPersistence.list_sessions
403
+ if sessions.empty?
404
+ puts "[girb] No saved sessions"
405
+ else
406
+ puts "[girb] Saved sessions:"
407
+ sessions.each do |s|
408
+ puts " - #{s[:id]} (#{s[:message_count]} messages, saved: #{s[:saved_at]})"
409
+ end
410
+ end
411
+ when "status"
412
+ if Girb::SessionPersistence.current_session_id
413
+ puts "[girb] Current session: #{Girb::SessionPersistence.current_session_id}"
414
+ elsif Girb.debug_session
415
+ puts "[girb] Session configured: #{Girb.debug_session} (not started)"
416
+ else
417
+ puts "[girb] No session configured (use Girb.debug_session = 'name' to enable)"
418
+ end
419
+ else
420
+ puts "[girb] Unknown session command: #{cmd}"
421
+ puts "[girb] Available: clear, list, status"
422
+ end
423
+ end
424
+ end
425
+ end
426
+ end
@@ -0,0 +1,241 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Girb
4
+ class DebugPromptBuilder
5
+ SYSTEM_PROMPT = <<~PROMPT
6
+ You are girb, an AI debugging assistant embedded in a Ruby debugger session.
7
+ You are integrated with Ruby's debug gem and can help developers debug their code.
8
+
9
+ ## CRITICAL: Context Information
10
+ The user is stopped at a breakpoint or debugger statement.
11
+ You have access to the current execution context including:
12
+ - Local variables and their values
13
+ - Instance variables of the current object
14
+ - The current file and line number
15
+ - The call stack (backtrace)
16
+
17
+ ## Language
18
+ Respond in the same language the user is using.
19
+
20
+ ## Your Role
21
+ - Help debug issues by analyzing the current state
22
+ - Explain what the code is doing and why it might be failing
23
+ - Use tools to inspect objects, evaluate code, or read source files
24
+ - Provide actionable advice to fix issues
25
+
26
+ ## When to Investigate Proactively
27
+ When the user asks about code, debugging, variables, errors, or anything related to their program,
28
+ you should investigate before responding:
29
+ - Use `read_file` to read the source file shown in "Source Location" if relevant to the question
30
+ - Use `evaluate_code` to run and verify code rather than guessing or reasoning about results
31
+ - NEVER ask the user for code, file names, or variable definitions that you can look up
32
+ yourself with `read_file`, `evaluate_code`, `inspect_object`, or `find_file`
33
+
34
+ However, for simple greetings or conversational messages (e.g., "hello", "hi", "こんにちは", "thanks"),
35
+ just respond naturally without using tools. Not every message requires investigation.
36
+
37
+ ## CRITICAL: Variable Persistence Across Frames
38
+ Local variables created via `evaluate_code` do NOT persist after `step`, `next`, or `continue`.
39
+ When the program moves to a new frame, those local variables are lost.
40
+
41
+ To track values across multiple breakpoints or frames, use:
42
+ - Instance variables: `@x_values = []` then `@x_values << x`
43
+ - Global variables: `$x_values = []` then `$x_values << x`
44
+
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
+ **Efficient approach for loops with many iterations:**
50
+ 1. `evaluate_code("$tracked = []")` - initialize tracking array
51
+ 2. Use a conditional breakpoint that records AND stops on condition:
52
+ `break file.rb:10 if: ($tracked << x; x == 1)`
53
+ This appends x to $tracked on EVERY hit, but only stops when x == 1.
54
+ 3. `continue` - run through all iterations at full speed
55
+ 4. When stopped (or at end): `evaluate_code("$tracked")` to see all collected values
56
+
57
+ This completes in 2-3 API turns instead of many turns with repeated stepping.
58
+
59
+ **When to use repeated stepping (next/step):**
60
+ - Understanding complex logic flow (few lines)
61
+ - Checking which branch is taken
62
+ - Loops with only 2-3 iterations
63
+ - User explicitly wants to see execution step by step
64
+
65
+ **When to use conditional breakpoints:**
66
+ - Loops with many iterations (5+)
67
+ - "Track variable X until condition Y" requests
68
+ - "Find when X becomes Y" requests
69
+ - Collecting history of values
70
+
71
+ ## CRITICAL: Executing Debugger Commands
72
+ When the user asks you to perform a debugging action (e.g., "go to the next line", "step into",
73
+ "continue", "advance to line N", "set a breakpoint"), you MUST use the `run_debug_command` tool.
74
+ Do NOT just print or suggest the command as text — actually call the tool.
75
+ You can also use the `evaluate_code` tool to run Ruby expressions in the current context.
76
+
77
+ Available debugger commands for run_debug_command:
78
+ - `step` / `s`: Step into method calls
79
+ - `next` / `n`: Step over to next line
80
+ - `continue` / `c`: Continue execution
81
+ - `finish`: Run until current method returns
82
+ - `up` / `down`: Navigate the call stack
83
+ - `break <file>:<line>`: Set a breakpoint (e.g., `break sample.rb:14`)
84
+ - `info locals`: Show local variables
85
+ - `pp <expr>`: Pretty print an expression
86
+
87
+ IMPORTANT: For conditional breakpoints, use `if:` (with colon), NOT `if` (without colon).
88
+ Example: `break sample.rb:14 if: x == 1`
89
+
90
+ IMPORTANT: Each `run_debug_command` call must contain exactly ONE debugger command.
91
+ NEVER combine multiple commands with `;` or append debugger commands to breakpoint conditions.
92
+ BAD: `break sample.rb:14 if: x == 1; continue` ("; continue" becomes part of the Ruby condition and causes an error)
93
+ GOOD: Call `run_debug_command("break sample.rb:14 if: x == 1")` then `run_debug_command("c")` separately.
94
+
95
+ ## Response Guidelines
96
+ - Keep responses concise and actionable
97
+ - Focus on the immediate debugging task
98
+ - When the user requests a debugger action, execute it via run_debug_command — do not just describe it
99
+ - NEVER repeat the same failed action. If a tool call fails, analyze the error and try a different approach
100
+ - If you encounter an error about undefined variables after continue/step, remember to use instance or global variables
101
+ - IMPORTANT: When a task is complete (tracking finished, script ended, etc.), ALWAYS report the results.
102
+ Don't just execute commands and stop — check the collected data and summarize findings for the user.
103
+ For example, after tracking variables: use evaluate_code to retrieve $tracked and present the results.
104
+
105
+ ## Available Tools
106
+ Use tools to inspect the runtime state:
107
+ - evaluate_code: Execute Ruby code in the current context
108
+ - inspect_object: Get detailed information about objects
109
+ - get_source: Read method or class source code
110
+ - list_methods: List available methods on an object
111
+ - read_file: Read source files
112
+ - find_file: Find files in the project
113
+ - get_session_history: Get past debugger commands and AI conversations
114
+ - run_debug_command: Execute a debugger command (n, s, c, finish, up, down, break, info, bt, etc.)
115
+
116
+ ## Session History
117
+ The "Session History" section in the context shows recent debugger commands and AI conversations.
118
+ Use this to understand the user's past actions and questions. Format:
119
+ - [cmd] ... : Debugger command entered by user
120
+ - [ai] Q: ... A: ... : Previous AI question and response
121
+
122
+ ## Interactive Debugging with auto_continue
123
+ When you need to execute a debugger command AND see the result before deciding your next action,
124
+ use `run_debug_command` with `auto_continue: true`.
125
+
126
+ After the command executes and the program stops at a new point, you will be automatically
127
+ re-invoked with the updated debug context (new file/line, new variable values).
128
+ You can then inspect variables, evaluate code, and decide whether to continue stepping or
129
+ give your final answer.
130
+
131
+ Use `auto_continue: true` when:
132
+ - Stepping through code to find where a variable changes
133
+ - Continuing to a breakpoint and then analyzing the state
134
+ - Any scenario where you need to see the result of a navigation command
135
+ - When the user asks you to track/collect data and report results — you need to be re-invoked
136
+ after the program stops so you can check the collected data and report back
137
+
138
+ Do NOT use `auto_continue: true` when:
139
+ - You've already collected and reported all the information the user asked for
140
+ - The user explicitly asks to just run a command without analysis
141
+
142
+ You can call `run_debug_command` multiple times in a single turn to batch commands.
143
+ Non-navigation commands (break, info, bt) should come before navigation commands (step, next, continue).
144
+ PROMPT
145
+
146
+ def initialize(question, context)
147
+ @question = question
148
+ @context = context
149
+ end
150
+
151
+ def system_prompt
152
+ custom = Girb.configuration&.custom_prompt
153
+ if custom && !custom.empty?
154
+ "#{SYSTEM_PROMPT}\n\n## User-Defined Instructions\n#{custom}"
155
+ else
156
+ SYSTEM_PROMPT
157
+ end
158
+ end
159
+
160
+ def user_message
161
+ <<~MSG
162
+ ## Current Debug Context
163
+ #{build_context_section}
164
+
165
+ ## Question
166
+ #{@question}
167
+ MSG
168
+ end
169
+
170
+ private
171
+
172
+ def build_context_section
173
+ <<~CONTEXT
174
+ ### Source Location
175
+ #{format_source_location}
176
+
177
+ ### Local Variables
178
+ #{format_locals}
179
+
180
+ ### Instance Variables
181
+ #{format_instance_variables}
182
+
183
+ ### Current Object (self)
184
+ #{format_self_info}
185
+
186
+ ### Backtrace
187
+ #{format_backtrace}
188
+
189
+ ### Session History (recent commands and AI conversations)
190
+ #{format_session_history}
191
+ CONTEXT
192
+ end
193
+
194
+ def format_source_location
195
+ loc = @context[:source_location]
196
+ return "(unknown)" unless loc
197
+
198
+ "File: #{loc[:file]}\nLine: #{loc[:line]}"
199
+ end
200
+
201
+ def format_locals
202
+ locals = @context[:local_variables]
203
+ return "(none)" if locals.nil? || locals.empty?
204
+
205
+ locals.map { |name, value| "- #{name}: #{value}" }.join("\n")
206
+ end
207
+
208
+ def format_instance_variables
209
+ ivars = @context[:instance_variables]
210
+ return "(none)" if ivars.nil? || ivars.empty?
211
+
212
+ ivars.map { |name, value| "- #{name}: #{value}" }.join("\n")
213
+ end
214
+
215
+ def format_self_info
216
+ info = @context[:self_info]
217
+ return "(unknown)" unless info
218
+
219
+ lines = ["Class: #{info[:class]}"]
220
+ lines << "inspect: #{info[:inspect]}"
221
+ if info[:methods]&.any?
222
+ lines << "Defined methods: #{info[:methods].join(', ')}"
223
+ end
224
+ lines.join("\n")
225
+ end
226
+
227
+ def format_backtrace
228
+ bt = @context[:backtrace]
229
+ return "(not available)" unless bt
230
+
231
+ bt
232
+ end
233
+
234
+ def format_session_history
235
+ history = @context[:session_history]
236
+ return "(no history yet)" if history.nil? || history.empty?
237
+
238
+ history
239
+ end
240
+ end
241
+ end