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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +56 -1
- data/README.md +271 -154
- data/README_ja.md +268 -151
- data/lib/girb/ai_client.rb +23 -5
- data/lib/girb/auto_continue.rb +1 -1
- data/lib/girb/conversation_history.rb +12 -7
- data/lib/girb/debug_integration.rb +158 -10
- data/lib/girb/debug_prompt_builder.rb +71 -11
- data/lib/girb/exception_capture.rb +4 -0
- data/lib/girb/irb_integration.rb +232 -0
- data/lib/girb/prompt_builder.rb +128 -51
- data/lib/girb/session_persistence.rb +170 -0
- data/lib/girb/tools/debug_session_history_tool.rb +61 -18
- data/lib/girb/tools/run_irb_debug_command.rb +58 -0
- data/lib/girb/tools/session_history_tool.rb +41 -8
- data/lib/girb/tools.rb +2 -1
- data/lib/girb/version.rb +1 -1
- data/lib/girb.rb +16 -7
- data/lib/irb/command/qq.rb +41 -0
- metadata +3 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
137
|
-
if
|
|
138
|
-
|
|
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?("
|
|
174
|
-
question = line.sub(/^
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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内部の例外を除外
|
data/lib/girb/irb_integration.rb
CHANGED
|
@@ -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
|