openclacky 0.5.6 → 0.6.1

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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +71 -0
  3. data/docs/ui2-architecture.md +124 -0
  4. data/lib/clacky/agent.rb +376 -346
  5. data/lib/clacky/agent_config.rb +1 -7
  6. data/lib/clacky/cli.rb +167 -398
  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 +66 -10
  12. data/lib/clacky/tools/grep.rb +6 -122
  13. data/lib/clacky/tools/run_project.rb +10 -5
  14. data/lib/clacky/tools/safe_shell.rb +149 -20
  15. data/lib/clacky/tools/shell.rb +3 -51
  16. data/lib/clacky/tools/todo_manager.rb +50 -3
  17. data/lib/clacky/tools/trash_manager.rb +1 -1
  18. data/lib/clacky/tools/web_fetch.rb +4 -4
  19. data/lib/clacky/tools/web_search.rb +40 -28
  20. data/lib/clacky/ui2/README.md +214 -0
  21. data/lib/clacky/ui2/components/base_component.rb +163 -0
  22. data/lib/clacky/ui2/components/common_component.rb +98 -0
  23. data/lib/clacky/ui2/components/inline_input.rb +187 -0
  24. data/lib/clacky/ui2/components/input_area.rb +1124 -0
  25. data/lib/clacky/ui2/components/message_component.rb +80 -0
  26. data/lib/clacky/ui2/components/output_area.rb +112 -0
  27. data/lib/clacky/ui2/components/todo_area.rb +130 -0
  28. data/lib/clacky/ui2/components/tool_component.rb +106 -0
  29. data/lib/clacky/ui2/components/welcome_banner.rb +103 -0
  30. data/lib/clacky/ui2/layout_manager.rb +437 -0
  31. data/lib/clacky/ui2/line_editor.rb +201 -0
  32. data/lib/clacky/ui2/markdown_renderer.rb +80 -0
  33. data/lib/clacky/ui2/screen_buffer.rb +257 -0
  34. data/lib/clacky/ui2/theme_manager.rb +68 -0
  35. data/lib/clacky/ui2/themes/base_theme.rb +85 -0
  36. data/lib/clacky/ui2/themes/hacker_theme.rb +58 -0
  37. data/lib/clacky/ui2/themes/minimal_theme.rb +52 -0
  38. data/lib/clacky/ui2/ui_controller.rb +778 -0
  39. data/lib/clacky/ui2/view_renderer.rb +177 -0
  40. data/lib/clacky/ui2.rb +37 -0
  41. data/lib/clacky/utils/file_ignore_helper.rb +126 -0
  42. data/lib/clacky/version.rb +1 -1
  43. data/lib/clacky.rb +1 -6
  44. metadata +53 -6
  45. data/lib/clacky/ui/banner.rb +0 -155
  46. data/lib/clacky/ui/enhanced_prompt.rb +0 -786
  47. data/lib/clacky/ui/formatter.rb +0 -209
  48. data/lib/clacky/ui/statusbar.rb +0 -96
@@ -0,0 +1,177 @@
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
+ require_relative "markdown_renderer"
7
+
8
+ module Clacky
9
+ module UI2
10
+ # ViewRenderer coordinates all UI components and provides a unified rendering interface
11
+ class ViewRenderer
12
+ def initialize
13
+ @message_component = Components::MessageComponent.new
14
+ @tool_component = Components::ToolComponent.new
15
+ @common_component = Components::CommonComponent.new
16
+ end
17
+
18
+ # Render a user message
19
+ # @param content [String] Message content
20
+ # @param timestamp [Time, nil] Optional timestamp
21
+ # @return [String] Rendered message
22
+ def render_user_message(content, timestamp: nil)
23
+ @message_component.render(
24
+ role: "user",
25
+ content: content,
26
+ timestamp: timestamp
27
+ )
28
+ end
29
+
30
+ # Render an assistant message
31
+ # @param content [String] Message content
32
+ # @param timestamp [Time, nil] Optional timestamp
33
+ # @return [String] Rendered message
34
+ def render_assistant_message(content, timestamp: nil)
35
+ # Render markdown if content contains markdown syntax
36
+ rendered_content = if MarkdownRenderer.markdown?(content)
37
+ MarkdownRenderer.render(content)
38
+ else
39
+ content
40
+ end
41
+
42
+ @message_component.render(
43
+ role: "assistant",
44
+ content: rendered_content,
45
+ timestamp: timestamp
46
+ )
47
+ end
48
+
49
+ # Render a system message
50
+ # @param content [String] Message content
51
+ # @param timestamp [Time, nil] Optional timestamp
52
+ # @param prefix_newline [Boolean] Whether to add newline before message
53
+ # @return [String] Rendered message
54
+ def render_system_message(content, timestamp: nil, prefix_newline: true)
55
+ @message_component.render(
56
+ role: "system",
57
+ content: content,
58
+ timestamp: timestamp,
59
+ prefix_newline: prefix_newline
60
+ )
61
+ end
62
+
63
+ # Render a tool call
64
+ # @param tool_name [String] Tool name
65
+ # @param formatted_call [String] Formatted call description
66
+ # @return [String] Rendered tool call
67
+ def render_tool_call(tool_name:, formatted_call:)
68
+ @tool_component.render(
69
+ type: :call,
70
+ tool_name: tool_name,
71
+ formatted_call: formatted_call
72
+ )
73
+ end
74
+
75
+ # Render a tool result
76
+ # @param result [String] Tool result
77
+ # @return [String] Rendered tool result
78
+ def render_tool_result(result:)
79
+ @tool_component.render(
80
+ type: :result,
81
+ result: result
82
+ )
83
+ end
84
+
85
+ # Render a tool error
86
+ # @param error [String] Error message
87
+ # @return [String] Rendered tool error
88
+ def render_tool_error(error:)
89
+ @tool_component.render(
90
+ type: :error,
91
+ error: error
92
+ )
93
+ end
94
+
95
+ # Render a tool denied message
96
+ # @param tool_name [String] Tool name
97
+ # @return [String] Rendered tool denied
98
+ def render_tool_denied(tool_name:)
99
+ @tool_component.render(
100
+ type: :denied,
101
+ tool_name: tool_name
102
+ )
103
+ end
104
+
105
+ # Render a tool planned message
106
+ # @param tool_name [String] Tool name
107
+ # @return [String] Rendered tool planned
108
+ def render_tool_planned(tool_name:)
109
+ @tool_component.render(
110
+ type: :planned,
111
+ tool_name: tool_name
112
+ )
113
+ end
114
+
115
+ # Render thinking indicator
116
+ # @return [String] Thinking indicator
117
+ def render_thinking
118
+ @common_component.render_thinking
119
+ end
120
+
121
+ # Render progress message (stopped state, gray)
122
+ # @param message [String] Progress message
123
+ # @return [String] Progress indicator
124
+ def render_progress(message)
125
+ @common_component.render_progress(message)
126
+ end
127
+
128
+ # Render working message (active state, yellow)
129
+ # @param message [String] Progress message
130
+ # @return [String] Working indicator
131
+ def render_working(message)
132
+ @common_component.render_working(message)
133
+ end
134
+
135
+ # Render success message
136
+ # @param message [String] Success message
137
+ # @return [String] Success message
138
+ def render_success(message)
139
+ @common_component.render_success(message)
140
+ end
141
+
142
+ # Render error message
143
+ # @param message [String] Error message
144
+ # @return [String] Error message
145
+ def render_error(message)
146
+ @common_component.render_error(message)
147
+ end
148
+
149
+ # Render warning message
150
+ # @param message [String] Warning message
151
+ # @return [String] Warning message
152
+ def render_warning(message)
153
+ @common_component.render_warning(message)
154
+ end
155
+
156
+ # Render task completion summary
157
+ # @param iterations [Integer] Number of iterations
158
+ # @param cost [Float] Cost in USD
159
+ # @param duration [Float] Duration in seconds
160
+ # @param cache_tokens [Integer] Cache read tokens
161
+ # @param cache_requests [Integer] Total cache requests count
162
+ # @param cache_hits [Integer] Cache hit requests count
163
+ # @return [String] Formatted completion summary
164
+ def render_task_complete(iterations:, cost:, duration: nil, cache_tokens: nil, cache_requests: nil, cache_hits: nil)
165
+ @common_component.render_task_complete(
166
+ iterations: iterations,
167
+ cost: cost,
168
+ duration: duration,
169
+ cache_tokens: cache_tokens,
170
+ cache_requests: cache_requests,
171
+ cache_hits: cache_hits
172
+ )
173
+ end
174
+
175
+ end
176
+ end
177
+ 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.1"
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.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - windy
@@ -107,6 +107,34 @@ dependencies:
107
107
  - - "~>"
108
108
  - !ruby/object:Gem::Version
109
109
  version: '0.8'
110
+ - !ruby/object:Gem::Dependency
111
+ name: tty-markdown
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '0.7'
117
+ type: :runtime
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: '0.7'
124
+ - !ruby/object:Gem::Dependency
125
+ name: base64
126
+ requirement: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - "~>"
129
+ - !ruby/object:Gem::Version
130
+ version: 0.3.0
131
+ type: :runtime
132
+ prerelease: false
133
+ version_requirements: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - "~>"
136
+ - !ruby/object:Gem::Version
137
+ version: 0.3.0
110
138
  description: OpenClacky is a Ruby CLI tool for interacting with AI models via OpenAI-compatible
111
139
  APIs. It provides chat functionality and autonomous AI agent capabilities with tool
112
140
  use.
@@ -130,6 +158,7 @@ files:
130
158
  - bin/openclacky
131
159
  - clacky-legacy/clacky.gemspec
132
160
  - clacky-legacy/clarky.gemspec
161
+ - docs/ui2-architecture.md
133
162
  - lib/clacky.rb
134
163
  - lib/clacky/agent.rb
135
164
  - lib/clacky/agent_config.rb
@@ -157,11 +186,29 @@ files:
157
186
  - lib/clacky/tools/web_search.rb
158
187
  - lib/clacky/tools/write.rb
159
188
  - 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
189
+ - lib/clacky/ui2.rb
190
+ - lib/clacky/ui2/README.md
191
+ - lib/clacky/ui2/components/base_component.rb
192
+ - lib/clacky/ui2/components/common_component.rb
193
+ - lib/clacky/ui2/components/inline_input.rb
194
+ - lib/clacky/ui2/components/input_area.rb
195
+ - lib/clacky/ui2/components/message_component.rb
196
+ - lib/clacky/ui2/components/output_area.rb
197
+ - lib/clacky/ui2/components/todo_area.rb
198
+ - lib/clacky/ui2/components/tool_component.rb
199
+ - lib/clacky/ui2/components/welcome_banner.rb
200
+ - lib/clacky/ui2/layout_manager.rb
201
+ - lib/clacky/ui2/line_editor.rb
202
+ - lib/clacky/ui2/markdown_renderer.rb
203
+ - lib/clacky/ui2/screen_buffer.rb
204
+ - lib/clacky/ui2/theme_manager.rb
205
+ - lib/clacky/ui2/themes/base_theme.rb
206
+ - lib/clacky/ui2/themes/hacker_theme.rb
207
+ - lib/clacky/ui2/themes/minimal_theme.rb
208
+ - lib/clacky/ui2/ui_controller.rb
209
+ - lib/clacky/ui2/view_renderer.rb
164
210
  - lib/clacky/utils/arguments_parser.rb
211
+ - lib/clacky/utils/file_ignore_helper.rb
165
212
  - lib/clacky/utils/limit_stack.rb
166
213
  - lib/clacky/utils/path_helper.rb
167
214
  - lib/clacky/version.rb
@@ -187,7 +234,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
187
234
  - !ruby/object:Gem::Version
188
235
  version: '0'
189
236
  requirements: []
190
- rubygems_version: 3.6.9
237
+ rubygems_version: 4.0.4
191
238
  specification_version: 4
192
239
  summary: A command-line interface for AI models (Claude, OpenAI, etc.)
193
240
  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