ruby-claw 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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +94 -0
  3. data/README.md +214 -10
  4. data/exe/claw +42 -1
  5. data/lib/claw/auto_forge.rb +66 -0
  6. data/lib/claw/benchmark/benchmark.rb +79 -0
  7. data/lib/claw/benchmark/diff.rb +69 -0
  8. data/lib/claw/benchmark/report.rb +87 -0
  9. data/lib/claw/benchmark/runner.rb +91 -0
  10. data/lib/claw/benchmark/scorer.rb +69 -0
  11. data/lib/claw/benchmark/task.rb +63 -0
  12. data/lib/claw/benchmark/tasks/claw_remember.rb +20 -0
  13. data/lib/claw/benchmark/tasks/claw_session.rb +18 -0
  14. data/lib/claw/benchmark/tasks/evolution_trace.rb +18 -0
  15. data/lib/claw/benchmark/tasks/mana_call_func.rb +21 -0
  16. data/lib/claw/benchmark/tasks/mana_eval.rb +18 -0
  17. data/lib/claw/benchmark/tasks/mana_knowledge.rb +19 -0
  18. data/lib/claw/benchmark/tasks/mana_var_readwrite.rb +18 -0
  19. data/lib/claw/benchmark/tasks/runtime_fork.rb +18 -0
  20. data/lib/claw/benchmark/tasks/runtime_snapshot.rb +18 -0
  21. data/lib/claw/benchmark/trigger.rb +68 -0
  22. data/lib/claw/chat.rb +119 -6
  23. data/lib/claw/child_runtime.rb +196 -0
  24. data/lib/claw/cli.rb +177 -0
  25. data/lib/claw/commands.rb +131 -0
  26. data/lib/claw/config.rb +5 -1
  27. data/lib/claw/console/event_logger.rb +69 -0
  28. data/lib/claw/console/public/app.js +264 -0
  29. data/lib/claw/console/public/style.css +330 -0
  30. data/lib/claw/console/server.rb +253 -0
  31. data/lib/claw/console/sse.rb +28 -0
  32. data/lib/claw/console/views/experiments.erb +8 -0
  33. data/lib/claw/console/views/index.erb +27 -0
  34. data/lib/claw/console/views/layout.erb +29 -0
  35. data/lib/claw/console/views/memory.erb +13 -0
  36. data/lib/claw/console/views/monitor.erb +15 -0
  37. data/lib/claw/console/views/prompt.erb +15 -0
  38. data/lib/claw/console/views/snapshots.erb +12 -0
  39. data/lib/claw/console/views/tools.erb +13 -0
  40. data/lib/claw/console/views/traces.erb +9 -0
  41. data/lib/claw/console.rb +5 -0
  42. data/lib/claw/evolution.rb +227 -0
  43. data/lib/claw/forge.rb +144 -0
  44. data/lib/claw/hub.rb +67 -0
  45. data/lib/claw/init.rb +199 -0
  46. data/lib/claw/knowledge.rb +36 -2
  47. data/lib/claw/memory_store.rb +2 -2
  48. data/lib/claw/plan_mode.rb +110 -0
  49. data/lib/claw/resource.rb +35 -0
  50. data/lib/claw/resources/binding_resource.rb +128 -0
  51. data/lib/claw/resources/context_resource.rb +73 -0
  52. data/lib/claw/resources/filesystem_resource.rb +107 -0
  53. data/lib/claw/resources/memory_resource.rb +74 -0
  54. data/lib/claw/resources/worktree_resource.rb +133 -0
  55. data/lib/claw/roles.rb +56 -0
  56. data/lib/claw/runtime.rb +189 -0
  57. data/lib/claw/serializer.rb +10 -7
  58. data/lib/claw/tool.rb +99 -0
  59. data/lib/claw/tool_index.rb +84 -0
  60. data/lib/claw/tool_registry.rb +100 -0
  61. data/lib/claw/trace.rb +86 -0
  62. data/lib/claw/tui/agent_executor.rb +92 -0
  63. data/lib/claw/tui/chat_panel.rb +81 -0
  64. data/lib/claw/tui/command_bar.rb +22 -0
  65. data/lib/claw/tui/file_card.rb +88 -0
  66. data/lib/claw/tui/folding.rb +80 -0
  67. data/lib/claw/tui/input_handler.rb +73 -0
  68. data/lib/claw/tui/layout.rb +34 -0
  69. data/lib/claw/tui/messages.rb +31 -0
  70. data/lib/claw/tui/model.rb +411 -0
  71. data/lib/claw/tui/object_explorer.rb +136 -0
  72. data/lib/claw/tui/status_bar.rb +30 -0
  73. data/lib/claw/tui/status_panel.rb +133 -0
  74. data/lib/claw/tui/styles.rb +58 -0
  75. data/lib/claw/tui/tui.rb +54 -0
  76. data/lib/claw/version.rb +1 -1
  77. data/lib/claw.rb +99 -1
  78. metadata +223 -7
@@ -0,0 +1,411 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bubbletea"
4
+ require "bubbles"
5
+ require "io/console"
6
+
7
+ module Claw
8
+ module TUI
9
+ # MVU Model — central state for the TUI application.
10
+ # Implements Bubbletea's init/update/view protocol.
11
+ class Model
12
+ attr_reader :runtime, :chat_history, :mode, :chat_viewport, :executor
13
+ attr_accessor :input_text
14
+
15
+ def initialize(caller_binding)
16
+ @caller_binding = caller_binding
17
+ @runtime = init_runtime(caller_binding)
18
+ @chat_history = []
19
+ @mode = :normal # :normal | :plan
20
+ @input_text = ""
21
+ @input_focused = true
22
+ @scrolled_up = false
23
+ @text_buffer = +"" # accumulates streaming text
24
+
25
+ # Bubbles components
26
+ @chat_viewport = Bubbles::Viewport.new(width: 80, height: 20)
27
+ @spinner = Bubbles::Spinner.new
28
+ @spinner.style = Styles::SPINNER_STYLE
29
+
30
+ # Agent executor
31
+ @executor = AgentExecutor.new(@runtime)
32
+
33
+ # Runtime state change observer → deliver MVU messages
34
+ @runtime&.on_state_change do |old_state, new_state, step|
35
+ Bubbletea.send_message(StateChangeMsg.new(
36
+ old_state: old_state, new_state: new_state, step: step
37
+ ))
38
+ end
39
+ end
40
+
41
+ def init
42
+ @chat_history << { role: :system, content: "Claw agent ready · type 'exit' to quit" }
43
+ cmd = @spinner.init
44
+ Bubbletea.batch(cmd, Bubbletea.tick(1.0) { TickMsg.new(time: Time.now) })
45
+ end
46
+
47
+ def update(msg)
48
+ case msg
49
+ when Bubbletea::KeyMessage
50
+ handle_key(msg)
51
+ when TickMsg
52
+ cmd = @spinner.update(@spinner.tick)
53
+ Bubbletea.batch(cmd, Bubbletea.tick(1.0) { TickMsg.new(time: Time.now) })
54
+ when AgentTextMsg
55
+ @text_buffer << msg.text
56
+ Bubbletea.none
57
+ when ToolCallMsg
58
+ flush_text_buffer
59
+ detail = format_tool_detail(msg.name, msg.input)
60
+ @chat_history << { role: :tool_call, icon: "⚡", detail: detail }
61
+ Bubbletea.none
62
+ when ToolResultMsg
63
+ @chat_history << { role: :tool_result, result: msg.result } unless msg.result.to_s.start_with?("ok:")
64
+ Bubbletea.none
65
+ when ExecutionDoneMsg
66
+ flush_text_buffer
67
+ write_trace(msg.trace)
68
+ Claw.memory&.schedule_compaction
69
+ Bubbletea.none
70
+ when ExecutionErrorMsg
71
+ flush_text_buffer
72
+ @chat_history << { role: :error, content: msg.error.message }
73
+ Bubbletea.none
74
+ when CommandResultMsg
75
+ handle_command_result(msg)
76
+ Bubbletea.none
77
+ when StateChangeMsg
78
+ # Runtime state changed — triggers re-render via Bubbletea
79
+ Bubbletea.none
80
+ else
81
+ Bubbletea.none
82
+ end
83
+ end
84
+
85
+ def view
86
+ h, w = IO.console&.winsize || [24, 80]
87
+ w = 80 if w < 40
88
+ h = 24 if h < 12
89
+ Layout.render(self, w, h)
90
+ end
91
+
92
+ # --- Query methods for panels ---
93
+
94
+ def last_snapshot_id
95
+ @runtime&.snapshots&.last&.id || 0
96
+ end
97
+
98
+ def token_display
99
+ ctx = Mana::Context.current
100
+ used = ctx.token_count
101
+ limit = Mana.config.context_window
102
+ "#{format_tokens(used)}/#{format_tokens(limit)}"
103
+ end
104
+
105
+ def input_focused? = @input_focused
106
+ def scrolled_up? = @scrolled_up
107
+ def spinner_view = @spinner.view
108
+
109
+ private
110
+
111
+ def handle_key(msg)
112
+ key = msg.to_s
113
+
114
+ case key
115
+ when "ctrl+c", "ctrl+d"
116
+ save_state
117
+ return Bubbletea.quit
118
+ when "enter"
119
+ return submit_input
120
+ when "pgup"
121
+ @chat_viewport.page_up
122
+ @scrolled_up = true
123
+ return Bubbletea.none
124
+ when "pgdown"
125
+ @chat_viewport.page_down
126
+ @scrolled_up = @chat_viewport.at_bottom? ? false : true
127
+ return Bubbletea.none
128
+ when "backspace"
129
+ @input_text = @input_text[0..-2] if @input_text.length > 0
130
+ return Bubbletea.none
131
+ end
132
+
133
+ # Regular character input
134
+ if key.length == 1 && key.ord >= 32
135
+ @input_text << key
136
+ end
137
+ Bubbletea.none
138
+ end
139
+
140
+ def submit_input
141
+ text = @input_text.strip
142
+ @input_text = ""
143
+ return Bubbletea.none if text.empty?
144
+
145
+ # Busy guard — prevent concurrent LLM executions
146
+ if @executor.running? && !text.start_with?("/") && !text.start_with?("!") && !text.match?(/\A(exit|quit|bye|q)\z/i)
147
+ @chat_history << { role: :system, content: "Agent is busy — please wait for the current execution to finish." }
148
+ return Bubbletea.none
149
+ end
150
+
151
+ # Exit
152
+ if text.match?(/\A(exit|quit|bye|q)\z/i)
153
+ save_state
154
+ return Bubbletea.quit
155
+ end
156
+
157
+ @chat_history << { role: :user, content: text }
158
+ @scrolled_up = false
159
+
160
+ if text.start_with?("/")
161
+ handle_slash(text)
162
+ elsif text.start_with?("!")
163
+ handle_ruby(text[1..].strip)
164
+ elsif ruby_syntax?(text)
165
+ handle_ruby_or_llm(text)
166
+ else
167
+ handle_llm(text)
168
+ end
169
+ end
170
+
171
+ def handle_slash(text)
172
+ cmd, *args = text.sub(/\A\//, "").split(" ", 2)
173
+ arg = args.first
174
+
175
+ if cmd == "plan"
176
+ @mode = @mode == :plan ? :normal : :plan
177
+ @chat_history << { role: :system, content: "mode: #{@mode}" }
178
+ return Bubbletea.none
179
+ end
180
+
181
+ # Object explorer commands
182
+ case cmd
183
+ when "ls"
184
+ result = ObjectExplorer.ls(@caller_binding)
185
+ if result[:type] == :data
186
+ lines = result[:data].flat_map { |section, items| ["#{section}:", *items] }
187
+ @chat_history << { role: :system, content: lines.join("\n") }
188
+ end
189
+ return Bubbletea.none
190
+ when "cd"
191
+ @nav_stack ||= []
192
+ result = ObjectExplorer.cd(arg || "..", @caller_binding, @nav_stack)
193
+ if result[:type] == :success
194
+ @caller_binding = result[:data][:binding]
195
+ @chat_history << { role: :system, content: "cd → #{result[:data][:label]}" }
196
+ else
197
+ @chat_history << { role: :error, content: result[:message] }
198
+ end
199
+ return Bubbletea.none
200
+ when "source"
201
+ result = ObjectExplorer.source(arg.to_s, @caller_binding)
202
+ if result[:type] == :data
203
+ @chat_history << { role: :system, content: "#{result[:data][:file]}:#{result[:data][:line]}\n#{result[:data][:source]}" }
204
+ else
205
+ @chat_history << { role: :error, content: result[:message] }
206
+ end
207
+ return Bubbletea.none
208
+ when "doc"
209
+ result = ObjectExplorer.doc(arg.to_s, @caller_binding)
210
+ @chat_history << { role: :system, content: result[:data][:doc].to_s }
211
+ return Bubbletea.none
212
+ when "find"
213
+ result = ObjectExplorer.find(arg.to_s, @caller_binding)
214
+ if result[:type] == :data
215
+ @chat_history << { role: :system, content: result[:data][:matches].join(", ") }
216
+ else
217
+ @chat_history << { role: :system, content: result[:message] }
218
+ end
219
+ return Bubbletea.none
220
+ when "whereami"
221
+ result = ObjectExplorer.whereami(@caller_binding)
222
+ d = result[:data]
223
+ @chat_history << { role: :system, content: "#{d[:file]}:#{d[:line]} (#{d[:receiver]})" }
224
+ return Bubbletea.none
225
+ end
226
+
227
+ result = Claw::Commands.dispatch(cmd, arg, runtime: @runtime)
228
+ handle_command_result(CommandResultMsg.new(result: result, cmd: cmd))
229
+ Bubbletea.none
230
+ end
231
+
232
+ def handle_ruby(code)
233
+ eval_result = @executor.eval_ruby(code, @caller_binding)
234
+ if eval_result[:success]
235
+ @chat_history << { role: :ruby, content: eval_result[:result].inspect }
236
+ # Track method definitions for session persistence
237
+ if eval_result[:result].is_a?(Symbol) && code.strip.match?(/\Adef\s/)
238
+ track_definition(@caller_binding, code, eval_result[:result])
239
+ end
240
+ else
241
+ @chat_history << { role: :error, content: "#{eval_result[:error].class}: #{eval_result[:error].message}" }
242
+ end
243
+ @runtime&.resources&.dig("binding")&.scan_binding
244
+ Bubbletea.none
245
+ end
246
+
247
+ def handle_ruby_or_llm(text)
248
+ eval_result = @executor.eval_ruby(text, @caller_binding)
249
+ if eval_result[:success]
250
+ @chat_history << { role: :ruby, content: eval_result[:result].inspect }
251
+ @runtime&.resources&.dig("binding")&.scan_binding
252
+ Bubbletea.none
253
+ elsif eval_result[:error].is_a?(NameError) && (text.include?(" ") || text.match?(/[^\x00-\x7F]/))
254
+ handle_llm(text)
255
+ else
256
+ @chat_history << { role: :error, content: "#{eval_result[:error].class}: #{eval_result[:error].message}" }
257
+ Bubbletea.none
258
+ end
259
+ end
260
+
261
+ def handle_llm(text)
262
+ # Extract @file references and inject file context
263
+ refs = FileCard.extract_refs(text)
264
+ unless refs.empty?
265
+ refs.each do |ref|
266
+ paths = FileCard.resolve(ref)
267
+ paths.each do |path|
268
+ @chat_history << { role: :system, content: FileCard.render_card(path) }
269
+ text = "#{text}\n\n#{FileCard.read_for_context(path)}"
270
+ end
271
+ end
272
+ end
273
+
274
+ @executor.execute(text, @caller_binding) do |event|
275
+ Bubbletea.send_message(event)
276
+ end
277
+ Bubbletea.none
278
+ end
279
+
280
+ def handle_command_result(msg)
281
+ result = msg.result
282
+ case result[:type]
283
+ when :success
284
+ @chat_history << { role: :system, content: "✓ #{result[:message]}" }
285
+ when :error
286
+ @chat_history << { role: :error, content: result[:message] }
287
+ when :info
288
+ @chat_history << { role: :system, content: result[:message] }
289
+ when :data
290
+ case msg.cmd
291
+ when "diff"
292
+ data = result[:data]
293
+ lines = ["Diff ##{data[:from]} → ##{data[:to]}:"]
294
+ data[:diffs].each do |name, d|
295
+ lines << " #{name}:"
296
+ d.each_line { |l| lines << " #{l.rstrip}" }
297
+ end
298
+ @chat_history << { role: :system, content: lines.join("\n") }
299
+ when "history"
300
+ lines = result[:data][:snapshots].map { |s| " ##{s[:id]} #{s[:label]} — #{s[:timestamp]}" }
301
+ @chat_history << { role: :system, content: lines.join("\n") }
302
+ when "status"
303
+ @chat_history << { role: :system, content: result[:data][:markdown] }
304
+ when "evolve"
305
+ evo = result[:data]
306
+ msg_text = case evo[:status]
307
+ when :accept then "✓ accepted: #{evo[:proposal]}"
308
+ when :reject then "✗ rejected: #{evo[:proposal] || 'n/a'}"
309
+ when :skip then "· skipped: #{evo[:reason]}"
310
+ else result[:message]
311
+ end
312
+ @chat_history << { role: :system, content: msg_text }
313
+ else
314
+ @chat_history << { role: :system, content: result[:message] }
315
+ end
316
+ end
317
+ end
318
+
319
+ def flush_text_buffer
320
+ return if @text_buffer.empty?
321
+ @chat_history << { role: :agent, content: @text_buffer.dup }
322
+ @text_buffer.clear
323
+ end
324
+
325
+ def format_tool_detail(name, input)
326
+ input ||= {}
327
+ case name
328
+ when "call_func"
329
+ func = input[:name] || input["name"]
330
+ args = input[:args] || input["args"] || []
331
+ desc = func.to_s
332
+ desc += "(#{args.map(&:inspect).join(', ')})" if args.any?
333
+ desc
334
+ when "read_var", "write_var"
335
+ var = input[:name] || input["name"]
336
+ val = input[:value] || input["value"]
337
+ val ? "#{var} = #{val.inspect[0, 60]}" : var.to_s
338
+ when "read_attr", "write_attr"
339
+ obj = input[:obj] || input["obj"]
340
+ attr = input[:attr] || input["attr"]
341
+ "#{obj}.#{attr}"
342
+ when "remember"
343
+ "remember: #{(input[:content] || input["content"]).to_s[0, 60]}"
344
+ when "knowledge"
345
+ topic = input[:topic] || input["topic"]
346
+ "knowledge(#{topic})"
347
+ else
348
+ name.to_s
349
+ end
350
+ end
351
+
352
+ def ruby_syntax?(input)
353
+ RubyVM::InstructionSequence.compile(input)
354
+ true
355
+ rescue SyntaxError
356
+ false
357
+ end
358
+
359
+ def write_trace(trace_data)
360
+ return unless trace_data
361
+ claw_dir = File.join(Dir.pwd, ".ruby-claw")
362
+ return unless File.directory?(claw_dir)
363
+ Claw::Trace.write(trace_data, claw_dir)
364
+ rescue
365
+ # ignore trace write failures
366
+ end
367
+
368
+ def save_state
369
+ Claw::Serializer.save(@caller_binding, File.join(Dir.pwd, ".ruby-claw")) if Claw.config.persist_session
370
+ Claw.memory&.save_session
371
+ rescue
372
+ # ignore save failures
373
+ end
374
+
375
+ def track_definition(caller_binding, code, method_name)
376
+ receiver = caller_binding.receiver
377
+ defs = receiver.instance_variable_defined?(:@__claw_definitions__) ?
378
+ receiver.instance_variable_get(:@__claw_definitions__) : {}
379
+ defs[method_name.to_s] = code
380
+ receiver.instance_variable_set(:@__claw_definitions__, defs)
381
+ end
382
+
383
+ def format_tokens(n)
384
+ n < 1000 ? n.to_s : "#{(n / 1000.0).round(1)}k"
385
+ end
386
+
387
+ def init_runtime(caller_binding)
388
+ runtime = Claw::Runtime.new
389
+
390
+ context = Mana::Context.current
391
+ runtime.register("context", Claw::Resources::ContextResource.new(context))
392
+
393
+ memory = Claw.memory
394
+ runtime.register("memory", Claw::Resources::MemoryResource.new(memory)) if memory
395
+
396
+ runtime.register("binding", Claw::Resources::BindingResource.new(caller_binding))
397
+
398
+ claw_dir = File.join(Dir.pwd, ".ruby-claw")
399
+ if File.directory?(claw_dir)
400
+ runtime.register("filesystem", Claw::Resources::FilesystemResource.new(claw_dir))
401
+ end
402
+
403
+ runtime.snapshot!(label: "session_start")
404
+ runtime
405
+ rescue => e
406
+ $stderr.puts " ⚠ runtime init failed: #{e.message}" if Mana.config.verbose
407
+ nil
408
+ end
409
+ end
410
+ end
411
+ end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Claw
4
+ module TUI
5
+ # Object exploration commands (pry-style): /ls, /cd, /source, /doc, /find, /whereami.
6
+ module ObjectExplorer
7
+ # @param binding [Binding]
8
+ # @return [Hash] { type:, data: }
9
+ def self.ls(binding)
10
+ receiver = binding.eval("self")
11
+ sections = {}
12
+
13
+ # Local variables
14
+ locals = binding.local_variables.map do |sym|
15
+ val = binding.local_variable_get(sym)
16
+ " #{sym}: #{val.class} = #{safe_inspect(val)}"
17
+ end
18
+ sections["Local Variables"] = locals unless locals.empty?
19
+
20
+ # Instance variables
21
+ ivars = receiver.instance_variables.map do |ivar|
22
+ val = receiver.instance_variable_get(ivar)
23
+ " #{ivar}: #{val.class} = #{safe_inspect(val)}"
24
+ end
25
+ sections["Instance Variables"] = ivars unless ivars.empty?
26
+
27
+ # Public methods (own, not inherited from Object)
28
+ own_methods = (receiver.methods - Object.instance_methods).sort
29
+ sections["Methods"] = own_methods.map { |m| " #{m}" } unless own_methods.empty?
30
+
31
+ { type: :data, data: sections }
32
+ end
33
+
34
+ # Navigate into an object's context.
35
+ # Returns [new_binding, prompt_label] or error.
36
+ def self.cd(expression, binding, nav_stack)
37
+ if expression == ".."
38
+ if nav_stack.empty?
39
+ return { type: :error, message: "Already at top level" }
40
+ end
41
+ prev = nav_stack.pop
42
+ return { type: :success, data: { binding: prev[:binding], label: prev[:label] } }
43
+ end
44
+
45
+ begin
46
+ obj = binding.eval(expression)
47
+ nav_stack.push({ binding: binding, label: binding.eval("self").class.name })
48
+ # Create a new binding inside the object's context
49
+ new_binding = obj.instance_eval { binding }
50
+ label = "#{obj.class.name}(#{safe_inspect_short(obj)})"
51
+ { type: :success, data: { binding: new_binding, label: label } }
52
+ rescue => e
53
+ { type: :error, message: "#{e.class}: #{e.message}" }
54
+ end
55
+ end
56
+
57
+ # Show source code of a method.
58
+ def self.source(method_name, binding)
59
+ receiver = binding.eval("self")
60
+ meth = if receiver.respond_to?(method_name.to_sym)
61
+ receiver.method(method_name.to_sym)
62
+ elsif receiver.class.method_defined?(method_name.to_sym)
63
+ receiver.class.instance_method(method_name.to_sym)
64
+ else
65
+ return { type: :error, message: "Method '#{method_name}' not found" }
66
+ end
67
+
68
+ file, line = meth.source_location
69
+ unless file && File.exist?(file)
70
+ return { type: :error, message: "Source not available (native method or no source location)" }
71
+ end
72
+
73
+ lines = File.readlines(file)
74
+ # Show context: method line +/- 10
75
+ start = [line - 1, 0].max
76
+ finish = [line + 19, lines.size - 1].min
77
+ source_lines = lines[start..finish].each_with_index.map do |l, i|
78
+ num = start + i + 1
79
+ marker = num == line ? "→ " : " "
80
+ "#{marker}#{num.to_s.rjust(4)}: #{l.rstrip}"
81
+ end
82
+
83
+ { type: :data, data: { file: file, line: line, source: source_lines.join("\n") } }
84
+ end
85
+
86
+ # Query documentation via Mana Knowledge.
87
+ def self.doc(method_name, binding)
88
+ receiver = binding.eval("self")
89
+ klass = receiver.is_a?(Class) ? receiver.name : receiver.class.name
90
+ query = "#{klass}##{method_name}"
91
+
92
+ result = Claw::Knowledge.query(query)
93
+ { type: :data, data: { topic: query, doc: result } }
94
+ end
95
+
96
+ # Find methods matching a pattern.
97
+ def self.find(pattern, binding)
98
+ receiver = binding.eval("self")
99
+ regex = begin
100
+ Regexp.new(pattern, Regexp::IGNORECASE)
101
+ rescue RegexpError
102
+ return { type: :error, message: "Invalid pattern: #{pattern}" }
103
+ end
104
+ matches = receiver.methods.select { |m| m.to_s.match?(regex) }.sort
105
+
106
+ if matches.empty?
107
+ { type: :info, message: "No methods matching '#{pattern}'" }
108
+ else
109
+ { type: :data, data: { pattern: pattern, matches: matches.map(&:to_s) } }
110
+ end
111
+ end
112
+
113
+ # Show current source location.
114
+ def self.whereami(binding)
115
+ file = binding.eval("__FILE__") rescue "(unknown)"
116
+ line = binding.eval("__LINE__") rescue 0
117
+ receiver = binding.eval("self")
118
+ { type: :data, data: { file: file, line: line, receiver: receiver.class.name } }
119
+ end
120
+
121
+ # --- Helpers ---
122
+
123
+ def self.safe_inspect(val)
124
+ str = val.inspect
125
+ str.length > 60 ? "#{str[0, 57]}..." : str
126
+ end
127
+
128
+ def self.safe_inspect_short(val)
129
+ str = val.inspect
130
+ str.length > 20 ? "#{str[0, 17]}..." : str
131
+ end
132
+
133
+ private_class_method :safe_inspect, :safe_inspect_short
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Claw
4
+ module TUI
5
+ # Top status bar: version | model | snapshot id | token usage | mode
6
+ module StatusBar
7
+ def self.render(model, width)
8
+ parts = []
9
+ parts << "ruby-claw v#{Claw::VERSION}"
10
+ parts << Mana.config.model
11
+ parts << "snap: ##{model.last_snapshot_id}"
12
+ parts << "#{model.token_display}"
13
+ parts << "mode: #{model.mode}" if model.mode != :normal
14
+
15
+ state = model.runtime&.state
16
+ case state
17
+ when :thinking
18
+ parts << "#{model.spinner_view} thinking..."
19
+ when :executing_tool
20
+ step = model.runtime&.current_step
21
+ label = step ? "#{model.spinner_view} #{step.tool_name}" : "#{model.spinner_view} executing..."
22
+ parts << label
23
+ end
24
+
25
+ text = parts.join(" | ")
26
+ Styles::STATUS_BAR.width(width).render(text)
27
+ end
28
+ end
29
+ end
30
+ end