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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +80 -0
- data/README.md +283 -129
- data/README_ja.md +280 -126
- data/lib/girb/ai_client.rb +209 -29
- data/lib/girb/auto_continue.rb +33 -0
- data/lib/girb/conversation_history.rb +8 -7
- data/lib/girb/debug_context_builder.rb +113 -0
- data/lib/girb/debug_integration.rb +426 -0
- data/lib/girb/debug_prompt_builder.rb +241 -0
- data/lib/girb/debug_session_history.rb +121 -0
- data/lib/girb/exception_capture.rb +4 -0
- data/lib/girb/girbrc_loader.rb +15 -9
- data/lib/girb/irb_integration.rb +233 -1
- data/lib/girb/prompt_builder.rb +156 -46
- data/lib/girb/session_persistence.rb +170 -0
- data/lib/girb/tools/continue_analysis.rb +45 -0
- data/lib/girb/tools/debug_session_history_tool.rb +132 -0
- data/lib/girb/tools/evaluate_code.rb +24 -3
- data/lib/girb/tools/run_debug_command.rb +49 -0
- data/lib/girb/tools/run_irb_debug_command.rb +58 -0
- data/lib/girb/tools/session_history_tool.rb +46 -8
- data/lib/girb/tools.rb +23 -2
- data/lib/girb/version.rb +1 -1
- data/lib/girb.rb +24 -7
- data/lib/irb/command/qq.rb +48 -6
- metadata +11 -1
|
@@ -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内部の例外を除外
|
data/lib/girb/girbrc_loader.rb
CHANGED
|
@@ -5,25 +5,31 @@ require "pathname"
|
|
|
5
5
|
module Girb
|
|
6
6
|
module GirbrcLoader
|
|
7
7
|
class << self
|
|
8
|
-
# Find
|
|
9
|
-
# then fall back to
|
|
10
|
-
def
|
|
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
|
|
13
|
+
# Traverse up to find config file
|
|
14
14
|
while dir != dir.parent
|
|
15
|
-
candidate = dir.join(
|
|
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(
|
|
21
|
+
root_candidate = dir.join(filename)
|
|
22
22
|
return root_candidate if root_candidate.exist?
|
|
23
23
|
|
|
24
|
-
# Fall back to
|
|
25
|
-
|
|
26
|
-
|
|
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
|
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
|
-
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
|