loki-mode 6.30.1 → 6.30.2

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.
@@ -8,7 +8,7 @@
8
8
  <link rel="preconnect" href="https://fonts.googleapis.com">
9
9
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
10
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
11
- <script type="module" crossorigin src="/assets/index-Doj1Bzyy.js"></script>
11
+ <script type="module" crossorigin src="/assets/index-BTtIHGw7.js"></script>
12
12
  <link rel="stylesheet" crossorigin href="/assets/index-CxHyjUh7.css">
13
13
  </head>
14
14
  <body class="bg-background text-charcoal font-sans antialiased">
package/web-app/server.py CHANGED
@@ -502,6 +502,105 @@ async def get_template_content(filename: str) -> JSONResponse:
502
502
  # ---------------------------------------------------------------------------
503
503
 
504
504
 
505
+ async def _push_state_to_client(ws: WebSocket) -> None:
506
+ """Background task: push state snapshots to a single WebSocket client.
507
+
508
+ Pushes every 2s when a session is running, every 30s when idle.
509
+ """
510
+ while True:
511
+ is_running = (
512
+ session.process is not None
513
+ and session.running
514
+ and session.process.poll() is None
515
+ )
516
+ interval = 2.0 if is_running else 30.0
517
+
518
+ # Build status payload (same logic as GET /api/session/status)
519
+ loki_dir = _loki_dir()
520
+ phase = "idle"
521
+ iteration = 0
522
+ complexity = "standard"
523
+ current_task = ""
524
+ pending_tasks = 0
525
+
526
+ state_file = loki_dir / "state" / "session.json"
527
+ if state_file.exists():
528
+ try:
529
+ with open(state_file) as f:
530
+ state_data = json.load(f)
531
+ phase = state_data.get("phase", phase)
532
+ iteration = state_data.get("iteration", iteration)
533
+ complexity = state_data.get("complexity", complexity)
534
+ current_task = state_data.get("current_task", current_task)
535
+ pending_tasks = state_data.get("pending_tasks", pending_tasks)
536
+ except (json.JSONDecodeError, OSError):
537
+ pass
538
+
539
+ uptime = time.time() - session.start_time if is_running else 0
540
+ status_payload = {
541
+ "running": session.running,
542
+ "paused": False,
543
+ "phase": phase,
544
+ "iteration": iteration,
545
+ "complexity": complexity,
546
+ "mode": "autonomous",
547
+ "provider": session.provider,
548
+ "current_task": current_task,
549
+ "pending_tasks": pending_tasks,
550
+ "running_agents": 0,
551
+ "uptime": round(uptime),
552
+ "version": "",
553
+ "pid": str(session.process.pid) if session.process else "",
554
+ "projectDir": session.project_dir,
555
+ }
556
+
557
+ # Build agents payload
558
+ agents_payload: list = []
559
+ agents_file = loki_dir / "state" / "agents.json"
560
+ if agents_file.exists():
561
+ try:
562
+ with open(agents_file) as f:
563
+ agents_data = json.load(f)
564
+ if isinstance(agents_data, list):
565
+ agents_payload = agents_data
566
+ except (json.JSONDecodeError, OSError):
567
+ pass
568
+
569
+ # Build logs payload (last 50 lines)
570
+ recent = session.log_lines[-50:] if session.log_lines else []
571
+ logs_payload = []
572
+ for line in recent:
573
+ level = "info"
574
+ lower = line.lower()
575
+ if "error" in lower or "fail" in lower:
576
+ level = "error"
577
+ elif "warn" in lower:
578
+ level = "warning"
579
+ elif "debug" in lower:
580
+ level = "debug"
581
+ logs_payload.append({
582
+ "timestamp": "",
583
+ "level": level,
584
+ "message": line,
585
+ "source": "loki",
586
+ })
587
+
588
+ try:
589
+ await ws.send_json({
590
+ "type": "state_update",
591
+ "data": {
592
+ "status": status_payload,
593
+ "agents": agents_payload,
594
+ "logs": logs_payload,
595
+ },
596
+ })
597
+ except Exception:
598
+ # Client disconnected; exit task
599
+ return
600
+
601
+ await asyncio.sleep(interval)
602
+
603
+
505
604
  @app.websocket("/ws")
506
605
  async def websocket_endpoint(ws: WebSocket) -> None:
507
606
  """Real-time stream of loki output and events."""
@@ -521,6 +620,9 @@ async def websocket_endpoint(ws: WebSocket) -> None:
521
620
  "data": {"line": line, "timestamp": ""},
522
621
  }))
523
622
 
623
+ # Start server-push state task for this connection
624
+ push_task = asyncio.create_task(_push_state_to_client(ws))
625
+
524
626
  try:
525
627
  while True:
526
628
  # Keep connection alive; handle client messages if needed
@@ -535,6 +637,7 @@ async def websocket_endpoint(ws: WebSocket) -> None:
535
637
  except WebSocketDisconnect:
536
638
  pass
537
639
  finally:
640
+ push_task.cancel()
538
641
  session.ws_clients.discard(ws)
539
642
 
540
643
 
@@ -574,7 +677,7 @@ def main() -> None:
574
677
  host = os.environ.get("PURPLE_LAB_HOST", HOST)
575
678
  port = int(os.environ.get("PURPLE_LAB_PORT", str(PORT)))
576
679
  print(f"Purple Lab starting on http://{host}:{port}")
577
- uvicorn.run(app, host=host, port=port, log_level="info")
680
+ uvicorn.run(app, host=host, port=port, log_level="info", timeout_keep_alive=30)
578
681
 
579
682
 
580
683
  if __name__ == "__main__":