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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +94 -0
- data/README.md +214 -10
- data/exe/claw +42 -1
- data/lib/claw/auto_forge.rb +66 -0
- data/lib/claw/benchmark/benchmark.rb +79 -0
- data/lib/claw/benchmark/diff.rb +69 -0
- data/lib/claw/benchmark/report.rb +87 -0
- data/lib/claw/benchmark/runner.rb +91 -0
- data/lib/claw/benchmark/scorer.rb +69 -0
- data/lib/claw/benchmark/task.rb +63 -0
- data/lib/claw/benchmark/tasks/claw_remember.rb +20 -0
- data/lib/claw/benchmark/tasks/claw_session.rb +18 -0
- data/lib/claw/benchmark/tasks/evolution_trace.rb +18 -0
- data/lib/claw/benchmark/tasks/mana_call_func.rb +21 -0
- data/lib/claw/benchmark/tasks/mana_eval.rb +18 -0
- data/lib/claw/benchmark/tasks/mana_knowledge.rb +19 -0
- data/lib/claw/benchmark/tasks/mana_var_readwrite.rb +18 -0
- data/lib/claw/benchmark/tasks/runtime_fork.rb +18 -0
- data/lib/claw/benchmark/tasks/runtime_snapshot.rb +18 -0
- data/lib/claw/benchmark/trigger.rb +68 -0
- data/lib/claw/chat.rb +119 -6
- data/lib/claw/child_runtime.rb +196 -0
- data/lib/claw/cli.rb +177 -0
- data/lib/claw/commands.rb +131 -0
- data/lib/claw/config.rb +5 -1
- data/lib/claw/console/event_logger.rb +69 -0
- data/lib/claw/console/public/app.js +264 -0
- data/lib/claw/console/public/style.css +330 -0
- data/lib/claw/console/server.rb +253 -0
- data/lib/claw/console/sse.rb +28 -0
- data/lib/claw/console/views/experiments.erb +8 -0
- data/lib/claw/console/views/index.erb +27 -0
- data/lib/claw/console/views/layout.erb +29 -0
- data/lib/claw/console/views/memory.erb +13 -0
- data/lib/claw/console/views/monitor.erb +15 -0
- data/lib/claw/console/views/prompt.erb +15 -0
- data/lib/claw/console/views/snapshots.erb +12 -0
- data/lib/claw/console/views/tools.erb +13 -0
- data/lib/claw/console/views/traces.erb +9 -0
- data/lib/claw/console.rb +5 -0
- data/lib/claw/evolution.rb +227 -0
- data/lib/claw/forge.rb +144 -0
- data/lib/claw/hub.rb +67 -0
- data/lib/claw/init.rb +199 -0
- data/lib/claw/knowledge.rb +36 -2
- data/lib/claw/memory_store.rb +2 -2
- data/lib/claw/plan_mode.rb +110 -0
- data/lib/claw/resource.rb +35 -0
- data/lib/claw/resources/binding_resource.rb +128 -0
- data/lib/claw/resources/context_resource.rb +73 -0
- data/lib/claw/resources/filesystem_resource.rb +107 -0
- data/lib/claw/resources/memory_resource.rb +74 -0
- data/lib/claw/resources/worktree_resource.rb +133 -0
- data/lib/claw/roles.rb +56 -0
- data/lib/claw/runtime.rb +189 -0
- data/lib/claw/serializer.rb +10 -7
- data/lib/claw/tool.rb +99 -0
- data/lib/claw/tool_index.rb +84 -0
- data/lib/claw/tool_registry.rb +100 -0
- data/lib/claw/trace.rb +86 -0
- data/lib/claw/tui/agent_executor.rb +92 -0
- data/lib/claw/tui/chat_panel.rb +81 -0
- data/lib/claw/tui/command_bar.rb +22 -0
- data/lib/claw/tui/file_card.rb +88 -0
- data/lib/claw/tui/folding.rb +80 -0
- data/lib/claw/tui/input_handler.rb +73 -0
- data/lib/claw/tui/layout.rb +34 -0
- data/lib/claw/tui/messages.rb +31 -0
- data/lib/claw/tui/model.rb +411 -0
- data/lib/claw/tui/object_explorer.rb +136 -0
- data/lib/claw/tui/status_bar.rb +30 -0
- data/lib/claw/tui/status_panel.rb +133 -0
- data/lib/claw/tui/styles.rb +58 -0
- data/lib/claw/tui/tui.rb +54 -0
- data/lib/claw/version.rb +1 -1
- data/lib/claw.rb +99 -1
- metadata +223 -7
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "glamour"
|
|
4
|
+
|
|
5
|
+
module Claw
|
|
6
|
+
module TUI
|
|
7
|
+
# Left panel: chat history + input box.
|
|
8
|
+
# Uses Bubbles::Viewport for scrollable content and Glamour for markdown rendering.
|
|
9
|
+
module ChatPanel
|
|
10
|
+
def self.render(model, width, height)
|
|
11
|
+
# Reserve 3 lines for input box
|
|
12
|
+
chat_height = height - 3
|
|
13
|
+
|
|
14
|
+
# Render chat messages
|
|
15
|
+
content = render_messages(model.chat_history, width - 4)
|
|
16
|
+
|
|
17
|
+
# Set up viewport
|
|
18
|
+
viewport = model.chat_viewport
|
|
19
|
+
viewport.width = width - 4
|
|
20
|
+
viewport.height = chat_height
|
|
21
|
+
viewport.content = content
|
|
22
|
+
viewport.goto_bottom unless model.scrolled_up?
|
|
23
|
+
|
|
24
|
+
# Input box
|
|
25
|
+
input_line = render_input(model, width - 4)
|
|
26
|
+
|
|
27
|
+
# Compose with border
|
|
28
|
+
chat_view = viewport.view
|
|
29
|
+
panel = "#{chat_view}\n#{input_line}"
|
|
30
|
+
|
|
31
|
+
Styles::PANEL_BORDER.width(width).height(height).render(panel)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.render_messages(messages, width)
|
|
35
|
+
# Fold consecutive tool calls
|
|
36
|
+
messages = Folding.fold_tool_calls(messages)
|
|
37
|
+
|
|
38
|
+
lines = []
|
|
39
|
+
messages.each do |msg|
|
|
40
|
+
case msg[:role]
|
|
41
|
+
when :user
|
|
42
|
+
lines << Styles::USER_STYLE.render("you> #{msg[:content]}")
|
|
43
|
+
when :agent
|
|
44
|
+
rendered = begin
|
|
45
|
+
Glamour.render(msg[:content].to_s)
|
|
46
|
+
rescue
|
|
47
|
+
msg[:content].to_s
|
|
48
|
+
end
|
|
49
|
+
folded = Folding.fold_text(rendered.rstrip)
|
|
50
|
+
lines << Styles::AGENT_STYLE.render("claw> ") + folded[:display]
|
|
51
|
+
when :tool_call
|
|
52
|
+
lines << Styles::TOOL_STYLE.render(" #{msg[:icon] || "⚡"} #{msg[:detail]}")
|
|
53
|
+
when :tool_result
|
|
54
|
+
lines << Styles::RESULT_STYLE.render(" ↩ #{truncate(msg[:result].to_s, width - 6)}")
|
|
55
|
+
when :ruby
|
|
56
|
+
highlighted = InputHandler.highlight(msg[:content].to_s)
|
|
57
|
+
lines << Styles::RUBY_STYLE.render("=> #{highlighted}")
|
|
58
|
+
when :error
|
|
59
|
+
lines << Styles::ERROR_STYLE.render("error: #{msg[:content]}")
|
|
60
|
+
when :system
|
|
61
|
+
lines << Styles::TOOL_STYLE.render(" #{msg[:content]}")
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
lines.join("\n")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def self.render_input(model, width)
|
|
68
|
+
prompt = model.mode == :plan ? "plan> " : "claw> "
|
|
69
|
+
cursor = model.input_focused? ? "█" : ""
|
|
70
|
+
text = "#{prompt}#{model.input_text}#{cursor}"
|
|
71
|
+
Lipgloss::Style.new.foreground(Styles::CYAN).width(width).render(text)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def self.truncate(str, max)
|
|
75
|
+
str.length > max ? "#{str[0, max - 3]}..." : str
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private_class_method :render_messages, :render_input, :truncate
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Claw
|
|
4
|
+
module TUI
|
|
5
|
+
# Bottom bar: slash command hints + keyboard shortcuts.
|
|
6
|
+
module CommandBar
|
|
7
|
+
HINTS = %w[/snapshot /rollback /diff /history /status /evolve /plan /role].freeze
|
|
8
|
+
|
|
9
|
+
def self.render(model, width)
|
|
10
|
+
left = HINTS.join(" ")
|
|
11
|
+
right = "ctrl+c quit"
|
|
12
|
+
left_w, _ = Lipgloss.size(left)
|
|
13
|
+
right_w, _ = Lipgloss.size(right)
|
|
14
|
+
spacing = width - left_w - right_w - 2
|
|
15
|
+
spacing = 1 if spacing < 1
|
|
16
|
+
|
|
17
|
+
text = "#{left}#{" " * spacing}#{right}"
|
|
18
|
+
Styles::COMMAND_BAR.width(width).render(text)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Claw
|
|
4
|
+
module TUI
|
|
5
|
+
# Detect @filename references in user input and render file cards.
|
|
6
|
+
module FileCard
|
|
7
|
+
# Pattern to match @filename references (supports glob patterns).
|
|
8
|
+
FILE_REF_PATTERN = /@([\w.*\/\-]+(?:\.\w+)?)/
|
|
9
|
+
|
|
10
|
+
# Extract file references from input text.
|
|
11
|
+
#
|
|
12
|
+
# @param input [String] user input
|
|
13
|
+
# @return [Array<String>] list of file reference patterns
|
|
14
|
+
def self.extract_refs(input)
|
|
15
|
+
input.scan(FILE_REF_PATTERN).flatten
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Resolve a file reference to actual file paths.
|
|
19
|
+
# Supports glob patterns like @*.rb.
|
|
20
|
+
#
|
|
21
|
+
# @param ref [String] file reference (e.g., "user.rb" or "*.rb")
|
|
22
|
+
# @return [Array<String>] resolved file paths
|
|
23
|
+
def self.resolve(ref)
|
|
24
|
+
if ref.include?("*")
|
|
25
|
+
Dir.glob(ref)
|
|
26
|
+
elsif File.exist?(ref)
|
|
27
|
+
[ref]
|
|
28
|
+
else
|
|
29
|
+
[]
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Render a compact file card for display.
|
|
34
|
+
#
|
|
35
|
+
# @param path [String] file path
|
|
36
|
+
# @return [String] rendered card
|
|
37
|
+
def self.render_card(path)
|
|
38
|
+
return " (file not found: #{path})" unless File.exist?(path)
|
|
39
|
+
|
|
40
|
+
stat = File.stat(path)
|
|
41
|
+
ext = File.extname(path).delete(".")
|
|
42
|
+
lines = File.readlines(path).size rescue 0
|
|
43
|
+
size = format_size(stat.size)
|
|
44
|
+
lang = language_for(ext)
|
|
45
|
+
|
|
46
|
+
card = Lipgloss::Style.new
|
|
47
|
+
.border(:rounded)
|
|
48
|
+
.border_foreground(Styles::DIM_GRAY)
|
|
49
|
+
.padding(0, 1)
|
|
50
|
+
.render("#{path} | #{lines} lines | #{lang} | #{size}")
|
|
51
|
+
|
|
52
|
+
card
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Read file content for injection into LLM context.
|
|
56
|
+
#
|
|
57
|
+
# @param path [String] file path
|
|
58
|
+
# @return [String] file content (truncated if large)
|
|
59
|
+
def self.read_for_context(path)
|
|
60
|
+
return "" unless File.exist?(path)
|
|
61
|
+
|
|
62
|
+
content = File.read(path, 50_001) || ""
|
|
63
|
+
if content.length > 50_000
|
|
64
|
+
content = content[0, 50_000] + "\n... (truncated)"
|
|
65
|
+
end
|
|
66
|
+
"# File: #{path}\n```\n#{content}\n```"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# --- Helpers ---
|
|
70
|
+
|
|
71
|
+
def self.format_size(bytes)
|
|
72
|
+
return "#{bytes}B" if bytes < 1024
|
|
73
|
+
return "#{(bytes / 1024.0).round(1)}KB" if bytes < 1024 * 1024
|
|
74
|
+
"#{(bytes / (1024.0 * 1024)).round(1)}MB"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def self.language_for(ext)
|
|
78
|
+
{ "rb" => "Ruby", "py" => "Python", "js" => "JavaScript", "ts" => "TypeScript",
|
|
79
|
+
"rs" => "Rust", "go" => "Go", "java" => "Java", "c" => "C", "cpp" => "C++",
|
|
80
|
+
"md" => "Markdown", "json" => "JSON", "yml" => "YAML", "yaml" => "YAML",
|
|
81
|
+
"sh" => "Shell", "sql" => "SQL", "html" => "HTML", "css" => "CSS"
|
|
82
|
+
}.fetch(ext, ext.upcase)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private_class_method :format_size, :language_for
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Claw
|
|
4
|
+
module TUI
|
|
5
|
+
# Content folding for Claude Code-style UX:
|
|
6
|
+
# - Large text blocks collapsed
|
|
7
|
+
# - Consecutive same-type tool calls collapsed
|
|
8
|
+
# - Colored diff rendering
|
|
9
|
+
module Folding
|
|
10
|
+
# Fold text that exceeds a line threshold.
|
|
11
|
+
#
|
|
12
|
+
# @param text [String] the text to potentially fold
|
|
13
|
+
# @param threshold [Integer] max lines before folding (default 10)
|
|
14
|
+
# @return [Hash] { folded: bool, display: String, full: String }
|
|
15
|
+
def self.fold_text(text, threshold: 10)
|
|
16
|
+
lines = text.lines
|
|
17
|
+
if lines.size <= threshold
|
|
18
|
+
{ folded: false, display: text, full: text }
|
|
19
|
+
else
|
|
20
|
+
preview = lines[0, 3].join
|
|
21
|
+
summary = "[+#{lines.size - 3} more lines · Ctrl+E to expand]"
|
|
22
|
+
display = "#{preview}#{Styles::TOOL_STYLE.render(summary)}"
|
|
23
|
+
{ folded: true, display: display, full: text }
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Fold consecutive same-type tool calls into a summary.
|
|
28
|
+
#
|
|
29
|
+
# @param calls [Array<Hash>] tool call messages ({ role: :tool_call, ... })
|
|
30
|
+
# @return [Array<Hash>] folded messages (may be shorter)
|
|
31
|
+
def self.fold_tool_calls(calls)
|
|
32
|
+
return calls if calls.size <= 2
|
|
33
|
+
|
|
34
|
+
groups = calls.chunk { |c| c[:detail]&.split("(")&.first || c[:detail] }
|
|
35
|
+
groups.flat_map do |tool_name, group|
|
|
36
|
+
if group.size > 2
|
|
37
|
+
targets = group.map { |c| c[:detail].to_s.split("(").last&.tr(")", "") || "" }
|
|
38
|
+
summary = "#{group.size}x #{tool_name} (#{targets.first(3).join(', ')}#{targets.size > 3 ? ', ...' : ''})"
|
|
39
|
+
[{ role: :tool_call, icon: "⚡", detail: summary,
|
|
40
|
+
folded: true, children: group }]
|
|
41
|
+
else
|
|
42
|
+
group
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Render a diff hash with colors (green for additions, red for removals).
|
|
48
|
+
#
|
|
49
|
+
# @param diff_text [String] unified diff text
|
|
50
|
+
# @return [String] colored diff
|
|
51
|
+
def self.render_diff(diff_text)
|
|
52
|
+
diff_text.lines.map do |line|
|
|
53
|
+
case line[0]
|
|
54
|
+
when "+"
|
|
55
|
+
Lipgloss::Style.new.foreground("#32CD32").render(line.rstrip)
|
|
56
|
+
when "-"
|
|
57
|
+
Lipgloss::Style.new.foreground("#FF4444").render(line.rstrip)
|
|
58
|
+
when "~"
|
|
59
|
+
Lipgloss::Style.new.foreground("#FFD700").render(line.rstrip)
|
|
60
|
+
else
|
|
61
|
+
line.rstrip
|
|
62
|
+
end
|
|
63
|
+
end.join("\n")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Render a resource diff hash from Runtime#diff.
|
|
67
|
+
#
|
|
68
|
+
# @param diffs [Hash] { resource_name => diff_string }
|
|
69
|
+
# @return [String] colored output
|
|
70
|
+
def self.render_resource_diff(diffs)
|
|
71
|
+
sections = diffs.map do |name, diff_text|
|
|
72
|
+
header = Lipgloss::Style.new.bold(true).render("#{name}:")
|
|
73
|
+
colored = render_diff(diff_text)
|
|
74
|
+
"#{header}\n#{colored}"
|
|
75
|
+
end
|
|
76
|
+
sections.join("\n\n")
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Claw
|
|
4
|
+
module TUI
|
|
5
|
+
# Enhanced input handling: syntax highlighting, tab completion, auto-indent.
|
|
6
|
+
module InputHandler
|
|
7
|
+
# Syntax highlight Ruby code using basic token patterns.
|
|
8
|
+
# Returns ANSI-colored string.
|
|
9
|
+
def self.highlight(code)
|
|
10
|
+
code
|
|
11
|
+
.gsub(/\b(def|end|class|module|if|else|elsif|unless|do|while|until|for|begin|rescue|ensure|return|yield|raise|require|require_relative|include|extend|attr_\w+)\b/) { "\e[35m#{$&}\e[0m" }
|
|
12
|
+
.gsub(/\b(true|false|nil|self)\b/) { "\e[36m#{$&}\e[0m" }
|
|
13
|
+
.gsub(/(#.*)$/) { "\e[2m#{$1}\e[0m" }
|
|
14
|
+
.gsub(/(:[\w!?]+)/) { "\e[33m#{$1}\e[0m" }
|
|
15
|
+
.gsub(/"([^"]*)"/) { "\e[32m\"#{$1}\"\e[0m" }
|
|
16
|
+
.gsub(/'([^']*)'/) { "\e[32m'#{$1}'\e[0m" }
|
|
17
|
+
.gsub(/\b(\d+\.?\d*)\b/) { "\e[34m#{$1}\e[0m" }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Generate tab completion candidates from binding, memory, and commands.
|
|
21
|
+
#
|
|
22
|
+
# @param prefix [String] current input prefix
|
|
23
|
+
# @param binding [Binding] caller's binding
|
|
24
|
+
# @param memory [Claw::Memory, nil] memory for fact keywords
|
|
25
|
+
# @return [Array<String>] completion candidates
|
|
26
|
+
def self.completions(prefix, binding:, memory: nil)
|
|
27
|
+
candidates = []
|
|
28
|
+
|
|
29
|
+
# Local variables
|
|
30
|
+
candidates.concat(binding.local_variables.map(&:to_s))
|
|
31
|
+
|
|
32
|
+
# Receiver methods (filtered)
|
|
33
|
+
receiver = binding.eval("self")
|
|
34
|
+
candidates.concat(
|
|
35
|
+
receiver.methods.map(&:to_s).reject { |m| m.start_with?("_") || m.include?("!") && m.length < 3 }
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# Slash commands
|
|
39
|
+
candidates.concat(Claw::Commands::COMMANDS.map { |c| "/#{c}" })
|
|
40
|
+
candidates.concat(%w[/plan /role /ls /cd /source /doc /find /whereami /shell /memory /forget])
|
|
41
|
+
|
|
42
|
+
# Memory keywords
|
|
43
|
+
if memory
|
|
44
|
+
memory.long_term.each do |m|
|
|
45
|
+
words = m[:content].to_s.split(/\s+/).select { |w| w.length > 3 }
|
|
46
|
+
candidates.concat(words)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
candidates.uniq.select { |c| c.start_with?(prefix) }.sort
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Check if code has unclosed blocks (for multi-line continuation).
|
|
54
|
+
def self.incomplete?(code)
|
|
55
|
+
RubyVM::InstructionSequence.compile(code)
|
|
56
|
+
false
|
|
57
|
+
rescue SyntaxError => e
|
|
58
|
+
e.message.include?("unexpected end-of-input") ||
|
|
59
|
+
e.message.include?("unterminated")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Calculate auto-indent level based on code structure.
|
|
63
|
+
#
|
|
64
|
+
# @param code [String] current multi-line buffer
|
|
65
|
+
# @return [Integer] number of spaces to indent
|
|
66
|
+
def self.indent_level(code)
|
|
67
|
+
opens = code.scan(/\b(def|class|module|if|unless|while|until|for|do|begin|case)\b/).size
|
|
68
|
+
closes = code.scan(/\bend\b/).size
|
|
69
|
+
[(opens - closes) * 2, 0].max
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Claw
|
|
4
|
+
module TUI
|
|
5
|
+
# Composes the 4-zone TUI layout: top status bar, left chat, right status, bottom commands.
|
|
6
|
+
module Layout
|
|
7
|
+
CHAT_RATIO = 0.65
|
|
8
|
+
|
|
9
|
+
def self.render(model, width, height)
|
|
10
|
+
# Top status bar: 1 line
|
|
11
|
+
top = StatusBar.render(model, width)
|
|
12
|
+
_, top_h = Lipgloss.size(top)
|
|
13
|
+
|
|
14
|
+
# Bottom command bar: 1 line
|
|
15
|
+
bottom = CommandBar.render(model, width)
|
|
16
|
+
_, bottom_h = Lipgloss.size(bottom)
|
|
17
|
+
|
|
18
|
+
# Middle area
|
|
19
|
+
middle_h = height - top_h - bottom_h
|
|
20
|
+
middle_h = 6 if middle_h < 6
|
|
21
|
+
|
|
22
|
+
left_w = (width * CHAT_RATIO).to_i
|
|
23
|
+
right_w = width - left_w
|
|
24
|
+
|
|
25
|
+
left = ChatPanel.render(model, left_w, middle_h)
|
|
26
|
+
right = StatusPanel.render(model, right_w, middle_h)
|
|
27
|
+
|
|
28
|
+
middle = Lipgloss.join_horizontal(Lipgloss::TOP, left, right)
|
|
29
|
+
|
|
30
|
+
Lipgloss.join_vertical(Lipgloss::LEFT, top, middle, bottom)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Claw
|
|
4
|
+
module TUI
|
|
5
|
+
# MVU message types sent between components and the model.
|
|
6
|
+
|
|
7
|
+
# Agent emitted a text chunk (streaming).
|
|
8
|
+
AgentTextMsg = Struct.new(:text, keyword_init: true)
|
|
9
|
+
|
|
10
|
+
# Agent started a tool call.
|
|
11
|
+
ToolCallMsg = Struct.new(:name, :input, keyword_init: true)
|
|
12
|
+
|
|
13
|
+
# Agent finished a tool call.
|
|
14
|
+
ToolResultMsg = Struct.new(:name, :result, keyword_init: true)
|
|
15
|
+
|
|
16
|
+
# Agent execution completed.
|
|
17
|
+
ExecutionDoneMsg = Struct.new(:result, :trace, keyword_init: true)
|
|
18
|
+
|
|
19
|
+
# Agent execution failed.
|
|
20
|
+
ExecutionErrorMsg = Struct.new(:error, keyword_init: true)
|
|
21
|
+
|
|
22
|
+
# Runtime state changed (idle/thinking/executing_tool/failed).
|
|
23
|
+
StateChangeMsg = Struct.new(:old_state, :new_state, :step, keyword_init: true)
|
|
24
|
+
|
|
25
|
+
# Tick for spinner/progress animations.
|
|
26
|
+
TickMsg = Struct.new(:time, keyword_init: true)
|
|
27
|
+
|
|
28
|
+
# Command result from a slash command.
|
|
29
|
+
CommandResultMsg = Struct.new(:result, :cmd, keyword_init: true)
|
|
30
|
+
end
|
|
31
|
+
end
|