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.
- 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 +0 -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,311 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "tty-prompt"
|
4
|
+
require "pastel"
|
5
|
+
require_relative "../base"
|
6
|
+
require_relative "menu_item"
|
7
|
+
require_relative "menu_state"
|
8
|
+
require_relative "menu_formatter"
|
9
|
+
|
10
|
+
module Aidp
|
11
|
+
module Harness
|
12
|
+
module UI
|
13
|
+
module Navigation
|
14
|
+
# Main hierarchical navigation menu system
|
15
|
+
class MainMenu < Base
|
16
|
+
class MenuError < StandardError; end
|
17
|
+
class InvalidMenuError < MenuError; end
|
18
|
+
class NavigationError < MenuError; end
|
19
|
+
|
20
|
+
def initialize(ui_components = {})
|
21
|
+
super()
|
22
|
+
@prompt = ui_components[:prompt] || TTY::Prompt.new
|
23
|
+
@pastel = Pastel.new
|
24
|
+
@formatter = ui_components[:formatter] || MenuFormatter.new
|
25
|
+
@state_manager = ui_components[:state_manager] || MenuState.new
|
26
|
+
@menu_items = []
|
27
|
+
@current_level = 0
|
28
|
+
@breadcrumb = []
|
29
|
+
@navigation_history = [] # Track all navigation actions
|
30
|
+
end
|
31
|
+
|
32
|
+
def add_menu_item(item)
|
33
|
+
validate_menu_item(item)
|
34
|
+
@menu_items << item
|
35
|
+
end
|
36
|
+
|
37
|
+
def add_menu_items(items)
|
38
|
+
validate_menu_items(items)
|
39
|
+
items.each { |item| add_menu_item(item) }
|
40
|
+
end
|
41
|
+
|
42
|
+
def show_menu(title = "Main Menu")
|
43
|
+
validate_title(title)
|
44
|
+
|
45
|
+
loop do
|
46
|
+
display_menu_header(title)
|
47
|
+
display_breadcrumb
|
48
|
+
display_menu_items
|
49
|
+
|
50
|
+
selection = prompt_for_selection
|
51
|
+
break if handle_selection(selection) == :exit
|
52
|
+
end
|
53
|
+
rescue => e
|
54
|
+
raise NavigationError, "Failed to show menu: #{e.message}"
|
55
|
+
end
|
56
|
+
|
57
|
+
def display_menu(title, menu_items)
|
58
|
+
# Handle nil title case
|
59
|
+
title = "Main Menu" if title.nil?
|
60
|
+
validate_title(title)
|
61
|
+
validate_menu_items(menu_items)
|
62
|
+
|
63
|
+
# Set the menu items for display
|
64
|
+
@menu_items = menu_items
|
65
|
+
|
66
|
+
display_menu_header(title)
|
67
|
+
display_breadcrumb
|
68
|
+
display_menu_items
|
69
|
+
|
70
|
+
# Add navigation instructions if items exist
|
71
|
+
if menu_items.any?
|
72
|
+
puts "Use arrow keys to navigate, Enter to select"
|
73
|
+
else
|
74
|
+
puts "No options available"
|
75
|
+
end
|
76
|
+
rescue => e
|
77
|
+
raise NavigationError, "Failed to display menu: #{e.message}"
|
78
|
+
end
|
79
|
+
|
80
|
+
def navigate_to_submenu(submenu_title)
|
81
|
+
validate_title(submenu_title)
|
82
|
+
|
83
|
+
submenu = find_submenu(submenu_title)
|
84
|
+
raise InvalidMenuError, "Submenu '#{submenu_title}' not found" unless submenu
|
85
|
+
|
86
|
+
@breadcrumb << submenu_title
|
87
|
+
@current_level += 1
|
88
|
+
|
89
|
+
submenu.show_menu(submenu_title)
|
90
|
+
rescue => e
|
91
|
+
raise NavigationError, "Failed to navigate to submenu: #{e.message}"
|
92
|
+
end
|
93
|
+
|
94
|
+
def go_back
|
95
|
+
return false if @breadcrumb.empty?
|
96
|
+
|
97
|
+
@breadcrumb.pop
|
98
|
+
@current_level -= 1
|
99
|
+
true
|
100
|
+
end
|
101
|
+
|
102
|
+
def current_path
|
103
|
+
@breadcrumb.join(" > ")
|
104
|
+
end
|
105
|
+
|
106
|
+
def menu_depth
|
107
|
+
@current_level
|
108
|
+
end
|
109
|
+
|
110
|
+
# Methods expected by tests
|
111
|
+
def select_option(menu_items)
|
112
|
+
@menu_items = menu_items
|
113
|
+
display_menu_items
|
114
|
+
|
115
|
+
max_attempts = 10 # Prevent infinite loops
|
116
|
+
attempts = 0
|
117
|
+
|
118
|
+
loop do
|
119
|
+
attempts += 1
|
120
|
+
break if attempts > max_attempts
|
121
|
+
|
122
|
+
# Use get_user_input for testing compatibility
|
123
|
+
input = get_user_input("Select an option: ")
|
124
|
+
|
125
|
+
# Handle cancel/quit
|
126
|
+
if input.downcase == "q" || input.downcase == "quit" || input.downcase == "cancel"
|
127
|
+
return nil
|
128
|
+
end
|
129
|
+
|
130
|
+
# Handle numeric input
|
131
|
+
if input.match?(/^\d+$/)
|
132
|
+
index = input.to_i - 1
|
133
|
+
if index >= 0 && index < menu_items.length
|
134
|
+
return menu_items[index]
|
135
|
+
else
|
136
|
+
puts "Invalid selection"
|
137
|
+
next
|
138
|
+
end
|
139
|
+
else
|
140
|
+
# Handle text input
|
141
|
+
selected_item = menu_items.find { |item| item.title.downcase == input.downcase }
|
142
|
+
if selected_item
|
143
|
+
return selected_item
|
144
|
+
else
|
145
|
+
puts "Invalid selection"
|
146
|
+
next
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
# If we get here, we've exceeded max attempts
|
152
|
+
puts "Too many invalid attempts. Returning first option."
|
153
|
+
menu_items.first
|
154
|
+
end
|
155
|
+
|
156
|
+
def get_user_input(prompt)
|
157
|
+
# Mock method for testing - will be stubbed in tests
|
158
|
+
@prompt.ask(prompt)
|
159
|
+
end
|
160
|
+
|
161
|
+
def navigate_to(section_name)
|
162
|
+
@breadcrumb << section_name
|
163
|
+
@current_level += 1
|
164
|
+
@navigation_history << {action: :navigate_to, section: section_name}
|
165
|
+
end
|
166
|
+
|
167
|
+
def navigate_back
|
168
|
+
return false if @breadcrumb.empty?
|
169
|
+
|
170
|
+
section = @breadcrumb.pop
|
171
|
+
@current_level -= 1
|
172
|
+
@navigation_history << {action: :navigate_back, section: section}
|
173
|
+
true
|
174
|
+
end
|
175
|
+
|
176
|
+
def navigate_to_root
|
177
|
+
@breadcrumb.clear
|
178
|
+
@current_level = 0
|
179
|
+
end
|
180
|
+
|
181
|
+
def get_navigation_history
|
182
|
+
# Return the complete navigation history
|
183
|
+
@navigation_history
|
184
|
+
end
|
185
|
+
|
186
|
+
def clear_navigation_history
|
187
|
+
@breadcrumb.clear
|
188
|
+
@current_level = 0
|
189
|
+
@navigation_history.clear
|
190
|
+
end
|
191
|
+
|
192
|
+
def at_root?
|
193
|
+
@breadcrumb.empty?
|
194
|
+
end
|
195
|
+
|
196
|
+
def display_navigation_help
|
197
|
+
puts "Navigation Help"
|
198
|
+
puts "Use arrow keys to navigate"
|
199
|
+
puts "Press Enter to select"
|
200
|
+
puts "Press Escape to go back"
|
201
|
+
end
|
202
|
+
|
203
|
+
# Additional methods expected by tests
|
204
|
+
def current_section
|
205
|
+
@breadcrumb.last || "Home"
|
206
|
+
end
|
207
|
+
|
208
|
+
def get_breadcrumb_path
|
209
|
+
["Home"] + @breadcrumb
|
210
|
+
end
|
211
|
+
|
212
|
+
def navigation_depth
|
213
|
+
@current_level
|
214
|
+
end
|
215
|
+
|
216
|
+
def can_navigate_back?
|
217
|
+
!@breadcrumb.empty?
|
218
|
+
end
|
219
|
+
|
220
|
+
# Make display_breadcrumb public for testing
|
221
|
+
def display_breadcrumb
|
222
|
+
if @breadcrumb.empty?
|
223
|
+
puts "Home"
|
224
|
+
else
|
225
|
+
breadcrumb_text = @formatter.format_breadcrumb(@breadcrumb)
|
226
|
+
puts(breadcrumb_text)
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
private
|
231
|
+
|
232
|
+
def validate_menu_item(item)
|
233
|
+
raise InvalidMenuError, "Menu item must be a MenuItem" unless item.is_a?(MenuItem)
|
234
|
+
end
|
235
|
+
|
236
|
+
def validate_menu_items(items)
|
237
|
+
raise InvalidMenuError, "Menu items must be an array" unless items.is_a?(Array)
|
238
|
+
end
|
239
|
+
|
240
|
+
def validate_title(title)
|
241
|
+
raise InvalidMenuError, "Title cannot be empty" if title.to_s.strip.empty?
|
242
|
+
end
|
243
|
+
|
244
|
+
def display_menu_header(title)
|
245
|
+
formatted_title = @formatter.format_menu_title(title)
|
246
|
+
puts(formatted_title)
|
247
|
+
puts(@formatter.format_separator)
|
248
|
+
end
|
249
|
+
|
250
|
+
def display_menu_items
|
251
|
+
@menu_items.each_with_index do |item, index|
|
252
|
+
formatted_item = @formatter.format_menu_item(item, index + 1)
|
253
|
+
puts(formatted_item)
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
def prompt_for_selection
|
258
|
+
options = build_selection_options
|
259
|
+
@prompt.ask("Select an option:") do |handler|
|
260
|
+
options.each { |option| handler.option(option) }
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
def build_selection_options
|
265
|
+
options = @menu_items.map(&:title)
|
266
|
+
options << "Back" unless @breadcrumb.empty?
|
267
|
+
options << "Exit"
|
268
|
+
options
|
269
|
+
end
|
270
|
+
|
271
|
+
def handle_selection(selection)
|
272
|
+
return :exit if selection == "Exit"
|
273
|
+
return :back if selection == "Back"
|
274
|
+
|
275
|
+
selected_item = find_menu_item(selection)
|
276
|
+
execute_menu_item(selected_item)
|
277
|
+
end
|
278
|
+
|
279
|
+
def find_menu_item(title)
|
280
|
+
@menu_items.find { |item| item.title == title }
|
281
|
+
end
|
282
|
+
|
283
|
+
def find_submenu(title)
|
284
|
+
@menu_items.find { |item| item.submenu? && item.title == title }
|
285
|
+
end
|
286
|
+
|
287
|
+
def execute_menu_item(item)
|
288
|
+
case item.type
|
289
|
+
when :action
|
290
|
+
execute_action(item)
|
291
|
+
when :submenu
|
292
|
+
navigate_to_submenu(item.title)
|
293
|
+
when :workflow
|
294
|
+
execute_workflow(item)
|
295
|
+
else
|
296
|
+
raise InvalidMenuError, "Unknown menu item type: #{item.type}"
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
def execute_action(item)
|
301
|
+
item.action&.call
|
302
|
+
end
|
303
|
+
|
304
|
+
def execute_workflow(item)
|
305
|
+
item.workflow&.call
|
306
|
+
end
|
307
|
+
end
|
308
|
+
end
|
309
|
+
end
|
310
|
+
end
|
311
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pastel"
|
4
|
+
|
5
|
+
module Aidp
|
6
|
+
module Harness
|
7
|
+
module UI
|
8
|
+
module Navigation
|
9
|
+
# Formats menu display elements
|
10
|
+
class MenuFormatter
|
11
|
+
def initialize
|
12
|
+
@pastel = Pastel.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def format_menu_title(title)
|
16
|
+
@pastel.bold(@pastel.blue("📋 #{title}"))
|
17
|
+
end
|
18
|
+
|
19
|
+
def format_separator
|
20
|
+
"─" * 50
|
21
|
+
end
|
22
|
+
|
23
|
+
def format_breadcrumb(breadcrumbs)
|
24
|
+
breadcrumb_text = breadcrumbs.join(" > ")
|
25
|
+
@pastel.dim("📍 #{breadcrumb_text}")
|
26
|
+
end
|
27
|
+
|
28
|
+
def format_menu_item(item, index)
|
29
|
+
return format_separator_item if item.separator?
|
30
|
+
|
31
|
+
prefix = format_item_prefix(index)
|
32
|
+
title = format_item_title(item)
|
33
|
+
description = format_item_description(item)
|
34
|
+
status = format_item_status(item)
|
35
|
+
|
36
|
+
"#{prefix} #{title}#{description}#{status}"
|
37
|
+
end
|
38
|
+
|
39
|
+
def format_item_prefix(index)
|
40
|
+
@pastel.bold("#{index}.")
|
41
|
+
end
|
42
|
+
|
43
|
+
def format_item_title(item)
|
44
|
+
if item.disabled?
|
45
|
+
@pastel.dim(item.title)
|
46
|
+
elsif item.hidden?
|
47
|
+
@pastel.dim(item.title)
|
48
|
+
else
|
49
|
+
@pastel.bold(item.title)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def format_item_description(item)
|
54
|
+
return "" unless item.description
|
55
|
+
|
56
|
+
" #{@pastel.dim("- #{item.description}")}"
|
57
|
+
end
|
58
|
+
|
59
|
+
def format_item_status(item)
|
60
|
+
return " #{@pastel.red("[DISABLED]")}" if item.disabled?
|
61
|
+
return " #{@pastel.yellow("[HIDDEN]")}" if item.hidden?
|
62
|
+
""
|
63
|
+
end
|
64
|
+
|
65
|
+
def format_separator_item
|
66
|
+
@pastel.dim("────────────────────────────────────────")
|
67
|
+
end
|
68
|
+
|
69
|
+
def format_shortcut(shortcut)
|
70
|
+
@pastel.dim("(#{shortcut})")
|
71
|
+
end
|
72
|
+
|
73
|
+
def format_menu_depth(depth)
|
74
|
+
@pastel.dim("Level #{depth}")
|
75
|
+
end
|
76
|
+
|
77
|
+
def format_navigation_prompt
|
78
|
+
@pastel.bold("Select an option:")
|
79
|
+
end
|
80
|
+
|
81
|
+
def format_back_option
|
82
|
+
@pastel.dim("← Back")
|
83
|
+
end
|
84
|
+
|
85
|
+
def format_exit_option
|
86
|
+
@pastel.red("✗ Exit")
|
87
|
+
end
|
88
|
+
|
89
|
+
def format_workflow_title(workflow_name)
|
90
|
+
@pastel.bold(@pastel.green("🔄 #{workflow_name} Workflow"))
|
91
|
+
end
|
92
|
+
|
93
|
+
def format_action_title(action_name)
|
94
|
+
@pastel.bold(@pastel.blue("⚡ #{action_name}"))
|
95
|
+
end
|
96
|
+
|
97
|
+
def format_submenu_title(submenu_name)
|
98
|
+
@pastel.bold(@pastel.yellow("📁 #{submenu_name}"))
|
99
|
+
end
|
100
|
+
|
101
|
+
def format_error_message(error)
|
102
|
+
"#{@pastel.red("❌ Error:")} #{error}"
|
103
|
+
end
|
104
|
+
|
105
|
+
def format_success_message(message)
|
106
|
+
@pastel.green("✅ #{message}")
|
107
|
+
end
|
108
|
+
|
109
|
+
def format_warning_message(message)
|
110
|
+
@pastel.yellow("⚠️ #{message}")
|
111
|
+
end
|
112
|
+
|
113
|
+
def format_info_message(message)
|
114
|
+
@pastel.blue("ℹ️ #{message}")
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Aidp
|
4
|
+
module Harness
|
5
|
+
module UI
|
6
|
+
module Navigation
|
7
|
+
# Represents a single menu item in the navigation system
|
8
|
+
class MenuItem
|
9
|
+
class MenuItemError < StandardError; end
|
10
|
+
class InvalidTypeError < MenuItemError; end
|
11
|
+
|
12
|
+
VALID_TYPES = [:action, :submenu, :workflow, :separator].freeze
|
13
|
+
|
14
|
+
def initialize(title, type = :action, options = {})
|
15
|
+
@title = title
|
16
|
+
@type = type
|
17
|
+
@action = options[:action]
|
18
|
+
@workflow = options[:workflow]
|
19
|
+
@submenu = options[:submenu]
|
20
|
+
@description = options[:description]
|
21
|
+
@shortcut = options[:shortcut]
|
22
|
+
@enabled = options.fetch(:enabled, true)
|
23
|
+
@visible = options.fetch(:visible, true)
|
24
|
+
|
25
|
+
validate_attributes
|
26
|
+
end
|
27
|
+
|
28
|
+
attr_reader :title, :type, :action, :workflow, :submenu, :description, :shortcut
|
29
|
+
attr_accessor :enabled, :visible
|
30
|
+
|
31
|
+
def action?
|
32
|
+
@type == :action
|
33
|
+
end
|
34
|
+
|
35
|
+
def submenu?
|
36
|
+
@type == :submenu
|
37
|
+
end
|
38
|
+
|
39
|
+
def workflow?
|
40
|
+
@type == :workflow
|
41
|
+
end
|
42
|
+
|
43
|
+
def separator?
|
44
|
+
@type == :separator
|
45
|
+
end
|
46
|
+
|
47
|
+
def enabled?
|
48
|
+
@enabled
|
49
|
+
end
|
50
|
+
|
51
|
+
def visible?
|
52
|
+
@visible
|
53
|
+
end
|
54
|
+
|
55
|
+
def disabled?
|
56
|
+
!enabled?
|
57
|
+
end
|
58
|
+
|
59
|
+
def hidden?
|
60
|
+
!visible?
|
61
|
+
end
|
62
|
+
|
63
|
+
def execute
|
64
|
+
case @type
|
65
|
+
when :action
|
66
|
+
execute_action
|
67
|
+
when :workflow
|
68
|
+
execute_workflow
|
69
|
+
when :submenu
|
70
|
+
execute_submenu
|
71
|
+
when :separator
|
72
|
+
execute_separator
|
73
|
+
else
|
74
|
+
raise InvalidTypeError, "Unknown menu item type: #{@type}"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def to_s
|
79
|
+
"#{@title} (#{@type})"
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def validate_attributes
|
85
|
+
validate_title
|
86
|
+
validate_type
|
87
|
+
validate_type_specific_attributes
|
88
|
+
end
|
89
|
+
|
90
|
+
def validate_title
|
91
|
+
raise MenuItemError, "Title cannot be empty" if @title.to_s.strip.empty?
|
92
|
+
end
|
93
|
+
|
94
|
+
def validate_type
|
95
|
+
unless VALID_TYPES.include?(@type)
|
96
|
+
raise InvalidTypeError, "Invalid type: #{@type}. Must be one of: #{VALID_TYPES.join(", ")}"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def validate_type_specific_attributes
|
101
|
+
case @type
|
102
|
+
when :action
|
103
|
+
validate_action_attributes
|
104
|
+
when :workflow
|
105
|
+
validate_workflow_attributes
|
106
|
+
when :submenu
|
107
|
+
validate_submenu_attributes
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def validate_action_attributes
|
112
|
+
raise MenuItemError, "Action items must have an action" unless @action.respond_to?(:call)
|
113
|
+
end
|
114
|
+
|
115
|
+
def validate_workflow_attributes
|
116
|
+
raise MenuItemError, "Workflow items must have a workflow" unless @workflow.respond_to?(:call)
|
117
|
+
end
|
118
|
+
|
119
|
+
def validate_submenu_attributes
|
120
|
+
raise MenuItemError, "Submenu items must have a submenu" unless @submenu.is_a?(MainMenu)
|
121
|
+
end
|
122
|
+
|
123
|
+
def execute_action
|
124
|
+
@action&.call
|
125
|
+
end
|
126
|
+
|
127
|
+
def execute_workflow
|
128
|
+
@workflow&.call
|
129
|
+
end
|
130
|
+
|
131
|
+
def execute_submenu
|
132
|
+
@submenu&.show_menu(@title)
|
133
|
+
end
|
134
|
+
|
135
|
+
def execute_separator
|
136
|
+
# Separators don't execute anything
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Aidp
|
4
|
+
module Harness
|
5
|
+
module UI
|
6
|
+
module Navigation
|
7
|
+
# Manages navigation state and history
|
8
|
+
class MenuState
|
9
|
+
class StateError < StandardError; end
|
10
|
+
class InvalidStateError < StateError; end
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@navigation_history = []
|
14
|
+
@current_menu = nil
|
15
|
+
@menu_stack = []
|
16
|
+
@breadcrumbs = []
|
17
|
+
@last_selection = nil
|
18
|
+
end
|
19
|
+
|
20
|
+
def push_menu(menu_title)
|
21
|
+
validate_menu_title(menu_title)
|
22
|
+
|
23
|
+
@menu_stack << menu_title
|
24
|
+
@breadcrumbs << menu_title
|
25
|
+
@current_menu = menu_title
|
26
|
+
end
|
27
|
+
|
28
|
+
def pop_menu
|
29
|
+
return nil if @menu_stack.empty?
|
30
|
+
|
31
|
+
@menu_stack.pop
|
32
|
+
@breadcrumbs.pop
|
33
|
+
@current_menu = @menu_stack.last
|
34
|
+
@current_menu
|
35
|
+
end
|
36
|
+
|
37
|
+
def set_current_menu(menu_title)
|
38
|
+
validate_menu_title(menu_title)
|
39
|
+
@current_menu = menu_title
|
40
|
+
end
|
41
|
+
|
42
|
+
def record_selection(selection)
|
43
|
+
@last_selection = selection
|
44
|
+
@navigation_history << {
|
45
|
+
menu: @current_menu,
|
46
|
+
selection: selection,
|
47
|
+
timestamp: Time.now
|
48
|
+
}
|
49
|
+
end
|
50
|
+
|
51
|
+
def record_workflow_mode_selection(mode)
|
52
|
+
@navigation_history << {
|
53
|
+
menu: @current_menu,
|
54
|
+
selection: "workflow_mode: #{mode}",
|
55
|
+
timestamp: Time.now
|
56
|
+
}
|
57
|
+
end
|
58
|
+
|
59
|
+
def record_keyboard_navigation(key, action)
|
60
|
+
@navigation_history << {
|
61
|
+
menu: @current_menu,
|
62
|
+
selection: "keyboard: #{key} -> #{action}",
|
63
|
+
timestamp: Time.now
|
64
|
+
}
|
65
|
+
end
|
66
|
+
|
67
|
+
def record_progressive_disclosure(level, action)
|
68
|
+
@navigation_history << {
|
69
|
+
menu: @current_menu,
|
70
|
+
selection: "progressive: level #{level} -> #{action}",
|
71
|
+
timestamp: Time.now
|
72
|
+
}
|
73
|
+
end
|
74
|
+
|
75
|
+
def get_navigation_history
|
76
|
+
@navigation_history.dup
|
77
|
+
end
|
78
|
+
|
79
|
+
def get_breadcrumbs
|
80
|
+
@breadcrumbs.dup
|
81
|
+
end
|
82
|
+
|
83
|
+
def get_menu_stack
|
84
|
+
@menu_stack.dup
|
85
|
+
end
|
86
|
+
|
87
|
+
attr_reader :current_menu
|
88
|
+
|
89
|
+
attr_reader :last_selection
|
90
|
+
|
91
|
+
def menu_depth
|
92
|
+
@menu_stack.size
|
93
|
+
end
|
94
|
+
|
95
|
+
def can_go_back?
|
96
|
+
@menu_stack.size > 1
|
97
|
+
end
|
98
|
+
|
99
|
+
def clear_history
|
100
|
+
@navigation_history.clear
|
101
|
+
end
|
102
|
+
|
103
|
+
def clear_breadcrumbs
|
104
|
+
@breadcrumbs.clear
|
105
|
+
end
|
106
|
+
|
107
|
+
def clear_menu_stack
|
108
|
+
@menu_stack.clear
|
109
|
+
@current_menu = nil
|
110
|
+
end
|
111
|
+
|
112
|
+
def reset
|
113
|
+
clear_history
|
114
|
+
clear_breadcrumbs
|
115
|
+
clear_menu_stack
|
116
|
+
@last_selection = nil
|
117
|
+
end
|
118
|
+
|
119
|
+
def export_state
|
120
|
+
{
|
121
|
+
current_menu: @current_menu,
|
122
|
+
menu_stack: @menu_stack,
|
123
|
+
breadcrumbs: @breadcrumbs,
|
124
|
+
last_selection: @last_selection,
|
125
|
+
navigation_history: @navigation_history,
|
126
|
+
menu_depth: menu_depth
|
127
|
+
}
|
128
|
+
end
|
129
|
+
|
130
|
+
private
|
131
|
+
|
132
|
+
def validate_menu_title(menu_title)
|
133
|
+
raise InvalidStateError, "Menu title cannot be empty" if menu_title.to_s.strip.empty?
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|