aidp 0.5.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 (122) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +128 -151
  3. data/bin/aidp +1 -1
  4. data/lib/aidp/analysis/kb_inspector.rb +471 -0
  5. data/lib/aidp/analysis/seams.rb +159 -0
  6. data/lib/aidp/analysis/tree_sitter_grammar_loader.rb +480 -0
  7. data/lib/aidp/analysis/tree_sitter_scan.rb +686 -0
  8. data/lib/aidp/analyze/error_handler.rb +2 -78
  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/analyze/steps.rb +6 -0
  15. data/lib/aidp/cli/jobs_command.rb +103 -435
  16. data/lib/aidp/cli.rb +317 -191
  17. data/lib/aidp/config.rb +298 -10
  18. data/lib/aidp/debug_logger.rb +195 -0
  19. data/lib/aidp/debug_mixin.rb +187 -0
  20. data/lib/aidp/execute/progress.rb +9 -0
  21. data/lib/aidp/execute/runner.rb +221 -40
  22. data/lib/aidp/execute/steps.rb +17 -7
  23. data/lib/aidp/execute/workflow_selector.rb +211 -0
  24. data/lib/aidp/harness/completion_checker.rb +268 -0
  25. data/lib/aidp/harness/condition_detector.rb +1526 -0
  26. data/lib/aidp/harness/config_loader.rb +373 -0
  27. data/lib/aidp/harness/config_manager.rb +382 -0
  28. data/lib/aidp/harness/config_schema.rb +1006 -0
  29. data/lib/aidp/harness/config_validator.rb +355 -0
  30. data/lib/aidp/harness/configuration.rb +477 -0
  31. data/lib/aidp/harness/enhanced_runner.rb +494 -0
  32. data/lib/aidp/harness/error_handler.rb +616 -0
  33. data/lib/aidp/harness/provider_config.rb +423 -0
  34. data/lib/aidp/harness/provider_factory.rb +306 -0
  35. data/lib/aidp/harness/provider_manager.rb +1269 -0
  36. data/lib/aidp/harness/provider_type_checker.rb +88 -0
  37. data/lib/aidp/harness/runner.rb +411 -0
  38. data/lib/aidp/harness/state/errors.rb +28 -0
  39. data/lib/aidp/harness/state/metrics.rb +219 -0
  40. data/lib/aidp/harness/state/persistence.rb +128 -0
  41. data/lib/aidp/harness/state/provider_state.rb +132 -0
  42. data/lib/aidp/harness/state/ui_state.rb +68 -0
  43. data/lib/aidp/harness/state/workflow_state.rb +123 -0
  44. data/lib/aidp/harness/state_manager.rb +586 -0
  45. data/lib/aidp/harness/status_display.rb +888 -0
  46. data/lib/aidp/harness/ui/base.rb +16 -0
  47. data/lib/aidp/harness/ui/enhanced_tui.rb +545 -0
  48. data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +252 -0
  49. data/lib/aidp/harness/ui/error_handler.rb +132 -0
  50. data/lib/aidp/harness/ui/frame_manager.rb +361 -0
  51. data/lib/aidp/harness/ui/job_monitor.rb +500 -0
  52. data/lib/aidp/harness/ui/navigation/main_menu.rb +311 -0
  53. data/lib/aidp/harness/ui/navigation/menu_formatter.rb +120 -0
  54. data/lib/aidp/harness/ui/navigation/menu_item.rb +142 -0
  55. data/lib/aidp/harness/ui/navigation/menu_state.rb +139 -0
  56. data/lib/aidp/harness/ui/navigation/submenu.rb +202 -0
  57. data/lib/aidp/harness/ui/navigation/workflow_selector.rb +176 -0
  58. data/lib/aidp/harness/ui/progress_display.rb +280 -0
  59. data/lib/aidp/harness/ui/question_collector.rb +141 -0
  60. data/lib/aidp/harness/ui/spinner_group.rb +184 -0
  61. data/lib/aidp/harness/ui/spinner_helper.rb +152 -0
  62. data/lib/aidp/harness/ui/status_manager.rb +312 -0
  63. data/lib/aidp/harness/ui/status_widget.rb +280 -0
  64. data/lib/aidp/harness/ui/workflow_controller.rb +312 -0
  65. data/lib/aidp/harness/user_interface.rb +2381 -0
  66. data/lib/aidp/provider_manager.rb +131 -7
  67. data/lib/aidp/providers/anthropic.rb +28 -109
  68. data/lib/aidp/providers/base.rb +170 -0
  69. data/lib/aidp/providers/cursor.rb +52 -183
  70. data/lib/aidp/providers/gemini.rb +24 -109
  71. data/lib/aidp/providers/macos_ui.rb +99 -5
  72. data/lib/aidp/providers/opencode.rb +194 -0
  73. data/lib/aidp/storage/csv_storage.rb +172 -0
  74. data/lib/aidp/storage/file_manager.rb +214 -0
  75. data/lib/aidp/storage/json_storage.rb +140 -0
  76. data/lib/aidp/version.rb +1 -1
  77. data/lib/aidp.rb +56 -35
  78. data/templates/ANALYZE/06a_tree_sitter_scan.md +217 -0
  79. data/templates/COMMON/AGENT_BASE.md +11 -0
  80. data/templates/EXECUTE/00_PRD.md +4 -4
  81. data/templates/EXECUTE/02_ARCHITECTURE.md +5 -4
  82. data/templates/EXECUTE/07_TEST_PLAN.md +4 -1
  83. data/templates/EXECUTE/08_TASKS.md +4 -4
  84. data/templates/EXECUTE/10_IMPLEMENTATION_AGENT.md +4 -4
  85. data/templates/README.md +279 -0
  86. data/templates/aidp-development.yml.example +373 -0
  87. data/templates/aidp-minimal.yml.example +48 -0
  88. data/templates/aidp-production.yml.example +475 -0
  89. data/templates/aidp.yml.example +598 -0
  90. metadata +106 -64
  91. data/lib/aidp/analyze/agent_personas.rb +0 -71
  92. data/lib/aidp/analyze/agent_tool_executor.rb +0 -445
  93. data/lib/aidp/analyze/data_retention_manager.rb +0 -426
  94. data/lib/aidp/analyze/database.rb +0 -260
  95. data/lib/aidp/analyze/dependencies.rb +0 -335
  96. data/lib/aidp/analyze/export_manager.rb +0 -425
  97. data/lib/aidp/analyze/focus_guidance.rb +0 -517
  98. data/lib/aidp/analyze/incremental_analyzer.rb +0 -543
  99. data/lib/aidp/analyze/language_analysis_strategies.rb +0 -897
  100. data/lib/aidp/analyze/large_analysis_progress.rb +0 -504
  101. data/lib/aidp/analyze/memory_manager.rb +0 -365
  102. data/lib/aidp/analyze/metrics_storage.rb +0 -336
  103. data/lib/aidp/analyze/parallel_processor.rb +0 -460
  104. data/lib/aidp/analyze/performance_optimizer.rb +0 -694
  105. data/lib/aidp/analyze/repository_chunker.rb +0 -704
  106. data/lib/aidp/analyze/static_analysis_detector.rb +0 -577
  107. data/lib/aidp/analyze/storage.rb +0 -662
  108. data/lib/aidp/analyze/tool_configuration.rb +0 -456
  109. data/lib/aidp/analyze/tool_modernization.rb +0 -750
  110. data/lib/aidp/database/pg_adapter.rb +0 -148
  111. data/lib/aidp/database_config.rb +0 -69
  112. data/lib/aidp/database_connection.rb +0 -72
  113. data/lib/aidp/database_migration.rb +0 -158
  114. data/lib/aidp/job_manager.rb +0 -41
  115. data/lib/aidp/jobs/base_job.rb +0 -47
  116. data/lib/aidp/jobs/provider_execution_job.rb +0 -96
  117. data/lib/aidp/project_detector.rb +0 -117
  118. data/lib/aidp/providers/agent_supervisor.rb +0 -348
  119. data/lib/aidp/providers/supervised_base.rb +0 -317
  120. data/lib/aidp/providers/supervised_cursor.rb +0 -22
  121. data/lib/aidp/sync.rb +0 -13
  122. 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