openclacky 0.5.6 → 0.6.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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +43 -0
  3. data/docs/ui2-architecture.md +124 -0
  4. data/lib/clacky/agent.rb +245 -340
  5. data/lib/clacky/agent_config.rb +1 -7
  6. data/lib/clacky/cli.rb +156 -397
  7. data/lib/clacky/client.rb +68 -36
  8. data/lib/clacky/gitignore_parser.rb +26 -12
  9. data/lib/clacky/model_pricing.rb +6 -2
  10. data/lib/clacky/session_manager.rb +6 -2
  11. data/lib/clacky/tools/glob.rb +65 -9
  12. data/lib/clacky/tools/grep.rb +4 -120
  13. data/lib/clacky/tools/run_project.rb +5 -0
  14. data/lib/clacky/tools/safe_shell.rb +49 -13
  15. data/lib/clacky/tools/shell.rb +1 -49
  16. data/lib/clacky/tools/web_fetch.rb +2 -2
  17. data/lib/clacky/tools/web_search.rb +38 -26
  18. data/lib/clacky/ui2/README.md +214 -0
  19. data/lib/clacky/ui2/components/base_component.rb +163 -0
  20. data/lib/clacky/ui2/components/common_component.rb +89 -0
  21. data/lib/clacky/ui2/components/inline_input.rb +187 -0
  22. data/lib/clacky/ui2/components/input_area.rb +1029 -0
  23. data/lib/clacky/ui2/components/message_component.rb +76 -0
  24. data/lib/clacky/ui2/components/output_area.rb +112 -0
  25. data/lib/clacky/ui2/components/todo_area.rb +137 -0
  26. data/lib/clacky/ui2/components/tool_component.rb +106 -0
  27. data/lib/clacky/ui2/components/welcome_banner.rb +93 -0
  28. data/lib/clacky/ui2/layout_manager.rb +331 -0
  29. data/lib/clacky/ui2/line_editor.rb +201 -0
  30. data/lib/clacky/ui2/screen_buffer.rb +238 -0
  31. data/lib/clacky/ui2/theme_manager.rb +68 -0
  32. data/lib/clacky/ui2/themes/base_theme.rb +99 -0
  33. data/lib/clacky/ui2/themes/hacker_theme.rb +56 -0
  34. data/lib/clacky/ui2/themes/minimal_theme.rb +50 -0
  35. data/lib/clacky/ui2/ui_controller.rb +720 -0
  36. data/lib/clacky/ui2/view_renderer.rb +160 -0
  37. data/lib/clacky/ui2.rb +37 -0
  38. data/lib/clacky/utils/file_ignore_helper.rb +126 -0
  39. data/lib/clacky/version.rb +1 -1
  40. data/lib/clacky.rb +1 -6
  41. metadata +38 -6
  42. data/lib/clacky/ui/banner.rb +0 -155
  43. data/lib/clacky/ui/enhanced_prompt.rb +0 -786
  44. data/lib/clacky/ui/formatter.rb +0 -209
  45. data/lib/clacky/ui/statusbar.rb +0 -96
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "components/message_component"
4
+ require_relative "components/tool_component"
5
+ require_relative "components/common_component"
6
+
7
+ module Clacky
8
+ module UI2
9
+ # ViewRenderer coordinates all UI components and provides a unified rendering interface
10
+ class ViewRenderer
11
+ def initialize
12
+ @message_component = Components::MessageComponent.new
13
+ @tool_component = Components::ToolComponent.new
14
+ @common_component = Components::CommonComponent.new
15
+ end
16
+
17
+ # Render a user message
18
+ # @param content [String] Message content
19
+ # @param timestamp [Time, nil] Optional timestamp
20
+ # @return [String] Rendered message
21
+ def render_user_message(content, timestamp: nil)
22
+ @message_component.render(
23
+ role: "user",
24
+ content: content,
25
+ timestamp: timestamp
26
+ )
27
+ end
28
+
29
+ # Render an assistant message
30
+ # @param content [String] Message content
31
+ # @param timestamp [Time, nil] Optional timestamp
32
+ # @return [String] Rendered message
33
+ def render_assistant_message(content, timestamp: nil)
34
+ @message_component.render(
35
+ role: "assistant",
36
+ content: content,
37
+ timestamp: timestamp
38
+ )
39
+ end
40
+
41
+ # Render a system message
42
+ # @param content [String] Message content
43
+ # @param timestamp [Time, nil] Optional timestamp
44
+ # @return [String] Rendered message
45
+ def render_system_message(content, timestamp: nil)
46
+ @message_component.render(
47
+ role: "system",
48
+ content: content,
49
+ timestamp: timestamp
50
+ )
51
+ end
52
+
53
+ # Render a tool call
54
+ # @param tool_name [String] Tool name
55
+ # @param formatted_call [String] Formatted call description
56
+ # @return [String] Rendered tool call
57
+ def render_tool_call(tool_name:, formatted_call:)
58
+ @tool_component.render(
59
+ type: :call,
60
+ tool_name: tool_name,
61
+ formatted_call: formatted_call
62
+ )
63
+ end
64
+
65
+ # Render a tool result
66
+ # @param result [String] Tool result
67
+ # @return [String] Rendered tool result
68
+ def render_tool_result(result:)
69
+ @tool_component.render(
70
+ type: :result,
71
+ result: result
72
+ )
73
+ end
74
+
75
+ # Render a tool error
76
+ # @param error [String] Error message
77
+ # @return [String] Rendered tool error
78
+ def render_tool_error(error:)
79
+ @tool_component.render(
80
+ type: :error,
81
+ error: error
82
+ )
83
+ end
84
+
85
+ # Render a tool denied message
86
+ # @param tool_name [String] Tool name
87
+ # @return [String] Rendered tool denied
88
+ def render_tool_denied(tool_name:)
89
+ @tool_component.render(
90
+ type: :denied,
91
+ tool_name: tool_name
92
+ )
93
+ end
94
+
95
+ # Render a tool planned message
96
+ # @param tool_name [String] Tool name
97
+ # @return [String] Rendered tool planned
98
+ def render_tool_planned(tool_name:)
99
+ @tool_component.render(
100
+ type: :planned,
101
+ tool_name: tool_name
102
+ )
103
+ end
104
+
105
+ # Render thinking indicator
106
+ # @return [String] Thinking indicator
107
+ def render_thinking
108
+ @common_component.render_thinking
109
+ end
110
+
111
+ # Render progress message
112
+ # @param message [String] Progress message
113
+ # @return [String] Progress indicator
114
+ def render_progress(message)
115
+ @common_component.render_progress(message)
116
+ end
117
+
118
+ # Render success message
119
+ # @param message [String] Success message
120
+ # @return [String] Success message
121
+ def render_success(message)
122
+ @common_component.render_success(message)
123
+ end
124
+
125
+ # Render error message
126
+ # @param message [String] Error message
127
+ # @return [String] Error message
128
+ def render_error(message)
129
+ @common_component.render_error(message)
130
+ end
131
+
132
+ # Render warning message
133
+ # @param message [String] Warning message
134
+ # @return [String] Warning message
135
+ def render_warning(message)
136
+ @common_component.render_warning(message)
137
+ end
138
+
139
+ # Render task completion summary
140
+ # @param iterations [Integer] Number of iterations
141
+ # @param cost [Float] Cost in USD
142
+ # @param duration [Float] Duration in seconds
143
+ # @param cache_tokens [Integer] Cache read tokens
144
+ # @param cache_requests [Integer] Total cache requests count
145
+ # @param cache_hits [Integer] Cache hit requests count
146
+ # @return [String] Formatted completion summary
147
+ def render_task_complete(iterations:, cost:, duration: nil, cache_tokens: nil, cache_requests: nil, cache_hits: nil)
148
+ @common_component.render_task_complete(
149
+ iterations: iterations,
150
+ cost: cost,
151
+ duration: duration,
152
+ cache_tokens: cache_tokens,
153
+ cache_requests: cache_requests,
154
+ cache_hits: cache_hits
155
+ )
156
+ end
157
+
158
+ end
159
+ end
160
+ end
data/lib/clacky/ui2.rb ADDED
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ # UI2 - MVC-based terminal UI system for Clacky
4
+ # Provides split-screen interface with scrollable output and fixed input
5
+
6
+ require_relative "ui2/theme_manager"
7
+ require_relative "ui2/screen_buffer"
8
+ require_relative "ui2/layout_manager"
9
+ require_relative "ui2/view_renderer"
10
+ require_relative "ui2/ui_controller"
11
+
12
+ require_relative "ui2/components/base_component"
13
+ require_relative "ui2/components/output_area"
14
+ require_relative "ui2/components/input_area"
15
+ require_relative "ui2/components/message_component"
16
+ require_relative "ui2/components/tool_component"
17
+ require_relative "ui2/components/common_component"
18
+ require_relative "ui2/components/welcome_banner"
19
+
20
+ module Clacky
21
+ module UI2
22
+ # Version of the UI2 system
23
+ VERSION = "1.0.0"
24
+
25
+ # Quick start: Create a UI controller and run
26
+ # @param config [Hash] Optional configuration (working_dir, mode, model)
27
+ # @example
28
+ # controller = Clacky::UI2::UIController.new
29
+ # controller.on_input { |input| puts "Got: #{input}" }
30
+ # controller.start
31
+ def self.start(config = {}, &block)
32
+ controller = UIController.new(config)
33
+ controller.on_input(&block) if block_given?
34
+ controller.start
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clacky
4
+ module Utils
5
+ # Helper module for file ignoring functionality shared between tools
6
+ module FileIgnoreHelper
7
+ # Default patterns to ignore when .gitignore is not available
8
+ DEFAULT_IGNORED_PATTERNS = [
9
+ 'node_modules',
10
+ 'vendor/bundle',
11
+ '.git',
12
+ '.svn',
13
+ 'tmp',
14
+ 'log',
15
+ 'coverage',
16
+ 'dist',
17
+ 'build',
18
+ '.bundle',
19
+ '.sass-cache',
20
+ '.DS_Store',
21
+ '*.log'
22
+ ].freeze
23
+
24
+ # Config file patterns that should always be searchable/visible
25
+ CONFIG_FILE_PATTERNS = [
26
+ /\.env/,
27
+ /\.ya?ml$/,
28
+ /\.json$/,
29
+ /\.toml$/,
30
+ /\.ini$/,
31
+ /\.conf$/,
32
+ /\.config$/,
33
+ /config\//,
34
+ /\.config\//
35
+ ].freeze
36
+
37
+ # Find .gitignore file in the search path or parent directories
38
+ # Only searches within the search path and up to the current working directory
39
+ def self.find_gitignore(path)
40
+ search_path = File.directory?(path) ? path : File.dirname(path)
41
+
42
+ # Look for .gitignore in current and parent directories
43
+ current = File.expand_path(search_path)
44
+ cwd = File.expand_path(Dir.pwd)
45
+ root = File.expand_path('/')
46
+
47
+ # Limit search: only go up to current working directory
48
+ # This prevents finding .gitignore files from unrelated parent directories
49
+ # when searching in temporary directories (like /tmp in tests)
50
+ search_limit = if current.start_with?(cwd)
51
+ cwd
52
+ else
53
+ current
54
+ end
55
+
56
+ loop do
57
+ gitignore = File.join(current, '.gitignore')
58
+ return gitignore if File.exist?(gitignore)
59
+
60
+ # Stop if we've reached the search limit or root
61
+ break if current == search_limit || current == root
62
+ current = File.dirname(current)
63
+ end
64
+
65
+ nil
66
+ end
67
+
68
+ # Check if file should be ignored based on .gitignore or default patterns
69
+ def self.should_ignore_file?(file, base_path, gitignore)
70
+ # Always calculate path relative to base_path for consistency
71
+ # Expand both paths to handle symlinks and relative paths correctly
72
+ expanded_file = File.expand_path(file)
73
+ expanded_base = File.expand_path(base_path)
74
+
75
+ # For files, use the directory as base
76
+ expanded_base = File.dirname(expanded_base) if File.file?(expanded_base)
77
+
78
+ # Calculate relative path
79
+ if expanded_file.start_with?(expanded_base)
80
+ relative_path = expanded_file[(expanded_base.length + 1)..-1] || File.basename(expanded_file)
81
+ else
82
+ # File is outside base path - use just the filename
83
+ relative_path = File.basename(expanded_file)
84
+ end
85
+
86
+ # Clean up relative path
87
+ relative_path = relative_path.sub(/^\.\//, '') if relative_path
88
+
89
+ if gitignore
90
+ # Use .gitignore rules
91
+ gitignore.ignored?(relative_path)
92
+ else
93
+ # Use default ignore patterns - only match against relative path components
94
+ DEFAULT_IGNORED_PATTERNS.any? do |pattern|
95
+ if pattern.include?('*')
96
+ File.fnmatch(pattern, relative_path, File::FNM_PATHNAME | File::FNM_DOTMATCH)
97
+ else
98
+ # Match pattern as a path component (not substring of absolute path)
99
+ relative_path.start_with?("#{pattern}/") ||
100
+ relative_path.include?("/#{pattern}/") ||
101
+ relative_path == pattern ||
102
+ File.basename(relative_path) == pattern
103
+ end
104
+ end
105
+ end
106
+ end
107
+
108
+ # Check if file is a config file (should not be ignored even if in .gitignore)
109
+ def self.is_config_file?(file)
110
+ CONFIG_FILE_PATTERNS.any? { |pattern| file.match?(pattern) }
111
+ end
112
+
113
+ # Check if file is binary (contains null bytes)
114
+ def self.binary_file?(file)
115
+ # Simple heuristic: check if file contains null bytes in first 8KB
116
+ return false unless File.exist?(file)
117
+ return false if File.size(file).zero?
118
+
119
+ sample = File.read(file, 8192, encoding: "ASCII-8BIT")
120
+ sample.include?("\x00")
121
+ rescue StandardError
122
+ true
123
+ end
124
+ end
125
+ end
126
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Clacky
4
- VERSION = "0.5.6"
4
+ VERSION = "0.6.0"
5
5
  end
data/lib/clacky.rb CHANGED
@@ -15,6 +15,7 @@ require_relative "clacky/session_manager"
15
15
  require_relative "clacky/gitignore_parser"
16
16
  require_relative "clacky/utils/limit_stack"
17
17
  require_relative "clacky/utils/path_helper"
18
+ require_relative "clacky/utils/file_ignore_helper"
18
19
  require_relative "clacky/tools/base"
19
20
 
20
21
  require_relative "clacky/tools/shell"
@@ -31,12 +32,6 @@ require_relative "clacky/tools/safe_shell"
31
32
  require_relative "clacky/tools/trash_manager"
32
33
  require_relative "clacky/agent"
33
34
 
34
- # UI components
35
- require_relative "clacky/ui/banner"
36
- require_relative "clacky/ui/enhanced_prompt"
37
- require_relative "clacky/ui/statusbar"
38
- require_relative "clacky/ui/formatter"
39
-
40
35
  require_relative "clacky/cli"
41
36
 
42
37
  module Clacky
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: openclacky
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.6
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - windy
@@ -107,6 +107,20 @@ dependencies:
107
107
  - - "~>"
108
108
  - !ruby/object:Gem::Version
109
109
  version: '0.8'
110
+ - !ruby/object:Gem::Dependency
111
+ name: base64
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: 0.3.0
117
+ type: :runtime
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: 0.3.0
110
124
  description: OpenClacky is a Ruby CLI tool for interacting with AI models via OpenAI-compatible
111
125
  APIs. It provides chat functionality and autonomous AI agent capabilities with tool
112
126
  use.
@@ -130,6 +144,7 @@ files:
130
144
  - bin/openclacky
131
145
  - clacky-legacy/clacky.gemspec
132
146
  - clacky-legacy/clarky.gemspec
147
+ - docs/ui2-architecture.md
133
148
  - lib/clacky.rb
134
149
  - lib/clacky/agent.rb
135
150
  - lib/clacky/agent_config.rb
@@ -157,11 +172,28 @@ files:
157
172
  - lib/clacky/tools/web_search.rb
158
173
  - lib/clacky/tools/write.rb
159
174
  - lib/clacky/trash_directory.rb
160
- - lib/clacky/ui/banner.rb
161
- - lib/clacky/ui/enhanced_prompt.rb
162
- - lib/clacky/ui/formatter.rb
163
- - lib/clacky/ui/statusbar.rb
175
+ - lib/clacky/ui2.rb
176
+ - lib/clacky/ui2/README.md
177
+ - lib/clacky/ui2/components/base_component.rb
178
+ - lib/clacky/ui2/components/common_component.rb
179
+ - lib/clacky/ui2/components/inline_input.rb
180
+ - lib/clacky/ui2/components/input_area.rb
181
+ - lib/clacky/ui2/components/message_component.rb
182
+ - lib/clacky/ui2/components/output_area.rb
183
+ - lib/clacky/ui2/components/todo_area.rb
184
+ - lib/clacky/ui2/components/tool_component.rb
185
+ - lib/clacky/ui2/components/welcome_banner.rb
186
+ - lib/clacky/ui2/layout_manager.rb
187
+ - lib/clacky/ui2/line_editor.rb
188
+ - lib/clacky/ui2/screen_buffer.rb
189
+ - lib/clacky/ui2/theme_manager.rb
190
+ - lib/clacky/ui2/themes/base_theme.rb
191
+ - lib/clacky/ui2/themes/hacker_theme.rb
192
+ - lib/clacky/ui2/themes/minimal_theme.rb
193
+ - lib/clacky/ui2/ui_controller.rb
194
+ - lib/clacky/ui2/view_renderer.rb
164
195
  - lib/clacky/utils/arguments_parser.rb
196
+ - lib/clacky/utils/file_ignore_helper.rb
165
197
  - lib/clacky/utils/limit_stack.rb
166
198
  - lib/clacky/utils/path_helper.rb
167
199
  - lib/clacky/version.rb
@@ -187,7 +219,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
187
219
  - !ruby/object:Gem::Version
188
220
  version: '0'
189
221
  requirements: []
190
- rubygems_version: 3.6.9
222
+ rubygems_version: 4.0.4
191
223
  specification_version: 4
192
224
  summary: A command-line interface for AI models (Claude, OpenAI, etc.)
193
225
  test_files: []
@@ -1,155 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "pastel"
4
-
5
- module Clacky
6
- module UI
7
- # ASCII art banner and startup screen with Matrix/hacker aesthetic
8
- class Banner
9
- LOGO = <<~'LOGO'
10
- ██████╗ ██████╗ ███████╗███╗ ██╗ ██████╗██╗ █████╗ ██████╗██╗ ██╗██╗ ██╗
11
- ██╔═══██╗██╔══██╗██╔════╝████╗ ██║██╔════╝██║ ██╔══██╗██╔════╝██║ ██╔╝╚██╗ ██╔╝
12
- ██║ ██║██████╔╝█████╗ ██╔██╗ ██║██║ ██║ ███████║██║ █████╔╝ ╚████╔╝
13
- ██║ ██║██╔═══╝ ██╔══╝ ██║╚██╗██║██║ ██║ ██╔══██║██║ ██╔═██╗ ╚██╔╝
14
- ╚██████╔╝██║ ███████╗██║ ╚████║╚██████╗███████╗██║ ██║╚██████╗██║ ██╗ ██║
15
- ╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝ ╚═════╝╚══════╝╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ ╚═╝
16
- LOGO
17
-
18
- TAGLINE = "[>] AI Coding Assistant & Technical Co-founder"
19
-
20
- TIPS = [
21
- "[*] Ask questions, edit files, or run commands",
22
- "[*] Be specific for the best results",
23
- "[*] Create .clackyrules to customize interactions",
24
- "[*] Type /help for more commands"
25
- ]
26
-
27
- def initialize
28
- @pastel = Pastel.new
29
- end
30
-
31
- # Display full startup banner
32
- def display_startup
33
- puts
34
- puts @pastel.bright_green(LOGO)
35
- puts
36
- puts @pastel.bright_cyan(TAGLINE)
37
- puts
38
- display_tips
39
- puts
40
- end
41
-
42
- # Display welcome message for agent mode
43
- def display_agent_welcome(working_dir:, mode:, max_iterations:, max_cost:)
44
- puts
45
- puts separator("=")
46
- puts @pastel.bright_green("[+] AGENT MODE INITIALIZED")
47
- puts separator("=")
48
- puts
49
- puts info_line("Working Directory", working_dir)
50
- puts info_line("Permission Mode", mode)
51
- puts info_line("Max Iterations", max_iterations)
52
- puts info_line("Max Cost", "$#{max_cost}")
53
- puts
54
- puts @pastel.dim("[!] Type 'exit' or 'quit' to terminate session")
55
- puts separator("-")
56
- puts
57
- end
58
-
59
- # Display session continuation info
60
- def display_session_continue(session_id:, created_at:, tasks:, cost:)
61
- puts
62
- puts separator("=")
63
- puts @pastel.bright_yellow("[~] RESUMING SESSION")
64
- puts separator("=")
65
- puts
66
- puts info_line("Session ID", session_id)
67
- puts info_line("Started", created_at)
68
- puts info_line("Tasks Completed", tasks)
69
- puts info_line("Total Cost", "$#{cost}")
70
- puts separator("-")
71
- puts
72
- end
73
-
74
- # Display task completion summary
75
- def display_task_complete(iterations:, cost:, total_tasks:, total_cost:, cost_source:, cache_stats: {})
76
- puts
77
- puts separator("-")
78
- puts @pastel.bright_green("[✓] TASK COMPLETED")
79
- puts info_line("Iterations", iterations)
80
-
81
- # Add cost source indicator
82
- cost_suffix = case cost_source
83
- when :api
84
- " (by API)"
85
- when :price
86
- " (by PRICE)"
87
- when :default
88
- " (by DEFAULT)"
89
- else
90
- " (estimated)"
91
- end
92
- puts info_line("Cost", "$#{cost}#{cost_suffix}")
93
- puts info_line("Session Total", "#{total_tasks} tasks, $#{total_cost}")
94
-
95
- # Display cache statistics if available
96
- if cache_stats[:total_requests] && cache_stats[:total_requests] > 0
97
- puts
98
- puts @pastel.cyan(" [Prompt Caching]")
99
- puts info_line(" Cache Reads/Writes", "#{cache_stats[:cache_read_input_tokens]}/#{cache_stats[:cache_creation_input_tokens]} tokens")
100
-
101
- hit_rate = (cache_stats[:cache_hit_requests].to_f / cache_stats[:total_requests] * 100).round(1)
102
- puts info_line(" Cache Hit Rate", "#{hit_rate}% (#{cache_stats[:cache_hit_requests]}/#{cache_stats[:total_requests]} requests)")
103
- end
104
-
105
- puts separator("-")
106
- puts
107
- end
108
-
109
- # Display error message
110
- def display_error(message, details: nil)
111
- puts
112
- puts separator("-")
113
- puts @pastel.bright_red("[✗] ERROR")
114
- puts @pastel.red(" #{message}")
115
- if details
116
- puts @pastel.dim(" #{details}")
117
- end
118
- puts separator("-")
119
- puts
120
- end
121
-
122
- # Display session end
123
- def display_goodbye(total_tasks:, total_cost:)
124
- puts
125
- puts separator("=")
126
- puts @pastel.bright_cyan("[#] SESSION TERMINATED")
127
- puts separator("=")
128
- puts
129
- puts info_line("Total Tasks", total_tasks)
130
- puts info_line("Total Cost", "$#{total_cost}")
131
- puts
132
- puts @pastel.dim("[*] All session data has been saved")
133
- puts
134
- end
135
-
136
- private
137
-
138
- def display_tips
139
- TIPS.each do |tip|
140
- puts @pastel.dim(tip)
141
- end
142
- end
143
-
144
- def info_line(label, value)
145
- label_text = @pastel.cyan("[#{label}]")
146
- value_text = @pastel.white(value)
147
- " #{label_text} #{value_text}"
148
- end
149
-
150
- def separator(char = "-")
151
- @pastel.dim(char * 80)
152
- end
153
- end
154
- end
155
- end