teddy-cli 0.1.0__py3-none-any.whl

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 (143) hide show
  1. teddy_cli-0.1.0.dist-info/LICENSE +677 -0
  2. teddy_cli-0.1.0.dist-info/METADATA +33 -0
  3. teddy_cli-0.1.0.dist-info/RECORD +143 -0
  4. teddy_cli-0.1.0.dist-info/WHEEL +4 -0
  5. teddy_cli-0.1.0.dist-info/entry_points.txt +3 -0
  6. teddy_executor/__init__.py +1 -0
  7. teddy_executor/__main__.py +335 -0
  8. teddy_executor/adapters/__init__.py +0 -0
  9. teddy_executor/adapters/inbound/__init__.py +0 -0
  10. teddy_executor/adapters/inbound/cli_formatter.py +107 -0
  11. teddy_executor/adapters/inbound/cli_helpers.py +249 -0
  12. teddy_executor/adapters/inbound/console_plan_reviewer.py +69 -0
  13. teddy_executor/adapters/inbound/session_cli_handlers.py +366 -0
  14. teddy_executor/adapters/inbound/textual_plan_reviewer.py +78 -0
  15. teddy_executor/adapters/inbound/textual_plan_reviewer_app.py +367 -0
  16. teddy_executor/adapters/inbound/textual_plan_reviewer_editor.py +281 -0
  17. teddy_executor/adapters/inbound/textual_plan_reviewer_execution.py +213 -0
  18. teddy_executor/adapters/inbound/textual_plan_reviewer_helpers.py +308 -0
  19. teddy_executor/adapters/inbound/textual_plan_reviewer_logic.py +345 -0
  20. teddy_executor/adapters/inbound/textual_plan_reviewer_previews.py +227 -0
  21. teddy_executor/adapters/inbound/textual_plan_reviewer_widgets.py +246 -0
  22. teddy_executor/adapters/outbound/__init__.py +7 -0
  23. teddy_executor/adapters/outbound/console_interactor.py +212 -0
  24. teddy_executor/adapters/outbound/console_interactor_ask_loop.py +121 -0
  25. teddy_executor/adapters/outbound/console_interactor_helpers.py +95 -0
  26. teddy_executor/adapters/outbound/console_tooling.py +62 -0
  27. teddy_executor/adapters/outbound/filesystem_helpers.py +61 -0
  28. teddy_executor/adapters/outbound/litellm_adapter.py +462 -0
  29. teddy_executor/adapters/outbound/local_file_system_adapter.py +300 -0
  30. teddy_executor/adapters/outbound/local_repo_tree_generator.py +96 -0
  31. teddy_executor/adapters/outbound/openrouter_hydrator.py +89 -0
  32. teddy_executor/adapters/outbound/shell_adapter.py +344 -0
  33. teddy_executor/adapters/outbound/shell_command_builder.py +105 -0
  34. teddy_executor/adapters/outbound/system_environment_adapter.py +62 -0
  35. teddy_executor/adapters/outbound/system_environment_inspector.py +54 -0
  36. teddy_executor/adapters/outbound/system_time_adapter.py +22 -0
  37. teddy_executor/adapters/outbound/web_scraper_adapter.py +346 -0
  38. teddy_executor/adapters/outbound/web_searcher_adapter.py +122 -0
  39. teddy_executor/adapters/outbound/yaml_config_adapter.py +105 -0
  40. teddy_executor/container.py +333 -0
  41. teddy_executor/core/__init__.py +0 -0
  42. teddy_executor/core/domain/__init__.py +0 -0
  43. teddy_executor/core/domain/models/__init__.py +44 -0
  44. teddy_executor/core/domain/models/action_ports.py +28 -0
  45. teddy_executor/core/domain/models/change_set.py +10 -0
  46. teddy_executor/core/domain/models/exceptions.py +40 -0
  47. teddy_executor/core/domain/models/execution_report.py +65 -0
  48. teddy_executor/core/domain/models/orchestrator_ports.py +26 -0
  49. teddy_executor/core/domain/models/plan.py +85 -0
  50. teddy_executor/core/domain/models/planning_ports.py +43 -0
  51. teddy_executor/core/domain/models/project_context.py +56 -0
  52. teddy_executor/core/domain/models/report_assembly_data.py +18 -0
  53. teddy_executor/core/domain/models/session.py +17 -0
  54. teddy_executor/core/domain/models/shell_output.py +12 -0
  55. teddy_executor/core/domain/models/web_search_results.py +26 -0
  56. teddy_executor/core/ports/__init__.py +0 -0
  57. teddy_executor/core/ports/inbound/__init__.py +0 -0
  58. teddy_executor/core/ports/inbound/edit_simulator.py +33 -0
  59. teddy_executor/core/ports/inbound/get_context_use_case.py +32 -0
  60. teddy_executor/core/ports/inbound/init.py +15 -0
  61. teddy_executor/core/ports/inbound/plan_parser.py +52 -0
  62. teddy_executor/core/ports/inbound/plan_reviewer.py +44 -0
  63. teddy_executor/core/ports/inbound/plan_validator.py +26 -0
  64. teddy_executor/core/ports/inbound/planning_use_case.py +30 -0
  65. teddy_executor/core/ports/inbound/run_plan_use_case.py +60 -0
  66. teddy_executor/core/ports/outbound/__init__.py +34 -0
  67. teddy_executor/core/ports/outbound/config_service.py +29 -0
  68. teddy_executor/core/ports/outbound/environment_inspector.py +30 -0
  69. teddy_executor/core/ports/outbound/execution_report_assembler.py +19 -0
  70. teddy_executor/core/ports/outbound/file_system_manager.py +131 -0
  71. teddy_executor/core/ports/outbound/llm_client.py +90 -0
  72. teddy_executor/core/ports/outbound/markdown_report_formatter.py +26 -0
  73. teddy_executor/core/ports/outbound/prompt_manager.py +55 -0
  74. teddy_executor/core/ports/outbound/repo_tree_generator.py +17 -0
  75. teddy_executor/core/ports/outbound/session_loop_guard.py +16 -0
  76. teddy_executor/core/ports/outbound/session_manager.py +97 -0
  77. teddy_executor/core/ports/outbound/session_repository.py +65 -0
  78. teddy_executor/core/ports/outbound/shell_executor.py +24 -0
  79. teddy_executor/core/ports/outbound/system_environment.py +25 -0
  80. teddy_executor/core/ports/outbound/time_service.py +28 -0
  81. teddy_executor/core/ports/outbound/user_interactor.py +126 -0
  82. teddy_executor/core/ports/outbound/web_scraper.py +24 -0
  83. teddy_executor/core/ports/outbound/web_searcher.py +25 -0
  84. teddy_executor/core/services/__init__.py +0 -0
  85. teddy_executor/core/services/action_changeset_builder.py +90 -0
  86. teddy_executor/core/services/action_diff_manager.py +110 -0
  87. teddy_executor/core/services/action_dispatcher.py +142 -0
  88. teddy_executor/core/services/action_executor.py +209 -0
  89. teddy_executor/core/services/action_factory.py +197 -0
  90. teddy_executor/core/services/action_parser_complex.py +216 -0
  91. teddy_executor/core/services/action_parser_strategies.py +84 -0
  92. teddy_executor/core/services/context_service.py +437 -0
  93. teddy_executor/core/services/edit_simulator.py +128 -0
  94. teddy_executor/core/services/execution_orchestrator.py +295 -0
  95. teddy_executor/core/services/execution_report_assembler.py +62 -0
  96. teddy_executor/core/services/init_service.py +80 -0
  97. teddy_executor/core/services/markdown_plan_parser.py +309 -0
  98. teddy_executor/core/services/markdown_report_formatter.py +143 -0
  99. teddy_executor/core/services/parser_infrastructure.py +222 -0
  100. teddy_executor/core/services/parser_metadata.py +153 -0
  101. teddy_executor/core/services/parser_reporting.py +267 -0
  102. teddy_executor/core/services/plan_validator.py +82 -0
  103. teddy_executor/core/services/planning_service.py +242 -0
  104. teddy_executor/core/services/prompt_manager.py +146 -0
  105. teddy_executor/core/services/session_lifecycle_manager.py +228 -0
  106. teddy_executor/core/services/session_loop_guard.py +46 -0
  107. teddy_executor/core/services/session_orchestrator.py +538 -0
  108. teddy_executor/core/services/session_planner.py +43 -0
  109. teddy_executor/core/services/session_pruning_service.py +438 -0
  110. teddy_executor/core/services/session_replanner.py +105 -0
  111. teddy_executor/core/services/session_repository.py +194 -0
  112. teddy_executor/core/services/session_service.py +529 -0
  113. teddy_executor/core/services/templates/execution_report.md.j2 +290 -0
  114. teddy_executor/core/services/validation_rules/__init__.py +4 -0
  115. teddy_executor/core/services/validation_rules/edit.py +207 -0
  116. teddy_executor/core/services/validation_rules/edit_matcher.py +247 -0
  117. teddy_executor/core/services/validation_rules/edit_matcher_heuristics.py +84 -0
  118. teddy_executor/core/services/validation_rules/execute.py +37 -0
  119. teddy_executor/core/services/validation_rules/filesystem.py +73 -0
  120. teddy_executor/core/services/validation_rules/helpers.py +178 -0
  121. teddy_executor/core/services/validation_rules/message.py +29 -0
  122. teddy_executor/core/utils/__init__.py +1 -0
  123. teddy_executor/core/utils/diff.py +57 -0
  124. teddy_executor/core/utils/io.py +75 -0
  125. teddy_executor/core/utils/markdown.py +131 -0
  126. teddy_executor/core/utils/serialization.py +39 -0
  127. teddy_executor/core/utils/string.py +351 -0
  128. teddy_executor/prompts.py +45 -0
  129. teddy_executor/registries/__init__.py +1 -0
  130. teddy_executor/registries/infrastructure.py +147 -0
  131. teddy_executor/registries/reviewer.py +57 -0
  132. teddy_executor/registries/validators.py +47 -0
  133. teddy_executor/resources/__init__.py +1 -0
  134. teddy_executor/resources/config/.gitignore +2 -0
  135. teddy_executor/resources/config/__init__.py +1 -0
  136. teddy_executor/resources/config/config.yaml +49 -0
  137. teddy_executor/resources/config/init.context +5 -0
  138. teddy_executor/resources/config/prompts/architect.xml +462 -0
  139. teddy_executor/resources/config/prompts/assistant.xml +336 -0
  140. teddy_executor/resources/config/prompts/debugger.xml +456 -0
  141. teddy_executor/resources/config/prompts/developer.xml +481 -0
  142. teddy_executor/resources/config/prompts/pathfinder.xml +502 -0
  143. teddy_executor/resources/config/prompts/prototyper.xml +425 -0
@@ -0,0 +1,78 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from typing import TYPE_CHECKING, Optional
5
+
6
+ from teddy_executor.adapters.inbound.textual_plan_reviewer_app import ReviewerApp
7
+ from teddy_executor.core.ports.inbound.plan_reviewer import IPlanReviewer
8
+
9
+ if TYPE_CHECKING:
10
+ from teddy_executor.adapters.outbound.console_tooling import ConsoleToolingHelper
11
+ from teddy_executor.core.domain.models.plan import ActionData, Plan
12
+ from teddy_executor.core.domain.models.project_context import ProjectContext
13
+ from teddy_executor.core.ports.outbound.file_system_manager import (
14
+ IFileSystemManager,
15
+ )
16
+ from teddy_executor.core.ports.outbound.system_environment import ISystemEnvironment
17
+ from teddy_executor.core.services.action_dispatcher import ActionDispatcher
18
+
19
+
20
+ class TextualPlanReviewer(IPlanReviewer):
21
+ """
22
+ Implements IPlanReviewer using the Textual TUI framework.
23
+ """
24
+
25
+ def __init__(
26
+ self,
27
+ system_env: ISystemEnvironment,
28
+ file_system: IFileSystemManager,
29
+ console_tooling: ConsoleToolingHelper,
30
+ action_dispatcher: ActionDispatcher,
31
+ ):
32
+ self._system_env = system_env
33
+ self._file_system = file_system
34
+ self._console_tooling = console_tooling
35
+ self._action_dispatcher = action_dispatcher
36
+
37
+ def review(
38
+ self, plan: Plan, project_context: Optional[ProjectContext] = None
39
+ ) -> Optional[Plan]:
40
+ """
41
+ Initiates the interactive review process using the Textual TUI.
42
+ """
43
+ return self._run_app(plan, project_context=project_context)
44
+
45
+ def review_action(
46
+ self,
47
+ action: "ActionData",
48
+ _total_actions: int,
49
+ agent_name: Optional[str] = None,
50
+ ) -> tuple[bool, str]:
51
+ """
52
+ For the TUI, per-action review is handled in bulk by review_plan.
53
+ This method always returns True to allow the loop to proceed with selections.
54
+ """
55
+ _ = agent_name # Mark as used for vulture
56
+ return True, ""
57
+
58
+ def _run_app(
59
+ self, plan: Plan, project_context: Optional[ProjectContext] = None
60
+ ) -> Optional[Plan]:
61
+ """
62
+ Internal helper to launch the Textual app.
63
+ Separated to allow for easier testing and mocking.
64
+ """
65
+ app = ReviewerApp(
66
+ plan=plan,
67
+ system_env=self._system_env,
68
+ console_tooling=self._console_tooling,
69
+ action_dispatcher=self._action_dispatcher,
70
+ file_system=self._file_system,
71
+ project_context=project_context,
72
+ )
73
+ result = app.run()
74
+ if os.getenv("TEDDY_DEBUG") and result:
75
+ print(
76
+ f"\n[DEBUG] ReviewerApp.run() returned plan with {len(result.actions)} actions."
77
+ )
78
+ return result
@@ -0,0 +1,367 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import os
5
+ from typing import TYPE_CHECKING, Any, Optional, TypeVar, cast
6
+
7
+ from textual import work
8
+ from textual.app import App, ComposeResult
9
+ from textual.binding import Binding
10
+ from textual.containers import Horizontal
11
+ from textual.widgets import ContentSwitcher, Footer, Header, ListView, Markdown, Tree
12
+
13
+ from teddy_executor.adapters.inbound.textual_plan_reviewer_logic import (
14
+ add_message_logic,
15
+ check_action_logic,
16
+ edit_action_logic,
17
+ execute_step_logic,
18
+ on_mount_logic,
19
+ on_tree_node_highlighted,
20
+ refresh_node_logic,
21
+ revert_logic,
22
+ toggle_all_logic,
23
+ toggle_selection_logic,
24
+ view_details_logic,
25
+ view_plan_logic,
26
+ )
27
+ from teddy_executor.adapters.inbound.textual_plan_reviewer_widgets import (
28
+ ActionTree,
29
+ ParameterDetail,
30
+ RationaleDetail,
31
+ TUI_CSS,
32
+ )
33
+ from teddy_executor.core.services.edit_simulator import EditSimulator
34
+
35
+ logger = logging.getLogger(__name__)
36
+
37
+ if TYPE_CHECKING:
38
+ from teddy_executor.core.domain.models.plan import Plan
39
+ from teddy_executor.core.domain.models.project_context import ProjectContext
40
+ from teddy_executor.core.ports.outbound.system_environment import ISystemEnvironment
41
+ from teddy_executor.core.ports.outbound.file_system_manager import (
42
+ IFileSystemManager,
43
+ )
44
+ from teddy_executor.adapters.outbound.console_tooling import ConsoleToolingHelper
45
+ from teddy_executor.core.services.action_dispatcher import ActionDispatcher
46
+
47
+ T = TypeVar("T")
48
+
49
+
50
+ class ReviewerApp(App):
51
+ """
52
+ The Textual application for reviewing and modifying plans.
53
+ """
54
+
55
+ INSTRUCTION_MARKER = "\n\n<!-- Please enter your response above this line. -->"
56
+
57
+ BINDINGS = [
58
+ ("s", "submit", "Submit"),
59
+ ("a", "toggle_all", "Toggle All"),
60
+ Binding("ctrl+down", "jump_next", "Next Section", show=False),
61
+ Binding("alt+down", "jump_next", "Next Section", show=False),
62
+ Binding("shift+down", "jump_next", "Next Section", show=False),
63
+ Binding("ctrl+up", "jump_prev", "Prev Section", show=False),
64
+ Binding("alt+up", "jump_prev", "Prev Section", show=False),
65
+ Binding("shift+up", "jump_prev", "Prev Section", show=False),
66
+ ("e", "edit_details", "Editor"),
67
+ ("d", "view_details", "Details"),
68
+ ("r", "revert", "Revert"),
69
+ ("v", "view_plan", "View Plan"),
70
+ ("x", "execute_step", "Execute Step"),
71
+ ("m", "add_message", "Add Message"),
72
+ ("q", "cancel", "Quit"),
73
+ ("left", "focus_left", "Focus Left"),
74
+ ("right", "focus_right", "Focus Right"),
75
+ Binding("tab", "focus_next", "Focus Next", show=False, priority=True),
76
+ Binding("shift+tab", "focus_prev", "Focus Prev", show=False, priority=True),
77
+ ]
78
+
79
+ CSS = TUI_CSS
80
+
81
+ def __init__( # noqa: PLR0913
82
+ self,
83
+ plan: Plan,
84
+ system_env: ISystemEnvironment,
85
+ console_tooling: ConsoleToolingHelper,
86
+ action_dispatcher: ActionDispatcher,
87
+ file_system: Optional[IFileSystemManager] = None,
88
+ project_context: Optional[ProjectContext] = None,
89
+ ):
90
+ super().__init__()
91
+ self.plan = plan
92
+ self.project_context = project_context
93
+ self._system_env = system_env
94
+ self._console_tooling = console_tooling
95
+ self._action_dispatcher = action_dispatcher
96
+ self._file_system = file_system
97
+ self._edit_simulator = EditSimulator()
98
+ self._user_message_cache: Optional[str] = None
99
+ self._log_preview_files: list[str] = []
100
+
101
+ def compose(self) -> ComposeResult:
102
+ """
103
+ Create child widgets for the app.
104
+ """
105
+ yield Header(show_clock=True)
106
+ with Horizontal(id="main-container"):
107
+ yield ActionTree("Action Plan", id="left-pane")
108
+ with ContentSwitcher(id="right-pane", initial="params-view"):
109
+ yield ParameterDetail(id="params-view")
110
+ rationale_view = RationaleDetail(id="rationale-view")
111
+ rationale_view.can_focus = True
112
+ with rationale_view:
113
+ yield Markdown(id="rationale-content")
114
+ yield Footer()
115
+
116
+ def on_mount(self) -> None:
117
+ """Populate the action tree when the app is mounted."""
118
+ on_mount_logic(self)
119
+
120
+ def on_tree_node_selected(self, event: Tree.NodeSelected) -> None:
121
+ """Toggle action selection when a node is selected."""
122
+ toggle_selection_logic(self, event.node)
123
+
124
+ def on_tree_node_highlighted(self, event: Tree.NodeHighlighted) -> None:
125
+ """Refresh footer bindings when a new node is highlighted."""
126
+ on_tree_node_highlighted(self, event)
127
+
128
+ @work
129
+ async def on_list_view_selected(self, event: ListView.Selected) -> None:
130
+ """Handle parameter editing when an item is selected in the right pane."""
131
+ from teddy_executor.adapters.inbound.textual_plan_reviewer_logic import (
132
+ on_list_view_selected_logic,
133
+ )
134
+
135
+ await on_list_view_selected_logic(self, event.item)
136
+
137
+ def on_descendant_focus(self, event: Any) -> None:
138
+ """Ensure selection state is maintained or initialized when focus moves."""
139
+ control = getattr(event, "control", None)
140
+ if control and getattr(control, "id", None) in ("right-pane", "params-view"):
141
+ list_view = self.query_one(ParameterDetail)
142
+ # Only reset to 0 if no item is currently selected (e.g. first focus after clear)
143
+ if list_view.children and list_view.index is None:
144
+ list_view.index = 0
145
+
146
+ def check_action(self, action: str, parameters: tuple[Any, ...]) -> bool:
147
+ """Gate for enabling/disabling bindings based on state."""
148
+ _ = parameters
149
+ return check_action_logic(self, action)
150
+
151
+ def action_revert(self) -> None:
152
+ """Revert manual modifications for the currently highlighted action."""
153
+ tree = self.query_one(Tree)
154
+ if tree.cursor_node:
155
+ revert_logic(self, tree.cursor_node)
156
+
157
+ @work(exclusive=True)
158
+ async def action_execute_step(self) -> None:
159
+ """Executes the currently highlighted action as a background worker."""
160
+ tree = self.query_one(Tree)
161
+ if tree.cursor_node:
162
+ await execute_step_logic(self, tree.cursor_node)
163
+
164
+ def action_submit(self) -> None:
165
+ """Exit the app and return the modified plan."""
166
+ # Harvest deferred changes from pending_temp_files
167
+ for action in self.plan.actions:
168
+ self._harvest_action_content(action)
169
+
170
+ self._finalize_user_message()
171
+
172
+ if self.project_context:
173
+ pruned_paths = [
174
+ item.path for item in self.project_context.items if not item.selected
175
+ ]
176
+ if pruned_paths:
177
+ self.plan.metadata["pruned_context"] = ",".join(pruned_paths)
178
+ else:
179
+ self.plan.metadata.pop("pruned_context", None)
180
+
181
+ for f in getattr(self, "_log_preview_files", []):
182
+ try:
183
+ self._system_env.delete_file(f)
184
+ except Exception as e:
185
+ logger.debug("Failed to delete temporary log preview file %s: %s", f, e)
186
+
187
+ self.exit(self.plan)
188
+
189
+ def action_cancel(self) -> None:
190
+ """Exit the app and return None (cancellation)."""
191
+ # Harvest message even on cancel so it can be propagated to the abort report
192
+ self._finalize_user_message()
193
+
194
+ # Cleanup any pending temp files
195
+ for action in self.plan.actions:
196
+ # Type guard for Mocks in tests
197
+ is_valid_path = isinstance(action.pending_temp_file, (str, os.PathLike))
198
+ if (
199
+ action.pending_temp_file
200
+ and is_valid_path
201
+ and os.path.exists(action.pending_temp_file)
202
+ ):
203
+ try:
204
+ os.remove(action.pending_temp_file)
205
+ action.pending_temp_file = None
206
+ except Exception as e:
207
+ logger.debug(
208
+ "Failed to remove pending temp file %s: %s",
209
+ action.pending_temp_file,
210
+ e,
211
+ )
212
+
213
+ for f in getattr(self, "_log_preview_files", []):
214
+ try:
215
+ self._system_env.delete_file(f)
216
+ except Exception as e:
217
+ logger.debug("Failed to delete temporary log preview file %s: %s", f, e)
218
+
219
+ self.exit(None)
220
+
221
+ @work
222
+ async def action_edit_details(self) -> None:
223
+ """Edit or preview the currently highlighted action or parameter."""
224
+ tree = self.query_one(Tree)
225
+ node = tree.cursor_node
226
+ if not node or not node.data:
227
+ return
228
+
229
+ from teddy_executor.core.domain.models.plan import ActionData
230
+
231
+ if isinstance(node.data, ActionData) and node.data.executed:
232
+ # Edit is disabled for executed actions; redirect to view_details
233
+ await cast(Any, self.action_view_details())
234
+ return
235
+
236
+ # Check if the right pane or any of its children has focus
237
+ right_pane = self.query_one(ParameterDetail)
238
+ is_right_pane_focused = right_pane.has_focus or (
239
+ self.focused and self.focused in right_pane.query("*")
240
+ )
241
+
242
+ if is_right_pane_focused and right_pane.highlighted_child:
243
+ from teddy_executor.adapters.inbound.textual_plan_reviewer_logic import (
244
+ on_list_view_selected_logic,
245
+ )
246
+
247
+ await on_list_view_selected_logic(self, right_pane.highlighted_child)
248
+ return
249
+
250
+ await edit_action_logic(self, node, node.data)
251
+
252
+ @work
253
+ async def action_view_details(self) -> None:
254
+ """View full execution logs or complex action details in an editor."""
255
+ await view_details_logic(self)
256
+
257
+ @work
258
+ async def action_view_plan(self) -> None:
259
+ """Open the full plan.md in an external editor."""
260
+ await view_plan_logic(self)
261
+
262
+ @work
263
+ async def action_add_message(self) -> None:
264
+ """Open the external editor to add/edit the user instruction message."""
265
+ await add_message_logic(self)
266
+
267
+ def action_focus_left(self) -> None:
268
+ """Switch focus to the Action Tree."""
269
+ self.query_one("#left-pane").focus()
270
+
271
+ def action_focus_right(self) -> None:
272
+ """Switch focus to the active child of the Parameter Detail pane."""
273
+ switcher = self.query_one(ContentSwitcher)
274
+ if switcher.current:
275
+ self.query_one(f"#{switcher.current}").focus()
276
+
277
+ def action_focus_next(self) -> None:
278
+ """Cycle focus between main panes."""
279
+ # Standard focus cycle
280
+ if self.focused and self.focused.id == "left-pane":
281
+ self.action_focus_right()
282
+ else:
283
+ self.action_focus_left()
284
+
285
+ def action_focus_prev(self) -> None:
286
+ """Cycle focus between main panes (reverse)."""
287
+ self.action_focus_next()
288
+
289
+ def action_jump_next(self) -> None:
290
+ """Jump to the next major section root."""
291
+ tree = self.query_one(ActionTree)
292
+ sections = [
293
+ ActionTree.CONTEXT_ROOT,
294
+ ActionTree.RATIONALE_ROOT,
295
+ ActionTree.ACTION_PLAN_ROOT,
296
+ ]
297
+
298
+ # Find current section of cursor
299
+ current_section = None
300
+ node = tree.cursor_node
301
+ while node:
302
+ if node.data in sections:
303
+ current_section = node.data
304
+ break
305
+ node = node.parent
306
+
307
+ # Find next section index
308
+ try:
309
+ current_idx = sections.index(current_section) if current_section else -1
310
+ next_idx = (current_idx + 1) % len(sections)
311
+ tree.jump_to_section(sections[next_idx])
312
+ except (ValueError, IndexError):
313
+ tree.jump_to_section(sections[0])
314
+ tree.focus()
315
+
316
+ def action_jump_prev(self) -> None:
317
+ """Jump to the current section root or the previous one."""
318
+ tree = self.query_one(ActionTree)
319
+ sections = [
320
+ ActionTree.CONTEXT_ROOT,
321
+ ActionTree.RATIONALE_ROOT,
322
+ ActionTree.ACTION_PLAN_ROOT,
323
+ ]
324
+
325
+ node = tree.cursor_node
326
+ if not node:
327
+ tree.jump_to_section(sections[-1])
328
+ tree.focus()
329
+ return
330
+
331
+ # If we are ALREADY on a root, jump to the previous one
332
+ if node.data in sections:
333
+ current_idx = sections.index(node.data)
334
+ prev_idx = (current_idx - 1) % len(sections)
335
+ tree.jump_to_section(sections[prev_idx])
336
+ else:
337
+ # If we are on a child, jump to the CURRENT root
338
+ while node:
339
+ if node.data in sections:
340
+ tree.move_cursor(node)
341
+ break
342
+ node = node.parent
343
+ tree.focus()
344
+
345
+ def action_toggle_all(self) -> None:
346
+ """Toggle selection for all actions."""
347
+ toggle_all_logic(self, self.plan)
348
+
349
+ def _refresh_node(self, node: Any) -> None:
350
+ """Refresh the label and state of a single tree node."""
351
+ refresh_node_logic(self, node)
352
+
353
+ def _harvest_action_content(self, action: Any) -> None:
354
+ """Harvest modified content from a pending temporary file back to the action."""
355
+ from teddy_executor.adapters.inbound.textual_plan_reviewer_execution import (
356
+ harvest_action_content,
357
+ )
358
+
359
+ harvest_action_content(action, self.INSTRUCTION_MARKER)
360
+
361
+ def _finalize_user_message(self) -> None:
362
+ """Extracts final message from cache, stripping marker."""
363
+ if self._user_message_cache is None:
364
+ return
365
+ marker = self.INSTRUCTION_MARKER.strip()
366
+ msg = self._user_message_cache
367
+ self.plan.metadata["user_request"] = msg.split(marker)[0].strip()