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.
- teddy_cli-0.1.0.dist-info/LICENSE +677 -0
- teddy_cli-0.1.0.dist-info/METADATA +33 -0
- teddy_cli-0.1.0.dist-info/RECORD +143 -0
- teddy_cli-0.1.0.dist-info/WHEEL +4 -0
- teddy_cli-0.1.0.dist-info/entry_points.txt +3 -0
- teddy_executor/__init__.py +1 -0
- teddy_executor/__main__.py +335 -0
- teddy_executor/adapters/__init__.py +0 -0
- teddy_executor/adapters/inbound/__init__.py +0 -0
- teddy_executor/adapters/inbound/cli_formatter.py +107 -0
- teddy_executor/adapters/inbound/cli_helpers.py +249 -0
- teddy_executor/adapters/inbound/console_plan_reviewer.py +69 -0
- teddy_executor/adapters/inbound/session_cli_handlers.py +366 -0
- teddy_executor/adapters/inbound/textual_plan_reviewer.py +78 -0
- teddy_executor/adapters/inbound/textual_plan_reviewer_app.py +367 -0
- teddy_executor/adapters/inbound/textual_plan_reviewer_editor.py +281 -0
- teddy_executor/adapters/inbound/textual_plan_reviewer_execution.py +213 -0
- teddy_executor/adapters/inbound/textual_plan_reviewer_helpers.py +308 -0
- teddy_executor/adapters/inbound/textual_plan_reviewer_logic.py +345 -0
- teddy_executor/adapters/inbound/textual_plan_reviewer_previews.py +227 -0
- teddy_executor/adapters/inbound/textual_plan_reviewer_widgets.py +246 -0
- teddy_executor/adapters/outbound/__init__.py +7 -0
- teddy_executor/adapters/outbound/console_interactor.py +212 -0
- teddy_executor/adapters/outbound/console_interactor_ask_loop.py +121 -0
- teddy_executor/adapters/outbound/console_interactor_helpers.py +95 -0
- teddy_executor/adapters/outbound/console_tooling.py +62 -0
- teddy_executor/adapters/outbound/filesystem_helpers.py +61 -0
- teddy_executor/adapters/outbound/litellm_adapter.py +462 -0
- teddy_executor/adapters/outbound/local_file_system_adapter.py +300 -0
- teddy_executor/adapters/outbound/local_repo_tree_generator.py +96 -0
- teddy_executor/adapters/outbound/openrouter_hydrator.py +89 -0
- teddy_executor/adapters/outbound/shell_adapter.py +344 -0
- teddy_executor/adapters/outbound/shell_command_builder.py +105 -0
- teddy_executor/adapters/outbound/system_environment_adapter.py +62 -0
- teddy_executor/adapters/outbound/system_environment_inspector.py +54 -0
- teddy_executor/adapters/outbound/system_time_adapter.py +22 -0
- teddy_executor/adapters/outbound/web_scraper_adapter.py +346 -0
- teddy_executor/adapters/outbound/web_searcher_adapter.py +122 -0
- teddy_executor/adapters/outbound/yaml_config_adapter.py +105 -0
- teddy_executor/container.py +333 -0
- teddy_executor/core/__init__.py +0 -0
- teddy_executor/core/domain/__init__.py +0 -0
- teddy_executor/core/domain/models/__init__.py +44 -0
- teddy_executor/core/domain/models/action_ports.py +28 -0
- teddy_executor/core/domain/models/change_set.py +10 -0
- teddy_executor/core/domain/models/exceptions.py +40 -0
- teddy_executor/core/domain/models/execution_report.py +65 -0
- teddy_executor/core/domain/models/orchestrator_ports.py +26 -0
- teddy_executor/core/domain/models/plan.py +85 -0
- teddy_executor/core/domain/models/planning_ports.py +43 -0
- teddy_executor/core/domain/models/project_context.py +56 -0
- teddy_executor/core/domain/models/report_assembly_data.py +18 -0
- teddy_executor/core/domain/models/session.py +17 -0
- teddy_executor/core/domain/models/shell_output.py +12 -0
- teddy_executor/core/domain/models/web_search_results.py +26 -0
- teddy_executor/core/ports/__init__.py +0 -0
- teddy_executor/core/ports/inbound/__init__.py +0 -0
- teddy_executor/core/ports/inbound/edit_simulator.py +33 -0
- teddy_executor/core/ports/inbound/get_context_use_case.py +32 -0
- teddy_executor/core/ports/inbound/init.py +15 -0
- teddy_executor/core/ports/inbound/plan_parser.py +52 -0
- teddy_executor/core/ports/inbound/plan_reviewer.py +44 -0
- teddy_executor/core/ports/inbound/plan_validator.py +26 -0
- teddy_executor/core/ports/inbound/planning_use_case.py +30 -0
- teddy_executor/core/ports/inbound/run_plan_use_case.py +60 -0
- teddy_executor/core/ports/outbound/__init__.py +34 -0
- teddy_executor/core/ports/outbound/config_service.py +29 -0
- teddy_executor/core/ports/outbound/environment_inspector.py +30 -0
- teddy_executor/core/ports/outbound/execution_report_assembler.py +19 -0
- teddy_executor/core/ports/outbound/file_system_manager.py +131 -0
- teddy_executor/core/ports/outbound/llm_client.py +90 -0
- teddy_executor/core/ports/outbound/markdown_report_formatter.py +26 -0
- teddy_executor/core/ports/outbound/prompt_manager.py +55 -0
- teddy_executor/core/ports/outbound/repo_tree_generator.py +17 -0
- teddy_executor/core/ports/outbound/session_loop_guard.py +16 -0
- teddy_executor/core/ports/outbound/session_manager.py +97 -0
- teddy_executor/core/ports/outbound/session_repository.py +65 -0
- teddy_executor/core/ports/outbound/shell_executor.py +24 -0
- teddy_executor/core/ports/outbound/system_environment.py +25 -0
- teddy_executor/core/ports/outbound/time_service.py +28 -0
- teddy_executor/core/ports/outbound/user_interactor.py +126 -0
- teddy_executor/core/ports/outbound/web_scraper.py +24 -0
- teddy_executor/core/ports/outbound/web_searcher.py +25 -0
- teddy_executor/core/services/__init__.py +0 -0
- teddy_executor/core/services/action_changeset_builder.py +90 -0
- teddy_executor/core/services/action_diff_manager.py +110 -0
- teddy_executor/core/services/action_dispatcher.py +142 -0
- teddy_executor/core/services/action_executor.py +209 -0
- teddy_executor/core/services/action_factory.py +197 -0
- teddy_executor/core/services/action_parser_complex.py +216 -0
- teddy_executor/core/services/action_parser_strategies.py +84 -0
- teddy_executor/core/services/context_service.py +437 -0
- teddy_executor/core/services/edit_simulator.py +128 -0
- teddy_executor/core/services/execution_orchestrator.py +295 -0
- teddy_executor/core/services/execution_report_assembler.py +62 -0
- teddy_executor/core/services/init_service.py +80 -0
- teddy_executor/core/services/markdown_plan_parser.py +309 -0
- teddy_executor/core/services/markdown_report_formatter.py +143 -0
- teddy_executor/core/services/parser_infrastructure.py +222 -0
- teddy_executor/core/services/parser_metadata.py +153 -0
- teddy_executor/core/services/parser_reporting.py +267 -0
- teddy_executor/core/services/plan_validator.py +82 -0
- teddy_executor/core/services/planning_service.py +242 -0
- teddy_executor/core/services/prompt_manager.py +146 -0
- teddy_executor/core/services/session_lifecycle_manager.py +228 -0
- teddy_executor/core/services/session_loop_guard.py +46 -0
- teddy_executor/core/services/session_orchestrator.py +538 -0
- teddy_executor/core/services/session_planner.py +43 -0
- teddy_executor/core/services/session_pruning_service.py +438 -0
- teddy_executor/core/services/session_replanner.py +105 -0
- teddy_executor/core/services/session_repository.py +194 -0
- teddy_executor/core/services/session_service.py +529 -0
- teddy_executor/core/services/templates/execution_report.md.j2 +290 -0
- teddy_executor/core/services/validation_rules/__init__.py +4 -0
- teddy_executor/core/services/validation_rules/edit.py +207 -0
- teddy_executor/core/services/validation_rules/edit_matcher.py +247 -0
- teddy_executor/core/services/validation_rules/edit_matcher_heuristics.py +84 -0
- teddy_executor/core/services/validation_rules/execute.py +37 -0
- teddy_executor/core/services/validation_rules/filesystem.py +73 -0
- teddy_executor/core/services/validation_rules/helpers.py +178 -0
- teddy_executor/core/services/validation_rules/message.py +29 -0
- teddy_executor/core/utils/__init__.py +1 -0
- teddy_executor/core/utils/diff.py +57 -0
- teddy_executor/core/utils/io.py +75 -0
- teddy_executor/core/utils/markdown.py +131 -0
- teddy_executor/core/utils/serialization.py +39 -0
- teddy_executor/core/utils/string.py +351 -0
- teddy_executor/prompts.py +45 -0
- teddy_executor/registries/__init__.py +1 -0
- teddy_executor/registries/infrastructure.py +147 -0
- teddy_executor/registries/reviewer.py +57 -0
- teddy_executor/registries/validators.py +47 -0
- teddy_executor/resources/__init__.py +1 -0
- teddy_executor/resources/config/.gitignore +2 -0
- teddy_executor/resources/config/__init__.py +1 -0
- teddy_executor/resources/config/config.yaml +49 -0
- teddy_executor/resources/config/init.context +5 -0
- teddy_executor/resources/config/prompts/architect.xml +462 -0
- teddy_executor/resources/config/prompts/assistant.xml +336 -0
- teddy_executor/resources/config/prompts/debugger.xml +456 -0
- teddy_executor/resources/config/prompts/developer.xml +481 -0
- teddy_executor/resources/config/prompts/pathfinder.xml +502 -0
- 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()
|