vibecore 0.2.0a1__py3-none-any.whl → 0.3.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 (38) hide show
  1. vibecore/agents/default.py +6 -11
  2. vibecore/agents/{task_agent.py → task.py} +2 -6
  3. vibecore/auth/__init__.py +15 -0
  4. vibecore/auth/config.py +38 -0
  5. vibecore/auth/interceptor.py +141 -0
  6. vibecore/auth/manager.py +173 -0
  7. vibecore/auth/models.py +54 -0
  8. vibecore/auth/oauth_flow.py +129 -0
  9. vibecore/auth/pkce.py +29 -0
  10. vibecore/auth/storage.py +111 -0
  11. vibecore/auth/token_manager.py +131 -0
  12. vibecore/cli.py +117 -9
  13. vibecore/flow.py +105 -0
  14. vibecore/handlers/stream_handler.py +11 -0
  15. vibecore/main.py +28 -6
  16. vibecore/models/anthropic_auth.py +226 -0
  17. vibecore/settings.py +61 -5
  18. vibecore/tools/task/executor.py +1 -1
  19. vibecore/tools/webfetch/__init__.py +7 -0
  20. vibecore/tools/webfetch/executor.py +127 -0
  21. vibecore/tools/webfetch/models.py +22 -0
  22. vibecore/tools/webfetch/tools.py +46 -0
  23. vibecore/tools/websearch/__init__.py +5 -0
  24. vibecore/tools/websearch/base.py +27 -0
  25. vibecore/tools/websearch/ddgs/__init__.py +5 -0
  26. vibecore/tools/websearch/ddgs/backend.py +64 -0
  27. vibecore/tools/websearch/executor.py +43 -0
  28. vibecore/tools/websearch/models.py +20 -0
  29. vibecore/tools/websearch/tools.py +49 -0
  30. vibecore/widgets/tool_message_factory.py +24 -0
  31. vibecore/widgets/tool_messages.py +219 -0
  32. vibecore/widgets/tool_messages.tcss +94 -0
  33. {vibecore-0.2.0a1.dist-info → vibecore-0.3.0.dist-info}/METADATA +107 -1
  34. {vibecore-0.2.0a1.dist-info → vibecore-0.3.0.dist-info}/RECORD +37 -15
  35. vibecore-0.3.0.dist-info/entry_points.txt +2 -0
  36. vibecore-0.2.0a1.dist-info/entry_points.txt +0 -2
  37. {vibecore-0.2.0a1.dist-info → vibecore-0.3.0.dist-info}/WHEEL +0 -0
  38. {vibecore-0.2.0a1.dist-info → vibecore-0.3.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,111 @@
1
+ """Secure storage for authentication credentials."""
2
+
3
+ import json
4
+ import os
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from vibecore.auth.models import AnthropicAuth, ApiKeyCredentials, OAuthCredentials
9
+
10
+
11
+ class SecureAuthStorage:
12
+ """Secure storage for authentication credentials."""
13
+
14
+ def __init__(self, app_name: str = "vibecore"):
15
+ """
16
+ Initialize secure storage.
17
+
18
+ Args:
19
+ app_name: Application name for storage directory.
20
+ """
21
+ # Store in user's local data directory
22
+ self.storage_path = Path.home() / ".local" / "share" / app_name / "auth.json"
23
+
24
+ async def save(self, provider: str, credentials: AnthropicAuth) -> None:
25
+ """
26
+ Save credentials securely.
27
+
28
+ Args:
29
+ provider: Provider name (e.g., "anthropic").
30
+ credentials: Authentication credentials.
31
+ """
32
+ # Ensure directory exists
33
+ self.storage_path.parent.mkdir(parents=True, exist_ok=True)
34
+
35
+ # Load existing data
36
+ data = await self._load_all()
37
+
38
+ # Convert credentials to dict
39
+ if isinstance(credentials, OAuthCredentials):
40
+ cred_dict = {
41
+ "type": "oauth",
42
+ "refresh": credentials.refresh,
43
+ "access": credentials.access,
44
+ "expires": credentials.expires,
45
+ }
46
+ elif isinstance(credentials, ApiKeyCredentials):
47
+ cred_dict = {"type": "api", "key": credentials.key}
48
+ else:
49
+ raise ValueError(f"Unknown credential type: {type(credentials)}")
50
+
51
+ # Update credentials
52
+ data[provider] = cred_dict
53
+
54
+ # Write with secure permissions (owner read/write only)
55
+ self.storage_path.write_text(json.dumps(data, indent=2))
56
+ os.chmod(self.storage_path, 0o600)
57
+
58
+ async def load(self, provider: str) -> AnthropicAuth | None:
59
+ """
60
+ Load credentials for a provider.
61
+
62
+ Args:
63
+ provider: Provider name.
64
+
65
+ Returns:
66
+ Authentication credentials or None if not found.
67
+ """
68
+ data = await self._load_all()
69
+ cred_dict = data.get(provider)
70
+
71
+ if not cred_dict:
72
+ return None
73
+
74
+ # Convert dict back to credentials object
75
+ if cred_dict.get("type") == "oauth":
76
+ return OAuthCredentials(
77
+ type="oauth",
78
+ refresh=cred_dict.get("refresh", ""),
79
+ access=cred_dict.get("access", ""),
80
+ expires=cred_dict.get("expires", 0),
81
+ )
82
+ elif cred_dict.get("type") == "api":
83
+ return ApiKeyCredentials(type="api", key=cred_dict.get("key", ""))
84
+
85
+ return None
86
+
87
+ async def remove(self, provider: str) -> None:
88
+ """
89
+ Remove credentials for a provider.
90
+
91
+ Args:
92
+ provider: Provider name.
93
+ """
94
+ data = await self._load_all()
95
+ data.pop(provider, None)
96
+ self.storage_path.write_text(json.dumps(data, indent=2))
97
+ os.chmod(self.storage_path, 0o600)
98
+
99
+ async def _load_all(self) -> dict[str, Any]:
100
+ """Load all stored credentials."""
101
+ if not self.storage_path.exists():
102
+ return {}
103
+
104
+ try:
105
+ return json.loads(self.storage_path.read_text())
106
+ except (json.JSONDecodeError, OSError):
107
+ return {}
108
+
109
+ def exists(self) -> bool:
110
+ """Check if any credentials are stored."""
111
+ return self.storage_path.exists() and self.storage_path.stat().st_size > 2
@@ -0,0 +1,131 @@
1
+ """Token management for OAuth authentication."""
2
+
3
+ import asyncio
4
+ import time
5
+
6
+ import httpx
7
+
8
+ from vibecore.auth.config import ANTHROPIC_CONFIG
9
+ from vibecore.auth.models import OAuthCredentials
10
+ from vibecore.auth.storage import SecureAuthStorage
11
+
12
+
13
+ class TokenRefreshManager:
14
+ """Manages OAuth token refresh with automatic renewal."""
15
+
16
+ def __init__(self, storage: SecureAuthStorage):
17
+ """
18
+ Initialize token refresh manager.
19
+
20
+ Args:
21
+ storage: Secure storage for credentials.
22
+ """
23
+ self.storage = storage
24
+ self.refresh_lock = asyncio.Lock()
25
+ self.refresh_task: asyncio.Task | None = None
26
+
27
+ async def get_valid_token(self) -> str:
28
+ """
29
+ Get a valid access token, refreshing if necessary.
30
+
31
+ Returns:
32
+ Valid access token.
33
+
34
+ Raises:
35
+ ValueError: If not authenticated or refresh fails.
36
+ """
37
+ auth = await self.storage.load("anthropic")
38
+
39
+ if not auth:
40
+ raise ValueError("Not authenticated")
41
+
42
+ if auth.type == "api": # API key auth
43
+ return auth.key # type: ignore
44
+
45
+ # OAuth auth - check if token needs refresh
46
+ now = int(time.time() * 1000)
47
+ buffer_ms = ANTHROPIC_CONFIG.TOKEN_REFRESH_BUFFER_SECONDS * 1000
48
+ needs_refresh = not auth.access or auth.expires <= now + buffer_ms # type: ignore
49
+
50
+ if not needs_refresh:
51
+ return auth.access # type: ignore
52
+
53
+ # Refresh token with lock to prevent concurrent refreshes
54
+ async with self.refresh_lock:
55
+ # Re-check after acquiring lock
56
+ auth = await self.storage.load("anthropic")
57
+ if auth and auth.type == "oauth":
58
+ now = int(time.time() * 1000)
59
+ if auth.access and auth.expires > now + buffer_ms: # type: ignore
60
+ return auth.access # type: ignore
61
+
62
+ # Perform refresh
63
+ if auth and auth.type == "oauth":
64
+ return await self._refresh_token(auth.refresh) # type: ignore
65
+ else:
66
+ raise ValueError("Cannot refresh non-OAuth credentials")
67
+
68
+ async def _refresh_token(self, refresh_token: str) -> str:
69
+ """
70
+ Refresh the access token.
71
+
72
+ Args:
73
+ refresh_token: Refresh token.
74
+
75
+ Returns:
76
+ New access token.
77
+
78
+ Raises:
79
+ httpx.HTTPError: If refresh fails after retries.
80
+ """
81
+ last_error: Exception | None = None
82
+
83
+ # Retry logic
84
+ for attempt in range(ANTHROPIC_CONFIG.TOKEN_MAX_RETRY_ATTEMPTS):
85
+ try:
86
+ # Exponential backoff for retries
87
+ if attempt > 0:
88
+ delay = ANTHROPIC_CONFIG.TOKEN_RETRY_DELAY_MS * (2 ** (attempt - 1)) / 1000
89
+ await asyncio.sleep(delay)
90
+
91
+ # Make refresh request
92
+ async with httpx.AsyncClient() as client:
93
+ response = await client.post(
94
+ ANTHROPIC_CONFIG.TOKEN_EXCHANGE,
95
+ headers={
96
+ "Content-Type": "application/json",
97
+ "Accept": "application/json",
98
+ },
99
+ json={
100
+ "grant_type": "refresh_token",
101
+ "refresh_token": refresh_token,
102
+ "client_id": ANTHROPIC_CONFIG.OAUTH_CLIENT_ID,
103
+ },
104
+ timeout=30.0,
105
+ )
106
+
107
+ if response.status_code != 200:
108
+ error_text = response.text
109
+ raise httpx.HTTPError(f"Token refresh failed: {response.status_code} - {error_text}")
110
+
111
+ tokens_data = response.json()
112
+
113
+ # Update stored credentials
114
+ new_credentials = OAuthCredentials(
115
+ type="oauth",
116
+ refresh=tokens_data.get("refresh_token", refresh_token),
117
+ access=tokens_data["access_token"],
118
+ expires=int(time.time() * 1000) + tokens_data["expires_in"] * 1000,
119
+ )
120
+
121
+ await self.storage.save("anthropic", new_credentials)
122
+ return tokens_data["access_token"]
123
+
124
+ except Exception as error:
125
+ last_error = error
126
+ print(f"Token refresh attempt {attempt + 1} failed: {error}")
127
+
128
+ # All retries failed
129
+ raise ValueError(
130
+ f"Token refresh failed after {ANTHROPIC_CONFIG.TOKEN_MAX_RETRY_ATTEMPTS} attempts: {last_error}"
131
+ )
vibecore/cli.py CHANGED
@@ -1,6 +1,8 @@
1
1
  """Vibecore CLI interface using typer."""
2
2
 
3
+ import asyncio
3
4
  import logging
5
+ from importlib.metadata import version
4
6
  from pathlib import Path
5
7
 
6
8
  import typer
@@ -14,6 +16,21 @@ from vibecore.settings import settings
14
16
 
15
17
  app = typer.Typer()
16
18
 
19
+ # Create auth subcommand group
20
+ auth_app = typer.Typer(help="Manage Anthropic authentication")
21
+ app.add_typer(auth_app, name="auth")
22
+
23
+
24
+ def version_callback(value: bool):
25
+ """Handle --version flag."""
26
+ if value:
27
+ try:
28
+ pkg_version = version("vibecore")
29
+ except Exception:
30
+ pkg_version = "unknown"
31
+ typer.echo(f"vibecore {pkg_version}")
32
+ raise typer.Exit()
33
+
17
34
 
18
35
  def find_latest_session(project_path: Path | None = None, base_dir: Path | None = None) -> str | None:
19
36
  """Find the most recent session file for the current project."""
@@ -45,9 +62,15 @@ def find_latest_session(project_path: Path | None = None, base_dir: Path | None
45
62
  return session_files[0].stem
46
63
 
47
64
 
48
- @app.command()
49
- def run(
50
- prompt: str | None = typer.Argument(None, help="Prompt text (requires -p flag)"),
65
+ @app.callback(invoke_without_command=True)
66
+ def main(
67
+ ctx: typer.Context,
68
+ prompt: str | None = typer.Option(
69
+ None,
70
+ "--prompt",
71
+ "-p",
72
+ help="Initial prompt to send to the agent (reads from stdin if -p is used without argument)",
73
+ ),
51
74
  continue_session: bool = typer.Option(
52
75
  False,
53
76
  "--continue",
@@ -63,11 +86,21 @@ def run(
63
86
  print_mode: bool = typer.Option(
64
87
  False,
65
88
  "--print",
66
- "-p",
67
89
  help="Print response and exit (useful for pipes)",
68
90
  ),
91
+ version: bool | None = typer.Option(
92
+ None,
93
+ "--version",
94
+ callback=version_callback,
95
+ is_eager=True,
96
+ help="Show version and exit",
97
+ ),
69
98
  ):
70
99
  """Run the Vibecore TUI application."""
100
+ # If a subcommand was invoked, don't run the main app
101
+ if ctx.invoked_subcommand is not None:
102
+ return
103
+
71
104
  # Set up logging
72
105
  logging.basicConfig(
73
106
  level="WARNING",
@@ -78,14 +111,14 @@ def run(
78
111
  logger.addHandler(TextualHandler())
79
112
 
80
113
  # Create context
81
- ctx = VibecoreContext()
114
+ vibecore_ctx = VibecoreContext()
82
115
 
83
116
  # Initialize MCP manager if configured
84
117
  mcp_servers = []
85
118
  if settings.mcp_servers:
86
119
  # Create MCP manager
87
120
  mcp_manager = MCPManager(settings.mcp_servers)
88
- ctx.mcp_manager = mcp_manager
121
+ vibecore_ctx.mcp_manager = mcp_manager
89
122
 
90
123
  # Get the MCP servers from the manager
91
124
  mcp_servers = mcp_manager.servers
@@ -106,7 +139,7 @@ def run(
106
139
  typer.echo(f"Loading session: {session_to_load}")
107
140
 
108
141
  # Create app
109
- app_instance = VibecoreApp(ctx, agent, session_id=session_to_load, print_mode=print_mode)
142
+ app_instance = VibecoreApp(vibecore_ctx, agent, session_id=session_to_load, print_mode=print_mode)
110
143
 
111
144
  if print_mode:
112
145
  # Run in print mode
@@ -122,10 +155,85 @@ def run(
122
155
  app_instance.run()
123
156
 
124
157
 
125
- def main():
158
+ @auth_app.command("login")
159
+ def auth_login(
160
+ provider: str = typer.Argument("anthropic", help="Authentication provider (currently only 'anthropic')"),
161
+ api_key: str = typer.Option(None, "--api-key", "-k", help="Use API key instead of OAuth"),
162
+ mode: str = typer.Option(
163
+ "max", "--mode", "-m", help="OAuth mode: 'max' for claude.ai, 'console' for console.anthropic.com"
164
+ ),
165
+ ):
166
+ """Authenticate with Anthropic Pro/Max or API key."""
167
+ if provider.lower() != "anthropic":
168
+ typer.echo(f"❌ Provider '{provider}' not supported. Currently only 'anthropic' is supported.")
169
+ raise typer.Exit(1)
170
+
171
+ from vibecore.auth.manager import AnthropicAuthManager
172
+
173
+ auth_manager = AnthropicAuthManager()
174
+
175
+ if api_key:
176
+ # API key authentication
177
+ success = asyncio.run(auth_manager.authenticate_with_api_key(api_key))
178
+ if not success:
179
+ raise typer.Exit(1)
180
+ else:
181
+ # OAuth Pro/Max authentication
182
+ success = asyncio.run(auth_manager.authenticate_pro_max(mode))
183
+ if not success:
184
+ raise typer.Exit(1)
185
+
186
+
187
+ @auth_app.command("logout")
188
+ def auth_logout(
189
+ provider: str = typer.Argument("anthropic", help="Authentication provider"),
190
+ ):
191
+ """Remove stored authentication."""
192
+ if provider.lower() != "anthropic":
193
+ typer.echo(f"❌ Provider '{provider}' not supported. Currently only 'anthropic' is supported.")
194
+ raise typer.Exit(1)
195
+
196
+ from vibecore.auth.manager import AnthropicAuthManager
197
+
198
+ auth_manager = AnthropicAuthManager()
199
+ asyncio.run(auth_manager.logout())
200
+
201
+
202
+ @auth_app.command("status")
203
+ def auth_status():
204
+ """Check authentication status."""
205
+ from vibecore.auth.manager import AnthropicAuthManager
206
+
207
+ auth_manager = AnthropicAuthManager()
208
+
209
+ if asyncio.run(auth_manager.is_authenticated()):
210
+ auth_type = asyncio.run(auth_manager.get_auth_type())
211
+ if auth_type == "oauth":
212
+ typer.echo("✅ Authenticated with Anthropic Pro/Max (OAuth)")
213
+ else:
214
+ typer.echo("✅ Authenticated with Anthropic API key")
215
+ else:
216
+ typer.echo("❌ Not authenticated with Anthropic")
217
+
218
+
219
+ @auth_app.command("test")
220
+ def auth_test():
221
+ """Test authentication by making a simple API call."""
222
+ from vibecore.auth.manager import AnthropicAuthManager
223
+
224
+ auth_manager = AnthropicAuthManager()
225
+
226
+ typer.echo("🔍 Testing authentication...")
227
+ success = asyncio.run(auth_manager.test_connection())
228
+
229
+ if not success:
230
+ raise typer.Exit(1)
231
+
232
+
233
+ def cli_main():
126
234
  """Entry point for the CLI."""
127
235
  app()
128
236
 
129
237
 
130
238
  if __name__ == "__main__":
131
- main()
239
+ cli_main()
vibecore/flow.py ADDED
@@ -0,0 +1,105 @@
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
@@ -15,6 +15,7 @@ from agents import (
15
15
  ToolCallOutputItem,
16
16
  )
17
17
  from openai.types.responses import (
18
+ ResponseCompletedEvent,
18
19
  ResponseFunctionToolCall,
19
20
  ResponseOutputItemAddedEvent,
20
21
  ResponseOutputItemDoneEvent,
@@ -123,6 +124,7 @@ class AgentStreamHandler:
123
124
  """
124
125
  match event:
125
126
  case RawResponsesStreamEvent(data=data):
127
+ # log(f"RawResponsesStreamEvent data: {data.type}")
126
128
  match data:
127
129
  case ResponseOutputItemAddedEvent(item=ResponseReasoningItem() as item):
128
130
  reasoning_id = item.id
@@ -180,7 +182,15 @@ class AgentStreamHandler:
180
182
  else:
181
183
  await self.handle_tool_call(tool_name, arguments, call_id)
182
184
 
185
+ case ResponseCompletedEvent():
186
+ # When in agent handoff or stop at tool situations, the tools should be in executing status.
187
+ # We find all the executing status tool messages and mark them as success.
188
+ for tool_message in self.tool_messages.values():
189
+ if tool_message.status == MessageStatus.EXECUTING:
190
+ tool_message.status = MessageStatus.SUCCESS
191
+
183
192
  case RunItemStreamEvent(item=item):
193
+ # log(f"RunItemStreamEvent item: {item.type}")
184
194
  match item:
185
195
  case ToolCallItem():
186
196
  pass
@@ -196,6 +206,7 @@ class AgentStreamHandler:
196
206
  await self.handle_message_complete()
197
207
 
198
208
  case AgentUpdatedStreamEvent(new_agent=new_agent):
209
+ # log(f"AgentUpdatedStreamEvent new_agent: {new_agent.name}")
199
210
  await self.message_handler.handle_agent_update(new_agent)
200
211
 
201
212
  async def handle_task_tool_event(self, tool_name: str, tool_call_id: str, event: StreamEvent) -> None:
vibecore/main.py CHANGED
@@ -30,7 +30,11 @@ from vibecore.widgets.core import AppFooter, MainScroll, MyTextArea
30
30
  from vibecore.widgets.info import Welcome
31
31
  from vibecore.widgets.messages import AgentMessage, BaseMessage, MessageStatus, SystemMessage, UserMessage
32
32
 
33
- AgentStatus = Literal["idle", "running"]
33
+ AgentStatus = Literal["idle", "running", "waiting_user_input"]
34
+
35
+
36
+ class AppIsExiting(Exception):
37
+ pass
34
38
 
35
39
 
36
40
  def detect_reasoning_effort(prompt: str) -> Literal["low", "medium", "high"] | None:
@@ -82,6 +86,7 @@ class VibecoreApp(App):
82
86
  agent: Agent,
83
87
  session_id: str | None = None,
84
88
  print_mode: bool = False,
89
+ show_welcome: bool = True,
85
90
  ) -> None:
86
91
  """Initialize the Vibecore app with context and agent.
87
92
 
@@ -90,6 +95,7 @@ class VibecoreApp(App):
90
95
  agent: The Agent instance to use
91
96
  session_id: Optional session ID to load existing session
92
97
  print_mode: Whether to run in print mode (useful for pipes)
98
+ show_welcome: Whether to show the welcome message (default: True)
93
99
  """
94
100
  self.context = context
95
101
  self.context.app = self # Set the app reference in context
@@ -99,6 +105,7 @@ class VibecoreApp(App):
99
105
  self.current_worker: Worker[None] | None = None
100
106
  self._session_id_provided = session_id is not None # Track if continuing session
101
107
  self.print_mode = print_mode
108
+ self.show_welcome = show_welcome
102
109
  self.message_queue: deque[str] = deque() # Queue for user messages
103
110
 
104
111
  # Initialize session based on settings
@@ -124,7 +131,8 @@ class VibecoreApp(App):
124
131
  yield Header()
125
132
  yield AppFooter()
126
133
  with MainScroll(id="messages"):
127
- yield Welcome()
134
+ if self.show_welcome:
135
+ yield Welcome()
128
136
 
129
137
  async def on_mount(self) -> None:
130
138
  """Called when the app is mounted."""
@@ -162,6 +170,8 @@ class VibecoreApp(App):
162
170
  Args:
163
171
  message: The message to add
164
172
  """
173
+ if not self.is_running:
174
+ raise AppIsExiting("App is not running")
165
175
  main_scroll = self.query_one("#messages", MainScroll)
166
176
  await main_scroll.mount(message)
167
177
 
@@ -225,6 +235,14 @@ class VibecoreApp(App):
225
235
  else:
226
236
  footer.hide_loading()
227
237
 
238
+ async def wait_for_user_input(self) -> str:
239
+ """Used in flow mode. See examples/basic_agent.py"""
240
+ self.agent_status = "waiting_user_input"
241
+ self.user_input_event = asyncio.Event()
242
+ await self.user_input_event.wait()
243
+ user_input = self.message_queue.pop()
244
+ return user_input
245
+
228
246
  async def on_my_text_area_user_message(self, event: MyTextArea.UserMessage) -> None:
229
247
  """Handle user messages from the text area."""
230
248
  if event.text:
@@ -248,8 +266,11 @@ class VibecoreApp(App):
248
266
  await self.add_message(user_message)
249
267
  user_message.scroll_visible()
250
268
 
251
- # If agent is running, queue the message
269
+ if self.agent_status == "waiting_user_input":
270
+ self.message_queue.append(event.text)
271
+ self.user_input_event.set()
252
272
  if self.agent_status == "running":
273
+ # If agent is running, queue the message
253
274
  self.message_queue.append(event.text)
254
275
  log(f"Message queued: {event.text}")
255
276
  footer = self.query_one(AppFooter)
@@ -268,7 +289,7 @@ class VibecoreApp(App):
268
289
  if reasoning_effort is not None:
269
290
  # Create a copy of the agent with updated model settings
270
291
  current_settings = self.agent.model_settings or ModelSettings()
271
- new_reasoning = Reasoning(effort=reasoning_effort, summary="auto")
292
+ new_reasoning = Reasoning(effort=reasoning_effort, summary=settings.reasoning_summary)
272
293
  updated_settings = ModelSettings(
273
294
  include_usage=current_settings.include_usage,
274
295
  reasoning=new_reasoning,
@@ -496,8 +517,9 @@ class VibecoreApp(App):
496
517
  for welcome in main_scroll.query("Welcome"):
497
518
  welcome.remove()
498
519
 
499
- # Add welcome widget back
500
- await main_scroll.mount(Welcome())
520
+ # Add welcome widget back if show_welcome is True
521
+ if self.show_welcome:
522
+ await main_scroll.mount(Welcome())
501
523
 
502
524
  # Show system message to confirm the clear operation
503
525
  system_message = SystemMessage(f"✨ Session cleared! Started new session: {new_session_id}")