aidp 0.7.0 → 0.8.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.
- checksums.yaml +4 -4
- data/README.md +60 -214
- data/bin/aidp +1 -1
- data/lib/aidp/analysis/kb_inspector.rb +38 -23
- data/lib/aidp/analysis/seams.rb +2 -31
- data/lib/aidp/analysis/tree_sitter_grammar_loader.rb +1 -13
- data/lib/aidp/analysis/tree_sitter_scan.rb +3 -20
- data/lib/aidp/analyze/error_handler.rb +2 -75
- data/lib/aidp/analyze/json_file_storage.rb +292 -0
- data/lib/aidp/analyze/progress.rb +12 -0
- data/lib/aidp/analyze/progress_visualizer.rb +12 -17
- data/lib/aidp/analyze/ruby_maat_integration.rb +13 -31
- data/lib/aidp/analyze/runner.rb +256 -87
- data/lib/aidp/cli/jobs_command.rb +100 -432
- data/lib/aidp/cli.rb +309 -239
- data/lib/aidp/config.rb +298 -10
- data/lib/aidp/debug_logger.rb +195 -0
- data/lib/aidp/debug_mixin.rb +187 -0
- data/lib/aidp/execute/progress.rb +9 -0
- data/lib/aidp/execute/runner.rb +221 -40
- data/lib/aidp/execute/steps.rb +17 -7
- data/lib/aidp/execute/workflow_selector.rb +211 -0
- data/lib/aidp/harness/completion_checker.rb +268 -0
- data/lib/aidp/harness/condition_detector.rb +1526 -0
- data/lib/aidp/harness/config_loader.rb +373 -0
- data/lib/aidp/harness/config_manager.rb +382 -0
- data/lib/aidp/harness/config_schema.rb +1006 -0
- data/lib/aidp/harness/config_validator.rb +355 -0
- data/lib/aidp/harness/configuration.rb +477 -0
- data/lib/aidp/harness/enhanced_runner.rb +494 -0
- data/lib/aidp/harness/error_handler.rb +616 -0
- data/lib/aidp/harness/provider_config.rb +423 -0
- data/lib/aidp/harness/provider_factory.rb +306 -0
- data/lib/aidp/harness/provider_manager.rb +1269 -0
- data/lib/aidp/harness/provider_type_checker.rb +88 -0
- data/lib/aidp/harness/runner.rb +411 -0
- data/lib/aidp/harness/state/errors.rb +28 -0
- data/lib/aidp/harness/state/metrics.rb +219 -0
- data/lib/aidp/harness/state/persistence.rb +128 -0
- data/lib/aidp/harness/state/provider_state.rb +132 -0
- data/lib/aidp/harness/state/ui_state.rb +68 -0
- data/lib/aidp/harness/state/workflow_state.rb +123 -0
- data/lib/aidp/harness/state_manager.rb +586 -0
- data/lib/aidp/harness/status_display.rb +888 -0
- data/lib/aidp/harness/ui/base.rb +16 -0
- data/lib/aidp/harness/ui/enhanced_tui.rb +545 -0
- data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +252 -0
- data/lib/aidp/harness/ui/error_handler.rb +132 -0
- data/lib/aidp/harness/ui/frame_manager.rb +361 -0
- data/lib/aidp/harness/ui/job_monitor.rb +500 -0
- data/lib/aidp/harness/ui/navigation/main_menu.rb +311 -0
- data/lib/aidp/harness/ui/navigation/menu_formatter.rb +120 -0
- data/lib/aidp/harness/ui/navigation/menu_item.rb +142 -0
- data/lib/aidp/harness/ui/navigation/menu_state.rb +139 -0
- data/lib/aidp/harness/ui/navigation/submenu.rb +202 -0
- data/lib/aidp/harness/ui/navigation/workflow_selector.rb +176 -0
- data/lib/aidp/harness/ui/progress_display.rb +280 -0
- data/lib/aidp/harness/ui/question_collector.rb +141 -0
- data/lib/aidp/harness/ui/spinner_group.rb +184 -0
- data/lib/aidp/harness/ui/spinner_helper.rb +152 -0
- data/lib/aidp/harness/ui/status_manager.rb +312 -0
- data/lib/aidp/harness/ui/status_widget.rb +280 -0
- data/lib/aidp/harness/ui/workflow_controller.rb +312 -0
- data/lib/aidp/harness/user_interface.rb +2381 -0
- data/lib/aidp/provider_manager.rb +131 -7
- data/lib/aidp/providers/anthropic.rb +28 -103
- data/lib/aidp/providers/base.rb +170 -0
- data/lib/aidp/providers/cursor.rb +52 -181
- data/lib/aidp/providers/gemini.rb +24 -107
- data/lib/aidp/providers/macos_ui.rb +99 -5
- data/lib/aidp/providers/opencode.rb +194 -0
- data/lib/aidp/storage/csv_storage.rb +172 -0
- data/lib/aidp/storage/file_manager.rb +214 -0
- data/lib/aidp/storage/json_storage.rb +140 -0
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp.rb +54 -39
- data/templates/COMMON/AGENT_BASE.md +11 -0
- data/templates/EXECUTE/00_PRD.md +4 -4
- data/templates/EXECUTE/02_ARCHITECTURE.md +5 -4
- data/templates/EXECUTE/07_TEST_PLAN.md +4 -1
- data/templates/EXECUTE/08_TASKS.md +4 -4
- data/templates/EXECUTE/10_IMPLEMENTATION_AGENT.md +4 -4
- data/templates/README.md +279 -0
- data/templates/aidp-development.yml.example +373 -0
- data/templates/aidp-minimal.yml.example +48 -0
- data/templates/aidp-production.yml.example +475 -0
- data/templates/aidp.yml.example +598 -0
- metadata +93 -69
- data/lib/aidp/analyze/agent_personas.rb +0 -71
- data/lib/aidp/analyze/agent_tool_executor.rb +0 -439
- data/lib/aidp/analyze/data_retention_manager.rb +0 -421
- data/lib/aidp/analyze/database.rb +0 -260
- data/lib/aidp/analyze/dependencies.rb +0 -335
- data/lib/aidp/analyze/export_manager.rb +0 -418
- data/lib/aidp/analyze/focus_guidance.rb +0 -517
- data/lib/aidp/analyze/incremental_analyzer.rb +0 -533
- data/lib/aidp/analyze/language_analysis_strategies.rb +0 -897
- data/lib/aidp/analyze/large_analysis_progress.rb +0 -499
- data/lib/aidp/analyze/memory_manager.rb +0 -339
- data/lib/aidp/analyze/metrics_storage.rb +0 -336
- data/lib/aidp/analyze/parallel_processor.rb +0 -454
- data/lib/aidp/analyze/performance_optimizer.rb +0 -691
- data/lib/aidp/analyze/repository_chunker.rb +0 -697
- data/lib/aidp/analyze/static_analysis_detector.rb +0 -577
- data/lib/aidp/analyze/storage.rb +0 -655
- data/lib/aidp/analyze/tool_configuration.rb +0 -441
- data/lib/aidp/analyze/tool_modernization.rb +0 -750
- data/lib/aidp/database/pg_adapter.rb +0 -148
- data/lib/aidp/database_config.rb +0 -69
- data/lib/aidp/database_connection.rb +0 -72
- data/lib/aidp/job_manager.rb +0 -41
- data/lib/aidp/jobs/base_job.rb +0 -45
- data/lib/aidp/jobs/provider_execution_job.rb +0 -83
- data/lib/aidp/project_detector.rb +0 -117
- data/lib/aidp/providers/agent_supervisor.rb +0 -348
- data/lib/aidp/providers/supervised_base.rb +0 -317
- data/lib/aidp/providers/supervised_cursor.rb +0 -22
- data/lib/aidp/sync.rb +0 -13
- 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
|