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,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Girb
4
+ # デバッグセッション中の入力とAI会話を管理するクラス
5
+ class DebugSessionHistory
6
+ Entry = Struct.new(:type, :content, :response, :timestamp, keyword_init: true)
7
+
8
+ class << self
9
+ def instance
10
+ @instance ||= new
11
+ end
12
+
13
+ def reset!
14
+ @instance = new
15
+ end
16
+
17
+ # 委譲メソッド
18
+ def record_command(command)
19
+ instance.record_command(command)
20
+ end
21
+
22
+ def record_ai_question(question)
23
+ instance.record_ai_question(question)
24
+ end
25
+
26
+ def record_ai_response(response)
27
+ instance.record_ai_response(response)
28
+ end
29
+
30
+ def entries
31
+ instance.entries
32
+ end
33
+
34
+ def recent(count = 20)
35
+ instance.recent(count)
36
+ end
37
+
38
+ def ai_conversations
39
+ instance.ai_conversations
40
+ end
41
+
42
+ def format_history(count = 20)
43
+ instance.format_history(count)
44
+ end
45
+ end
46
+
47
+ attr_reader :entries
48
+
49
+ def initialize
50
+ @entries = []
51
+ @pending_ai_entry = nil
52
+ end
53
+
54
+ # デバッガーコマンドを記録
55
+ def record_command(command)
56
+ return if command.nil? || command.strip.empty?
57
+
58
+ @entries << Entry.new(
59
+ type: :command,
60
+ content: command.strip,
61
+ response: nil,
62
+ timestamp: Time.now
63
+ )
64
+ end
65
+
66
+ # AI質問を記録(回答は後から追加)
67
+ def record_ai_question(question)
68
+ entry = Entry.new(
69
+ type: :ai_question,
70
+ content: question,
71
+ response: nil,
72
+ timestamp: Time.now
73
+ )
74
+ @entries << entry
75
+ @pending_ai_entry = entry
76
+ end
77
+
78
+ # AI回答を記録
79
+ def record_ai_response(response)
80
+ if @pending_ai_entry
81
+ @pending_ai_entry.response = response
82
+ @pending_ai_entry = nil
83
+ end
84
+ end
85
+
86
+ # 最近のエントリを取得
87
+ def recent(count = 20)
88
+ @entries.last(count)
89
+ end
90
+
91
+ # AI会話のみを取得
92
+ def ai_conversations
93
+ @entries.select { |e| e.type == :ai_question && e.response }
94
+ end
95
+
96
+ # フォーマットされた履歴を取得
97
+ def format_history(count = 20)
98
+ recent(count).map do |entry|
99
+ case entry.type
100
+ when :command
101
+ "[cmd] #{entry.content}"
102
+ when :ai_question
103
+ if entry.response
104
+ response_preview = truncate(entry.response, 150)
105
+ "[ai] Q: #{entry.content}\n A: #{response_preview}"
106
+ else
107
+ "[ai] Q: #{entry.content} (pending...)"
108
+ end
109
+ end
110
+ end.join("\n")
111
+ end
112
+
113
+ private
114
+
115
+ def truncate(str, max_length)
116
+ return str if str.nil?
117
+
118
+ str.length > max_length ? "#{str[0, max_length]}..." : str
119
+ end
120
+ end
121
+ end
@@ -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,25 +5,31 @@ require "pathname"
5
5
  module Girb
6
6
  module GirbrcLoader
7
7
  class << self
8
- # Find .girbrc by traversing from start_dir up to root,
9
- # then fall back to ~/.girbrc
10
- def find_girbrc(start_dir = Dir.pwd)
8
+ # Find config file by traversing from start_dir up to root,
9
+ # then fall back to home directory
10
+ def find_config(filename, start_dir = Dir.pwd)
11
11
  dir = Pathname.new(start_dir).expand_path
12
12
 
13
- # Traverse up to find .girbrc
13
+ # Traverse up to find config file
14
14
  while dir != dir.parent
15
- candidate = dir.join(".girbrc")
15
+ candidate = dir.join(filename)
16
16
  return candidate if candidate.exist?
17
17
  dir = dir.parent
18
18
  end
19
19
 
20
20
  # Check root directory
21
- root_candidate = dir.join(".girbrc")
21
+ root_candidate = dir.join(filename)
22
22
  return root_candidate if root_candidate.exist?
23
23
 
24
- # Fall back to ~/.girbrc
25
- home_girbrc = Pathname.new(File.expand_path("~/.girbrc"))
26
- home_girbrc.exist? ? home_girbrc : nil
24
+ # Fall back to home directory
25
+ home_config = Pathname.new(File.expand_path("~/#{filename}"))
26
+ home_config.exist? ? home_config : nil
27
+ end
28
+
29
+ # Find .girbrc by traversing from start_dir up to root,
30
+ # then fall back to ~/.girbrc
31
+ def find_girbrc(start_dir = Dir.pwd)
32
+ find_config(".girbrc", start_dir)
27
33
  end
28
34
 
29
35
  # Load .girbrc if found
@@ -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
- client.ask(question, context, binding: workspace.binding, line_no: line_no)
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