openclacky 0.7.2 → 0.7.4

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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/.clacky/skills/commit/SKILL.md +252 -74
  3. data/CHANGELOG.md +38 -0
  4. data/bin/openclacky +2 -0
  5. data/lib/clacky/agent/message_compressor_helper.rb +1 -1
  6. data/lib/clacky/agent/system_prompt_builder.rb +9 -8
  7. data/lib/clacky/agent/tool_executor.rb +4 -13
  8. data/lib/clacky/agent.rb +28 -7
  9. data/lib/clacky/cli.rb +22 -5
  10. data/lib/clacky/default_skills/new/SKILL.md +61 -30
  11. data/lib/clacky/default_skills/new/scripts/create_rails_project.sh +176 -0
  12. data/lib/clacky/default_skills/new/scripts/rails_env_checker.sh +389 -0
  13. data/lib/clacky/default_skills/skill-add/SKILL.md +251 -34
  14. data/lib/clacky/default_skills/skill-add/scripts/install_from_github.rb +189 -0
  15. data/lib/clacky/providers.rb +13 -11
  16. data/lib/clacky/tools/invoke_skill.rb +5 -1
  17. data/lib/clacky/tools/safe_shell.rb +2 -2
  18. data/lib/clacky/tools/shell.rb +48 -20
  19. data/lib/clacky/ui2/components/input_area.rb +27 -9
  20. data/lib/clacky/ui2/components/modal_component.rb +22 -2
  21. data/lib/clacky/ui2/components/welcome_banner.rb +33 -10
  22. data/lib/clacky/ui2/layout_manager.rb +107 -26
  23. data/lib/clacky/ui2/line_editor.rb +6 -5
  24. data/lib/clacky/ui2/screen_buffer.rb +0 -15
  25. data/lib/clacky/ui2/terminal_detector.rb +119 -0
  26. data/lib/clacky/ui2/theme_manager.rb +18 -0
  27. data/lib/clacky/ui2/themes/base_theme.rb +22 -1
  28. data/lib/clacky/ui2/themes/hacker_theme.rb +22 -18
  29. data/lib/clacky/ui2/themes/minimal_theme.rb +19 -15
  30. data/lib/clacky/ui2/ui_controller.rb +66 -15
  31. data/lib/clacky/ui2.rb +1 -0
  32. data/lib/clacky/utils/limit_stack.rb +5 -0
  33. data/lib/clacky/version.rb +1 -1
  34. metadata +5 -1
@@ -0,0 +1,189 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'fileutils'
5
+ require 'tmpdir'
6
+ require 'uri'
7
+ require 'find'
8
+
9
+ # Install skills from a GitHub repository
10
+ # Usage: ruby install_from_github.rb <github_url>
11
+ class SkillInstaller
12
+ GITHUB_URL_PATTERNS = [
13
+ %r{^https?://github\.com/[\w-]+/[\w.-]+(?:\.git)?$},
14
+ %r{^git@github\.com:[\w-]+/[\w.-]+\.git$}
15
+ ].freeze
16
+
17
+ def initialize(repo_url, target_dir: nil)
18
+ @repo_url = repo_url
19
+ @target_dir = target_dir || File.join(Dir.pwd, '.clacky', 'skills')
20
+ @installed_skills = []
21
+ @errors = []
22
+ end
23
+
24
+ # Main installation process
25
+ def install
26
+ unless valid_github_url?
27
+ raise ArgumentError, "Invalid GitHub URL: #{@repo_url}"
28
+ end
29
+
30
+ Dir.mktmpdir('clacky-skills-') do |tmpdir|
31
+ clone_repository(tmpdir)
32
+ discover_and_install_skills(tmpdir)
33
+ end
34
+
35
+ report_results
36
+ rescue StandardError => e
37
+ puts "❌ Installation failed: #{e.message}"
38
+ exit 1
39
+ end
40
+
41
+ # Validate GitHub URL format
42
+ private def valid_github_url?
43
+ GITHUB_URL_PATTERNS.any? { |pattern| @repo_url.match?(pattern) }
44
+ end
45
+
46
+ # Clone the repository to temporary directory
47
+ private def clone_repository(tmpdir)
48
+ puts "📦 Cloning repository..."
49
+ puts " #{@repo_url}"
50
+
51
+ clone_path = File.join(tmpdir, 'repo')
52
+
53
+ # Use git clone with depth=1 for faster cloning
54
+ system('git', 'clone', '--depth', '1', @repo_url, clone_path,
55
+ out: File::NULL, err: File::NULL)
56
+
57
+ unless $?.success?
58
+ raise "Failed to clone repository. Please check the URL and your network connection."
59
+ end
60
+
61
+ @clone_path = clone_path
62
+ end
63
+
64
+ # Discover all skills directories and install them
65
+ private def discover_and_install_skills(tmpdir)
66
+ skills_found = false
67
+
68
+ # Search for any directory named 'skills' containing SKILL.md files
69
+ Find.find(@clone_path) do |path|
70
+ next unless File.directory?(path)
71
+ next unless File.basename(path) == 'skills'
72
+
73
+ # Check if this skills directory contains subdirectories with SKILL.md
74
+ skill_dirs = Dir.glob(File.join(path, '*/SKILL.md')).map { |f| File.dirname(f) }
75
+
76
+ next if skill_dirs.empty?
77
+
78
+ skills_found = true
79
+ install_skills_from_directory(path, skill_dirs)
80
+ end
81
+
82
+ unless skills_found
83
+ raise "No skills found in repository. Looking for directories named 'skills/' containing SKILL.md files."
84
+ end
85
+ end
86
+
87
+ # Install skills from a specific skills directory
88
+ private def install_skills_from_directory(skills_dir, skill_dirs)
89
+ skill_dirs.each do |skill_dir|
90
+ skill_name = File.basename(skill_dir)
91
+ target_path = File.join(@target_dir, skill_name)
92
+
93
+ begin
94
+ # Create target directory
95
+ FileUtils.mkdir_p(@target_dir)
96
+
97
+ # Check if skill already exists
98
+ if File.exist?(target_path)
99
+ puts "⚠️ Skill '#{skill_name}' already exists, skipping..."
100
+ @errors << "Skill '#{skill_name}' already exists at #{target_path}"
101
+ next
102
+ end
103
+
104
+ # Copy skill directory
105
+ FileUtils.cp_r(skill_dir, target_path)
106
+
107
+ # Read skill description from SKILL.md
108
+ description = extract_description(File.join(target_path, 'SKILL.md'))
109
+
110
+ @installed_skills << {
111
+ name: skill_name,
112
+ path: target_path,
113
+ description: description
114
+ }
115
+
116
+ rescue StandardError => e
117
+ @errors << "Failed to install '#{skill_name}': #{e.message}"
118
+ end
119
+ end
120
+ end
121
+
122
+ # Extract description from SKILL.md frontmatter
123
+ private def extract_description(skill_file)
124
+ return "No description" unless File.exist?(skill_file)
125
+
126
+ content = File.read(skill_file)
127
+
128
+ # Parse YAML frontmatter
129
+ if content =~ /\A---\s*\n(.*?)\n---/m
130
+ frontmatter = $1
131
+ if frontmatter =~ /^description:\s*(.+)$/
132
+ return $1.strip
133
+ end
134
+ end
135
+
136
+ "No description"
137
+ rescue StandardError
138
+ "No description"
139
+ end
140
+
141
+ # Report installation results
142
+ private def report_results
143
+ puts "\n" + "=" * 60
144
+
145
+ if @installed_skills.empty?
146
+ puts "❌ No skills were installed."
147
+
148
+ if @errors.any?
149
+ puts "\nErrors encountered:"
150
+ @errors.each { |err| puts " • #{err}" }
151
+ end
152
+
153
+ exit 1
154
+ end
155
+
156
+ puts "✅ Installation complete!"
157
+ puts "\nInstalled #{@installed_skills.size} skill(s):\n\n"
158
+
159
+ @installed_skills.each do |skill|
160
+ puts " ✓ #{skill[:name]}"
161
+ puts " #{skill[:description]}"
162
+ puts " → #{skill[:path]}"
163
+ puts
164
+ end
165
+
166
+ if @errors.any?
167
+ puts "⚠️ Warnings:"
168
+ @errors.each { |err| puts " • #{err}" }
169
+ puts
170
+ end
171
+
172
+ puts "You can now use these skills with /skill-name"
173
+ puts "=" * 60
174
+ end
175
+ end
176
+
177
+ # Run installer if called directly
178
+ if __FILE__ == $0
179
+ if ARGV.empty?
180
+ puts "Usage: ruby install_from_github.rb <github_url>"
181
+ puts "\nExamples:"
182
+ puts " ruby install_from_github.rb https://github.com/username/repo"
183
+ puts " ruby install_from_github.rb https://github.com/username/repo.git"
184
+ exit 1
185
+ end
186
+
187
+ installer = SkillInstaller.new(ARGV[0])
188
+ installer.install
189
+ end
@@ -11,28 +11,21 @@ module Clacky
11
11
  # - api: API type (anthropic-messages, openai-responses, openai-completions)
12
12
  # - default_model: Recommended default model
13
13
  PRESETS = {
14
- "anthropic" => {
15
- "name" => "Anthropic (Claude)",
16
- "base_url" => "https://api.anthropic.com",
17
- "api" => "anthropic-messages",
18
- "default_model" => "claude-sonnet-4-6",
19
- "models" => ["claude-opus-4-6", "claude-sonnet-4-6", "claude-haiku-4"]
20
- }.freeze,
21
14
 
22
15
  "openrouter" => {
23
16
  "name" => "OpenRouter",
24
17
  "base_url" => "https://openrouter.ai/api/v1",
25
18
  "api" => "openai-responses",
26
- "default_model" => "anthropic/claude-sonnet-4-5",
19
+ "default_model" => "anthropic/claude-sonnet-4-6",
27
20
  "models" => [] # Dynamic - fetched from API
28
21
  }.freeze,
29
22
 
30
23
  "minimax" => {
31
24
  "name" => "Minimax",
32
- "base_url" => "https://api.minimax.chat/v1",
25
+ "base_url" => "https://api.minimaxi.com/v1",
33
26
  "api" => "openai-completions",
34
- "default_model" => "MiniMax-Text-01",
35
- "models" => ["MiniMax-Text-01", "MiniMax-M2"]
27
+ "default_model" => "MiniMax-M2.5",
28
+ "models" => ["MiniMax-M2.1", "MiniMax-M2.5"]
36
29
  }.freeze,
37
30
 
38
31
  "kimi" => {
@@ -41,7 +34,16 @@ module Clacky
41
34
  "api" => "openai-completions",
42
35
  "default_model" => "kimi-k2.5",
43
36
  "models" => ["kimi-k2.5"]
37
+ }.freeze,
38
+
39
+ "anthropic" => {
40
+ "name" => "Anthropic (Claude)",
41
+ "base_url" => "https://api.anthropic.com",
42
+ "api" => "anthropic-messages",
43
+ "default_model" => "claude-sonnet-4.6",
44
+ "models" => ["claude-opus-4-6", "claude-sonnet-4.6", "claude-haiku-4.5"]
44
45
  }.freeze
46
+
45
47
  }.freeze
46
48
 
47
49
  class << self
@@ -55,9 +55,13 @@ module Clacky
55
55
  else
56
56
  # Expand skill content inline
57
57
  expanded = skill.process_content(task)
58
+
59
+ # Add skill directory path information for script execution
60
+ skill_dir_info = "\n\n---\n**Skill Directory:** `#{skill.directory}`\n\nWhen executing scripts from Supporting Files, use the full path:\n`#{skill.directory}/scripts/script_name`\n---\n"
61
+
58
62
  {
59
63
  message: "Skill '#{skill_name}' content expanded",
60
- content: expanded,
64
+ content: expanded + skill_dir_info,
61
65
  skill_type: "inline",
62
66
  note: "The expanded content has been added to the conversation. Continue following its instructions."
63
67
  }
@@ -32,7 +32,7 @@ module Clacky
32
32
  required: ["command"]
33
33
  }
34
34
 
35
- def execute(command:, timeout: nil, max_output_lines: 1000, skip_safety_check: false)
35
+ def execute(command:, timeout: nil, max_output_lines: 1000, skip_safety_check: false, output_buffer: nil)
36
36
  # Get project root directory
37
37
  project_root = Dir.pwd
38
38
 
@@ -58,7 +58,7 @@ module Clacky
58
58
  hard_timeout = calculate_hard_timeout(command, timeout)
59
59
 
60
60
  # 4. Call parent class execution method
61
- result = super(command: safe_command, soft_timeout: soft_timeout, hard_timeout: hard_timeout, max_output_lines: max_output_lines)
61
+ result = super(command: safe_command, soft_timeout: soft_timeout, hard_timeout: hard_timeout, max_output_lines: max_output_lines, output_buffer: output_buffer)
62
62
 
63
63
  # 5. Enhance result information
64
64
  enhance_result(result, command, safe_command, safety_replacer)
@@ -57,7 +57,7 @@ module Clacky
57
57
  'go build'
58
58
  ].freeze
59
59
 
60
- def execute(command:, soft_timeout: nil, hard_timeout: nil, max_output_lines: 1000)
60
+ def execute(command:, soft_timeout: nil, hard_timeout: nil, max_output_lines: 1000, output_buffer: nil)
61
61
  require "open3"
62
62
  require "stringio"
63
63
 
@@ -67,6 +67,15 @@ module Clacky
67
67
  stderr_buffer = StringIO.new
68
68
  soft_timeout_triggered = false
69
69
  process_pid = nil
70
+
71
+ # Store output buffer reference for real-time access (use LimitStack for memory efficiency)
72
+ @output_buffer = output_buffer
73
+ if @output_buffer
74
+ @output_buffer[:stdout_lines] = Utils::LimitStack.new(max_size: 1000)
75
+ @output_buffer[:stderr_lines] = Utils::LimitStack.new(max_size: 200)
76
+ end
77
+ @stdout_buffer = stdout_buffer
78
+ @stderr_buffer = stderr_buffer
70
79
 
71
80
  begin
72
81
  Open3.popen3(command) do |stdin, stdout, stderr, wait_thr|
@@ -126,6 +135,8 @@ module Clacky
126
135
  else
127
136
  stderr_buffer.write(data)
128
137
  end
138
+ # Update shared output buffer for real-time access
139
+ update_output_buffer
129
140
  rescue IO::WaitReadable, EOFError
130
141
  end
131
142
  end
@@ -306,7 +317,7 @@ module Clacky
306
317
 
307
318
  # Format result for LLM consumption - limit output size to save tokens
308
319
  # Maximum characters to include in LLM output
309
- MAX_LLM_OUTPUT_CHARS = 1000
320
+ MAX_LLM_OUTPUT_CHARS = 4000
310
321
 
311
322
  def format_result_for_llm(result)
312
323
  # Return error info as-is if command failed or timed out
@@ -367,45 +378,62 @@ module Clacky
367
378
  # Write full output to temp file
368
379
  File.write(temp_file, output)
369
380
 
370
- # For LLM display: show first and last lines to preserve structure
381
+ # For LLM display: show first N lines to preserve most useful information
371
382
  lines = output.lines
372
383
  return { content: output, temp_file: nil } if lines.length <= 2
373
384
 
374
- # Reserve 60 chars for truncation notice
375
- available_chars = max_chars - 60
385
+ # Reserve space for truncation notice (including temp file path)
386
+ notice_overhead = 200
387
+ available_chars = max_chars - notice_overhead
376
388
 
377
- # Take first few lines
389
+ # Prioritize first lines as they usually contain the most important information
378
390
  first_part = []
379
391
  accumulated = 0
380
392
  lines.each do |line|
381
- break if accumulated + line.length > available_chars / 2
393
+ break if accumulated + line.length > available_chars
382
394
  first_part << line
383
395
  accumulated += line.length
384
396
  end
385
397
 
386
- # Take last few lines
387
- last_part = []
388
- accumulated = 0
389
- lines.reverse_each do |line|
390
- break if accumulated + line.length > available_chars / 2
391
- last_part.unshift(line)
392
- accumulated += line.length
393
- end
394
-
395
398
  total_lines = lines.length
399
+ shown_lines = first_part.length
396
400
 
397
- # Create notice message based on label
401
+ # Create prominent notice message with temp file path
398
402
  if label == "stderr"
399
- notice = "... [Error output truncated for LLM: showing #{first_part.length} of #{total_lines} lines, full content: #{temp_file} (use grep to search)] ..."
403
+ notice = <<~NOTICE
404
+
405
+ ... [Error output truncated for LLM: showing #{shown_lines} of #{total_lines} lines, full content: #{temp_file} (use grep to search)] ...
406
+ NOTICE
400
407
  else
401
- notice = "... [Output truncated for LLM: showing #{first_part.length} of #{total_lines} lines, full content: #{temp_file} (use grep to search)] ..."
408
+ notice = <<~NOTICE
409
+
410
+ ... [Output truncated for LLM: showing #{shown_lines} of #{total_lines} lines, full content: #{temp_file} (use grep to search)] ...
411
+ NOTICE
402
412
  end
403
413
 
404
414
  # Combine with compact notice
405
- content = first_part.join + "\n#{notice}\n" + last_part.join
415
+ content = first_part.join + notice
406
416
 
407
417
  { content: content, temp_file: temp_file }
408
418
  end
419
+
420
+ # Update shared output buffer for real-time access
421
+ # Uses LimitStack to automatically manage memory and keep only recent output
422
+ private def update_output_buffer
423
+ return unless @output_buffer
424
+
425
+ # Push new lines to LimitStack (automatically handles size limit)
426
+ stdout_lines = @stdout_buffer.string.lines
427
+ stderr_lines = @stderr_buffer.string.lines
428
+
429
+ @output_buffer[:stdout_lines].clear
430
+ @output_buffer[:stdout_lines].push_lines(stdout_lines)
431
+
432
+ @output_buffer[:stderr_lines].clear
433
+ @output_buffer[:stderr_lines].push_lines(stderr_lines)
434
+
435
+ @output_buffer[:timestamp] = Time.now
436
+ end
409
437
  end
410
438
  end
411
439
  end
@@ -1058,15 +1058,33 @@ module Clacky
1058
1058
  end
1059
1059
 
1060
1060
  # Render a segment of a line with cursor if cursor is in this segment
1061
+ # Applies theme colors to the text
1061
1062
  # @param line [String] Full line text
1062
1063
  # @param segment_start [Integer] Start position of segment in line (char index)
1063
1064
  # @param segment_end [Integer] End position of segment in line (char index)
1064
- # @return [String] Rendered segment with cursor if applicable
1065
+ # @return [String] Rendered segment with cursor and theme colors applied
1065
1066
  def render_line_segment_with_cursor(line, segment_start, segment_end)
1066
- # Delegate to LineEditor's shared implementation
1067
- rendered = super(line, segment_start, segment_end)
1068
- # Apply theme colors for InputArea
1069
- theme.format_text(rendered, :user)
1067
+ chars = line.chars
1068
+ segment_chars = chars[segment_start...segment_end]
1069
+
1070
+ # Check if cursor is in this segment
1071
+ if @cursor_position >= segment_start && @cursor_position < segment_end
1072
+ # Cursor is in this segment
1073
+ cursor_pos_in_segment = @cursor_position - segment_start
1074
+ before_cursor = segment_chars[0...cursor_pos_in_segment].join
1075
+ cursor_char = segment_chars[cursor_pos_in_segment] || " "
1076
+ after_cursor = segment_chars[(cursor_pos_in_segment + 1)..-1]&.join || ""
1077
+
1078
+ # Apply theme color to text parts, keep cursor highlight as is
1079
+ "#{theme.format_text(before_cursor, :user)}#{@pastel.on_white(@pastel.black(cursor_char))}#{theme.format_text(after_cursor, :user)}"
1080
+ elsif @cursor_position == segment_end && segment_end == line.length
1081
+ # Cursor is at the very end of the line, show it in last segment
1082
+ segment_text = segment_chars.join
1083
+ "#{theme.format_text(segment_text, :user)}#{@pastel.on_white(@pastel.black(' '))}"
1084
+ else
1085
+ # Cursor is not in this segment, apply theme color
1086
+ theme.format_text(segment_chars.join, :user)
1087
+ end
1070
1088
  end
1071
1089
 
1072
1090
  # Render a separator line (ensures it doesn't exceed screen width)
@@ -1122,7 +1140,7 @@ module Clacky
1122
1140
  # Working directory (shortened if too long)
1123
1141
  if @sessionbar_info[:working_dir]
1124
1142
  dir_display = shorten_path(@sessionbar_info[:working_dir])
1125
- parts << @pastel.dim(@pastel.cyan(dir_display))
1143
+ parts << theme.format_text(dir_display, :statusbar_path)
1126
1144
  end
1127
1145
 
1128
1146
  # Permission mode
@@ -1133,15 +1151,15 @@ module Clacky
1133
1151
 
1134
1152
  # Model
1135
1153
  if @sessionbar_info[:model]
1136
- parts << @pastel.dim(@pastel.white(@sessionbar_info[:model]))
1154
+ parts << theme.format_text(@sessionbar_info[:model], :statusbar_secondary)
1137
1155
  end
1138
1156
 
1139
1157
  # Tasks count
1140
- parts << @pastel.dim(@pastel.white("#{@sessionbar_info[:tasks]} tasks"))
1158
+ parts << theme.format_text("#{@sessionbar_info[:tasks]} tasks", :statusbar_secondary)
1141
1159
 
1142
1160
  # Cost
1143
1161
  cost_display = format("$%.1f", @sessionbar_info[:cost])
1144
- parts << @pastel.dim(@pastel.white(cost_display))
1162
+ parts << theme.format_text(cost_display, :statusbar_secondary)
1145
1163
 
1146
1164
  " " + parts.join(separator)
1147
1165
  end
@@ -30,8 +30,9 @@ module Clacky
30
30
  # Example: [{ name: "Option 1", value: :opt1 }, { name: "---", disabled: true }]
31
31
  # @param validator [Proc, nil] Optional validation callback that receives values hash
32
32
  # Should return { success: true } or { success: false, error: "message" }
33
+ # @param on_close [Proc, nil] Optional callback to execute when modal closes (e.g., to re-render screen)
33
34
  # @return [Hash, nil] Hash of field values or selected value, or nil if cancelled
34
- def show(title:, fields: nil, choices: nil, validator: nil)
35
+ def show(title:, fields: nil, choices: nil, validator: nil, on_close: nil)
35
36
  @title = title
36
37
  @mode = choices ? :menu : :form
37
38
  @fields = fields || []
@@ -45,10 +46,16 @@ module Clacky
45
46
  @selected_index = @choices.index { |c| !c[:disabled] } || 0
46
47
  end
47
48
 
48
- # Adjust height for menu mode
49
+ # Adjust height based on mode
49
50
  if @mode == :menu
50
51
  visible_items = [@choices.length, 15].min
51
52
  @height = visible_items + 4 # +4 for title, borders, and instructions
53
+ else
54
+ # Form mode - adjust height based on number of fields
55
+ # Each field takes 2 rows (label + input)
56
+ # +3 for title and top border
57
+ # +5 for error message area, buttons, and bottom border
58
+ @height = (@fields.length * 2) + 3 + 5
52
59
  end
53
60
 
54
61
  # Get terminal size
@@ -69,6 +76,8 @@ module Clacky
69
76
  ensure
70
77
  # Clear modal area
71
78
  clear_modal(start_row, start_col)
79
+ # Call on_close callback if provided (e.g., to re-render screen)
80
+ on_close&.call
72
81
  end
73
82
  end
74
83
 
@@ -167,6 +176,17 @@ module Clacky
167
176
  # Clear testing messages
168
177
  print "\e[#{testing_row};#{testing_col}H\e[K"
169
178
  print "\e[#{testing_row + 1};#{testing_col}H\e[K"
179
+
180
+ if validation_result[:success]
181
+ # Validation passed - hide cursor and return values
182
+ print "\e[?25l"
183
+ return @values
184
+ else
185
+ # Validation failed - show error and loop again to let user correct input
186
+ @error_message = validation_result[:error] || "Validation failed"
187
+ # Don't clear modal - just loop again to redraw with error message
188
+ # This prevents the modal from flickering
189
+ end
170
190
  else
171
191
  # No validator - return immediately
172
192
  print "\e[?25l"
@@ -26,26 +26,48 @@ module Clacky
26
26
  "[*] Type /help for more commands"
27
27
  ].freeze
28
28
 
29
+ # Minimum terminal width required for full logo display
30
+ MIN_WIDTH_FOR_LOGO = 90
31
+
29
32
  def initialize
30
33
  @pastel = Pastel.new
31
34
  end
32
35
 
33
- # Render only the logo (ASCII art)
36
+ # Get current theme from ThemeManager
37
+ def theme
38
+ UI2::ThemeManager.current_theme
39
+ end
40
+
41
+ # Render only the logo (ASCII art or simple text based on terminal width)
42
+ # @param width [Integer] Terminal width
34
43
  # @return [String] Formatted logo only
35
- def render_logo
44
+ def render_logo(width:)
36
45
  lines = []
37
46
  lines << ""
38
- lines << @pastel.bright_green(LOGO)
47
+
48
+ if width >= MIN_WIDTH_FOR_LOGO
49
+ lines << @pastel.bright_green(LOGO)
50
+ else
51
+ lines << @pastel.bright_green("Welcome, OpenClacky is here")
52
+ end
53
+
39
54
  lines << ""
40
55
  lines.join("\n")
41
56
  end
42
57
 
43
58
  # Render startup banner
59
+ # @param width [Integer] Terminal width
44
60
  # @return [String] Formatted startup banner
45
- def render_startup
61
+ def render_startup(width:)
46
62
  lines = []
47
63
  lines << ""
48
- lines << @pastel.bright_green(LOGO)
64
+
65
+ if width >= MIN_WIDTH_FOR_LOGO
66
+ lines << @pastel.bright_green(LOGO)
67
+ else
68
+ lines << @pastel.bright_green("Welcome, OpenClacky is here")
69
+ end
70
+
49
71
  lines << ""
50
72
  lines << @pastel.bright_cyan(TAGLINE)
51
73
  lines << @pastel.dim(" Version #{Clacky::VERSION}")
@@ -71,7 +93,7 @@ module Clacky
71
93
  lines << info_line("Working Directory", working_dir)
72
94
  lines << info_line("Permission Mode", mode)
73
95
  lines << ""
74
- lines << @pastel.dim("[!] Type 'exit' or 'quit' to terminate session")
96
+ lines << theme.format_text("[!] Type 'exit' or 'quit' to terminate session", :thinking)
75
97
  lines << separator("-")
76
98
  lines << ""
77
99
  lines.join("\n")
@@ -80,9 +102,10 @@ module Clacky
80
102
  # Render full welcome (startup + agent info)
81
103
  # @param working_dir [String] Working directory
82
104
  # @param mode [String] Permission mode
105
+ # @param width [Integer] Terminal width
83
106
  # @return [String] Full welcome content
84
- def render_full(working_dir:, mode:)
85
- render_startup + render_agent_welcome(
107
+ def render_full(working_dir:, mode:, width:)
108
+ render_startup(width: width) + render_agent_welcome(
86
109
  working_dir: working_dir,
87
110
  mode: mode
88
111
  )
@@ -92,12 +115,12 @@ module Clacky
92
115
 
93
116
  def info_line(label, value)
94
117
  label_text = @pastel.cyan("[#{label}]")
95
- value_text = @pastel.white(value)
118
+ value_text = theme.format_text(value, :info)
96
119
  " #{label_text} #{value_text}"
97
120
  end
98
121
 
99
122
  def separator(char = "-")
100
- @pastel.dim(char * 80)
123
+ theme.format_text(char * 80, :thinking) # Use :thinking for subtle separator
101
124
  end
102
125
  end
103
126
  end