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,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)
|