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,281 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import os
5
+ import pathlib
6
+ from typing import TYPE_CHECKING, Any, Optional, cast
7
+
8
+ if TYPE_CHECKING:
9
+ from teddy_executor.adapters.inbound.textual_plan_reviewer_app import ReviewerApp
10
+ from teddy_executor.core.domain.models.plan import ActionData
11
+
12
+ from teddy_executor.adapters.inbound.textual_plan_reviewer_widgets import (
13
+ ConfirmScreen,
14
+ )
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ # Low-level editor helpers
20
+ def handle_mock_editor(path: Any, output: str) -> str:
21
+ """Helper for mock editor output in tests."""
22
+ if path and isinstance(path, (str, os.PathLike)):
23
+ with open(path, "w", encoding="utf-8") as f:
24
+ f.write(output)
25
+ return output
26
+
27
+
28
+ def spawn_editor(cmd: list[str], path: Any) -> None:
29
+ """Spawns an external editor process."""
30
+ import subprocess # nosec B404
31
+
32
+ try:
33
+ subprocess.Popen( # nosec B603
34
+ cmd + [str(path)],
35
+ stdin=subprocess.DEVNULL,
36
+ stdout=subprocess.DEVNULL,
37
+ stderr=subprocess.DEVNULL,
38
+ )
39
+ except Exception as e:
40
+ logger.debug("Failed to spawn editor: %s", e)
41
+
42
+
43
+ def handle_mock_diff(p_file: Any, before: str, delete_fn: Any) -> bool:
44
+ """Helper for mock diff output in tests."""
45
+ mock_out = os.environ.get("TEDDY_TEST_MOCK_EDITOR_OUTPUT")
46
+ if mock_out:
47
+ with open(p_file, "w", encoding="utf-8") as f:
48
+ f.write(mock_out)
49
+ delete_fn(before)
50
+ return True
51
+ return False
52
+
53
+
54
+ def prepare_after_file(path: Any, proposed: str) -> None:
55
+ """Ensures the 'after' file is ready for diffing/editing."""
56
+ if os.path.exists(path):
57
+ os.chmod(path, 0o644)
58
+ if not os.path.exists(path) or os.path.getsize(path) == 0:
59
+ with open(path, "w", encoding="utf-8") as f:
60
+ f.write(str(proposed))
61
+
62
+
63
+ def harvest_edit_diff(action: Any, p_file: Any, original: str, proposed: str) -> None:
64
+ """Helper to harvest diff results and update action params."""
65
+ try:
66
+ with open(p_file, "r", encoding="utf-8") as f:
67
+ final: Optional[str] = f.read()
68
+ except Exception:
69
+ final = None
70
+ if final is not None and str(final) != str(proposed):
71
+ action.params["edits"] = [{"find": original, "replace": str(final)}]
72
+ action.params.pop("content", None)
73
+
74
+
75
+ async def launch_editor(
76
+ app: "ReviewerApp",
77
+ initial_content: str,
78
+ suffix: str = ".txt",
79
+ persistent_path: Optional[str] = None,
80
+ skip_confirm: bool = False,
81
+ ) -> Optional[str]:
82
+ """Launches an external editor non-blockingly and waits for TUI confirmation."""
83
+ mock_out = os.environ.get("TEDDY_TEST_MOCK_EDITOR_OUTPUT")
84
+ temp_file = persistent_path or app._system_env.create_temp_file(suffix=suffix)
85
+ is_temp = persistent_path is None
86
+
87
+ if mock_out:
88
+ handle_mock_editor(temp_file, mock_out)
89
+ confirmed = (
90
+ True
91
+ if app.is_headless or skip_confirm
92
+ else await app.push_screen_wait(ConfirmScreen())
93
+ )
94
+ return mock_out if confirmed else None
95
+
96
+ try:
97
+ if is_temp or (
98
+ not os.path.exists(temp_file) or os.path.getsize(temp_file) == 0
99
+ ):
100
+ with open(temp_file, "w", encoding="utf-8") as f:
101
+ f.write(str(initial_content))
102
+
103
+ editor_cmd = app._console_tooling.find_editor()
104
+ if not editor_cmd:
105
+ return None
106
+
107
+ if os.path.exists(temp_file):
108
+ os.chmod(temp_file, 0o644)
109
+
110
+ spawn_editor(editor_cmd, temp_file)
111
+ return await _confirm_and_harvest(
112
+ app, temp_file, initial_content, is_temp, skip_confirm=skip_confirm
113
+ )
114
+ except Exception as e:
115
+ logger.debug("Failed to launch editor flow: %s", e)
116
+ return None
117
+ finally:
118
+ if is_temp:
119
+ app._system_env.delete_file(temp_file)
120
+
121
+
122
+ async def _confirm_and_harvest(
123
+ app: ReviewerApp, path: Any, initial: str, is_temp: bool, skip_confirm: bool = False
124
+ ) -> Optional[str]:
125
+ confirmed = (
126
+ True
127
+ if app.is_headless or skip_confirm
128
+ else await app.push_screen_wait(ConfirmScreen())
129
+ )
130
+ if confirmed:
131
+ with open(path, "r", encoding="utf-8") as f:
132
+ return f.read()
133
+ if not is_temp:
134
+ with open(path, "w", encoding="utf-8") as f:
135
+ f.write(str(initial))
136
+ return None
137
+
138
+
139
+ async def preview_edit_diff_viewer(
140
+ app: ReviewerApp,
141
+ action: ActionData,
142
+ diff_viewer: list[str],
143
+ original: str,
144
+ proposed: str,
145
+ ) -> bool:
146
+ import subprocess # nosec B404
147
+
148
+ path_str = cast(str, action.params.get("path", ""))
149
+ before = _setup_before_file(app, path_str, original)
150
+ p_file = action.pending_temp_file
151
+
152
+ if p_file and isinstance(p_file, (str, os.PathLike)):
153
+ if handle_mock_diff(p_file, before, app._system_env.delete_file):
154
+ return True
155
+ prepare_after_file(p_file, proposed)
156
+ try:
157
+ subprocess.Popen( # nosec B603
158
+ diff_viewer + [str(before), str(p_file)],
159
+ stdin=subprocess.DEVNULL,
160
+ stdout=subprocess.DEVNULL,
161
+ stderr=subprocess.DEVNULL,
162
+ )
163
+ except Exception as e:
164
+ logger.debug("Failed to launch diff viewer: %s", e)
165
+
166
+ confirmed = True if app.is_headless else await app.push_screen_wait(ConfirmScreen())
167
+ app._system_env.delete_file(before)
168
+ return _process_diff_result(confirmed, action, p_file, original, proposed)
169
+
170
+
171
+ def _setup_before_file(app: ReviewerApp, path: str, content: str) -> str:
172
+ suffix = pathlib.Path(path).suffix or ".txt"
173
+ before = app._system_env.create_temp_file(suffix=f".before{suffix}")
174
+ with open(before, "w", encoding="utf-8") as f:
175
+ f.write(content)
176
+ os.chmod(before, 0o444)
177
+ return before
178
+
179
+
180
+ def _process_diff_result(
181
+ confirmed: bool, action: ActionData, p_file: Any, original: str, proposed: str
182
+ ) -> bool:
183
+ if confirmed and p_file and isinstance(p_file, (str, os.PathLike)):
184
+ action.modified = True
185
+ if "edits" not in action.modified_fields:
186
+ action.modified_fields.append("edits")
187
+ harvest_edit_diff(action, p_file, original, proposed)
188
+ return True
189
+ if not confirmed and p_file and isinstance(p_file, (str, os.PathLike)):
190
+ with open(p_file, "w", encoding="utf-8") as f:
191
+ f.write(str(proposed))
192
+ return False
193
+
194
+
195
+ async def handle_list_view_selected(
196
+ app: "ReviewerApp", item: Any, update_fn: Any
197
+ ) -> None:
198
+ """Handle parameter editing when a DetailItem is selected in the right pane."""
199
+ from teddy_executor.adapters.inbound.textual_plan_reviewer_widgets import (
200
+ ActionTree,
201
+ PathInputScreen,
202
+ ParameterEditModal,
203
+ )
204
+
205
+ node = app.query_one(ActionTree).cursor_node
206
+ if not node or not node.data or not hasattr(item, "data"):
207
+ return
208
+
209
+ action, key, val = node.data, item.data.get("key"), item.data.get("val")
210
+ from teddy_executor.core.domain.models.plan import ActionData
211
+
212
+ if not isinstance(action, ActionData) or action.executed:
213
+ return
214
+
215
+ if key == "path":
216
+ new_val = await cast(Any, app.push_screen_wait(PathInputScreen(str(val))))
217
+ else:
218
+ if not isinstance(val, (str, int, float, bool, list)) and val is not None:
219
+ return
220
+ v_str = ", ".join(map(str, val)) if isinstance(val, list) else str(val)
221
+ new_val = await cast(
222
+ Any, app.push_screen_wait(ParameterEditModal(f"{key}:", v_str))
223
+ )
224
+
225
+ if new_val is not None and str(new_val) != str(val):
226
+ from teddy_executor.adapters.inbound.textual_plan_reviewer_helpers import (
227
+ _apply_param_edit,
228
+ )
229
+
230
+ _apply_param_edit(action, key, new_val)
231
+ action.modified = True
232
+ if key and key not in action.modified_fields:
233
+ action.modified_fields.append(key)
234
+ app._refresh_node(node)
235
+ update_fn(app, action)
236
+
237
+
238
+ async def handle_edit_action(
239
+ app: "ReviewerApp", node: Any, action: Any, update_fn: Any
240
+ ) -> None:
241
+ """Handles the (e)dit key logic by branching to modals or external editor."""
242
+ from teddy_executor.adapters.inbound.textual_plan_reviewer_widgets import (
243
+ ParameterEditModal,
244
+ )
245
+ from teddy_executor.adapters.inbound.textual_plan_reviewer_previews import (
246
+ do_preview_logic,
247
+ )
248
+
249
+ if action.type == "EXECUTE":
250
+ val = action.params.get("command", "")
251
+ new_val = await cast(
252
+ Any, app.push_screen_wait(ParameterEditModal("Command:", val))
253
+ )
254
+ if new_val is not None and new_val != val:
255
+ action.params["command"] = new_val
256
+ action.modified = True
257
+ if "command" not in action.modified_fields:
258
+ action.modified_fields.append("command")
259
+ app._refresh_node(node)
260
+ update_fn(app, action)
261
+ elif action.type == "RESEARCH":
262
+ val = action.params.get("queries", [])
263
+ val_str = ", ".join(val) if isinstance(val, list) else str(val)
264
+ new_val = await cast(
265
+ Any,
266
+ app.push_screen_wait(
267
+ ParameterEditModal("Queries (comma separated):", val_str)
268
+ ),
269
+ )
270
+ if new_val is not None and new_val != val_str:
271
+ action.params["queries"] = [
272
+ q.strip() for q in new_val.split(",") if q.strip()
273
+ ]
274
+ action.modified = True
275
+ if "queries" not in action.modified_fields:
276
+ action.modified_fields.append("queries")
277
+ app._refresh_node(node)
278
+ update_fn(app, action)
279
+ else:
280
+ await do_preview_logic(app, node, action)
281
+ update_fn(app, action)
@@ -0,0 +1,213 @@
1
+ from __future__ import annotations
2
+
3
+ import contextlib
4
+ import io
5
+ import logging
6
+ from typing import TYPE_CHECKING, Any
7
+
8
+ if TYPE_CHECKING:
9
+ from teddy_executor.adapters.inbound.textual_plan_reviewer_app import ReviewerApp
10
+ from teddy_executor.core.domain.models.plan import ActionData
11
+ from teddy_executor.core.domain.models.execution_report import ActionLog
12
+
13
+
14
+ def harvest_action_content(action: Any, instruction_marker: str) -> None:
15
+ """Harvest modified content from a pending temporary file back to the action."""
16
+ import os
17
+
18
+ is_valid_path = isinstance(action.pending_temp_file, (str, os.PathLike))
19
+ if not (
20
+ action.pending_temp_file
21
+ and is_valid_path
22
+ and os.path.exists(action.pending_temp_file)
23
+ ):
24
+ return
25
+ try:
26
+ with open(action.pending_temp_file, "r", encoding="utf-8") as f:
27
+ new_content = f.read()
28
+ mapping = {"CREATE": "content", "EXECUTE": "command", "RESEARCH": "queries"}
29
+ if action.type in mapping:
30
+ action.params[mapping[action.type]] = new_content
31
+ os.remove(action.pending_temp_file)
32
+ action.pending_temp_file = None
33
+ except Exception as e:
34
+ import logging
35
+
36
+ logging.getLogger(__name__).debug(
37
+ "Failed to harvest action content from %s: %s", action.pending_temp_file, e
38
+ )
39
+
40
+
41
+ def format_action_log(log: ActionLog) -> str:
42
+ """
43
+ Formats an ActionLog entry using the global MarkdownReportFormatter to ensure
44
+ exact formatting consistency with the final execution report.
45
+ """
46
+ from datetime import datetime, timezone
47
+
48
+ from teddy_executor.core.domain.models.execution_report import (
49
+ ExecutionReport,
50
+ RunStatus,
51
+ RunSummary,
52
+ )
53
+ from teddy_executor.core.services.markdown_report_formatter import (
54
+ MarkdownReportFormatter,
55
+ )
56
+
57
+ now = datetime.now(timezone.utc)
58
+ # Map the action status to a run status for the synthetic report summary
59
+ run_status = RunStatus.SUCCESS
60
+ if log.status.value in ["FAILURE", "SKIPPED"]:
61
+ # Fallback mapping if ActionStatus enum string matches RunStatus
62
+ run_status = getattr(RunStatus, log.status.value, RunStatus.FAILURE)
63
+
64
+ synthetic_report = ExecutionReport(
65
+ plan_title=f"{log.action_type} Details",
66
+ run_summary=RunSummary(
67
+ status=run_status,
68
+ start_time=now,
69
+ end_time=now,
70
+ ),
71
+ action_logs=[log],
72
+ )
73
+
74
+ formatter = MarkdownReportFormatter()
75
+ return formatter.format(synthetic_report)
76
+
77
+
78
+ def resolve_action_parameters(action: ActionData) -> dict[str, Any]:
79
+ """Resolves the full set of parameters for an action, including defaults."""
80
+ from teddy_executor.core.domain.models.plan import (
81
+ DEFAULT_SIMILARITY_THRESHOLD,
82
+ )
83
+
84
+ # Base defaults for all actions
85
+ defaults: dict[str, Any] = {
86
+ "overwrite": False,
87
+ "match_all": False,
88
+ "allow_failure": False,
89
+ "background": False,
90
+ "timeout": 60.0,
91
+ "similarity_threshold": DEFAULT_SIMILARITY_THRESHOLD,
92
+ }
93
+
94
+ # Type-specific relevant parameters
95
+ param_map = {
96
+ "CREATE": ["path", "overwrite", "description"],
97
+ "EDIT": ["path", "match_all", "similarity_threshold", "description"],
98
+ "EXECUTE": [
99
+ "command",
100
+ "allow_failure",
101
+ "background",
102
+ "timeout",
103
+ "tail",
104
+ "description",
105
+ ],
106
+ "READ": ["resource", "lines", "description"],
107
+ "RESEARCH": ["queries", "description"],
108
+ }
109
+
110
+ keys = param_map.get(action.type, [])
111
+ # Hide description from the detail view to reduce clutter
112
+ keys = [k for k in keys if k != "description"]
113
+ resolved: dict[str, Any] = {}
114
+ for key in keys:
115
+ # Use provided value if exists, else fallback to default (if one exists for that key)
116
+ val = action.params.get(key)
117
+ if val is None and key in defaults:
118
+ val = defaults[key]
119
+
120
+ # Format lists as comma-separated strings for clean UI display
121
+ if isinstance(val, list):
122
+ val = ", ".join(map(str, val))
123
+
124
+ resolved[key] = val
125
+
126
+ # After execution, some parameters are hidden from the preview to reduce clutter
127
+ if action.executed:
128
+ # Hide large content/queries/commands once executed; view via 'd'
129
+ for hidden_key in ["content", "queries", "command", "edits"]:
130
+ resolved.pop(hidden_key, None)
131
+
132
+ log = action.action_log
133
+ if log:
134
+ resolved["status"] = log.status.value
135
+ if log.failed_command:
136
+ resolved["failed_command"] = log.failed_command
137
+ # Details are intentionally omitted here; they are viewed via 'd' binding
138
+
139
+ return resolved
140
+
141
+
142
+ async def orchestrate_execution(app: ReviewerApp, node: Any, update_fn: Any) -> None:
143
+ """Orchestrates the execution of a single action node."""
144
+ from teddy_executor.core.domain.models.plan import ActionData, ExecutionStatus
145
+
146
+ action: Any = node.data
147
+ if (
148
+ not isinstance(action, ActionData)
149
+ or action.executed
150
+ or action.state == ExecutionStatus.RUNNING
151
+ ):
152
+ return
153
+
154
+ action.state = ExecutionStatus.RUNNING
155
+ app._refresh_node(node)
156
+
157
+ try:
158
+ import anyio
159
+
160
+ from teddy_executor.core.domain.models.execution_report import ActionStatus
161
+
162
+ log = await anyio.to_thread.run_sync(
163
+ _execute_silently, app._action_dispatcher, action
164
+ )
165
+
166
+ action.executed, action.action_log = True, log
167
+ action.state = (
168
+ ExecutionStatus.SUCCESS
169
+ if log.status == ActionStatus.SUCCESS
170
+ else ExecutionStatus.FAILURE
171
+ )
172
+
173
+ # For MESSAGE actions, print the user's typed reply to the console
174
+ # since the TUI execution path bypasses SessionOrchestrator.execute()
175
+ # where _print_user_message is called.
176
+ if (
177
+ log.status == ActionStatus.SUCCESS
178
+ and action.type.upper() == "MESSAGE"
179
+ and log.details
180
+ and isinstance(log.details, str)
181
+ and log.details.strip()
182
+ ):
183
+ import typer
184
+
185
+ typer.secho("")
186
+ typer.secho("User Message:")
187
+ typer.secho(log.details.strip())
188
+ except Exception as e:
189
+ logging.getLogger(__name__).debug("Background execution failed: %s", e)
190
+ action.executed, action.state = True, ExecutionStatus.FAILURE
191
+ finally:
192
+ app._refresh_node(node)
193
+ update_fn(app, action)
194
+ app.refresh_bindings()
195
+
196
+
197
+ def _execute_silently(dispatcher: Any, act: Any) -> Any:
198
+ """Helper to run dispatcher silently."""
199
+
200
+ logger = logging.getLogger("teddy_executor.core.services.action_dispatcher")
201
+ old_level = logger.level
202
+ logger.setLevel(logging.WARNING)
203
+ f = io.StringIO()
204
+ try:
205
+ with contextlib.redirect_stdout(f), contextlib.redirect_stderr(f):
206
+ return dispatcher.dispatch_and_execute(act)
207
+ finally:
208
+ logger.setLevel(old_level)
209
+
210
+
211
+ async def execute_step_logic(app: ReviewerApp, node: Any, update_fn: Any) -> None:
212
+ """Executes the action with real-time state transitions and feedback."""
213
+ await orchestrate_execution(app, node, update_fn)