aidp 0.7.0 → 0.8.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 (119) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +60 -214
  3. data/bin/aidp +1 -1
  4. data/lib/aidp/analysis/kb_inspector.rb +38 -23
  5. data/lib/aidp/analysis/seams.rb +2 -31
  6. data/lib/aidp/analysis/tree_sitter_grammar_loader.rb +0 -13
  7. data/lib/aidp/analysis/tree_sitter_scan.rb +3 -20
  8. data/lib/aidp/analyze/error_handler.rb +2 -75
  9. data/lib/aidp/analyze/json_file_storage.rb +292 -0
  10. data/lib/aidp/analyze/progress.rb +12 -0
  11. data/lib/aidp/analyze/progress_visualizer.rb +12 -17
  12. data/lib/aidp/analyze/ruby_maat_integration.rb +13 -31
  13. data/lib/aidp/analyze/runner.rb +256 -87
  14. data/lib/aidp/cli/jobs_command.rb +100 -432
  15. data/lib/aidp/cli.rb +309 -239
  16. data/lib/aidp/config.rb +298 -10
  17. data/lib/aidp/debug_logger.rb +195 -0
  18. data/lib/aidp/debug_mixin.rb +187 -0
  19. data/lib/aidp/execute/progress.rb +9 -0
  20. data/lib/aidp/execute/runner.rb +221 -40
  21. data/lib/aidp/execute/steps.rb +17 -7
  22. data/lib/aidp/execute/workflow_selector.rb +211 -0
  23. data/lib/aidp/harness/completion_checker.rb +268 -0
  24. data/lib/aidp/harness/condition_detector.rb +1526 -0
  25. data/lib/aidp/harness/config_loader.rb +373 -0
  26. data/lib/aidp/harness/config_manager.rb +382 -0
  27. data/lib/aidp/harness/config_schema.rb +1006 -0
  28. data/lib/aidp/harness/config_validator.rb +355 -0
  29. data/lib/aidp/harness/configuration.rb +477 -0
  30. data/lib/aidp/harness/enhanced_runner.rb +494 -0
  31. data/lib/aidp/harness/error_handler.rb +616 -0
  32. data/lib/aidp/harness/provider_config.rb +423 -0
  33. data/lib/aidp/harness/provider_factory.rb +306 -0
  34. data/lib/aidp/harness/provider_manager.rb +1269 -0
  35. data/lib/aidp/harness/provider_type_checker.rb +88 -0
  36. data/lib/aidp/harness/runner.rb +411 -0
  37. data/lib/aidp/harness/state/errors.rb +28 -0
  38. data/lib/aidp/harness/state/metrics.rb +219 -0
  39. data/lib/aidp/harness/state/persistence.rb +128 -0
  40. data/lib/aidp/harness/state/provider_state.rb +132 -0
  41. data/lib/aidp/harness/state/ui_state.rb +68 -0
  42. data/lib/aidp/harness/state/workflow_state.rb +123 -0
  43. data/lib/aidp/harness/state_manager.rb +586 -0
  44. data/lib/aidp/harness/status_display.rb +888 -0
  45. data/lib/aidp/harness/ui/base.rb +16 -0
  46. data/lib/aidp/harness/ui/enhanced_tui.rb +545 -0
  47. data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +252 -0
  48. data/lib/aidp/harness/ui/error_handler.rb +132 -0
  49. data/lib/aidp/harness/ui/frame_manager.rb +361 -0
  50. data/lib/aidp/harness/ui/job_monitor.rb +500 -0
  51. data/lib/aidp/harness/ui/navigation/main_menu.rb +311 -0
  52. data/lib/aidp/harness/ui/navigation/menu_formatter.rb +120 -0
  53. data/lib/aidp/harness/ui/navigation/menu_item.rb +142 -0
  54. data/lib/aidp/harness/ui/navigation/menu_state.rb +139 -0
  55. data/lib/aidp/harness/ui/navigation/submenu.rb +202 -0
  56. data/lib/aidp/harness/ui/navigation/workflow_selector.rb +176 -0
  57. data/lib/aidp/harness/ui/progress_display.rb +280 -0
  58. data/lib/aidp/harness/ui/question_collector.rb +141 -0
  59. data/lib/aidp/harness/ui/spinner_group.rb +184 -0
  60. data/lib/aidp/harness/ui/spinner_helper.rb +152 -0
  61. data/lib/aidp/harness/ui/status_manager.rb +312 -0
  62. data/lib/aidp/harness/ui/status_widget.rb +280 -0
  63. data/lib/aidp/harness/ui/workflow_controller.rb +312 -0
  64. data/lib/aidp/harness/user_interface.rb +2381 -0
  65. data/lib/aidp/provider_manager.rb +131 -7
  66. data/lib/aidp/providers/anthropic.rb +28 -103
  67. data/lib/aidp/providers/base.rb +170 -0
  68. data/lib/aidp/providers/cursor.rb +52 -181
  69. data/lib/aidp/providers/gemini.rb +24 -107
  70. data/lib/aidp/providers/macos_ui.rb +99 -5
  71. data/lib/aidp/providers/opencode.rb +194 -0
  72. data/lib/aidp/storage/csv_storage.rb +172 -0
  73. data/lib/aidp/storage/file_manager.rb +214 -0
  74. data/lib/aidp/storage/json_storage.rb +140 -0
  75. data/lib/aidp/version.rb +1 -1
  76. data/lib/aidp.rb +54 -39
  77. data/templates/COMMON/AGENT_BASE.md +11 -0
  78. data/templates/EXECUTE/00_PRD.md +4 -4
  79. data/templates/EXECUTE/02_ARCHITECTURE.md +5 -4
  80. data/templates/EXECUTE/07_TEST_PLAN.md +4 -1
  81. data/templates/EXECUTE/08_TASKS.md +4 -4
  82. data/templates/EXECUTE/10_IMPLEMENTATION_AGENT.md +4 -4
  83. data/templates/README.md +279 -0
  84. data/templates/aidp-development.yml.example +373 -0
  85. data/templates/aidp-minimal.yml.example +48 -0
  86. data/templates/aidp-production.yml.example +475 -0
  87. data/templates/aidp.yml.example +598 -0
  88. metadata +93 -69
  89. data/lib/aidp/analyze/agent_personas.rb +0 -71
  90. data/lib/aidp/analyze/agent_tool_executor.rb +0 -439
  91. data/lib/aidp/analyze/data_retention_manager.rb +0 -421
  92. data/lib/aidp/analyze/database.rb +0 -260
  93. data/lib/aidp/analyze/dependencies.rb +0 -335
  94. data/lib/aidp/analyze/export_manager.rb +0 -418
  95. data/lib/aidp/analyze/focus_guidance.rb +0 -517
  96. data/lib/aidp/analyze/incremental_analyzer.rb +0 -533
  97. data/lib/aidp/analyze/language_analysis_strategies.rb +0 -897
  98. data/lib/aidp/analyze/large_analysis_progress.rb +0 -499
  99. data/lib/aidp/analyze/memory_manager.rb +0 -339
  100. data/lib/aidp/analyze/metrics_storage.rb +0 -336
  101. data/lib/aidp/analyze/parallel_processor.rb +0 -454
  102. data/lib/aidp/analyze/performance_optimizer.rb +0 -691
  103. data/lib/aidp/analyze/repository_chunker.rb +0 -697
  104. data/lib/aidp/analyze/static_analysis_detector.rb +0 -577
  105. data/lib/aidp/analyze/storage.rb +0 -655
  106. data/lib/aidp/analyze/tool_configuration.rb +0 -441
  107. data/lib/aidp/analyze/tool_modernization.rb +0 -750
  108. data/lib/aidp/database/pg_adapter.rb +0 -148
  109. data/lib/aidp/database_config.rb +0 -69
  110. data/lib/aidp/database_connection.rb +0 -72
  111. data/lib/aidp/job_manager.rb +0 -41
  112. data/lib/aidp/jobs/base_job.rb +0 -45
  113. data/lib/aidp/jobs/provider_execution_job.rb +0 -83
  114. data/lib/aidp/project_detector.rb +0 -117
  115. data/lib/aidp/providers/agent_supervisor.rb +0 -348
  116. data/lib/aidp/providers/supervised_base.rb +0 -317
  117. data/lib/aidp/providers/supervised_cursor.rb +0 -22
  118. data/lib/aidp/sync.rb +0 -13
  119. data/lib/aidp/workspace.rb +0 -19
@@ -0,0 +1,202 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "main_menu"
4
+
5
+ module Aidp
6
+ module Harness
7
+ module UI
8
+ module Navigation
9
+ # Specialized submenu for drill-down functionality
10
+ class SubMenu < MainMenu
11
+ class SubMenuError < MenuError; end
12
+ class InvalidSubMenuError < SubMenuError; end
13
+
14
+ def initialize(title, parent_menu = nil, ui_components = {})
15
+ super(ui_components)
16
+ @title = title
17
+ @parent_menu = parent_menu
18
+ @submenu_items = []
19
+ @drill_down_enabled = true
20
+ @max_depth = 5
21
+ end
22
+
23
+ attr_reader :title, :parent_menu
24
+ attr_accessor :drill_down_enabled, :max_depth
25
+
26
+ def add_submenu_item(item)
27
+ validate_submenu_item(item)
28
+ @submenu_items << item
29
+ add_menu_item(item)
30
+ end
31
+
32
+ def add_submenu_items(items)
33
+ validate_submenu_items(items)
34
+ items.each { |item| add_submenu_item(item) }
35
+ end
36
+
37
+ def show_submenu
38
+ return unless can_show_submenu?
39
+
40
+ display_submenu_header
41
+ display_submenu_items
42
+ handle_submenu_interaction
43
+ rescue => e
44
+ raise SubMenuError, "Failed to show submenu: #{e.message}"
45
+ end
46
+
47
+ def can_show_submenu?
48
+ @drill_down_enabled && @submenu_items.any? && within_depth_limit?
49
+ end
50
+
51
+ def within_depth_limit?
52
+ @current_level < @max_depth
53
+ end
54
+
55
+ def has_parent?
56
+ !@parent_menu.nil?
57
+ end
58
+
59
+ def get_parent_path
60
+ return [] unless has_parent?
61
+
62
+ path = [@title]
63
+ current_parent = @parent_menu
64
+
65
+ while current_parent&.parent_menu
66
+ path.unshift(current_parent.title)
67
+ current_parent = current_parent.parent_menu
68
+ end
69
+
70
+ path
71
+ end
72
+
73
+ def get_full_path
74
+ parent_path = get_parent_path
75
+ parent_path << @title
76
+ parent_path
77
+ end
78
+
79
+ def create_child_submenu(title)
80
+ validate_title(title)
81
+ raise InvalidSubMenuError, "Maximum depth reached" unless within_depth_limit?
82
+
83
+ child_submenu = SubMenu.new(title, self, @ui_components)
84
+ child_submenu.max_depth = @max_depth
85
+ child_submenu
86
+ end
87
+
88
+ def navigate_to_parent
89
+ return false unless has_parent?
90
+
91
+ @parent_menu.show_menu(@parent_menu.title)
92
+ true
93
+ end
94
+
95
+ private
96
+
97
+ def validate_submenu_item(item)
98
+ validate_menu_item(item)
99
+ raise InvalidSubMenuError, "Submenu items must be MenuItems" unless item.is_a?(MenuItem)
100
+ end
101
+
102
+ def validate_submenu_items(items)
103
+ raise InvalidSubMenuError, "Submenu items must be an array" unless items.is_a?(Array)
104
+ end
105
+
106
+ def display_submenu_header
107
+ @prompt.say(@formatter.format_submenu_title(@title))
108
+ @prompt.say(@formatter.format_separator)
109
+
110
+ if has_parent?
111
+ parent_path = get_parent_path.join(" > ")
112
+ @prompt.say(@formatter.format_parent_path(parent_path))
113
+ end
114
+ end
115
+
116
+ def display_submenu_items
117
+ @submenu_items.each_with_index do |item, index|
118
+ formatted_item = @formatter.format_submenu_item(item, index + 1)
119
+ @prompt.say(formatted_item)
120
+ end
121
+ end
122
+
123
+ def handle_submenu_interaction
124
+ selection = prompt_for_submenu_selection
125
+ handle_submenu_selection(selection)
126
+ end
127
+
128
+ def prompt_for_submenu_selection
129
+ options = build_submenu_options
130
+ @prompt.ask("Select an option:") do |handler|
131
+ options.each { |option| handler.option(option) }
132
+ end
133
+ end
134
+
135
+ def build_submenu_options
136
+ options = @submenu_items.map(&:title)
137
+ options << "Back to Parent" if has_parent?
138
+ options << "Back to Main Menu"
139
+ options << "Exit"
140
+ options
141
+ end
142
+
143
+ def handle_submenu_selection(selection)
144
+ case selection
145
+ when "Back to Parent"
146
+ navigate_to_parent
147
+ when "Back to Main Menu"
148
+ navigate_to_main_menu
149
+ when "Exit"
150
+ :exit
151
+ else
152
+ handle_item_selection(selection)
153
+ end
154
+ end
155
+
156
+ def handle_item_selection(selection)
157
+ selected_item = find_submenu_item(selection)
158
+ return unless selected_item
159
+
160
+ execute_submenu_item(selected_item)
161
+ end
162
+
163
+ def find_submenu_item(title)
164
+ @submenu_items.find { |item| item.title == title }
165
+ end
166
+
167
+ def execute_submenu_item(item)
168
+ case item.type
169
+ when :action
170
+ execute_action(item)
171
+ when :submenu
172
+ navigate_to_child_submenu(item)
173
+ when :workflow
174
+ execute_workflow(item)
175
+ else
176
+ raise InvalidSubMenuError, "Unknown submenu item type: #{item.type}"
177
+ end
178
+ end
179
+
180
+ def navigate_to_child_submenu(item)
181
+ child_submenu = create_child_submenu(item.title)
182
+ child_submenu.show_submenu
183
+ end
184
+
185
+ def navigate_to_main_menu
186
+ # Navigate to the root menu
187
+ root_menu = find_root_menu
188
+ root_menu&.show_menu
189
+ end
190
+
191
+ def find_root_menu
192
+ current = self
193
+ while current.parent_menu
194
+ current = current.parent_menu
195
+ end
196
+ current
197
+ end
198
+ end
199
+ end
200
+ end
201
+ end
202
+ end
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tty-prompt"
4
+ require "pastel"
5
+ require_relative "../base"
6
+ require_relative "menu_item"
7
+
8
+ module Aidp
9
+ module Harness
10
+ module UI
11
+ module Navigation
12
+ # Handles workflow mode selection (simple vs advanced)
13
+ class WorkflowSelector < Base
14
+ class WorkflowError < StandardError; end
15
+ class InvalidModeError < WorkflowError; end
16
+ class SelectionError < WorkflowError; end
17
+
18
+ WORKFLOW_MODES = {
19
+ simple: {
20
+ name: "Simple Mode",
21
+ description: "Predefined workflows with guided templates",
22
+ icon: "🚀"
23
+ },
24
+ advanced: {
25
+ name: "Advanced Mode",
26
+ description: "Custom workflow configuration",
27
+ icon: "⚙️"
28
+ }
29
+ }.freeze
30
+
31
+ def initialize(ui_components = {})
32
+ super()
33
+ @prompt = ui_components[:prompt] || TTY::Prompt.new
34
+ @pastel = Pastel.new
35
+ @formatter = ui_components[:formatter] || WorkflowFormatter.new
36
+ @state_manager = ui_components[:state_manager]
37
+ end
38
+
39
+ def select_workflow_mode
40
+ display_mode_selection
41
+ selection = prompt_for_mode_selection
42
+ validate_selection(selection)
43
+
44
+ selected_mode = parse_selection(selection)
45
+ record_selection(selected_mode)
46
+ selected_mode
47
+ rescue => e
48
+ raise SelectionError, "Failed to select workflow mode: #{e.message}"
49
+ end
50
+
51
+ def show_mode_description(mode)
52
+ validate_mode(mode)
53
+
54
+ mode_info = WORKFLOW_MODES[mode]
55
+ display_mode_info(mode_info)
56
+ end
57
+
58
+ def get_available_modes
59
+ WORKFLOW_MODES.keys
60
+ end
61
+
62
+ def get_mode_info(mode)
63
+ validate_mode(mode)
64
+ WORKFLOW_MODES[mode]
65
+ end
66
+
67
+ def is_simple_mode?(mode)
68
+ mode == :simple
69
+ end
70
+
71
+ def is_advanced_mode?(mode)
72
+ mode == :advanced
73
+ end
74
+
75
+ private
76
+
77
+ def display_mode_selection
78
+ @prompt.say(@formatter.format_selector_title)
79
+ @prompt.say(@formatter.format_separator)
80
+
81
+ WORKFLOW_MODES.each_with_index do |(key, info), index|
82
+ display_mode_option(key, info, index + 1)
83
+ end
84
+
85
+ @prompt.say(@formatter.format_separator)
86
+ end
87
+
88
+ def display_mode_option(mode_key, mode_info, index)
89
+ formatted_option = @formatter.format_mode_option(mode_key, mode_info, index)
90
+ @prompt.say(formatted_option)
91
+ end
92
+
93
+ def display_mode_info(mode_info)
94
+ @prompt.say(@formatter.format_mode_info(mode_info))
95
+ end
96
+
97
+ def prompt_for_mode_selection
98
+ options = build_mode_options
99
+ @prompt.ask("Select workflow mode:") do |handler|
100
+ options.each { |option| handler.option(option) }
101
+ end
102
+ end
103
+
104
+ def build_mode_options
105
+ WORKFLOW_MODES.map { |key, info| "#{info[:icon]} #{info[:name]}" }
106
+ end
107
+
108
+ def validate_selection(selection)
109
+ raise InvalidModeError, "Selection cannot be empty" if selection.to_s.strip.empty?
110
+ end
111
+
112
+ def parse_selection(selection)
113
+ WORKFLOW_MODES.each do |key, info|
114
+ return key if selection.include?(info[:name])
115
+ end
116
+
117
+ raise InvalidModeError, "Invalid selection: #{selection}"
118
+ end
119
+
120
+ def validate_mode(mode)
121
+ unless WORKFLOW_MODES.key?(mode)
122
+ raise InvalidModeError, "Invalid mode: #{mode}. Must be one of: #{WORKFLOW_MODES.keys.join(", ")}"
123
+ end
124
+ end
125
+
126
+ def record_selection(mode)
127
+ @state_manager&.record_workflow_mode_selection(mode)
128
+ end
129
+ end
130
+
131
+ # Formats workflow selection display
132
+ class WorkflowFormatter
133
+ def initialize
134
+ @pastel = Pastel.new
135
+ end
136
+
137
+ def format_selector_title
138
+ @pastel.bold(@pastel.blue("🎯 Workflow Mode Selection"))
139
+ end
140
+
141
+ def format_separator
142
+ "─" * 60
143
+ end
144
+
145
+ def format_mode_option(mode_key, mode_info, index)
146
+ icon = mode_info[:icon]
147
+ name = mode_info[:name]
148
+ description = mode_info[:description]
149
+
150
+ "#{@pastel.bold("#{index}.")} #{@pastel.bold("#{icon} #{name}")}\n #{@pastel.dim(description)}"
151
+ end
152
+
153
+ def format_mode_info(mode_info)
154
+ icon = mode_info[:icon]
155
+ name = mode_info[:name]
156
+ description = mode_info[:description]
157
+
158
+ "#{@pastel.bold(@pastel.green("#{icon} #{name}"))}\n#{@pastel.dim(description)}"
159
+ end
160
+
161
+ def format_selected_mode(mode)
162
+ mode_info = WorkflowSelector::WORKFLOW_MODES[mode]
163
+ "#{@pastel.green("✓ Selected:")} #{@pastel.bold("#{mode_info[:icon]} #{mode_info[:name]}")}"
164
+ end
165
+
166
+ def format_mode_switch(from_mode, to_mode)
167
+ from_info = WorkflowSelector::WORKFLOW_MODES[from_mode]
168
+ to_info = WorkflowSelector::WORKFLOW_MODES[to_mode]
169
+
170
+ "#{@pastel.yellow("🔄 Switching from")} #{@pastel.bold(from_info[:name])} #{@pastel.yellow("to")} #{@pastel.bold(to_info[:name])}"
171
+ end
172
+ end
173
+ end
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,280 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tty-progressbar"
4
+ require "pastel"
5
+ require_relative "base"
6
+
7
+ module Aidp
8
+ module Harness
9
+ module UI
10
+ # Handles progress display using CLI UI progress bars
11
+ class ProgressDisplay < Base
12
+ class ProgressError < StandardError; end
13
+ class InvalidProgressError < ProgressError; end
14
+ class DisplayError < ProgressError; end
15
+
16
+ attr_reader :refresh_interval
17
+
18
+ def initialize(ui_components = {})
19
+ super()
20
+ @progress = ui_components[:progress] || TTY::ProgressBar
21
+ @pastel = Pastel.new
22
+ @formatter = ui_components[:formatter] || ProgressFormatter.new
23
+ @display_history = []
24
+ @auto_refresh_enabled = false
25
+ @refresh_interval = 1.0
26
+ @refresh_thread = nil
27
+ @output = ui_components[:output] || $stdout
28
+ @spinner_class = begin
29
+ ui_components[:spinner] || TTY::Spinner
30
+ rescue
31
+ nil
32
+ end
33
+ @spinner = nil
34
+ end
35
+
36
+ # Simple spinner management used by component specs
37
+ def start_spinner(message = "Loading...")
38
+ return unless @spinner_class
39
+ @spinner = @spinner_class.new("#{message} :spinner", format: :dots, output: @output)
40
+ @spinner.start
41
+ end
42
+
43
+ def stop_spinner
44
+ @spinner&.stop
45
+ @spinner = nil
46
+ end
47
+
48
+ def show_progress(total_steps, &block)
49
+ validate_total_steps(total_steps)
50
+
51
+ progress_bar = @progress.new(
52
+ "[:bar] :percent% :current/:total",
53
+ total: total_steps,
54
+ width: 30,
55
+ output: @output
56
+ )
57
+
58
+ execute_progress_steps(progress_bar, total_steps, &block)
59
+ rescue => e
60
+ raise DisplayError, "Failed to display progress: #{e.message}"
61
+ end
62
+
63
+ def update_progress(bar, message = nil)
64
+ validate_progress_bar(bar)
65
+
66
+ bar.tick
67
+ bar.update_title(message) if message
68
+ rescue => e
69
+ raise DisplayError, "Failed to update progress: #{e.message}"
70
+ end
71
+
72
+ def show_step_progress(step_name, total_substeps, &block)
73
+ validate_step_inputs(step_name, total_substeps)
74
+
75
+ formatted_title = @formatter.format_step_title(step_name)
76
+ @progress.progress do |bar|
77
+ execute_substeps(bar, total_substeps, formatted_title, &block)
78
+ end
79
+ rescue => e
80
+ raise DisplayError, "Failed to display step progress: #{e.message}"
81
+ end
82
+
83
+ def show_indeterminate_progress(message)
84
+ validate_message(message)
85
+
86
+ @progress.progress do |bar|
87
+ bar.update_title(message)
88
+ yield(bar) if block_given?
89
+ end
90
+ rescue => e
91
+ raise DisplayError, "Failed to display indeterminate progress: #{e.message}"
92
+ end
93
+
94
+ def display_progress(progress_data, display_type = :standard)
95
+ validate_progress_data(progress_data)
96
+ validate_display_type(display_type)
97
+
98
+ case display_type
99
+ when :standard
100
+ display_standard_progress(progress_data)
101
+ when :detailed
102
+ display_detailed_progress(progress_data)
103
+ when :minimal
104
+ display_minimal_progress(progress_data)
105
+ end
106
+
107
+ record_display_history(progress_data, display_type)
108
+ rescue InvalidProgressError => e
109
+ raise e
110
+ rescue => e
111
+ raise DisplayError, "Failed to display progress: #{e.message}"
112
+ end
113
+
114
+ def display_multiple_progress(progress_items, display_type = :standard)
115
+ raise ArgumentError, "Progress items must be an array" unless progress_items.is_a?(Array)
116
+
117
+ if progress_items.empty?
118
+ @output.puts @pastel.dim("No progress items to display.")
119
+ return
120
+ end
121
+
122
+ progress_items.each do |item|
123
+ display_progress(item, display_type)
124
+ end
125
+ end
126
+
127
+ def get_display_history
128
+ @display_history.dup
129
+ end
130
+
131
+ def clear_display_history
132
+ @display_history = []
133
+ end
134
+
135
+ def start_auto_refresh(interval)
136
+ return if @auto_refresh_enabled
137
+
138
+ @refresh_interval = interval
139
+ @auto_refresh_enabled = true
140
+ @refresh_thread = Thread.new do
141
+ while @auto_refresh_enabled
142
+ yield if block_given?
143
+ sleep @refresh_interval
144
+ end
145
+ end
146
+ end
147
+
148
+ def stop_auto_refresh
149
+ @auto_refresh_enabled = false
150
+ @refresh_thread&.join
151
+ @refresh_thread = nil
152
+ end
153
+
154
+ def auto_refresh_enabled?
155
+ @auto_refresh_enabled
156
+ end
157
+
158
+ private
159
+
160
+ def validate_total_steps(total_steps)
161
+ raise InvalidProgressError, "Total steps must be positive" unless total_steps > 0
162
+ end
163
+
164
+ def validate_progress_bar(bar)
165
+ raise InvalidProgressError, "Progress bar cannot be nil" if bar.nil?
166
+ end
167
+
168
+ def validate_step_inputs(step_name, total_substeps)
169
+ raise InvalidProgressError, "Step name cannot be empty" if step_name.to_s.strip.empty?
170
+ raise InvalidProgressError, "Total substeps must be positive" unless total_substeps > 0
171
+ end
172
+
173
+ def validate_message(message)
174
+ raise InvalidProgressError, "Message cannot be empty" if message.to_s.strip.empty?
175
+ end
176
+
177
+ def execute_progress_steps(bar, total_steps, &block)
178
+ total_steps.times do
179
+ yield(bar) if block_given?
180
+ bar.tick
181
+ end
182
+ end
183
+
184
+ def execute_substeps(bar, total_substeps, title, &block)
185
+ bar.update_title(title)
186
+ total_substeps.times do |index|
187
+ substep_title = @formatter.format_substep_title(title, index + 1, total_substeps)
188
+ bar.update_title(substep_title)
189
+ yield(bar, index) if block_given?
190
+ bar.tick
191
+ end
192
+ end
193
+
194
+ def display_standard_progress(progress_data)
195
+ progress = progress_data[:progress] || 0
196
+ message = progress_data[:message] || "Processing..."
197
+ step_info = (progress_data[:current_step] && progress_data[:total_steps]) ?
198
+ " (Step: #{progress_data[:current_step]}/#{progress_data[:total_steps]})" :
199
+ " (Step: #{progress_data[:current_step]})"
200
+ task_id = progress_data[:id] ? "[#{progress_data[:id]}] " : ""
201
+
202
+ @output.puts "#{task_id}#{progress}% #{message}#{step_info}"
203
+ end
204
+
205
+ def display_detailed_progress(progress_data)
206
+ progress = progress_data[:progress] || 0
207
+ message = progress_data[:message] || "Processing..."
208
+ current_step = progress_data[:current_step] || "N/A"
209
+ total_steps = progress_data[:total_steps] || "N/A"
210
+ started_at = progress_data[:started_at] ? progress_data[:started_at].strftime("%H:%M:%S") : "N/A"
211
+ eta = progress_data[:eta] || "N/A"
212
+
213
+ @output.puts "Progress: #{progress}% - #{message} (Step: #{current_step}/#{total_steps}, Started: #{started_at}, ETA: #{eta})"
214
+ end
215
+
216
+ def display_minimal_progress(progress_data)
217
+ progress = progress_data[:progress] || 0
218
+ message = progress_data[:message] || "Processing..."
219
+ @output.puts "#{@pastel.blue("Progress:")} #{progress}% - #{message}"
220
+ end
221
+
222
+ def create_progress_bar(progress)
223
+ TTY::ProgressBar.new(
224
+ "#{@pastel.green("[:bar]")} :percent",
225
+ total: 100,
226
+ width: 30,
227
+ current: progress,
228
+ output: @output
229
+ )
230
+ end
231
+
232
+ def validate_progress_data(progress_data)
233
+ raise ArgumentError, "Progress data must be a hash" unless progress_data.is_a?(Hash)
234
+ progress = progress_data[:progress]
235
+ if progress && (!progress.is_a?(Numeric) || progress < 0 || progress > 100)
236
+ raise InvalidProgressError, "Progress must be a number between 0 and 100"
237
+ end
238
+ end
239
+
240
+ def validate_display_type(display_type)
241
+ valid_types = [:standard, :detailed, :minimal]
242
+ unless valid_types.include?(display_type)
243
+ raise InvalidProgressError, "Invalid display type: #{display_type}. Must be one of: #{valid_types.join(", ")}"
244
+ end
245
+ end
246
+
247
+ def record_display_history(progress_data, display_type)
248
+ @display_history << {
249
+ progress_data: progress_data.dup,
250
+ display_type: display_type,
251
+ timestamp: Time.now
252
+ }
253
+ end
254
+ end
255
+
256
+ # Formats progress display text
257
+ class ProgressFormatter
258
+ def format_step_title(step_name)
259
+ "Step: #{step_name}"
260
+ end
261
+
262
+ def format_substep_title(step_title, current, total)
263
+ "#{step_title} (#{current}/#{total})"
264
+ end
265
+
266
+ def format_percentage(current, total)
267
+ percentage = (current.to_f / total * 100).round(1)
268
+ "#{percentage}%"
269
+ end
270
+
271
+ def format_eta(remaining_steps, average_time_per_step)
272
+ return "Unknown" unless average_time_per_step > 0
273
+
274
+ eta_seconds = remaining_steps * average_time_per_step
275
+ format_duration(eta_seconds)
276
+ end
277
+ end
278
+ end
279
+ end
280
+ end