Skip to content

Commit f62533a

Browse files
authored
Merge pull request #1027 from Pythagora-io/virtual-ui
add support for virtual UI, useful for automated end-to-end tests
2 parents 6fbfa8a + cd81cd5 commit f62533a

File tree

3 files changed

+137
-1
lines changed

3 files changed

+137
-1
lines changed

core/cli/helpers.py

+3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from core.ui.base import UIBase
1818
from core.ui.console import PlainConsoleUI
1919
from core.ui.ipc_client import IPCClientUI
20+
from core.ui.virtual import VirtualUI
2021

2122

2223
def parse_llm_endpoint(value: str) -> Optional[tuple[LLMProvider, str]]:
@@ -313,6 +314,8 @@ def init() -> tuple[UIBase, SessionManager, Namespace]:
313314

314315
if config.ui.type == UIAdapter.IPC_CLIENT:
315316
ui = IPCClientUI(config.ui)
317+
elif config.ui.type == UIAdapter.VIRTUAL:
318+
ui = VirtualUI(config.ui.inputs)
316319
else:
317320
ui = PlainConsoleUI()
318321

core/config/__init__.py

+11-1
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ class UIAdapter(str, Enum):
6969

7070
PLAIN = "plain"
7171
IPC_CLIENT = "ipc-client"
72+
VIRTUAL = "virtual"
7273

7374

7475
class ProviderConfig(_StrictModel):
@@ -254,8 +255,17 @@ class LocalIPCConfig(_StrictModel):
254255
port: int = 8125
255256

256257

258+
class VirtualUIConfig(_StrictModel):
259+
"""
260+
Configuration for the virtual UI.
261+
"""
262+
263+
type: Literal[UIAdapter.VIRTUAL] = UIAdapter.VIRTUAL
264+
inputs: list[Any]
265+
266+
257267
UIConfig = Annotated[
258-
Union[PlainUIConfig, LocalIPCConfig],
268+
Union[PlainUIConfig, LocalIPCConfig, VirtualUIConfig],
259269
Field(discriminator="type"),
260270
]
261271

core/ui/virtual.py

+123
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
from typing import Optional
2+
3+
from core.log import get_logger
4+
from core.ui.base import ProjectStage, UIBase, UISource, UserInput
5+
6+
log = get_logger(__name__)
7+
8+
9+
class VirtualUI(UIBase):
10+
"""
11+
Testing UI adapter.
12+
"""
13+
14+
def __init__(self, inputs: list[dict[str, str]]):
15+
self.virtual_inputs = [UserInput(**input) for input in inputs]
16+
17+
async def start(self) -> bool:
18+
log.debug("Starting test UI")
19+
return True
20+
21+
async def stop(self):
22+
log.debug("Stopping test UI")
23+
24+
async def send_stream_chunk(self, chunk: Optional[str], *, source: Optional[UISource] = None):
25+
if chunk is None:
26+
# end of stream
27+
print("", flush=True)
28+
else:
29+
print(chunk, end="", flush=True)
30+
31+
async def send_message(self, message: str, *, source: Optional[UISource] = None):
32+
if source:
33+
print(f"[{source}] {message}")
34+
else:
35+
print(message)
36+
37+
async def send_key_expired(self, message: Optional[str]):
38+
pass
39+
40+
async def ask_question(
41+
self,
42+
question: str,
43+
*,
44+
buttons: Optional[dict[str, str]] = None,
45+
default: Optional[str] = None,
46+
buttons_only: bool = False,
47+
allow_empty: bool = False,
48+
hint: Optional[str] = None,
49+
initial_text: Optional[str] = None,
50+
source: Optional[UISource] = None,
51+
) -> UserInput:
52+
if source:
53+
print(f"[{source}] {question}")
54+
else:
55+
print(f"{question}")
56+
57+
if self.virtual_inputs:
58+
ret = self.virtual_inputs[0]
59+
self.virtual_inputs = self.virtual_inputs[1:]
60+
return ret
61+
62+
if "continue" in buttons:
63+
return UserInput(button="continue", text=None)
64+
elif default:
65+
if buttons:
66+
return UserInput(button=default, text=None)
67+
else:
68+
return UserInput(text=default)
69+
elif buttons_only:
70+
return UserInput(button=list(buttons.keys)[0])
71+
else:
72+
return UserInput(text="")
73+
74+
async def send_project_stage(self, stage: ProjectStage):
75+
pass
76+
77+
async def send_task_progress(
78+
self,
79+
index: int,
80+
n_tasks: int,
81+
description: str,
82+
source: str,
83+
status: str,
84+
source_index: int = 1,
85+
tasks: list[dict] = None,
86+
):
87+
pass
88+
89+
async def send_step_progress(
90+
self,
91+
index: int,
92+
n_steps: int,
93+
step: dict,
94+
task_source: str,
95+
):
96+
pass
97+
98+
async def send_run_command(self, run_command: str):
99+
pass
100+
101+
async def open_editor(self, file: str, line: Optional[int] = None):
102+
pass
103+
104+
async def send_project_root(self, path: str):
105+
pass
106+
107+
async def send_project_stats(self, stats: dict):
108+
pass
109+
110+
async def loading_finished(self):
111+
pass
112+
113+
async def send_project_description(self, description: str):
114+
pass
115+
116+
async def send_features_list(self, features: list[str]):
117+
pass
118+
119+
async def import_project(self, project_dir: str):
120+
pass
121+
122+
123+
__all__ = ["VirtualUI"]

0 commit comments

Comments
 (0)