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,308 @@
1
+ from __future__ import annotations
2
+ import logging
3
+ import re
4
+ from typing import TYPE_CHECKING, Any, Optional
5
+
6
+ if TYPE_CHECKING:
7
+ from teddy_executor.adapters.inbound.textual_plan_reviewer_app import ReviewerApp
8
+ from teddy_executor.core.domain.models.plan import ActionData
9
+ from teddy_executor.core.domain.models.project_context import ContextItem
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ MAX_LABEL_LENGTH = 60
14
+
15
+
16
+ def extract_status_emoji(raw_status: str) -> str:
17
+ """Extracts the status emoji, preferring anchored status lines."""
18
+ # Priority 1: Anchored status line (matches SessionPruningService logic)
19
+ anchored_match = re.search(
20
+ r"^- \*\*Status:\*\*.*([🟢🟡🔴])", raw_status, re.MULTILINE
21
+ )
22
+ if anchored_match:
23
+ return anchored_match.group(1)
24
+
25
+ # Priority 2: Fallback to first occurring emoji (resilience for unanchored strings)
26
+ emojis = re.findall(r"[🟢🟡🔴]", raw_status)
27
+ return emojis[0] if emojis else ""
28
+
29
+
30
+ def populate_context_detail(app: "ReviewerApp", pane: Any, data: Any) -> None:
31
+ """Extract context-specific detail population logic."""
32
+ from teddy_executor.adapters.inbound.textual_plan_reviewer_widgets import DetailItem
33
+ from teddy_executor.core.domain.models.project_context import ContextItem
34
+ from teddy_executor.core.utils.markdown import is_session_history_path
35
+
36
+ if isinstance(data, ContextItem):
37
+ pane.append(DetailItem("Path", data.path))
38
+ pane.append(DetailItem("Tokens", f"{data.token_count / 1000.0:.1f}k"))
39
+ status_map = {
40
+ "M": "Modified",
41
+ "??": "Untracked",
42
+ "U": "Untracked",
43
+ "A": "Added",
44
+ "D": "Deleted",
45
+ }
46
+ status_text = status_map.get(data.git_status.strip(), "Unmodified")
47
+ pane.append(DetailItem("Git Status", status_text))
48
+ pane.append(DetailItem("Scope", data.scope))
49
+ if data.auto_prune_reason:
50
+ pane.append(DetailItem("Auto-Prune", data.auto_prune_reason))
51
+ elif isinstance(data, dict) and data.get("type") == "SYSTEM_PROMPT":
52
+ pane.append(DetailItem("Agent", data.get("agent", "Unknown")))
53
+ pane.append(DetailItem("Tokens", f"{data.get('tokens', 0) / 1000.0:.1f}k"))
54
+ elif app.project_context:
55
+ # Context Aggregate View - Only sum SELECTED items
56
+ selected_items = [i for i in app.project_context.items if i.selected]
57
+ selected_file_tokens = sum(i.token_count for i in selected_items)
58
+ system_info_tokens = app.project_context.content_tokens - selected_file_tokens
59
+ total_tokens = (
60
+ app.project_context.content_tokens
61
+ + app.project_context.system_prompt_tokens
62
+ )
63
+ window_val = (
64
+ f"{app.project_context.total_window / 1000.0:.0f}k"
65
+ if app.project_context.total_window > 0
66
+ else "???"
67
+ )
68
+ pane.append(
69
+ DetailItem(
70
+ "Total Context",
71
+ f"{total_tokens / 1000.0:.1f}k / {window_val} tokens",
72
+ )
73
+ )
74
+ pane.append(
75
+ DetailItem(
76
+ "• System",
77
+ f"{(app.project_context.system_prompt_tokens + system_info_tokens) / 1000.0:.1f}k",
78
+ )
79
+ )
80
+ session_tokens = sum(
81
+ i.token_count
82
+ for i in selected_items
83
+ if i.scope == "Session" and not is_session_history_path(i.path)
84
+ )
85
+ turn_tokens = sum(
86
+ i.token_count
87
+ for i in selected_items
88
+ if i.scope == "Turn" and not is_session_history_path(i.path)
89
+ )
90
+ history_tokens = sum(
91
+ i.token_count for i in selected_items if is_session_history_path(i.path)
92
+ )
93
+
94
+ pane.append(DetailItem("• Session", f"{session_tokens / 1000.0:.1f}k"))
95
+ pane.append(DetailItem("• Turn", f"{turn_tokens / 1000.0:.1f}k"))
96
+ pane.append(DetailItem("• History", f"{history_tokens / 1000.0:.1f}k"))
97
+
98
+
99
+ # Editor helpers moved to textual_plan_reviewer_editor.py
100
+
101
+
102
+ def format_context_item_label(item: "ContextItem") -> str:
103
+ """Format a context item label according to UI standards."""
104
+ status_colors = {
105
+ "M": "yellow",
106
+ "??": "green",
107
+ "A": "green",
108
+ "D": "red",
109
+ "U": "green",
110
+ }
111
+ clean_status = item.git_status.strip()
112
+ display_status = "U" if clean_status == "??" else clean_status
113
+ status_part = (
114
+ f" [[{status_colors.get(clean_status, 'white')}]{display_status}[/]]"
115
+ if clean_status
116
+ else ""
117
+ )
118
+ token_str = f"{item.token_count / 1000.0:.1f}k"
119
+
120
+ from teddy_executor.core.utils.markdown import get_session_history_display_name
121
+
122
+ disp_name = get_session_history_display_name(item.path)
123
+ display_path = disp_name if disp_name else item.path
124
+
125
+ if not item.selected:
126
+ return f" [s dim]{display_path}{status_part} {token_str}[/]"
127
+ return f" [bold]{display_path}[/]{status_part} [#888888]{token_str}[/]"
128
+
129
+
130
+ def build_context_section(app: "ReviewerApp", tree: Any) -> Any:
131
+ """Build the 'Context' tree section."""
132
+ if not app.project_context:
133
+ return None
134
+ from teddy_executor.adapters.inbound.textual_plan_reviewer_logic import (
135
+ CONTEXT_ROOT,
136
+ SYSTEM_LABEL,
137
+ SESSION_LABEL,
138
+ TURN_LABEL,
139
+ HISTORY_LABEL,
140
+ )
141
+ from teddy_executor.core.utils.markdown import (
142
+ is_session_file_path,
143
+ is_session_history_path,
144
+ get_session_history_sort_key,
145
+ )
146
+
147
+ con_root = tree.root.add("[bold]Context[/]", data=CONTEXT_ROOT, expand=False)
148
+ con_root.add_leaf("[#888888 italic]System:[/]", data=SYSTEM_LABEL)
149
+ token_str = f" [#888888]{app.project_context.system_prompt_tokens / 1000.0:.1f}k[/]"
150
+ con_root.add_leaf(
151
+ f" [bold]{app.project_context.agent_name}[/]{token_str}",
152
+ data={
153
+ "type": "SYSTEM_PROMPT",
154
+ "agent": app.project_context.agent_name,
155
+ "tokens": app.project_context.system_prompt_tokens,
156
+ },
157
+ )
158
+
159
+ # Standard context items grouped by scope
160
+ for scope, label in [("Session", SESSION_LABEL), ("Turn", TURN_LABEL)]:
161
+ con_root.add_leaf(f"[#888888 italic]{scope}:[/]", data=label)
162
+ count = 0
163
+ for item in app.project_context.items:
164
+ if item.scope == scope and not is_session_file_path(item.path):
165
+ con_root.add_leaf(format_context_item_label(item), data=item)
166
+ count += 1
167
+ if count == 0:
168
+ con_root.add_leaf(" [#888888](None)[/]", data=label)
169
+
170
+ # Chronological history turns
171
+ history_items = [
172
+ item for item in app.project_context.items if is_session_history_path(item.path)
173
+ ]
174
+ if history_items:
175
+ con_root.add_leaf("[#888888 italic]History:[/]", data=HISTORY_LABEL)
176
+ history_items.sort(key=lambda x: get_session_history_sort_key(x.path))
177
+ for item in history_items:
178
+ con_root.add_leaf(format_context_item_label(item), data=item)
179
+
180
+ return con_root
181
+
182
+
183
+ def handle_mount_logic(app: Any, update_detail_fn: Any) -> None:
184
+ """Populate the action tree and set title when the app is mounted."""
185
+ if getattr(app, "_tree_built", False) is True:
186
+ return
187
+
188
+ status_raw = (
189
+ app.plan.metadata.get("Status") or app.plan.metadata.get("status") or ""
190
+ )
191
+ status_emoji = extract_status_emoji(status_raw)
192
+ title_parts = [part for part in [status_emoji, app.plan.title] if part]
193
+ app.title = " ".join(title_parts)
194
+
195
+ from teddy_executor.adapters.inbound.textual_plan_reviewer_widgets import ActionTree
196
+
197
+ tree = app.query_one(ActionTree)
198
+ tree.show_root = False
199
+ tree.root.expand()
200
+
201
+ # 1. Context Section
202
+ con_root = build_context_section(app, tree)
203
+
204
+ # 2. Rationale Section
205
+ from teddy_executor.adapters.inbound.textual_plan_reviewer_logic import (
206
+ RATIONALE_ROOT,
207
+ ACTION_PLAN_ROOT,
208
+ ALLOWED_RATIONALE_SECTIONS,
209
+ )
210
+
211
+ rat_root = tree.root.add("[bold]Rationale[/]", data=RATIONALE_ROOT, expand=True)
212
+
213
+ # Split on '### ' OR '1. ' (numeric lists at start of line)
214
+ sections = re.split(r"\n(?=### |\d+\.\s+)", "\n" + app.plan.rationale)
215
+ current_node = None
216
+ for section in sections:
217
+ section = section.strip()
218
+ if not section:
219
+ continue
220
+ lines = section.split("\n")
221
+ title = re.sub(r"^(?:###\s*|\d+\.\s*)+", "", lines[0]).strip()
222
+ if title in ALLOWED_RATIONALE_SECTIONS:
223
+ content = "\n".join(lines[1:]).strip()
224
+ current_node = rat_root.add_leaf(
225
+ title,
226
+ data={"type": "RATIONALE_SECTION", "title": title, "content": content},
227
+ )
228
+ elif current_node:
229
+ current_node.data["content"] += "\n\n" + section
230
+
231
+ # 3. Action Plan Section
232
+ act_root = tree.root.add("[bold]Action Plan[/]", data=ACTION_PLAN_ROOT, expand=True)
233
+ for action in app.plan.actions:
234
+ if not hasattr(action, "_original_params"):
235
+ action._original_params = action.params.copy()
236
+ act_root.add_leaf(format_node_label(action), data=action)
237
+
238
+ # Initialize cursor: Context root if available, else Rationale root
239
+ initial_node = con_root if con_root else rat_root
240
+
241
+ tree.move_cursor(initial_node)
242
+ tree.focus()
243
+ app._tree_built = True
244
+ app.call_after_refresh(update_detail_fn, app, initial_node.data)
245
+
246
+
247
+ def format_node_label(action: "ActionData") -> str:
248
+ """Format the label for a tree node based on action state."""
249
+ from teddy_executor.core.domain.models.plan import ExecutionStatus
250
+
251
+ summary = get_action_summary(action)
252
+ # Ensure Enum types are stringified (e.g. ActionType.CREATE -> "CREATE")
253
+ type_str = action.type.value if hasattr(action.type, "value") else str(action.type)
254
+
255
+ if action.state == ExecutionStatus.RUNNING:
256
+ return f"[blue][RUNNING] {type_str}: {summary}[/]"
257
+
258
+ if action.executed:
259
+ color = "green" if action.state.value == "SUCCESS" else "red"
260
+ return f"[{color}][{action.state.value}] {type_str}: {summary}[/]"
261
+
262
+ prefix = "[✓]" if action.selected else "[ ]"
263
+ label = f"{prefix} {type_str}: {summary}"
264
+ if action.modified:
265
+ label += " *modified"
266
+ return label
267
+
268
+
269
+ def get_action_summary(action: "ActionData") -> str:
270
+ """Extract a concise summary for the action."""
271
+ summary = action.description or ""
272
+ if not summary:
273
+ params = action.params
274
+ summary = str(
275
+ params.get("path") or params.get("resource") or params.get("command", "")
276
+ )
277
+
278
+ if len(summary) > MAX_LABEL_LENGTH:
279
+ summary = summary[: MAX_LABEL_LENGTH - 3] + "..."
280
+ return summary
281
+
282
+
283
+ def _apply_param_edit(action: Any, key: str, new_val: str) -> None:
284
+ """Helper to apply parameter edits back to the action."""
285
+ list_keys = {"queries", "reference_files"}
286
+ if key in list_keys:
287
+ action.params[key] = [v.strip() for v in str(new_val).split(",") if v.strip()]
288
+ else:
289
+ action.params[key] = str(new_val)
290
+
291
+
292
+ def handle_revert(app: ReviewerApp, node: Any, update_fn: Any) -> None:
293
+ """Revert manual modifications for the currently highlighted action."""
294
+ action: Optional[ActionData] = node.data
295
+ if action and action.modified:
296
+ action.modified = False
297
+ action.modified_fields.clear()
298
+ if hasattr(action, "_original_params"):
299
+ action.params = action._original_params.copy()
300
+
301
+ ptf = getattr(action, "pending_temp_file", None)
302
+ if isinstance(ptf, str):
303
+ app._system_env.delete_file(ptf)
304
+ action.pending_temp_file = None
305
+
306
+ app._refresh_node(node)
307
+ update_fn(app, action)
308
+ app.refresh_bindings()
@@ -0,0 +1,345 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from typing import TYPE_CHECKING, Any, cast
5
+
6
+ from teddy_executor.adapters.inbound.textual_plan_reviewer_execution import (
7
+ resolve_action_parameters,
8
+ )
9
+ from teddy_executor.adapters.inbound.textual_plan_reviewer_helpers import (
10
+ format_node_label,
11
+ )
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ if TYPE_CHECKING:
16
+ from teddy_executor.adapters.inbound.textual_plan_reviewer_app import ReviewerApp
17
+ from teddy_executor.core.domain.models.plan import ActionData
18
+
19
+
20
+ ALLOWED_RATIONALE_SECTIONS = [
21
+ "Synthesis",
22
+ "Justification",
23
+ "Expectation",
24
+ "State Dashboard",
25
+ ]
26
+
27
+ # Tree Node Identifiers
28
+ CONTEXT_ROOT = "CONTEXT_ROOT"
29
+ SYSTEM_LABEL = "SYSTEM_LABEL"
30
+ SESSION_LABEL = "SESSION_LABEL"
31
+ TURN_LABEL = "TURN_LABEL"
32
+ HISTORY_LABEL = "HISTORY_LABEL"
33
+ RATIONALE_ROOT = "RATIONALE_ROOT"
34
+ ACTION_PLAN_ROOT = "ACTION_PLAN_ROOT"
35
+
36
+
37
+ # Summary logic moved to textual_plan_reviewer_helpers.py
38
+
39
+
40
+ def on_tree_node_highlighted(app: "ReviewerApp", event: Any) -> None:
41
+ """Handle node highlighting to update the detail view."""
42
+ # event is Tree.NodeHighlighted
43
+ if event.node and getattr(event.node.tree, "id", None) == "left-pane":
44
+ # Root node has no data usually, but our virtual roots do
45
+ _update_detail_view(app, event.node.data)
46
+ app.refresh_bindings()
47
+
48
+
49
+ def _update_detail_view(app: ReviewerApp, data: Any):
50
+ """Populate the ParameterDetail view or Rationale view."""
51
+ from teddy_executor.adapters.inbound.textual_plan_reviewer_widgets import (
52
+ ParameterDetail,
53
+ )
54
+ from textual.widgets import ContentSwitcher
55
+
56
+ try:
57
+ switcher = app.query_one(ContentSwitcher)
58
+ pane = app.query_one(ParameterDetail)
59
+ except Exception:
60
+ return
61
+
62
+ if not pane.is_attached:
63
+ return
64
+
65
+ if _is_context_data(data):
66
+ _update_context_detail(app, switcher, pane, data)
67
+ elif _is_rationale_section(data):
68
+ _update_rationale_detail(app, switcher, data)
69
+ else:
70
+ _update_action_detail(app, switcher, pane, data)
71
+
72
+
73
+ def _is_context_data(data: Any) -> bool:
74
+ """Check if data belongs to the context section."""
75
+ from teddy_executor.core.domain.models.project_context import ContextItem
76
+
77
+ return (
78
+ data in (CONTEXT_ROOT, SYSTEM_LABEL, SESSION_LABEL, TURN_LABEL, HISTORY_LABEL)
79
+ or isinstance(data, ContextItem)
80
+ or (isinstance(data, dict) and data.get("type") == "SYSTEM_PROMPT")
81
+ )
82
+
83
+
84
+ def _is_rationale_section(data: Any) -> bool:
85
+ """Check if data is a rationale section dict."""
86
+ return isinstance(data, dict) and data.get("type") == "RATIONALE_SECTION"
87
+
88
+
89
+ def _update_context_detail(app: ReviewerApp, switcher: Any, pane: Any, data: Any):
90
+ """Render details for context items or aggregates."""
91
+ switcher.current = "params-view"
92
+ pane.clear()
93
+ from teddy_executor.adapters.inbound.textual_plan_reviewer_helpers import (
94
+ populate_context_detail,
95
+ )
96
+
97
+ populate_context_detail(app, pane, data)
98
+
99
+
100
+ def _update_rationale_detail(app: ReviewerApp, switcher: Any, data: dict[str, Any]):
101
+ """Render details for a rationale section."""
102
+ from textual.widgets import Markdown
103
+
104
+ switcher.current = "rationale-view"
105
+ app.query_one("#rationale-content", Markdown).update(data["content"])
106
+
107
+
108
+ def _update_action_detail(app: ReviewerApp, switcher: Any, pane: Any, data: Any):
109
+ """Render details for action plan roots or individual actions."""
110
+ from textual.widgets import Label, ListItem
111
+
112
+ switcher.current = "params-view"
113
+ pane.clear()
114
+
115
+ if not data:
116
+ pane.mount(ListItem(Label("Select an item to view details")))
117
+ return
118
+
119
+ if data == RATIONALE_ROOT:
120
+ _render_rationale_root_detail(app, pane)
121
+ elif data == ACTION_PLAN_ROOT:
122
+ pane.mount(ListItem(Label("Select an action below to view details")))
123
+ else:
124
+ _render_action_data_detail(pane, data)
125
+
126
+
127
+ def _render_rationale_root_detail(app: ReviewerApp, pane: Any):
128
+ """Render metadata for the Rationale root node."""
129
+ from textual.widgets import Tree
130
+ from teddy_executor.adapters.inbound.textual_plan_reviewer_widgets import DetailItem
131
+
132
+ tree = app.query_one(Tree)
133
+ if tree.cursor_node and tree.cursor_node.data != RATIONALE_ROOT:
134
+ return
135
+
136
+ # Fallback to project_context for agent identity
137
+ agent = (
138
+ app.plan.metadata.get("Agent")
139
+ or app.plan.metadata.get("agent")
140
+ or (app.project_context.agent_name if app.project_context else "Unknown")
141
+ )
142
+ plan_type = (
143
+ app.plan.metadata.get("Plan Type")
144
+ or app.plan.metadata.get("plan_type")
145
+ or "Development"
146
+ )
147
+ status = app.plan.metadata.get("Status") or app.plan.metadata.get("status") or "N/A"
148
+ pane.append(DetailItem("Agent", agent))
149
+ pane.append(DetailItem("Plan Type", plan_type))
150
+ pane.append(DetailItem("Status", status))
151
+
152
+
153
+ def _render_action_data_detail(pane: Any, data: Any):
154
+ """Render parameters for an ActionData object."""
155
+ from teddy_executor.core.domain.models.plan import ActionData
156
+ from teddy_executor.adapters.inbound.textual_plan_reviewer_widgets import DetailItem
157
+ from textual.widgets import Label, ListItem
158
+
159
+ if isinstance(data, ActionData):
160
+ for key, val in resolve_action_parameters(data).items():
161
+ val_str = val.value if hasattr(val, "value") else str(val)
162
+ pane.append(DetailItem(key, val_str))
163
+ else:
164
+ pane.mount(ListItem(Label("Select an item to view details")))
165
+
166
+
167
+ async def edit_action_logic(app: ReviewerApp, node: Any, action: ActionData) -> None:
168
+ """Handles the (e)dit key logic by branching to modals or external editor."""
169
+ from teddy_executor.adapters.inbound.textual_plan_reviewer_editor import (
170
+ handle_edit_action,
171
+ )
172
+
173
+ await handle_edit_action(app, node, action, _update_detail_view)
174
+
175
+
176
+ def refresh_node_logic(app: ReviewerApp, node: Any) -> None:
177
+ """Refresh the label and state of a single tree node."""
178
+ from teddy_executor.core.domain.models.plan import ActionData
179
+ from teddy_executor.core.domain.models.project_context import ContextItem
180
+ from teddy_executor.adapters.inbound.textual_plan_reviewer_helpers import (
181
+ format_context_item_label,
182
+ )
183
+
184
+ if isinstance(node.data, ActionData):
185
+ node.label = format_node_label(node.data)
186
+ elif isinstance(node.data, ContextItem):
187
+ node.label = format_context_item_label(node.data)
188
+
189
+
190
+ def on_mount_logic(app: Any) -> None:
191
+ """Delegate tree population and title setting."""
192
+ from teddy_executor.adapters.inbound.textual_plan_reviewer_helpers import (
193
+ handle_mount_logic,
194
+ )
195
+
196
+ handle_mount_logic(app, _update_detail_view)
197
+
198
+
199
+ def check_action_logic(app: ReviewerApp, action_name: str) -> bool:
200
+ """Gate for enabling/disabling bindings based on state."""
201
+ from textual.widgets import Tree
202
+ from teddy_executor.core.domain.models.plan import ActionData
203
+
204
+ tree = app.query_one(Tree)
205
+ node = tree.cursor_node
206
+
207
+ # Navigation and universal actions are always allowed
208
+ if action_name in (
209
+ "focus_right",
210
+ "focus_left",
211
+ "focus_next",
212
+ "focus_prev",
213
+ "jump_next",
214
+ "jump_prev",
215
+ "cancel",
216
+ "submit",
217
+ "view_plan",
218
+ "add_message",
219
+ "toggle_all",
220
+ ):
221
+ return True
222
+
223
+ from teddy_executor.core.domain.models.project_context import ContextItem
224
+
225
+ if not node:
226
+ return False
227
+
228
+ if isinstance(node.data, ContextItem):
229
+ # Context items only support toggling (space) and universal navigation
230
+ return action_name in (
231
+ "toggle_selection",
232
+ "focus_right",
233
+ "focus_left",
234
+ "jump_next",
235
+ "jump_prev",
236
+ "cancel",
237
+ )
238
+
239
+ if not isinstance(node.data, ActionData):
240
+ return False
241
+
242
+ data = cast(ActionData, node.data)
243
+
244
+ data = cast(ActionData, node.data)
245
+ results = {
246
+ "execute_step": not data.executed,
247
+ "edit_details": not data.executed,
248
+ "view_details": bool(data.executed),
249
+ "revert": bool(data.modified) and not data.executed,
250
+ }
251
+ return results.get(action_name, True)
252
+
253
+
254
+ def toggle_selection_logic(app: ReviewerApp, node: Any) -> None:
255
+ """Toggle action selection when a node is selected."""
256
+ from teddy_executor.core.domain.models.plan import ActionData
257
+ from teddy_executor.core.domain.models.project_context import ContextItem
258
+
259
+ data: Any = node.data
260
+ if isinstance(data, ActionData) and not data.executed:
261
+ data.selected = not data.selected
262
+ app._refresh_node(node)
263
+ elif isinstance(data, ContextItem):
264
+ data.selected = not data.selected
265
+ app._refresh_node(node)
266
+ # Always refresh detail view based on CURRENT highlighting
267
+ # This ensures totals update if the root is selected but a child is toggled
268
+ from textual.widgets import Tree
269
+
270
+ tree = app.query_one(Tree)
271
+ if tree.cursor_node:
272
+ _update_detail_view(app, tree.cursor_node.data)
273
+
274
+
275
+ async def on_list_view_selected_logic(app: ReviewerApp, item: Any) -> None:
276
+ """Handle parameter editing when a DetailItem is selected in the right pane."""
277
+ from teddy_executor.adapters.inbound.textual_plan_reviewer_editor import (
278
+ handle_list_view_selected,
279
+ )
280
+
281
+ await handle_list_view_selected(app, item, _update_detail_view)
282
+
283
+
284
+ def revert_logic(app: ReviewerApp, node: Any) -> None:
285
+ """Revert manual modifications for the currently highlighted action."""
286
+ from teddy_executor.adapters.inbound.textual_plan_reviewer_helpers import (
287
+ handle_revert,
288
+ )
289
+
290
+ handle_revert(app, node, _update_detail_view)
291
+
292
+
293
+ async def execute_step_logic(app: ReviewerApp, node: Any) -> None:
294
+ """Executes the action with real-time state transitions and feedback."""
295
+ from teddy_executor.adapters.inbound.textual_plan_reviewer_execution import (
296
+ execute_step_logic as exec_logic,
297
+ )
298
+
299
+ await exec_logic(app, node, _update_detail_view)
300
+
301
+
302
+ async def view_details_logic(app: "ReviewerApp") -> None:
303
+ """View full execution logs for the currently highlighted action."""
304
+ from teddy_executor.adapters.inbound.textual_plan_reviewer_previews import (
305
+ view_details_handler,
306
+ )
307
+
308
+ await view_details_handler(app)
309
+
310
+
311
+ async def view_plan_logic(app: "ReviewerApp") -> None:
312
+ """Open the full plan.md in an external editor."""
313
+ from teddy_executor.adapters.inbound.textual_plan_reviewer_previews import (
314
+ view_plan_handler,
315
+ )
316
+
317
+ await view_plan_handler(app)
318
+
319
+
320
+ async def add_message_logic(app: "ReviewerApp") -> None:
321
+ """Open the external editor to add/edit the user instruction message."""
322
+ from teddy_executor.adapters.inbound.textual_plan_reviewer_previews import (
323
+ add_message_handler,
324
+ )
325
+
326
+ await add_message_handler(app)
327
+
328
+
329
+ def toggle_all_logic(app: "ReviewerApp", plan: Any) -> None:
330
+ """Toggle selection for all actions."""
331
+ new_state = any(not action.selected for action in plan.actions)
332
+ for action in plan.actions:
333
+ action.selected = new_state
334
+
335
+ from textual.widgets import Tree
336
+
337
+ tree = app.query_one(Tree)
338
+
339
+ # Recursively refresh all nodes that contain ActionData
340
+ def refresh_recursive(node: Any):
341
+ app._refresh_node(node)
342
+ for child in node.children:
343
+ refresh_recursive(child)
344
+
345
+ refresh_recursive(tree.root)