vibecore 0.4.1__tar.gz → 0.5.0__tar.gz
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.
Potentially problematic release.
This version of vibecore might be problematic. Click here for more details.
- {vibecore-0.4.1 → vibecore-0.5.0}/PKG-INFO +1 -1
- {vibecore-0.4.1 → vibecore-0.5.0}/pyproject.toml +1 -1
- vibecore-0.5.0/src/vibecore/flow.py +345 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/main.py +1 -3
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/session/jsonl_session.py +3 -1
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/tools/todo/manager.py +2 -10
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/tools/todo/models.py +5 -14
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/tools/todo/tools.py +3 -3
- vibecore-0.5.0/src/vibecore/widgets/feedback.py +164 -0
- vibecore-0.5.0/src/vibecore/widgets/feedback.tcss +121 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/widgets/messages.py +7 -1
- vibecore-0.4.1/src/vibecore/flow.py +0 -105
- {vibecore-0.4.1 → vibecore-0.5.0}/.gitignore +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/LICENSE +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/README.md +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/__init__.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/agents/default.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/agents/prompts.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/agents/task.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/auth/__init__.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/auth/config.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/auth/interceptor.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/auth/manager.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/auth/models.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/auth/oauth_flow.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/auth/pkce.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/auth/storage.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/auth/token_manager.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/cli.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/context.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/handlers/__init__.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/handlers/stream_handler.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/main.tcss +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/mcp/__init__.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/mcp/manager.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/mcp/server_wrapper.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/models/__init__.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/models/anthropic.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/models/anthropic_auth.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/prompts/common_system_prompt.txt +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/py.typed +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/session/__init__.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/session/file_lock.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/session/loader.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/session/path_utils.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/settings.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/tools/__init__.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/tools/base.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/tools/file/__init__.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/tools/file/executor.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/tools/file/tools.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/tools/file/utils.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/tools/path_validator.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/tools/python/__init__.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/tools/python/backends/__init__.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/tools/python/backends/terminal_backend.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/tools/python/helpers.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/tools/python/manager.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/tools/python/tools.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/tools/shell/__init__.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/tools/shell/executor.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/tools/shell/tools.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/tools/task/__init__.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/tools/task/executor.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/tools/task/tools.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/tools/todo/__init__.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/tools/webfetch/__init__.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/tools/webfetch/executor.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/tools/webfetch/models.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/tools/webfetch/tools.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/tools/websearch/__init__.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/tools/websearch/base.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/tools/websearch/ddgs/__init__.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/tools/websearch/ddgs/backend.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/tools/websearch/executor.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/tools/websearch/models.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/tools/websearch/tools.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/utils/__init__.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/utils/text.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/widgets/core.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/widgets/core.tcss +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/widgets/expandable.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/widgets/expandable.tcss +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/widgets/info.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/widgets/info.tcss +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/widgets/messages.tcss +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/widgets/tool_message_factory.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/widgets/tool_messages.py +0 -0
- {vibecore-0.4.1 → vibecore-0.5.0}/src/vibecore/widgets/tool_messages.tcss +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: vibecore
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Build your own AI-powered automation tools in the terminal with this extensible agent framework
|
|
5
5
|
Project-URL: Homepage, https://github.com/serialx/vibecore
|
|
6
6
|
Project-URL: Repository, https://github.com/serialx/vibecore
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import contextlib
|
|
3
|
+
import datetime
|
|
4
|
+
import sys
|
|
5
|
+
import threading
|
|
6
|
+
from collections.abc import Callable, Coroutine
|
|
7
|
+
from typing import Any, Generic, Protocol, TypeAlias, overload
|
|
8
|
+
|
|
9
|
+
from agents import (
|
|
10
|
+
Agent,
|
|
11
|
+
RunConfig,
|
|
12
|
+
RunHooks,
|
|
13
|
+
Runner,
|
|
14
|
+
Session,
|
|
15
|
+
TContext,
|
|
16
|
+
TResponseInputItem,
|
|
17
|
+
)
|
|
18
|
+
from agents.result import RunResultBase
|
|
19
|
+
from agents.run import DEFAULT_MAX_TURNS
|
|
20
|
+
from textual.pilot import Pilot
|
|
21
|
+
from typing_extensions import TypeVar
|
|
22
|
+
|
|
23
|
+
from vibecore.context import VibecoreContext
|
|
24
|
+
from vibecore.main import AppIsExiting, VibecoreApp
|
|
25
|
+
from vibecore.session import JSONLSession
|
|
26
|
+
from vibecore.settings import settings
|
|
27
|
+
from vibecore.widgets.core import MyTextArea
|
|
28
|
+
from vibecore.widgets.messages import SystemMessage
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class UserInputFunc(Protocol):
|
|
32
|
+
"""Protocol for user input function with optional prompt parameter."""
|
|
33
|
+
|
|
34
|
+
async def __call__(self, prompt: str = "") -> str:
|
|
35
|
+
"""Get user input with optional prompt message.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
prompt: Optional prompt to display before getting input.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
The user's input string.
|
|
42
|
+
"""
|
|
43
|
+
...
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
TWorkflowReturn = TypeVar("TWorkflowReturn", default=RunResultBase)
|
|
47
|
+
DecoratedCallable: TypeAlias = Callable[..., Coroutine[Any, Any, TWorkflowReturn]]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class VibecoreRunnerBase(Generic[TWorkflowReturn]):
|
|
51
|
+
def __init__(self, vibecore: "Vibecore[TWorkflowReturn]") -> None:
|
|
52
|
+
self.vibecore = vibecore
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def session(self) -> Session:
|
|
56
|
+
raise NotImplementedError("session property implemented.")
|
|
57
|
+
|
|
58
|
+
async def user_input(self, prompt: str = "") -> str:
|
|
59
|
+
raise NotImplementedError("user_input method not implemented.")
|
|
60
|
+
|
|
61
|
+
async def print(self, message: str) -> None:
|
|
62
|
+
print(message, file=sys.stderr)
|
|
63
|
+
|
|
64
|
+
async def run_agent(
|
|
65
|
+
self,
|
|
66
|
+
starting_agent: Agent[TContext],
|
|
67
|
+
input: str | list[TResponseInputItem],
|
|
68
|
+
*,
|
|
69
|
+
context: TContext | None = None,
|
|
70
|
+
max_turns: int = DEFAULT_MAX_TURNS,
|
|
71
|
+
hooks: RunHooks[TContext] | None = None,
|
|
72
|
+
run_config: RunConfig | None = None,
|
|
73
|
+
previous_response_id: str | None = None,
|
|
74
|
+
session: Session | None = None,
|
|
75
|
+
) -> RunResultBase:
|
|
76
|
+
result = await Runner.run(
|
|
77
|
+
starting_agent=starting_agent,
|
|
78
|
+
input=input, # Pass string directly when using session
|
|
79
|
+
context=context,
|
|
80
|
+
max_turns=max_turns,
|
|
81
|
+
hooks=hooks,
|
|
82
|
+
run_config=run_config,
|
|
83
|
+
previous_response_id=previous_response_id,
|
|
84
|
+
session=session,
|
|
85
|
+
)
|
|
86
|
+
return result
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class VibecoreSimpleRunner(VibecoreRunnerBase[TWorkflowReturn]):
|
|
90
|
+
def __init__(self, vibecore: "Vibecore[TWorkflowReturn]") -> None:
|
|
91
|
+
super().__init__(vibecore)
|
|
92
|
+
|
|
93
|
+
session_id = f"chat-{datetime.datetime.now().strftime('%Y%m%d-%H%M%S')}"
|
|
94
|
+
self._session = JSONLSession(
|
|
95
|
+
session_id=session_id,
|
|
96
|
+
project_path=None, # Will use current working directory
|
|
97
|
+
base_dir=settings.session.base_dir,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def session(self) -> Session:
|
|
102
|
+
return self._session
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class VibecoreCliRunner(VibecoreSimpleRunner[TWorkflowReturn]):
|
|
106
|
+
def __init__(self, vibecore: "Vibecore[TWorkflowReturn]") -> None:
|
|
107
|
+
super().__init__(vibecore)
|
|
108
|
+
|
|
109
|
+
async def user_input(self, prompt: str = "") -> str:
|
|
110
|
+
return input(prompt)
|
|
111
|
+
|
|
112
|
+
async def run(self) -> TWorkflowReturn:
|
|
113
|
+
assert self.vibecore.workflow_logic is not None, (
|
|
114
|
+
"Workflow logic not defined. Please use the @vibecore.workflow() decorator."
|
|
115
|
+
)
|
|
116
|
+
return await self.vibecore.workflow_logic()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class VibecoreStaticRunner(VibecoreSimpleRunner[TWorkflowReturn]):
|
|
120
|
+
def __init__(self, vibecore: "Vibecore[TWorkflowReturn]") -> None:
|
|
121
|
+
super().__init__(vibecore)
|
|
122
|
+
self.inputs: list[str] = []
|
|
123
|
+
self.prints: list[str] = []
|
|
124
|
+
|
|
125
|
+
async def user_input(self, prompt: str = "") -> str:
|
|
126
|
+
assert self.inputs, "No more user inputs available."
|
|
127
|
+
return self.inputs.pop()
|
|
128
|
+
|
|
129
|
+
async def print(self, message: str) -> None:
|
|
130
|
+
# Capture printed messages instead of displaying them
|
|
131
|
+
self.prints.append(message)
|
|
132
|
+
|
|
133
|
+
async def run(self, inputs: list[str] | None = None) -> TWorkflowReturn:
|
|
134
|
+
if inputs is None:
|
|
135
|
+
inputs = []
|
|
136
|
+
assert self.vibecore.workflow_logic is not None, (
|
|
137
|
+
"Workflow logic not defined. Please use the @vibecore.workflow() decorator."
|
|
138
|
+
)
|
|
139
|
+
self.inputs.extend(inputs)
|
|
140
|
+
return await self.vibecore.workflow_logic()
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class VibecoreTextualRunner(VibecoreRunnerBase[TWorkflowReturn]):
|
|
144
|
+
def __init__(self, vibecore: "Vibecore[TWorkflowReturn]") -> None:
|
|
145
|
+
super().__init__(vibecore)
|
|
146
|
+
self.app = VibecoreApp(self.vibecore.context, self.vibecore.starting_agent, show_welcome=False)
|
|
147
|
+
self.app_ready_event = asyncio.Event()
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def session(self) -> Session:
|
|
151
|
+
return self.app.session
|
|
152
|
+
|
|
153
|
+
async def user_input(self, prompt: str = "") -> str:
|
|
154
|
+
if prompt:
|
|
155
|
+
await self.print(prompt)
|
|
156
|
+
self.app.query_one(MyTextArea).disabled = False
|
|
157
|
+
self.app.query_one(MyTextArea).focus()
|
|
158
|
+
user_input = await self.app.wait_for_user_input()
|
|
159
|
+
if self.vibecore.disable_user_input:
|
|
160
|
+
self.app.query_one(MyTextArea).disabled = True
|
|
161
|
+
return user_input
|
|
162
|
+
|
|
163
|
+
async def print(self, message: str) -> None:
|
|
164
|
+
await self.app.add_message(SystemMessage(message))
|
|
165
|
+
|
|
166
|
+
async def run_agent(
|
|
167
|
+
self,
|
|
168
|
+
starting_agent: Agent[TContext],
|
|
169
|
+
input: str | list[TResponseInputItem],
|
|
170
|
+
*,
|
|
171
|
+
context: TContext | None = None,
|
|
172
|
+
max_turns: int = DEFAULT_MAX_TURNS,
|
|
173
|
+
hooks: RunHooks[TContext] | None = None,
|
|
174
|
+
run_config: RunConfig | None = None,
|
|
175
|
+
previous_response_id: str | None = None,
|
|
176
|
+
session: Session | None = None,
|
|
177
|
+
) -> RunResultBase:
|
|
178
|
+
result = Runner.run_streamed(
|
|
179
|
+
starting_agent=starting_agent,
|
|
180
|
+
input=input, # Pass string directly when using session
|
|
181
|
+
context=context,
|
|
182
|
+
max_turns=max_turns,
|
|
183
|
+
hooks=hooks,
|
|
184
|
+
run_config=run_config,
|
|
185
|
+
previous_response_id=previous_response_id,
|
|
186
|
+
session=session,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
self.app.current_worker = self.app.handle_streamed_response(result)
|
|
190
|
+
await self.app.current_worker.wait()
|
|
191
|
+
return result
|
|
192
|
+
|
|
193
|
+
def on_app_ready(self) -> None:
|
|
194
|
+
"""Called when app is ready to process events."""
|
|
195
|
+
self.app_ready_event.set()
|
|
196
|
+
|
|
197
|
+
async def _run_app(self) -> None:
|
|
198
|
+
"""Run the apps message loop.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
app: App to run.
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
with self.app._context():
|
|
205
|
+
try:
|
|
206
|
+
self.app._loop = asyncio.get_running_loop()
|
|
207
|
+
self.app._thread_id = threading.get_ident()
|
|
208
|
+
await self.app._process_messages(
|
|
209
|
+
ready_callback=self.on_app_ready,
|
|
210
|
+
headless=False,
|
|
211
|
+
)
|
|
212
|
+
finally:
|
|
213
|
+
self.app_ready_event.set()
|
|
214
|
+
|
|
215
|
+
async def _run_logic(self) -> TWorkflowReturn:
|
|
216
|
+
assert self.vibecore.workflow_logic is not None, (
|
|
217
|
+
"Workflow logic not defined. Please use the @vibecore.workflow() decorator."
|
|
218
|
+
)
|
|
219
|
+
try:
|
|
220
|
+
return await self.vibecore.workflow_logic()
|
|
221
|
+
except AppIsExiting:
|
|
222
|
+
raise
|
|
223
|
+
|
|
224
|
+
async def run(self, shutdown: bool = False) -> TWorkflowReturn:
|
|
225
|
+
self.app = VibecoreApp(self.vibecore.context, self.vibecore.starting_agent, show_welcome=False)
|
|
226
|
+
app_task = asyncio.create_task(self._run_app(), name=f"run_app({self.app})")
|
|
227
|
+
await self.app_ready_event.wait()
|
|
228
|
+
pilot = Pilot(self.app)
|
|
229
|
+
logic_task: asyncio.Task[TWorkflowReturn] | None = None
|
|
230
|
+
|
|
231
|
+
await pilot._wait_for_screen()
|
|
232
|
+
if self.vibecore.disable_user_input:
|
|
233
|
+
self.app.query_one(MyTextArea).disabled = True
|
|
234
|
+
logic_task = asyncio.create_task(self._run_logic(), name="logic_task")
|
|
235
|
+
done, pending = await asyncio.wait([logic_task, app_task], return_when=asyncio.FIRST_COMPLETED)
|
|
236
|
+
|
|
237
|
+
# If app has exited and logic is still running, cancel logic
|
|
238
|
+
if app_task in done and logic_task in pending:
|
|
239
|
+
logic_task.cancel()
|
|
240
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
241
|
+
await logic_task
|
|
242
|
+
raise AppIsExiting()
|
|
243
|
+
# If logic is finished and app is still running
|
|
244
|
+
elif logic_task in done and app_task in pending:
|
|
245
|
+
result = logic_task.result()
|
|
246
|
+
if shutdown:
|
|
247
|
+
await pilot._wait_for_screen()
|
|
248
|
+
await asyncio.sleep(1.0)
|
|
249
|
+
self.app.exit()
|
|
250
|
+
await app_task
|
|
251
|
+
else:
|
|
252
|
+
# Enable text input so users can interact freely
|
|
253
|
+
self.app.query_one(MyTextArea).disabled = False
|
|
254
|
+
# Wait until app is exited
|
|
255
|
+
await app_task
|
|
256
|
+
return result
|
|
257
|
+
|
|
258
|
+
raise RuntimeError("Unexpected state: both tasks completed")
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
class Vibecore(Generic[TWorkflowReturn]):
|
|
262
|
+
def __init__(self, starting_agent: Agent[TContext], disable_user_input: bool = True) -> None:
|
|
263
|
+
self.context = VibecoreContext()
|
|
264
|
+
self.workflow_logic: Callable[..., Coroutine[Any, Any, TWorkflowReturn]] | None = None
|
|
265
|
+
self.starting_agent = starting_agent
|
|
266
|
+
self.disable_user_input = disable_user_input
|
|
267
|
+
self.runner: VibecoreRunnerBase[TWorkflowReturn] = VibecoreRunnerBase(self)
|
|
268
|
+
|
|
269
|
+
@property
|
|
270
|
+
def session(self) -> Session:
|
|
271
|
+
return self.runner.session
|
|
272
|
+
|
|
273
|
+
async def user_input(self, prompt: str = "") -> str:
|
|
274
|
+
return await self.runner.user_input(prompt)
|
|
275
|
+
|
|
276
|
+
async def print(self, message: str) -> None:
|
|
277
|
+
return await self.runner.print(message)
|
|
278
|
+
|
|
279
|
+
def workflow(self) -> Callable[[DecoratedCallable[TWorkflowReturn]], DecoratedCallable[TWorkflowReturn]]:
|
|
280
|
+
"""Decorator to define the workflow logic for the app.
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
A decorator that wraps the workflow logic function.
|
|
284
|
+
"""
|
|
285
|
+
|
|
286
|
+
def decorator(
|
|
287
|
+
func: DecoratedCallable[TWorkflowReturn],
|
|
288
|
+
) -> DecoratedCallable[TWorkflowReturn]:
|
|
289
|
+
self.workflow_logic = func
|
|
290
|
+
return func
|
|
291
|
+
|
|
292
|
+
return decorator
|
|
293
|
+
|
|
294
|
+
async def run_agent(
|
|
295
|
+
self,
|
|
296
|
+
starting_agent: Agent[TContext],
|
|
297
|
+
input: str | list[TResponseInputItem],
|
|
298
|
+
*,
|
|
299
|
+
context: TContext | None = None,
|
|
300
|
+
max_turns: int = DEFAULT_MAX_TURNS,
|
|
301
|
+
hooks: RunHooks[TContext] | None = None,
|
|
302
|
+
run_config: RunConfig | None = None,
|
|
303
|
+
previous_response_id: str | None = None,
|
|
304
|
+
session: Session | None = None,
|
|
305
|
+
) -> RunResultBase:
|
|
306
|
+
return await self.runner.run_agent(
|
|
307
|
+
starting_agent=starting_agent,
|
|
308
|
+
input=input,
|
|
309
|
+
context=context,
|
|
310
|
+
max_turns=max_turns,
|
|
311
|
+
hooks=hooks,
|
|
312
|
+
run_config=run_config,
|
|
313
|
+
previous_response_id=previous_response_id,
|
|
314
|
+
session=session,
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
async def run_textual(self, shutdown: bool = False) -> TWorkflowReturn:
|
|
318
|
+
if self.workflow_logic is None:
|
|
319
|
+
raise ValueError("Workflow logic not defined. Please use the @vibecore.workflow() decorator.")
|
|
320
|
+
|
|
321
|
+
self.runner = VibecoreTextualRunner(self)
|
|
322
|
+
return await self.runner.run(shutdown=shutdown)
|
|
323
|
+
|
|
324
|
+
async def run_cli(self) -> TWorkflowReturn:
|
|
325
|
+
if self.workflow_logic is None:
|
|
326
|
+
raise ValueError("Workflow logic not defined. Please use the @vibecore.workflow() decorator.")
|
|
327
|
+
|
|
328
|
+
self.runner = VibecoreCliRunner(self)
|
|
329
|
+
return await self.runner.run()
|
|
330
|
+
|
|
331
|
+
@overload
|
|
332
|
+
async def run(self, inputs: str) -> TWorkflowReturn: ...
|
|
333
|
+
|
|
334
|
+
@overload
|
|
335
|
+
async def run(self, inputs: list[str]) -> TWorkflowReturn: ...
|
|
336
|
+
|
|
337
|
+
async def run(self, inputs: str | list[str]) -> TWorkflowReturn:
|
|
338
|
+
if isinstance(inputs, str):
|
|
339
|
+
inputs = [inputs]
|
|
340
|
+
|
|
341
|
+
if self.workflow_logic is None:
|
|
342
|
+
raise ValueError("Workflow logic not defined. Please use the @vibecore.workflow() decorator.")
|
|
343
|
+
|
|
344
|
+
self.runner = VibecoreStaticRunner(self)
|
|
345
|
+
return await self.runner.run(inputs=inputs)
|
|
@@ -65,6 +65,7 @@ class VibecoreApp(App):
|
|
|
65
65
|
CSS_PATH: ClassVar = [
|
|
66
66
|
"widgets/core.tcss",
|
|
67
67
|
"widgets/messages.tcss",
|
|
68
|
+
"widgets/feedback.tcss",
|
|
68
69
|
"widgets/tool_messages.tcss",
|
|
69
70
|
"widgets/expandable.tcss",
|
|
70
71
|
"widgets/info.tcss",
|
|
@@ -355,9 +356,6 @@ class VibecoreApp(App):
|
|
|
355
356
|
|
|
356
357
|
self.current_worker = self.handle_streamed_response(result)
|
|
357
358
|
|
|
358
|
-
def on_click(self) -> None:
|
|
359
|
-
self.query_one("#input-textarea").focus()
|
|
360
|
-
|
|
361
359
|
def _get_model_context_window(self) -> int:
|
|
362
360
|
from vibecore.settings import settings
|
|
363
361
|
|
|
@@ -5,6 +5,8 @@ import logging
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from typing import TYPE_CHECKING
|
|
7
7
|
|
|
8
|
+
from agents import Session
|
|
9
|
+
|
|
8
10
|
if TYPE_CHECKING:
|
|
9
11
|
from openai.types.responses import ResponseInputItemParam as TResponseInputItem
|
|
10
12
|
|
|
@@ -14,7 +16,7 @@ from .path_utils import get_session_file_path
|
|
|
14
16
|
logger = logging.getLogger(__name__)
|
|
15
17
|
|
|
16
18
|
|
|
17
|
-
class JSONLSession:
|
|
19
|
+
class JSONLSession(Session):
|
|
18
20
|
"""JSONL-based implementation of the agents.Session protocol.
|
|
19
21
|
|
|
20
22
|
Stores conversation history in JSON Lines format, with one JSON object
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from typing import Any
|
|
4
4
|
|
|
5
|
-
from .models import TodoItem
|
|
5
|
+
from .models import TodoItem
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class TodoManager:
|
|
@@ -20,12 +20,4 @@ class TodoManager:
|
|
|
20
20
|
|
|
21
21
|
def write(self, todos: list[dict[str, Any]]) -> None:
|
|
22
22
|
"""Replace the entire todo list."""
|
|
23
|
-
self.todos = [
|
|
24
|
-
TodoItem(
|
|
25
|
-
id=todo["id"],
|
|
26
|
-
content=todo["content"],
|
|
27
|
-
status=TodoStatus(todo["status"]),
|
|
28
|
-
priority=TodoPriority(todo["priority"]),
|
|
29
|
-
)
|
|
30
|
-
for todo in todos
|
|
31
|
-
]
|
|
23
|
+
self.todos = [TodoItem(**todo) for todo in todos]
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
"""Todo data models."""
|
|
2
2
|
|
|
3
3
|
import uuid
|
|
4
|
-
from dataclasses import dataclass, field
|
|
5
4
|
from enum import Enum
|
|
6
5
|
|
|
7
|
-
from pydantic import BaseModel
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
8
7
|
|
|
9
8
|
|
|
10
9
|
class TodoStatus(str, Enum):
|
|
@@ -19,18 +18,10 @@ class TodoPriority(str, Enum):
|
|
|
19
18
|
LOW = "low"
|
|
20
19
|
|
|
21
20
|
|
|
22
|
-
|
|
23
|
-
class TodoItem:
|
|
24
|
-
content: str
|
|
25
|
-
status: TodoStatus
|
|
26
|
-
priority: TodoPriority
|
|
27
|
-
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
class TodoItemModel(BaseModel):
|
|
21
|
+
class TodoItem(BaseModel):
|
|
31
22
|
"""Pydantic model for todo items."""
|
|
32
23
|
|
|
33
|
-
id: str
|
|
24
|
+
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
|
34
25
|
content: str
|
|
35
|
-
status:
|
|
36
|
-
priority:
|
|
26
|
+
status: TodoStatus
|
|
27
|
+
priority: TodoPriority
|
|
@@ -6,7 +6,7 @@ from agents import RunContextWrapper, function_tool
|
|
|
6
6
|
|
|
7
7
|
from vibecore.context import VibecoreContext
|
|
8
8
|
|
|
9
|
-
from .models import
|
|
9
|
+
from .models import TodoItem
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
@function_tool
|
|
@@ -38,7 +38,7 @@ async def todo_read(ctx: RunContextWrapper[VibecoreContext]) -> list[dict[str, A
|
|
|
38
38
|
|
|
39
39
|
|
|
40
40
|
@function_tool
|
|
41
|
-
async def todo_write(ctx: RunContextWrapper[VibecoreContext], todos: list[
|
|
41
|
+
async def todo_write(ctx: RunContextWrapper[VibecoreContext], todos: list[TodoItem]) -> str:
|
|
42
42
|
"""Use this tool to create and manage a structured task list for your current coding session. This helps you
|
|
43
43
|
track progress, organize complex tasks, and demonstrate thoroughness to the user. It also helps the user
|
|
44
44
|
understand the progress of the task and overall progress of their requests.
|
|
@@ -106,6 +106,6 @@ async def todo_write(ctx: RunContextWrapper[VibecoreContext], todos: list[TodoIt
|
|
|
106
106
|
Success message.
|
|
107
107
|
"""
|
|
108
108
|
# Convert Pydantic models to dicts for the implementation
|
|
109
|
-
todos_dict = [todo.model_dump() for todo in todos]
|
|
109
|
+
todos_dict = [todo.model_dump() if isinstance(todo, TodoItem) else TodoItem(**todo).model_dump() for todo in todos]
|
|
110
110
|
ctx.context.todo_manager.write(todos_dict)
|
|
111
111
|
return "Todo list updated successfully."
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Feedback widget for collecting user feedback."""
|
|
2
|
+
|
|
3
|
+
from textual.app import ComposeResult
|
|
4
|
+
from textual.containers import Horizontal, Vertical
|
|
5
|
+
from textual.content import Content
|
|
6
|
+
from textual.message import Message
|
|
7
|
+
from textual.reactive import reactive
|
|
8
|
+
from textual.widgets import Button, Checkbox, Static, TextArea
|
|
9
|
+
|
|
10
|
+
from vibecore.widgets.messages import BaseMessage, MessageHeader, MessageStatus
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class FeedbackWidget(BaseMessage):
|
|
14
|
+
"""A widget to collect user feedback with Good/Bad rating and optional text comment."""
|
|
15
|
+
|
|
16
|
+
class FeedbackSubmitted(Message):
|
|
17
|
+
"""Event emitted when feedback is submitted."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, rating: str, comment: str, criteria: dict[str, bool]) -> None:
|
|
20
|
+
"""
|
|
21
|
+
Construct a FeedbackSubmitted message.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
rating: The rating ("good" or "bad").
|
|
25
|
+
comment: Optional text comment from the user.
|
|
26
|
+
criteria: Dict of criterion name to boolean value.
|
|
27
|
+
"""
|
|
28
|
+
super().__init__()
|
|
29
|
+
self.rating = rating
|
|
30
|
+
self.comment = comment
|
|
31
|
+
self.criteria = criteria
|
|
32
|
+
|
|
33
|
+
rating: reactive[str | None] = reactive(None)
|
|
34
|
+
comment: reactive[str] = reactive("")
|
|
35
|
+
submitted: reactive[bool] = reactive(False)
|
|
36
|
+
show_comment_input: reactive[bool] = reactive(False)
|
|
37
|
+
show_criteria: reactive[bool] = reactive(False)
|
|
38
|
+
|
|
39
|
+
def __init__(self, prompt: str = "How was this response?", **kwargs) -> None:
|
|
40
|
+
"""
|
|
41
|
+
Construct a FeedbackWidget.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
prompt: The prompt text to display.
|
|
45
|
+
**kwargs: Additional keyword arguments for Widget.
|
|
46
|
+
"""
|
|
47
|
+
super().__init__(status=MessageStatus.IDLE, **kwargs)
|
|
48
|
+
self.prompt = prompt
|
|
49
|
+
self.add_class("feedback-widget")
|
|
50
|
+
|
|
51
|
+
def get_header_params(self) -> tuple[str, str, bool]:
|
|
52
|
+
"""Get parameters for MessageHeader."""
|
|
53
|
+
return ("⏺", self.prompt, False)
|
|
54
|
+
|
|
55
|
+
def compose(self) -> ComposeResult:
|
|
56
|
+
"""Create child widgets for the feedback widget."""
|
|
57
|
+
yield MessageHeader("⏺", self.prompt, status=self.status)
|
|
58
|
+
|
|
59
|
+
with Horizontal(classes="feedback-controls"):
|
|
60
|
+
yield Button("👍 Good", id="feedback-good", classes="feedback-button good-button", variant="success")
|
|
61
|
+
yield Button("👎 Bad", id="feedback-bad", classes="feedback-button bad-button", variant="error")
|
|
62
|
+
|
|
63
|
+
# Feedback form containing criteria, comment area, and submit button (shown after rating selection)
|
|
64
|
+
with Vertical(id="feedback-form", classes="feedback-form"):
|
|
65
|
+
# Structured feedback criteria checkboxes
|
|
66
|
+
with Vertical(classes="feedback-criteria"):
|
|
67
|
+
yield Static("Please check any criteria that apply:", classes="criteria-label")
|
|
68
|
+
yield Checkbox("Factual accuracy - Was the information correct?", id="criteria-accuracy")
|
|
69
|
+
yield Checkbox("Task completion - Did it fully address the request?", id="criteria-completion")
|
|
70
|
+
yield Checkbox(
|
|
71
|
+
"Instruction following - Did it follow specific constraints or requirements?",
|
|
72
|
+
id="criteria-instructions",
|
|
73
|
+
)
|
|
74
|
+
yield Checkbox(
|
|
75
|
+
"Good format/structure - Are the format and structure appropriate?", id="criteria-format"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Comment text area
|
|
79
|
+
with Vertical(classes="feedback-comment-area"):
|
|
80
|
+
yield Static("Optional comment:", classes="comment-label")
|
|
81
|
+
yield TextArea(id="feedback-textarea", classes="feedback-textarea", show_line_numbers=False)
|
|
82
|
+
|
|
83
|
+
# Submit button
|
|
84
|
+
yield Button("Submit", id="feedback-submit", classes="feedback-submit-button", variant="primary")
|
|
85
|
+
|
|
86
|
+
with Vertical(id="feedback-result", classes="feedback-result"):
|
|
87
|
+
yield Static("", id="feedback-result-text")
|
|
88
|
+
|
|
89
|
+
def on_mount(self) -> None:
|
|
90
|
+
"""Initialize widget state on mount."""
|
|
91
|
+
# Hide feedback form and result initially
|
|
92
|
+
self.query_one("#feedback-form").display = False
|
|
93
|
+
self.query_one("#feedback-result").display = False
|
|
94
|
+
|
|
95
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
96
|
+
"""Handle button press events."""
|
|
97
|
+
button_id = event.button.id
|
|
98
|
+
|
|
99
|
+
if self.submitted:
|
|
100
|
+
return # Ignore clicks after submission
|
|
101
|
+
|
|
102
|
+
if button_id == "feedback-good":
|
|
103
|
+
self.rating = "good"
|
|
104
|
+
self._show_feedback_form()
|
|
105
|
+
event.button.add_class("selected")
|
|
106
|
+
self.query_one("#feedback-bad").remove_class("selected")
|
|
107
|
+
|
|
108
|
+
elif button_id == "feedback-bad":
|
|
109
|
+
self.rating = "bad"
|
|
110
|
+
self._show_feedback_form()
|
|
111
|
+
event.button.add_class("selected")
|
|
112
|
+
self.query_one("#feedback-good").remove_class("selected")
|
|
113
|
+
|
|
114
|
+
elif button_id == "feedback-submit":
|
|
115
|
+
self._submit_feedback()
|
|
116
|
+
|
|
117
|
+
def _show_feedback_form(self) -> None:
|
|
118
|
+
"""Show the feedback form (criteria, comment area, and submit button)."""
|
|
119
|
+
self.show_criteria = True
|
|
120
|
+
self.show_comment_input = True
|
|
121
|
+
feedback_form = self.query_one("#feedback-form")
|
|
122
|
+
feedback_form.display = True
|
|
123
|
+
feedback_form.refresh()
|
|
124
|
+
|
|
125
|
+
def _get_criteria_values(self) -> dict[str, bool]:
|
|
126
|
+
"""Get the current state of all criteria checkboxes."""
|
|
127
|
+
return {
|
|
128
|
+
"accuracy": self.query_one("#criteria-accuracy", Checkbox).value,
|
|
129
|
+
"completion": self.query_one("#criteria-completion", Checkbox).value,
|
|
130
|
+
"instructions": self.query_one("#criteria-instructions", Checkbox).value,
|
|
131
|
+
"format": self.query_one("#criteria-format", Checkbox).value,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
def _submit_feedback(self) -> None:
|
|
135
|
+
"""Submit the feedback."""
|
|
136
|
+
if not self.rating:
|
|
137
|
+
# Require a rating before submission
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
self.submitted = True
|
|
141
|
+
self.comment = self.query_one("#feedback-textarea", TextArea).text
|
|
142
|
+
criteria = self._get_criteria_values()
|
|
143
|
+
|
|
144
|
+
# Hide controls and form, show result
|
|
145
|
+
self.query_one(".feedback-controls").display = False
|
|
146
|
+
self.query_one("#feedback-form").display = False
|
|
147
|
+
|
|
148
|
+
# Show result
|
|
149
|
+
result_container = self.query_one("#feedback-result")
|
|
150
|
+
result_text = self.query_one("#feedback-result-text", Static)
|
|
151
|
+
|
|
152
|
+
rating_emoji = "👍" if self.rating == "good" else "👎"
|
|
153
|
+
result_msg = f"{rating_emoji} Thank you for your feedback!"
|
|
154
|
+
if self.comment:
|
|
155
|
+
result_msg += f"\nComment: {self.comment}"
|
|
156
|
+
|
|
157
|
+
result_text.update(Content(result_msg))
|
|
158
|
+
result_container.display = True
|
|
159
|
+
|
|
160
|
+
# Update status to success
|
|
161
|
+
self.status = MessageStatus.SUCCESS
|
|
162
|
+
|
|
163
|
+
# Emit feedback event
|
|
164
|
+
self.post_message(self.FeedbackSubmitted(self.rating, self.comment, criteria))
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/* Feedback widget styles */
|
|
2
|
+
|
|
3
|
+
FeedbackWidget {
|
|
4
|
+
color: $text;
|
|
5
|
+
|
|
6
|
+
.feedback-controls {
|
|
7
|
+
height: auto;
|
|
8
|
+
width: 1fr;
|
|
9
|
+
padding: 0 0 0 4;
|
|
10
|
+
margin-top: 1;
|
|
11
|
+
|
|
12
|
+
.feedback-button {
|
|
13
|
+
height: 3;
|
|
14
|
+
min-width: 12;
|
|
15
|
+
margin-right: 1;
|
|
16
|
+
|
|
17
|
+
&.selected {
|
|
18
|
+
text-style: bold;
|
|
19
|
+
border: heavy;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
&.good-button {
|
|
23
|
+
background: $success;
|
|
24
|
+
color: $text;
|
|
25
|
+
|
|
26
|
+
&:hover {
|
|
27
|
+
background: $success-lighten-1;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
&.selected {
|
|
31
|
+
background: $success-darken-1;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
&.bad-button {
|
|
36
|
+
background: $error;
|
|
37
|
+
color: $text;
|
|
38
|
+
|
|
39
|
+
&:hover {
|
|
40
|
+
background: $error-lighten-1;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
&.selected {
|
|
44
|
+
background: $error-darken-1;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.feedback-form {
|
|
52
|
+
height: auto;
|
|
53
|
+
width: 1fr;
|
|
54
|
+
padding: 0 0 0 4;
|
|
55
|
+
margin-top: 1;
|
|
56
|
+
|
|
57
|
+
.feedback-criteria {
|
|
58
|
+
height: auto;
|
|
59
|
+
width: 1fr;
|
|
60
|
+
|
|
61
|
+
.criteria-label {
|
|
62
|
+
color: $text-muted;
|
|
63
|
+
margin-bottom: 0;
|
|
64
|
+
height: 1;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
Checkbox {
|
|
68
|
+
border: none;
|
|
69
|
+
margin: 0;
|
|
70
|
+
padding: 0;
|
|
71
|
+
color: $text;
|
|
72
|
+
|
|
73
|
+
&:focus {
|
|
74
|
+
background: $panel;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.feedback-comment-area {
|
|
80
|
+
height: auto;
|
|
81
|
+
width: 1fr;
|
|
82
|
+
margin-top: 1;
|
|
83
|
+
|
|
84
|
+
.comment-label {
|
|
85
|
+
color: $text-muted;
|
|
86
|
+
margin-bottom: 0;
|
|
87
|
+
height: 1;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.feedback-textarea {
|
|
91
|
+
height: 6;
|
|
92
|
+
width: 1fr;
|
|
93
|
+
border: tall $primary;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.feedback-submit-button {
|
|
98
|
+
height: 3;
|
|
99
|
+
min-width: 10;
|
|
100
|
+
margin-top: 1;
|
|
101
|
+
background: $primary;
|
|
102
|
+
color: $text;
|
|
103
|
+
|
|
104
|
+
&:hover {
|
|
105
|
+
background: $primary-lighten-1;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.feedback-result {
|
|
111
|
+
height: auto;
|
|
112
|
+
width: 1fr;
|
|
113
|
+
padding: 1 0 0 4;
|
|
114
|
+
margin-top: 1;
|
|
115
|
+
color: $success;
|
|
116
|
+
|
|
117
|
+
#feedback-result-text {
|
|
118
|
+
color: $success;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import os
|
|
1
2
|
from enum import StrEnum
|
|
2
3
|
|
|
3
4
|
from textual.app import ComposeResult
|
|
@@ -84,11 +85,16 @@ class MessageHeader(Widget):
|
|
|
84
85
|
# self.query_one(".prefix").visible = self._prefix_visible
|
|
85
86
|
|
|
86
87
|
def _on_mount(self, event) -> None:
|
|
88
|
+
disable_blink = bool(os.environ.get("TEXTUAL_SNAPSHOT_TEMPDIR"))
|
|
87
89
|
self.blink_timer = self.set_interval(
|
|
88
90
|
0.5,
|
|
89
91
|
self._toggle_cursor_blink_visible,
|
|
90
|
-
pause=(self.status != MessageStatus.EXECUTING),
|
|
92
|
+
pause=(self.status != MessageStatus.EXECUTING) or disable_blink,
|
|
91
93
|
)
|
|
94
|
+
# Ensure the prefix starts visible for executing statuses so snapshot tests
|
|
95
|
+
# and initial renders see the indicator before the first timer tick hides it.
|
|
96
|
+
if self.status == MessageStatus.EXECUTING:
|
|
97
|
+
self._prefix_visible = True
|
|
92
98
|
|
|
93
99
|
|
|
94
100
|
class BaseMessage(Widget):
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import threading
|
|
3
|
-
from collections.abc import Callable, Coroutine
|
|
4
|
-
from typing import Protocol
|
|
5
|
-
|
|
6
|
-
from agents import Agent
|
|
7
|
-
from textual.pilot import Pilot
|
|
8
|
-
|
|
9
|
-
from vibecore.context import VibecoreContext
|
|
10
|
-
from vibecore.main import AppIsExiting, VibecoreApp
|
|
11
|
-
from vibecore.widgets.core import MyTextArea
|
|
12
|
-
from vibecore.widgets.messages import SystemMessage
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class UserInputFunc(Protocol):
|
|
16
|
-
"""Protocol for user input function with optional prompt parameter."""
|
|
17
|
-
|
|
18
|
-
async def __call__(self, prompt: str = "") -> str:
|
|
19
|
-
"""Get user input with optional prompt message.
|
|
20
|
-
|
|
21
|
-
Args:
|
|
22
|
-
prompt: Optional prompt to display before getting input.
|
|
23
|
-
|
|
24
|
-
Returns:
|
|
25
|
-
The user's input string.
|
|
26
|
-
"""
|
|
27
|
-
...
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
async def flow(
|
|
31
|
-
agent: Agent,
|
|
32
|
-
logic: Callable[[VibecoreApp, VibecoreContext, UserInputFunc], Coroutine],
|
|
33
|
-
headless: bool = False,
|
|
34
|
-
shutdown: bool = False,
|
|
35
|
-
disable_user_input: bool = True,
|
|
36
|
-
):
|
|
37
|
-
ctx = VibecoreContext()
|
|
38
|
-
app = VibecoreApp(ctx, agent, show_welcome=False)
|
|
39
|
-
|
|
40
|
-
app_ready_event = asyncio.Event()
|
|
41
|
-
|
|
42
|
-
def on_app_ready() -> None:
|
|
43
|
-
"""Called when app is ready to process events."""
|
|
44
|
-
app_ready_event.set()
|
|
45
|
-
|
|
46
|
-
async def run_app(app: VibecoreApp) -> None:
|
|
47
|
-
"""Run the apps message loop.
|
|
48
|
-
|
|
49
|
-
Args:
|
|
50
|
-
app: App to run.
|
|
51
|
-
"""
|
|
52
|
-
|
|
53
|
-
with app._context():
|
|
54
|
-
try:
|
|
55
|
-
app._loop = asyncio.get_running_loop()
|
|
56
|
-
app._thread_id = threading.get_ident()
|
|
57
|
-
await app._process_messages(
|
|
58
|
-
ready_callback=on_app_ready,
|
|
59
|
-
headless=headless,
|
|
60
|
-
)
|
|
61
|
-
finally:
|
|
62
|
-
app_ready_event.set()
|
|
63
|
-
|
|
64
|
-
async def user_input(prompt: str = "") -> str:
|
|
65
|
-
if prompt:
|
|
66
|
-
await app.add_message(SystemMessage(prompt))
|
|
67
|
-
app.query_one(MyTextArea).disabled = False
|
|
68
|
-
app.query_one(MyTextArea).focus()
|
|
69
|
-
user_input = await app.wait_for_user_input()
|
|
70
|
-
if disable_user_input:
|
|
71
|
-
app.query_one(MyTextArea).disabled = True
|
|
72
|
-
return user_input
|
|
73
|
-
|
|
74
|
-
async def run_logic(app: VibecoreApp, ctx: VibecoreContext, user_input: UserInputFunc) -> None:
|
|
75
|
-
try:
|
|
76
|
-
await logic(app, ctx, user_input)
|
|
77
|
-
except AppIsExiting:
|
|
78
|
-
return
|
|
79
|
-
|
|
80
|
-
app_task = asyncio.create_task(run_app(app), name=f"with_app({app})")
|
|
81
|
-
await app_ready_event.wait()
|
|
82
|
-
pilot = Pilot(app)
|
|
83
|
-
logic_task: asyncio.Task | None = None
|
|
84
|
-
|
|
85
|
-
await pilot._wait_for_screen()
|
|
86
|
-
if disable_user_input:
|
|
87
|
-
app.query_one(MyTextArea).disabled = True
|
|
88
|
-
logic_task = asyncio.create_task(run_logic(app, ctx, user_input), name="logic_task")
|
|
89
|
-
done, pending = await asyncio.wait([logic_task, app_task], return_when=asyncio.FIRST_COMPLETED)
|
|
90
|
-
|
|
91
|
-
# If app has exited and logic is still running, cancel logic
|
|
92
|
-
if app_task in done and logic_task in pending:
|
|
93
|
-
logic_task.cancel()
|
|
94
|
-
# If logic is finished and app is still running
|
|
95
|
-
elif logic_task in done and app_task in pending:
|
|
96
|
-
if shutdown:
|
|
97
|
-
if not headless:
|
|
98
|
-
await pilot._wait_for_screen()
|
|
99
|
-
await asyncio.sleep(1.0)
|
|
100
|
-
app.exit()
|
|
101
|
-
else:
|
|
102
|
-
# Enable text input so users can interact freely
|
|
103
|
-
app.query_one(MyTextArea).disabled = False
|
|
104
|
-
# Wait until app is exited
|
|
105
|
-
await app_task
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|