|
| 1 | +"""pine chat / pine send — interactive and one-shot messaging.""" |
| 2 | + |
| 3 | +import json |
| 4 | +from typing import Optional |
| 5 | + |
| 6 | +import click |
| 7 | +from rich.console import Console |
| 8 | +from rich.panel import Panel |
| 9 | + |
| 10 | +from pine_assistant.models.events import S2CEvent |
| 11 | +from pine_cli.config import get_assistant_client, run_async, handle_api_errors |
| 12 | + |
| 13 | +console = Console() |
| 14 | + |
| 15 | + |
| 16 | +@click.command("chat") |
| 17 | +@click.argument("session_id", required=False) |
| 18 | +@handle_api_errors |
| 19 | +def chat_cmd(session_id: Optional[str]): |
| 20 | + """Interactive chat with Pine AI (REPL).""" |
| 21 | + async def _chat(): |
| 22 | + client = get_assistant_client() |
| 23 | + await client.connect() |
| 24 | + |
| 25 | + sid = session_id |
| 26 | + if not sid: |
| 27 | + with console.status("Creating session…"): |
| 28 | + s = await client.sessions.create() |
| 29 | + sid = s["id"] |
| 30 | + console.print(f"[dim]Session: {sid}[/dim]") |
| 31 | + |
| 32 | + await client.join_session(sid) |
| 33 | + console.print("[cyan]Type your message (Ctrl+C or /quit to exit)[/cyan]\n") |
| 34 | + |
| 35 | + try: |
| 36 | + while True: |
| 37 | + msg = click.prompt("You", prompt_suffix=": ") |
| 38 | + if msg.strip().lower() in ("/quit", "/exit"): |
| 39 | + break |
| 40 | + async for event in client.chat(sid, msg): |
| 41 | + _print_event(event) |
| 42 | + except (KeyboardInterrupt, EOFError): |
| 43 | + console.print() |
| 44 | + finally: |
| 45 | + client.leave_session(sid) |
| 46 | + await client.disconnect() |
| 47 | + |
| 48 | + run_async(_chat()) |
| 49 | + |
| 50 | + |
| 51 | +@click.command("send") |
| 52 | +@click.argument("message") |
| 53 | +@click.option("-s", "--session", "session_id", default=None, help="Existing session ID") |
| 54 | +@click.option("--json-output", "--json", is_flag=True, help="Output as JSON") |
| 55 | +@handle_api_errors |
| 56 | +def send_cmd(message: str, session_id: Optional[str], json_output: bool): |
| 57 | + """Send a one-shot message to Pine AI.""" |
| 58 | + async def _send(): |
| 59 | + client = get_assistant_client() |
| 60 | + await client.connect() |
| 61 | + |
| 62 | + sid = session_id |
| 63 | + try: |
| 64 | + if not sid: |
| 65 | + s = await client.sessions.create() |
| 66 | + sid = s["id"] |
| 67 | + if not json_output: |
| 68 | + console.print(f"[dim]Session: {sid}[/dim]") |
| 69 | + |
| 70 | + await client.join_session(sid) |
| 71 | + async for event in client.chat(sid, message): |
| 72 | + if json_output: |
| 73 | + click.echo(json.dumps({"type": event.type, "data": event.data})) |
| 74 | + else: |
| 75 | + _print_event(event) |
| 76 | + |
| 77 | + client.leave_session(sid) |
| 78 | + finally: |
| 79 | + await client.disconnect() |
| 80 | + |
| 81 | + run_async(_send()) |
| 82 | + |
| 83 | + |
| 84 | +def _print_event(event): |
| 85 | + """Render a chat event to the console.""" |
| 86 | + if event.type == S2CEvent.SESSION_TEXT: |
| 87 | + data = event.data if isinstance(event.data, dict) else {} |
| 88 | + content = data.get("content", "") |
| 89 | + if content: |
| 90 | + console.print(f"[green]Pine AI:[/green] {content}") |
| 91 | + elif event.type == S2CEvent.SESSION_FORM_TO_USER: |
| 92 | + data = event.data if isinstance(event.data, dict) else {} |
| 93 | + msg = data.get("message_to_user", "") |
| 94 | + console.print(Panel(f"[yellow]{msg}[/yellow]\n{json.dumps(data, indent=2)}", |
| 95 | + title="Form Required", border_style="yellow")) |
| 96 | + elif event.type == S2CEvent.SESSION_STATE: |
| 97 | + data = event.data if isinstance(event.data, dict) else {} |
| 98 | + state = data.get("content", "") |
| 99 | + if state: |
| 100 | + console.print(f"[dim] ● state → {state}[/dim]") |
| 101 | + elif event.type == S2CEvent.SESSION_THINKING: |
| 102 | + console.print("[dim] ● thinking…[/dim]") |
| 103 | + elif event.type == S2CEvent.SESSION_WORK_LOG: |
| 104 | + data = event.data if isinstance(event.data, dict) else {} |
| 105 | + steps = data.get("steps", []) |
| 106 | + for step in steps: |
| 107 | + console.print(f"[dim] ● {step.get('step_title', '')} [{step.get('status', '')}][/dim]") |
0 commit comments