feat: add permission modes and CLI orchestration layer
Permission Modes: - Add default/plan/auto_accept modes controlling tool access - Plan mode restricts Task agent to read-only tools only - Auto-accept mode bypasses approval prompts (with confirmation) Approval Scaffolding: - Add ApprovalRule/ApprovalRuleSet for granular tool control - Pattern-based matching on tool name and arguments - Default rules for common safe/dangerous patterns - Prep for future bidirectional approval flow CLI Refactor: - Default to Task agent (main orchestrator) - Add --mode flag and runtime mode switching - Integrate prompt_toolkit for better UX: - Persistent command history (~/.webber_history) - Tab completion for commands and file paths - Auto-suggest from history - Deprecate standalone 'explore' command Other: - Split CHANGELOG.md into per-package files - Update AGENTS.md release procedure for both packages Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
# Changelog - Webber CLI
|
||||
|
||||
All notable changes to the Webber CLI will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.1.0] - 2026-01-10
|
||||
|
||||
### Added
|
||||
- Initial CLI release as part of monorepo reorganization
|
||||
- Typer + Rich foundation with console theming
|
||||
- Commands:
|
||||
- `webber-cli status` - Check API connection
|
||||
- `webber-cli explore` - One-shot codebase exploration
|
||||
- `webber-cli chat` - Interactive conversation mode
|
||||
- Streaming support with `--stream` flag (default: enabled)
|
||||
- Markdown rendering for agent responses
|
||||
- Configurable API URL via `WEBBER_API_URL` environment variable
|
||||
@@ -2,3 +2,4 @@
|
||||
httpx~=0.28.1
|
||||
typer~=0.15.0
|
||||
rich~=13.9.0
|
||||
prompt_toolkit~=3.0.48
|
||||
|
||||
@@ -2,20 +2,36 @@
|
||||
Webber API client.
|
||||
|
||||
Communicates with the Webber API backend for agent execution.
|
||||
Supports permission modes for controlling agent tool access.
|
||||
"""
|
||||
import json
|
||||
import httpx
|
||||
from collections.abc import AsyncIterator
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
|
||||
class PermissionMode(str, Enum):
|
||||
"""
|
||||
Permission modes controlling agent tool access.
|
||||
|
||||
- default: All tools available (approval may be required)
|
||||
- plan: Read-only tools only
|
||||
- auto_accept: All tools, no approval prompts
|
||||
"""
|
||||
default = "default"
|
||||
plan = "plan"
|
||||
auto_accept = "auto_accept"
|
||||
|
||||
|
||||
@dataclass
|
||||
class AgentResponse:
|
||||
"""Response from agent execution."""
|
||||
response: str
|
||||
agent_type: str
|
||||
success: bool
|
||||
mode: PermissionMode = PermissionMode.default
|
||||
error: str | None = None
|
||||
|
||||
|
||||
@@ -104,14 +120,16 @@ class WebberClient:
|
||||
agent_type: str,
|
||||
prompt: str,
|
||||
working_dir: str = ".",
|
||||
mode: PermissionMode = PermissionMode.default,
|
||||
) -> AgentResponse:
|
||||
"""
|
||||
Run an agent with the given prompt.
|
||||
|
||||
Args:
|
||||
agent_type: Type of agent (e.g., "explore")
|
||||
agent_type: Type of agent (e.g., "task")
|
||||
prompt: User prompt/query
|
||||
working_dir: Working directory for the agent
|
||||
mode: Permission mode controlling tool access
|
||||
|
||||
Returns:
|
||||
AgentResponse with the result
|
||||
@@ -123,6 +141,7 @@ class WebberClient:
|
||||
"agent_type": agent_type,
|
||||
"prompt": prompt,
|
||||
"working_dir": working_dir,
|
||||
"mode": mode.value,
|
||||
},
|
||||
)
|
||||
response.raise_for_status()
|
||||
@@ -131,6 +150,7 @@ class WebberClient:
|
||||
response=data.get("response", ""),
|
||||
agent_type=data.get("agent_type", agent_type),
|
||||
success=data.get("success", True),
|
||||
mode=PermissionMode(data.get("mode", "default")),
|
||||
error=data.get("error"),
|
||||
)
|
||||
|
||||
@@ -139,14 +159,16 @@ class WebberClient:
|
||||
agent_type: str,
|
||||
prompt: str,
|
||||
working_dir: str = ".",
|
||||
mode: PermissionMode = PermissionMode.default,
|
||||
) -> AsyncIterator[str]:
|
||||
"""
|
||||
Run an agent with streaming response.
|
||||
|
||||
Args:
|
||||
agent_type: Type of agent (e.g., "explore")
|
||||
agent_type: Type of agent (e.g., "task")
|
||||
prompt: User prompt/query
|
||||
working_dir: Working directory for the agent
|
||||
mode: Permission mode controlling tool access
|
||||
|
||||
Yields:
|
||||
Text chunks as they arrive
|
||||
@@ -163,6 +185,7 @@ class WebberClient:
|
||||
"agent_type": agent_type,
|
||||
"prompt": prompt,
|
||||
"working_dir": working_dir,
|
||||
"mode": mode.value,
|
||||
},
|
||||
) as response:
|
||||
response.raise_for_status()
|
||||
|
||||
+273
-110
@@ -5,7 +5,7 @@ Webber CLI - Client for the Webber API.
|
||||
Usage:
|
||||
webber-cli --help
|
||||
webber-cli chat [OPTIONS]
|
||||
webber-cli explore QUERY [OPTIONS]
|
||||
webber-cli status
|
||||
"""
|
||||
import asyncio
|
||||
import os
|
||||
@@ -13,13 +13,87 @@ import sys
|
||||
from pathlib import Path
|
||||
|
||||
import typer
|
||||
from rich.live import Live
|
||||
from prompt_toolkit import PromptSession
|
||||
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
|
||||
from prompt_toolkit.completion import Completer, Completion, PathCompleter
|
||||
from prompt_toolkit.history import FileHistory
|
||||
from prompt_toolkit.styles import Style
|
||||
from rich.markdown import Markdown
|
||||
from rich.panel import Panel
|
||||
from rich.prompt import Confirm
|
||||
|
||||
from webber_cli.client import WebberClient
|
||||
from webber_cli.client import WebberClient, PermissionMode
|
||||
from webber_cli.theme import get_console, get_theme
|
||||
|
||||
|
||||
# === Prompt Toolkit Setup ===
|
||||
|
||||
# History file location
|
||||
HISTORY_FILE = Path.home() / ".webber_history"
|
||||
|
||||
# Built-in commands for completion
|
||||
BUILTIN_COMMANDS = [
|
||||
"exit",
|
||||
"quit",
|
||||
"clear",
|
||||
"mode plan",
|
||||
"mode default",
|
||||
"mode auto_accept",
|
||||
"cd ",
|
||||
]
|
||||
|
||||
|
||||
class WebberCompleter(Completer):
|
||||
"""Custom completer for Webber CLI commands."""
|
||||
|
||||
def __init__(self, working_dir: str):
|
||||
self.working_dir = working_dir
|
||||
self.path_completer = PathCompleter(expanduser=True)
|
||||
|
||||
def get_completions(self, document, complete_event):
|
||||
text = document.text_before_cursor.lower()
|
||||
|
||||
# Complete built-in commands
|
||||
if not text or not text.startswith("cd "):
|
||||
for cmd in BUILTIN_COMMANDS:
|
||||
if cmd.startswith(text):
|
||||
yield Completion(
|
||||
cmd,
|
||||
start_position=-len(text),
|
||||
display_meta="command",
|
||||
)
|
||||
|
||||
# Complete file paths after "cd "
|
||||
if text.startswith("cd "):
|
||||
path_text = text[3:]
|
||||
# Create a sub-document for path completion
|
||||
from prompt_toolkit.document import Document
|
||||
path_doc = Document(path_text, len(path_text))
|
||||
for completion in self.path_completer.get_completions(path_doc, complete_event):
|
||||
yield Completion(
|
||||
"cd " + (path_text + completion.text),
|
||||
start_position=-len(text),
|
||||
display_meta="directory",
|
||||
)
|
||||
|
||||
|
||||
# Prompt style matching Rich theme
|
||||
PROMPT_STYLE = Style.from_dict({
|
||||
"prompt": "#5f87d7 bold", # info color
|
||||
"": "", # default text
|
||||
})
|
||||
|
||||
|
||||
def create_prompt_session(working_dir: str) -> PromptSession:
|
||||
"""Create a configured prompt session with history and completion."""
|
||||
return PromptSession(
|
||||
history=FileHistory(str(HISTORY_FILE)),
|
||||
auto_suggest=AutoSuggestFromHistory(),
|
||||
completer=WebberCompleter(working_dir),
|
||||
style=PROMPT_STYLE,
|
||||
complete_while_typing=False, # Only complete on Tab
|
||||
)
|
||||
|
||||
app = typer.Typer(
|
||||
name="webber-cli",
|
||||
help="CLI client for the Webber API",
|
||||
@@ -37,11 +111,28 @@ DEFAULT_API_URL = os.environ.get("WEBBER_API_URL", "http://localhost:8095")
|
||||
def version_callback(value: bool) -> None:
|
||||
"""Display version and exit."""
|
||||
if value:
|
||||
from cli import __version__
|
||||
from webber_cli import __version__
|
||||
console.print(f"[title]webber-cli[/] version [success]{__version__}[/]")
|
||||
raise typer.Exit()
|
||||
|
||||
|
||||
def _confirm_auto_accept() -> bool:
|
||||
"""
|
||||
Prompt user to confirm auto_accept mode.
|
||||
|
||||
Returns True if user confirms, False otherwise.
|
||||
"""
|
||||
console.print()
|
||||
console.print("[warning]WARNING:[/] auto_accept mode bypasses all safety prompts.")
|
||||
console.print("The agent will execute write operations without confirmation.")
|
||||
console.print()
|
||||
return Confirm.ask(
|
||||
"[warning]Are you sure you want to enable auto_accept mode?[/]",
|
||||
default=False,
|
||||
console=console,
|
||||
)
|
||||
|
||||
|
||||
@app.callback()
|
||||
def main(
|
||||
version: bool = typer.Option(
|
||||
@@ -63,7 +154,7 @@ def chat(
|
||||
".",
|
||||
"--directory",
|
||||
"-d",
|
||||
help="Working directory for exploration",
|
||||
help="Working directory for the agent",
|
||||
),
|
||||
api_url: str = typer.Option(
|
||||
DEFAULT_API_URL,
|
||||
@@ -71,10 +162,11 @@ def chat(
|
||||
"-a",
|
||||
help="Webber API URL",
|
||||
),
|
||||
agent: str = typer.Option(
|
||||
"explore",
|
||||
"--agent",
|
||||
help="Agent to use",
|
||||
mode: str = typer.Option(
|
||||
"default",
|
||||
"--mode",
|
||||
"-m",
|
||||
help="Permission mode: default, plan (read-only), auto_accept (no prompts)",
|
||||
),
|
||||
stream: bool = typer.Option(
|
||||
True,
|
||||
@@ -84,9 +176,18 @@ def chat(
|
||||
),
|
||||
) -> None:
|
||||
"""
|
||||
Start interactive chat session.
|
||||
Start interactive chat session with the Task agent.
|
||||
|
||||
Connects to the Webber API backend for agent execution.
|
||||
The Task agent is the main orchestrator that can:
|
||||
- Explore and analyze codebases
|
||||
- Plan implementation strategies
|
||||
- Execute code modifications (in default/auto_accept modes)
|
||||
- Spawn sub-agents for focused tasks
|
||||
|
||||
Permission modes:
|
||||
- default: Full capabilities with approval prompts for writes
|
||||
- plan: Read-only mode for safe exploration and planning
|
||||
- auto_accept: Full capabilities without approval prompts (use with caution)
|
||||
"""
|
||||
working_dir = str(Path(directory).resolve())
|
||||
|
||||
@@ -94,17 +195,35 @@ def chat(
|
||||
console.print(f"[error]Error:[/] Directory not found: {working_dir}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Parse and validate mode
|
||||
try:
|
||||
asyncio.run(_chat_loop(api_url, agent, working_dir, stream))
|
||||
permission_mode = PermissionMode(mode)
|
||||
except ValueError:
|
||||
console.print(f"[error]Error:[/] Invalid mode: {mode}")
|
||||
console.print("[dim]Valid modes: default, plan, auto_accept[/]")
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Confirm auto_accept mode (security risk)
|
||||
if permission_mode == PermissionMode.auto_accept:
|
||||
if not _confirm_auto_accept():
|
||||
console.print("[dim]Cancelled. Using default mode instead.[/]")
|
||||
permission_mode = PermissionMode.default
|
||||
|
||||
try:
|
||||
asyncio.run(_chat_loop(api_url, working_dir, permission_mode, stream))
|
||||
except KeyboardInterrupt:
|
||||
console.print("\n[dim]Goodbye![/]")
|
||||
|
||||
|
||||
async def _chat_loop(
|
||||
api_url: str, agent_type: str, working_dir: str, stream: bool = True
|
||||
api_url: str,
|
||||
working_dir: str,
|
||||
mode: PermissionMode,
|
||||
stream: bool = True,
|
||||
) -> None:
|
||||
"""Interactive chat loop."""
|
||||
"""Interactive chat loop with the Task agent."""
|
||||
theme = get_theme()
|
||||
agent_type = "task"
|
||||
|
||||
async with WebberClient(api_url) as client:
|
||||
# Check API health
|
||||
@@ -116,28 +235,45 @@ async def _chat_loop(
|
||||
# Get agent info
|
||||
agent_info = await client.get_agent(agent_type)
|
||||
if not agent_info:
|
||||
console.print(f"[error]Error:[/] Unknown agent: {agent_type}")
|
||||
agents = await client.list_agents()
|
||||
console.print("[dim]Available agents:[/]")
|
||||
for a in agents:
|
||||
console.print(f" - {a.name}: {a.description}")
|
||||
console.print(f"[error]Error:[/] Task agent not found")
|
||||
return
|
||||
|
||||
# Mode display
|
||||
mode_display = {
|
||||
PermissionMode.default: "[info]default[/] (full with approvals)",
|
||||
PermissionMode.plan: "[success]plan[/] (read-only)",
|
||||
PermissionMode.auto_accept: "[warning]auto_accept[/] (no prompts)",
|
||||
}
|
||||
|
||||
# Welcome message
|
||||
console.print()
|
||||
console.print(f"[title]Webber CLI[/] [dim]→ {api_url}[/]")
|
||||
console.print(f"[dim]Working in:[/] [path]{working_dir}[/]")
|
||||
console.print(f"[dim]Agent:[/] {agent_info.name} - {agent_info.description}")
|
||||
mode = "streaming" if stream else "batch"
|
||||
console.print(f"[dim]Mode:[/] {mode}")
|
||||
console.print(f"[dim]Mode:[/] {mode_display[mode]}")
|
||||
console.print(f"[dim]Streaming:[/] {'enabled' if stream else 'disabled'}")
|
||||
console.print()
|
||||
console.print("[dim]Type 'exit' to quit, 'clear' to clear screen.[/]")
|
||||
console.print("[dim]Commands: 'exit' to quit, 'clear' to clear, 'mode <plan|default|auto_accept>' to switch[/]")
|
||||
console.print("[dim]Tab for completion, Up/Down for history[/]")
|
||||
console.print()
|
||||
|
||||
current_mode = mode
|
||||
|
||||
# Create prompt session with history and completion
|
||||
session = create_prompt_session(working_dir)
|
||||
|
||||
# Chat loop
|
||||
while True:
|
||||
try:
|
||||
user_input = console.input("[prompt]>[/] ").strip()
|
||||
# Use prompt_toolkit for input (with history and completion)
|
||||
try:
|
||||
user_input = await session.prompt_async(
|
||||
[("class:prompt", "> ")],
|
||||
)
|
||||
user_input = user_input.strip()
|
||||
except EOFError:
|
||||
# Ctrl+D pressed
|
||||
console.print("[dim]Goodbye![/]")
|
||||
break
|
||||
|
||||
if not user_input:
|
||||
continue
|
||||
@@ -152,33 +288,52 @@ async def _chat_loop(
|
||||
|
||||
if user_input.lower().startswith("cd "):
|
||||
new_dir = user_input[3:].strip()
|
||||
new_path = Path(new_dir).resolve()
|
||||
# Handle ~ expansion
|
||||
new_path = Path(new_dir).expanduser().resolve()
|
||||
if new_path.exists() and new_path.is_dir():
|
||||
working_dir = str(new_path)
|
||||
# Update completer's working directory
|
||||
session.completer.working_dir = working_dir
|
||||
console.print(f"[info]Changed to:[/] [path]{working_dir}[/]")
|
||||
else:
|
||||
console.print(f"[error]Directory not found:[/] {new_dir}")
|
||||
continue
|
||||
|
||||
# Mode switching
|
||||
if user_input.lower().startswith("mode "):
|
||||
new_mode_str = user_input[5:].strip()
|
||||
try:
|
||||
new_mode = PermissionMode(new_mode_str)
|
||||
if new_mode == PermissionMode.auto_accept:
|
||||
if not _confirm_auto_accept():
|
||||
console.print("[dim]Mode unchanged.[/]")
|
||||
continue
|
||||
current_mode = new_mode
|
||||
console.print(f"[info]Mode changed to:[/] {mode_display[current_mode]}")
|
||||
except ValueError:
|
||||
console.print(f"[error]Invalid mode:[/] {new_mode_str}")
|
||||
console.print("[dim]Valid modes: default, plan, auto_accept[/]")
|
||||
continue
|
||||
|
||||
console.print()
|
||||
|
||||
if stream:
|
||||
# Stream response in real-time
|
||||
full_response = ""
|
||||
try:
|
||||
async for chunk in client.run_agent_stream(
|
||||
agent_type, user_input, working_dir
|
||||
agent_type, user_input, working_dir, current_mode
|
||||
):
|
||||
sys.stdout.write(chunk)
|
||||
sys.stdout.flush()
|
||||
full_response += chunk
|
||||
console.print() # Newline after streaming
|
||||
except Exception as e:
|
||||
console.print(f"\n[error]Stream error:[/] {e}")
|
||||
else:
|
||||
# Batch mode with spinner
|
||||
with console.status("[info]Thinking...[/]", spinner=theme.spinner):
|
||||
result = await client.run_agent(agent_type, user_input, working_dir)
|
||||
result = await client.run_agent(
|
||||
agent_type, user_input, working_dir, current_mode
|
||||
)
|
||||
|
||||
if result.success:
|
||||
console.print(Markdown(result.response))
|
||||
@@ -194,87 +349,6 @@ async def _chat_loop(
|
||||
console.print(f"[error]Error:[/] {e}")
|
||||
|
||||
|
||||
@app.command()
|
||||
def explore(
|
||||
query: str = typer.Argument(..., help="What to search for"),
|
||||
directory: str = typer.Option(
|
||||
".",
|
||||
"--directory",
|
||||
"-d",
|
||||
help="Working directory",
|
||||
),
|
||||
api_url: str = typer.Option(
|
||||
DEFAULT_API_URL,
|
||||
"--api",
|
||||
"-a",
|
||||
help="Webber API URL",
|
||||
),
|
||||
stream: bool = typer.Option(
|
||||
True,
|
||||
"--stream/--no-stream",
|
||||
"-s",
|
||||
help="Stream responses in real-time",
|
||||
),
|
||||
) -> None:
|
||||
"""
|
||||
One-shot codebase exploration.
|
||||
|
||||
Sends a query to the Webber API and displays the result.
|
||||
"""
|
||||
working_dir = str(Path(directory).resolve())
|
||||
|
||||
if not Path(working_dir).exists():
|
||||
console.print(f"[error]Error:[/] Directory not found: {working_dir}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
asyncio.run(_explore(api_url, query, working_dir, stream))
|
||||
|
||||
|
||||
async def _explore(
|
||||
api_url: str, query: str, working_dir: str, stream: bool = True
|
||||
) -> None:
|
||||
"""Execute exploration query."""
|
||||
theme = get_theme()
|
||||
|
||||
async with WebberClient(api_url) as client:
|
||||
# Check API health
|
||||
if not await client.health_check():
|
||||
console.print(f"[error]Error:[/] Cannot connect to Webber API at {api_url}")
|
||||
console.print("[dim]Make sure the server is running: ./wakeup.sh[/]")
|
||||
return
|
||||
|
||||
console.print(f"[dim]Exploring:[/] [path]{working_dir}[/]")
|
||||
console.print(f"[dim]Query:[/] {query}")
|
||||
console.print()
|
||||
|
||||
if stream:
|
||||
# Stream response in real-time
|
||||
try:
|
||||
async for chunk in client.run_agent_stream("explore", query, working_dir):
|
||||
sys.stdout.write(chunk)
|
||||
sys.stdout.flush()
|
||||
console.print() # Newline after streaming
|
||||
except Exception as e:
|
||||
console.print(f"\n[error]Stream error:[/] {e}")
|
||||
else:
|
||||
# Batch mode with spinner
|
||||
with console.status("[info]Searching...[/]", spinner=theme.spinner):
|
||||
result = await client.run_agent("explore", query, working_dir)
|
||||
|
||||
if result.success:
|
||||
console.print(Panel(
|
||||
Markdown(result.response),
|
||||
title="[success]Findings[/]",
|
||||
border_style=theme.colors.border_success,
|
||||
))
|
||||
else:
|
||||
console.print(Panel(
|
||||
f"[error]{result.error}[/]",
|
||||
title="[error]Error[/]",
|
||||
border_style=theme.colors.border_error,
|
||||
))
|
||||
|
||||
|
||||
@app.command()
|
||||
def status(
|
||||
api_url: str = typer.Option(
|
||||
@@ -304,5 +378,94 @@ async def _status(api_url: str) -> None:
|
||||
console.print("[error]Status:[/] Cannot connect")
|
||||
|
||||
|
||||
# Keep 'explore' as an alias for 'chat --mode plan' for backwards compatibility
|
||||
@app.command(hidden=True)
|
||||
def explore(
|
||||
query: str = typer.Argument(..., help="What to search for"),
|
||||
directory: str = typer.Option(
|
||||
".",
|
||||
"--directory",
|
||||
"-d",
|
||||
help="Working directory",
|
||||
),
|
||||
api_url: str = typer.Option(
|
||||
DEFAULT_API_URL,
|
||||
"--api",
|
||||
"-a",
|
||||
help="Webber API URL",
|
||||
),
|
||||
stream: bool = typer.Option(
|
||||
True,
|
||||
"--stream/--no-stream",
|
||||
"-s",
|
||||
help="Stream responses in real-time",
|
||||
),
|
||||
) -> None:
|
||||
"""
|
||||
[DEPRECATED] One-shot exploration (use 'chat --mode plan' instead).
|
||||
|
||||
Runs the Task agent in plan (read-only) mode for a single query.
|
||||
"""
|
||||
console.print("[dim]Note: 'explore' is deprecated. Use 'chat --mode plan' for interactive mode.[/]")
|
||||
console.print()
|
||||
|
||||
working_dir = str(Path(directory).resolve())
|
||||
|
||||
if not Path(working_dir).exists():
|
||||
console.print(f"[error]Error:[/] Directory not found: {working_dir}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
asyncio.run(_explore(api_url, query, working_dir, stream))
|
||||
|
||||
|
||||
async def _explore(
|
||||
api_url: str, query: str, working_dir: str, stream: bool = True
|
||||
) -> None:
|
||||
"""Execute exploration query in plan mode."""
|
||||
theme = get_theme()
|
||||
mode = PermissionMode.plan
|
||||
|
||||
async with WebberClient(api_url) as client:
|
||||
# Check API health
|
||||
if not await client.health_check():
|
||||
console.print(f"[error]Error:[/] Cannot connect to Webber API at {api_url}")
|
||||
console.print("[dim]Make sure the server is running: ./wakeup.sh[/]")
|
||||
return
|
||||
|
||||
console.print(f"[dim]Exploring:[/] [path]{working_dir}[/]")
|
||||
console.print(f"[dim]Query:[/] {query}")
|
||||
console.print(f"[dim]Mode:[/] [success]plan[/] (read-only)")
|
||||
console.print()
|
||||
|
||||
if stream:
|
||||
# Stream response in real-time
|
||||
try:
|
||||
async for chunk in client.run_agent_stream(
|
||||
"task", query, working_dir, mode
|
||||
):
|
||||
sys.stdout.write(chunk)
|
||||
sys.stdout.flush()
|
||||
console.print() # Newline after streaming
|
||||
except Exception as e:
|
||||
console.print(f"\n[error]Stream error:[/] {e}")
|
||||
else:
|
||||
# Batch mode with spinner
|
||||
with console.status("[info]Searching...[/]", spinner=theme.spinner):
|
||||
result = await client.run_agent("task", query, working_dir, mode)
|
||||
|
||||
if result.success:
|
||||
console.print(Panel(
|
||||
Markdown(result.response),
|
||||
title="[success]Findings[/]",
|
||||
border_style=theme.colors.border_success,
|
||||
))
|
||||
else:
|
||||
console.print(Panel(
|
||||
f"[error]{result.error}[/]",
|
||||
title="[error]Error[/]",
|
||||
border_style=theme.colors.border_error,
|
||||
))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app()
|
||||
|
||||
Reference in New Issue
Block a user