| 444 | stop_event = asyncio.Event() |
| 445 | |
| 446 | async def _stream_logs() -> None: |
| 447 | nonlocal preview_url, ready |
| 448 | try: |
| 449 | async for line in cmd.logs(): |
| 450 | data = line.data or "" |
| 451 | # Append to UI timeline if requested |
| 452 | if stream_logs: |
| 453 | ctx.context.events.append( |
| 454 | { |
| 455 | "phase": "log", |
| 456 | "tool_id": tool_id, |
| 457 | "name": "sandbox_run", |
| 458 | "data": data, |
| 459 | } |
| 460 | ) |
| 461 | # Always collect for LLM summary |
| 462 | collected_logs.append(data) |
| 463 | # Detect readiness |
| 464 | if ready_patterns: |
| 465 | for pat in ready_patterns: |
| 466 | if pat and (pat in data): |
| 467 | ready = True |
| 468 | if port and not preview_url: |
| 469 | try: |
| 470 | url = sandbox.domain(port) |
| 471 | except Exception: |
| 472 | url = None |
| 473 | if url: |
| 474 | preview_url = url |
| 475 | ctx.context.events.append( |
| 476 | { |
| 477 | "phase": "log", |
| 478 | "tool_id": tool_id, |
| 479 | "name": "sandbox_run", |
| 480 | "data": f"[{sb_name}] Preview available at: {url}\n", |
| 481 | } |
| 482 | ) |
| 483 | stop_event.set() |
| 484 | return |
| 485 | # Stop if timeout/exit already signaled |
| 486 | if stop_event.is_set(): |
| 487 | return |
| 488 | except Exception: |
| 489 | # Ignore streaming errors but ensure we don't block forever |
| 490 | stop_event.set() |
| 491 | return |
| 492 | |
| 493 | async def _wait_for_exit() -> None: |
| 494 | nonlocal exit_code, exited_early |