adelie-ai 0.1.8 → 0.2.0
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.
- package/adelie/__init__.py +15 -0
- package/adelie/cli.py +254 -3
- package/adelie/dashboard.py +61 -16
- package/adelie/dashboard_html.py +180 -65
- package/adelie/interactive.py +5 -5
- package/package.json +1 -1
package/adelie/__init__.py
CHANGED
|
@@ -1 +1,16 @@
|
|
|
1
1
|
"""Adelie — Self-communicating autonomous AI loop system."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path as _Path
|
|
4
|
+
import json as _json
|
|
5
|
+
|
|
6
|
+
def _get_version() -> str:
|
|
7
|
+
"""Read version from package.json (single source of truth)."""
|
|
8
|
+
try:
|
|
9
|
+
pkg = _Path(__file__).resolve().parent.parent / "package.json"
|
|
10
|
+
if pkg.exists():
|
|
11
|
+
return _json.loads(pkg.read_text(encoding="utf-8")).get("version", "0.0.0")
|
|
12
|
+
except Exception:
|
|
13
|
+
pass
|
|
14
|
+
return "0.0.0"
|
|
15
|
+
|
|
16
|
+
__version__ = _get_version()
|
package/adelie/cli.py
CHANGED
|
@@ -167,10 +167,15 @@ def cmd_help(args: argparse.Namespace) -> None:
|
|
|
167
167
|
[green]adelie config[/green] Show current configuration
|
|
168
168
|
[green]adelie config --provider[/green] [dim]ollama[/dim] Switch LLM provider
|
|
169
169
|
[green]adelie config --model[/green] [dim]gemma3:12b[/dim] Set model
|
|
170
|
-
[green]adelie config --interval[/green] [dim]60[/dim] Set loop interval (seconds)
|
|
171
170
|
[green]adelie config --api-key[/green] [dim]KEY[/dim] Set Gemini API key
|
|
172
171
|
[green]adelie config --ollama-url[/green] [dim]URL[/dim] Set Ollama server URL
|
|
173
|
-
|
|
172
|
+
|
|
173
|
+
[bold]Settings[/bold]
|
|
174
|
+
[green]adelie settings[/green] Show all settings (workspace + global)
|
|
175
|
+
[green]adelie settings --global[/green] Show global-only settings
|
|
176
|
+
[green]adelie settings set[/green] [dim]<key> <val>[/dim] Change a workspace setting
|
|
177
|
+
[green]adelie settings set --global[/green] [dim]<key> <val>[/dim] Change a global setting
|
|
178
|
+
[green]adelie settings reset[/green] [dim]<key>[/dim] Reset setting to default
|
|
174
179
|
|
|
175
180
|
[bold]Monitoring[/bold]
|
|
176
181
|
[green]adelie status[/green] System health & provider status
|
|
@@ -783,6 +788,236 @@ def cmd_config(args: argparse.Namespace) -> None:
|
|
|
783
788
|
console.print(f"[dim]Config: {_workspace_config_path()}[/dim]")
|
|
784
789
|
|
|
785
790
|
|
|
791
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
792
|
+
# SETTINGS
|
|
793
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
794
|
+
|
|
795
|
+
# Settings definition: key -> (env_var, config_json_key, default, type, description)
|
|
796
|
+
# env_var: key in .env file (or None if stored in config.json)
|
|
797
|
+
# config_json_key: key in config.json (or None if stored in .env)
|
|
798
|
+
_SETTINGS_DEFS: dict[str, dict] = {
|
|
799
|
+
"dashboard": {"env": "DASHBOARD_ENABLED", "cfg": None, "default": "true", "type": "bool", "desc": "대시보드 on/off", "group": "🌐 Dashboard"},
|
|
800
|
+
"dashboard.port": {"env": "DASHBOARD_PORT", "cfg": None, "default": "5042", "type": "int", "desc": "대시보드 포트", "group": "🌐 Dashboard"},
|
|
801
|
+
"loop.interval": {"env": None, "cfg": "loop_interval", "default": "30", "type": "int", "desc": "루프 간격 (초)", "group": "⚡ Runtime"},
|
|
802
|
+
"plan.mode": {"env": "PLAN_MODE", "cfg": None, "default": "false", "type": "bool", "desc": "Plan Mode (승인 후 실행)", "group": "⚡ Runtime"},
|
|
803
|
+
"sandbox": {"env": "SANDBOX_MODE", "cfg": None, "default": "none", "type": "str", "desc": "샌드박스 (none/seatbelt/docker)", "group": "⚡ Runtime"},
|
|
804
|
+
"mcp": {"env": "MCP_ENABLED", "cfg": None, "default": "true", "type": "bool", "desc": "MCP 프로토콜 on/off", "group": "⚡ Runtime"},
|
|
805
|
+
"browser.search": {"env": "BROWSER_SEARCH_ENABLED", "cfg": None, "default": "true", "type": "bool", "desc": "브라우저 검색 on/off", "group": "🔍 Search"},
|
|
806
|
+
"browser.max_pages": {"env": "BROWSER_SEARCH_MAX_PAGES","cfg": None,"default": "3", "type": "int", "desc": "검색 최대 페이지", "group": "🔍 Search"},
|
|
807
|
+
"fallback.models": {"env": "FALLBACK_MODELS", "cfg": None, "default": "", "type": "str", "desc": "폴백 모델 체인", "group": "🔄 Fallback"},
|
|
808
|
+
"fallback.cooldown": {"env": "FALLBACK_COOLDOWN_SECONDS","cfg": None,"default": "60","type": "int", "desc": "폴백 쿨다운 (초)", "group": "🔄 Fallback"},
|
|
809
|
+
"language": {"env": "ADELIE_LANGUAGE", "cfg": None, "default": "ko", "type": "str", "desc": "언어 (ko/en)", "group": "🎨 Display"},
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
_GLOBAL_SETTINGS_FILE = Path.home() / ".adelie" / "settings.json"
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
def _load_global_settings() -> dict:
|
|
816
|
+
"""Load global settings from ~/.adelie/settings.json."""
|
|
817
|
+
if _GLOBAL_SETTINGS_FILE.exists():
|
|
818
|
+
try:
|
|
819
|
+
return json.loads(_GLOBAL_SETTINGS_FILE.read_text(encoding="utf-8"))
|
|
820
|
+
except json.JSONDecodeError:
|
|
821
|
+
return {}
|
|
822
|
+
return {}
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
def _save_global_settings(settings: dict) -> None:
|
|
826
|
+
"""Save global settings to ~/.adelie/settings.json."""
|
|
827
|
+
_GLOBAL_SETTINGS_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
828
|
+
_GLOBAL_SETTINGS_FILE.write_text(
|
|
829
|
+
json.dumps(settings, indent=2, ensure_ascii=False), encoding="utf-8"
|
|
830
|
+
)
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
def _read_ws_env_value(env_key: str) -> str | None:
|
|
834
|
+
"""Read a specific env key from workspace .env file (raw file read, not os.environ)."""
|
|
835
|
+
ws_root = _find_workspace_root()
|
|
836
|
+
env_path = ws_root / ".env"
|
|
837
|
+
if not env_path.exists():
|
|
838
|
+
return None
|
|
839
|
+
for line in env_path.read_text(encoding="utf-8").splitlines():
|
|
840
|
+
stripped = line.strip()
|
|
841
|
+
if stripped.startswith(f"{env_key}="):
|
|
842
|
+
return stripped.split("=", 1)[1].strip()
|
|
843
|
+
return None
|
|
844
|
+
|
|
845
|
+
|
|
846
|
+
def _resolve_setting(key: str, is_global: bool = False) -> tuple[str, str]:
|
|
847
|
+
"""
|
|
848
|
+
Resolve a setting value and its source.
|
|
849
|
+
Priority: workspace .env/config.json > global settings > default
|
|
850
|
+
Returns (value, source).
|
|
851
|
+
"""
|
|
852
|
+
defn = _SETTINGS_DEFS.get(key)
|
|
853
|
+
if not defn:
|
|
854
|
+
return ("", "unknown")
|
|
855
|
+
|
|
856
|
+
# If global-only, just check global settings
|
|
857
|
+
if is_global:
|
|
858
|
+
gs = _load_global_settings()
|
|
859
|
+
if key in gs:
|
|
860
|
+
return (str(gs[key]), "global")
|
|
861
|
+
return (defn["default"], "default")
|
|
862
|
+
|
|
863
|
+
# Workspace level: check .env or config.json
|
|
864
|
+
ws_value = None
|
|
865
|
+
if defn["env"]:
|
|
866
|
+
ws_value = _read_ws_env_value(defn["env"])
|
|
867
|
+
elif defn["cfg"]:
|
|
868
|
+
ws_config = _load_workspace_config()
|
|
869
|
+
if defn["cfg"] in ws_config:
|
|
870
|
+
ws_value = str(ws_config[defn["cfg"]])
|
|
871
|
+
|
|
872
|
+
if ws_value is not None:
|
|
873
|
+
return (ws_value, "workspace")
|
|
874
|
+
|
|
875
|
+
# Fall back to global
|
|
876
|
+
gs = _load_global_settings()
|
|
877
|
+
if key in gs:
|
|
878
|
+
return (str(gs[key]), "global")
|
|
879
|
+
|
|
880
|
+
return (defn["default"], "default")
|
|
881
|
+
|
|
882
|
+
|
|
883
|
+
def cmd_settings(args: argparse.Namespace) -> None:
|
|
884
|
+
"""View, update, or reset settings (global or workspace-level)."""
|
|
885
|
+
_ensure_adelie_config()
|
|
886
|
+
|
|
887
|
+
action = getattr(args, "settings_action", "show") or "show"
|
|
888
|
+
is_global = getattr(args, "use_global", False)
|
|
889
|
+
|
|
890
|
+
if action == "set":
|
|
891
|
+
key = getattr(args, "settings_key", None)
|
|
892
|
+
value = getattr(args, "settings_value", None)
|
|
893
|
+
if not key or value is None:
|
|
894
|
+
console.print("[red]Usage: adelie settings set <key> <value>[/red]")
|
|
895
|
+
return
|
|
896
|
+
|
|
897
|
+
if key not in _SETTINGS_DEFS:
|
|
898
|
+
console.print(f"[red]ERROR: Unknown setting: {key}[/red]")
|
|
899
|
+
console.print(f"[dim]Available: {', '.join(sorted(_SETTINGS_DEFS.keys()))}[/dim]")
|
|
900
|
+
return
|
|
901
|
+
|
|
902
|
+
defn = _SETTINGS_DEFS[key]
|
|
903
|
+
|
|
904
|
+
# Validate type
|
|
905
|
+
if defn["type"] == "bool" and value.lower() not in ("true", "false"):
|
|
906
|
+
console.print(f"[red]ERROR: '{key}' must be 'true' or 'false'[/red]")
|
|
907
|
+
return
|
|
908
|
+
if defn["type"] == "int":
|
|
909
|
+
try:
|
|
910
|
+
int(value)
|
|
911
|
+
except ValueError:
|
|
912
|
+
console.print(f"[red]ERROR: '{key}' must be a number[/red]")
|
|
913
|
+
return
|
|
914
|
+
|
|
915
|
+
if is_global:
|
|
916
|
+
gs = _load_global_settings()
|
|
917
|
+
gs[key] = value
|
|
918
|
+
_save_global_settings(gs)
|
|
919
|
+
console.print(f"[green]✅ [global] {key} → {value}[/green]")
|
|
920
|
+
else:
|
|
921
|
+
if defn["env"]:
|
|
922
|
+
_update_env_file({defn["env"]: value})
|
|
923
|
+
elif defn["cfg"]:
|
|
924
|
+
ws_config = _load_workspace_config()
|
|
925
|
+
ws_config[defn["cfg"]] = int(value) if defn["type"] == "int" else value
|
|
926
|
+
_save_workspace_config(ws_config)
|
|
927
|
+
console.print(f"[green]✅ [workspace] {key} → {value}[/green]")
|
|
928
|
+
|
|
929
|
+
elif action == "reset":
|
|
930
|
+
key = getattr(args, "settings_key", None)
|
|
931
|
+
if not key:
|
|
932
|
+
console.print("[red]Usage: adelie settings reset <key>[/red]")
|
|
933
|
+
return
|
|
934
|
+
|
|
935
|
+
if key not in _SETTINGS_DEFS:
|
|
936
|
+
console.print(f"[red]ERROR: Unknown setting: {key}[/red]")
|
|
937
|
+
return
|
|
938
|
+
|
|
939
|
+
defn = _SETTINGS_DEFS[key]
|
|
940
|
+
default_val = defn["default"]
|
|
941
|
+
|
|
942
|
+
if is_global:
|
|
943
|
+
gs = _load_global_settings()
|
|
944
|
+
gs.pop(key, None)
|
|
945
|
+
_save_global_settings(gs)
|
|
946
|
+
console.print(f"[green]✅ [global] {key} reset (removed)[/green]")
|
|
947
|
+
else:
|
|
948
|
+
# Reset workspace value to default
|
|
949
|
+
if defn["env"]:
|
|
950
|
+
_update_env_file({defn["env"]: default_val})
|
|
951
|
+
elif defn["cfg"]:
|
|
952
|
+
ws_config = _load_workspace_config()
|
|
953
|
+
if defn["type"] == "int":
|
|
954
|
+
ws_config[defn["cfg"]] = int(default_val)
|
|
955
|
+
else:
|
|
956
|
+
ws_config[defn["cfg"]] = default_val
|
|
957
|
+
_save_workspace_config(ws_config)
|
|
958
|
+
console.print(f"[green]✅ [workspace] {key} → {default_val} (default)[/green]")
|
|
959
|
+
|
|
960
|
+
else:
|
|
961
|
+
# Show all settings
|
|
962
|
+
scope_label = "Global Settings" if is_global else "Settings (workspace + global)"
|
|
963
|
+
table = Table(
|
|
964
|
+
title=f"Adelie {scope_label}",
|
|
965
|
+
show_header=True,
|
|
966
|
+
border_style="cyan",
|
|
967
|
+
)
|
|
968
|
+
table.add_column("Setting", style="bold")
|
|
969
|
+
table.add_column("Value")
|
|
970
|
+
table.add_column("Source", style="dim")
|
|
971
|
+
table.add_column("Description", style="dim")
|
|
972
|
+
|
|
973
|
+
current_group = ""
|
|
974
|
+
for key in _SETTINGS_DEFS:
|
|
975
|
+
defn = _SETTINGS_DEFS[key]
|
|
976
|
+
group = defn["group"]
|
|
977
|
+
|
|
978
|
+
# Group separator
|
|
979
|
+
if group != current_group:
|
|
980
|
+
if current_group:
|
|
981
|
+
table.add_row("", "", "", "", style="dim")
|
|
982
|
+
current_group = group
|
|
983
|
+
table.add_row(f"[bold cyan]{group}[/bold cyan]", "", "", "")
|
|
984
|
+
|
|
985
|
+
value, source = _resolve_setting(key, is_global)
|
|
986
|
+
|
|
987
|
+
# Color the source
|
|
988
|
+
if source == "workspace":
|
|
989
|
+
source_styled = "[green]workspace[/green]"
|
|
990
|
+
elif source == "global":
|
|
991
|
+
source_styled = "[yellow]global[/yellow]"
|
|
992
|
+
else:
|
|
993
|
+
source_styled = "[dim]default[/dim]"
|
|
994
|
+
|
|
995
|
+
# Color bool values
|
|
996
|
+
if defn["type"] == "bool":
|
|
997
|
+
if value.lower() == "true":
|
|
998
|
+
value_styled = "[green]true[/green]"
|
|
999
|
+
else:
|
|
1000
|
+
value_styled = "[dim]false[/dim]"
|
|
1001
|
+
elif not value:
|
|
1002
|
+
value_styled = "[dim](not set)[/dim]"
|
|
1003
|
+
else:
|
|
1004
|
+
value_styled = value
|
|
1005
|
+
|
|
1006
|
+
table.add_row(f" {key}", value_styled, source_styled, defn["desc"])
|
|
1007
|
+
|
|
1008
|
+
console.print(table)
|
|
1009
|
+
|
|
1010
|
+
if is_global:
|
|
1011
|
+
console.print(f"\n[dim]Global: {_GLOBAL_SETTINGS_FILE}[/dim]")
|
|
1012
|
+
else:
|
|
1013
|
+
ws_root = _find_workspace_root()
|
|
1014
|
+
console.print(f"\n[dim]Workspace: {ws_root / '.env'} + {_workspace_config_path()}[/dim]")
|
|
1015
|
+
console.print(f"[dim]Global: {_GLOBAL_SETTINGS_FILE}[/dim]")
|
|
1016
|
+
|
|
1017
|
+
console.print(f"\n[dim]Change: adelie settings set <key> <value>[/dim]")
|
|
1018
|
+
console.print(f"[dim]Global: adelie settings set --global <key> <value>[/dim]")
|
|
1019
|
+
|
|
1020
|
+
|
|
786
1021
|
def cmd_kb(args: argparse.Namespace) -> None:
|
|
787
1022
|
"""Knowledge Base management."""
|
|
788
1023
|
_ensure_adelie_config()
|
|
@@ -1457,12 +1692,15 @@ def cmd_metrics(args: argparse.Namespace) -> None:
|
|
|
1457
1692
|
|
|
1458
1693
|
|
|
1459
1694
|
def main() -> None:
|
|
1695
|
+
from adelie import __version__
|
|
1460
1696
|
parser = argparse.ArgumentParser(
|
|
1461
1697
|
prog="adelie",
|
|
1462
1698
|
description="Adelie — Self-Communicating Autonomous AI Loop",
|
|
1463
1699
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
1464
1700
|
epilog="Run [adelie help] for detailed command reference.",
|
|
1465
1701
|
)
|
|
1702
|
+
parser.add_argument("-v", "--version", action="version",
|
|
1703
|
+
version=f"adelie {__version__}")
|
|
1466
1704
|
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
|
1467
1705
|
|
|
1468
1706
|
# ── help ──────
|
|
@@ -1540,6 +1778,19 @@ def main() -> None:
|
|
|
1540
1778
|
p_config.add_argument("--plan-mode", type=str, dest="plan_mode", help="Plan mode: 'true' or 'false'")
|
|
1541
1779
|
p_config.set_defaults(func=cmd_config)
|
|
1542
1780
|
|
|
1781
|
+
# ── settings ──
|
|
1782
|
+
p_settings = subparsers.add_parser("settings", help="Manage runtime settings (global & workspace)")
|
|
1783
|
+
p_settings.add_argument("settings_action", nargs="?", default="show",
|
|
1784
|
+
choices=["show", "set", "reset"],
|
|
1785
|
+
help="show (default) / set / reset")
|
|
1786
|
+
p_settings.add_argument("settings_key", nargs="?", default=None,
|
|
1787
|
+
help="Setting key (e.g. dashboard, loop.interval)")
|
|
1788
|
+
p_settings.add_argument("settings_value", nargs="?", default=None,
|
|
1789
|
+
help="New value (for set)")
|
|
1790
|
+
p_settings.add_argument("--global", action="store_true", dest="use_global",
|
|
1791
|
+
help="Target global settings (~/.adelie/settings.json)")
|
|
1792
|
+
p_settings.set_defaults(func=cmd_settings)
|
|
1793
|
+
|
|
1543
1794
|
# ── kb ────────
|
|
1544
1795
|
p_kb = subparsers.add_parser("kb", help="Knowledge Base management")
|
|
1545
1796
|
p_kb.add_argument("--clear-errors", action="store_true", help="Clear error files")
|
|
@@ -1653,7 +1904,7 @@ def main() -> None:
|
|
|
1653
1904
|
except Exception:
|
|
1654
1905
|
provider_info = "(not configured)"
|
|
1655
1906
|
console.print(Panel.fit(
|
|
1656
|
-
f"[bold cyan]Adelie[/bold cyan] — Autonomous AI Loop\n"
|
|
1907
|
+
f"[bold cyan]Adelie[/bold cyan] v{__version__} — Autonomous AI Loop\n"
|
|
1657
1908
|
f"[dim]LLM: {provider_info}[/dim]",
|
|
1658
1909
|
border_style="cyan",
|
|
1659
1910
|
))
|
package/adelie/dashboard.py
CHANGED
|
@@ -7,7 +7,8 @@ Serves a single-page dashboard on a configurable port (default 5042).
|
|
|
7
7
|
Features:
|
|
8
8
|
- SSE (Server-Sent Events) for live push to browsers
|
|
9
9
|
- JSON REST API for initial state / history
|
|
10
|
-
- Thread-safe event broadcasting
|
|
10
|
+
- Thread-safe event broadcasting with batching
|
|
11
|
+
- ThreadingHTTPServer for concurrent SSE + API handling
|
|
11
12
|
- No external dependencies — uses stdlib http.server
|
|
12
13
|
|
|
13
14
|
Started automatically from interactive.py when `adelie run` is called.
|
|
@@ -15,9 +16,11 @@ Started automatically from interactive.py when `adelie run` is called.
|
|
|
15
16
|
|
|
16
17
|
from __future__ import annotations
|
|
17
18
|
|
|
19
|
+
import collections
|
|
18
20
|
import json
|
|
19
21
|
import queue
|
|
20
22
|
import re
|
|
23
|
+
import socketserver
|
|
21
24
|
import threading
|
|
22
25
|
import time
|
|
23
26
|
from datetime import datetime
|
|
@@ -30,14 +33,14 @@ from adelie.dashboard_html import DASHBOARD_HTML
|
|
|
30
33
|
# ── Event Bus ────────────────────────────────────────────────────────────────
|
|
31
34
|
|
|
32
35
|
class EventBus:
|
|
33
|
-
"""Thread-safe pub/sub for SSE clients."""
|
|
36
|
+
"""Thread-safe pub/sub for SSE clients with event coalescing."""
|
|
34
37
|
|
|
35
38
|
def __init__(self):
|
|
36
39
|
self._clients: list[queue.Queue] = []
|
|
37
40
|
self._lock = threading.Lock()
|
|
38
41
|
|
|
39
42
|
def subscribe(self) -> queue.Queue:
|
|
40
|
-
q: queue.Queue = queue.Queue(maxsize=
|
|
43
|
+
q: queue.Queue = queue.Queue(maxsize=500)
|
|
41
44
|
with self._lock:
|
|
42
45
|
self._clients.append(q)
|
|
43
46
|
return q
|
|
@@ -73,18 +76,15 @@ class EventBus:
|
|
|
73
76
|
# ── Log Ring Buffer ──────────────────────────────────────────────────────────
|
|
74
77
|
|
|
75
78
|
class LogBuffer:
|
|
76
|
-
"""Thread-safe ring buffer for recent log entries."""
|
|
79
|
+
"""Thread-safe ring buffer for recent log entries using deque for O(1) ops."""
|
|
77
80
|
|
|
78
81
|
def __init__(self, maxlen: int = 200):
|
|
79
|
-
self._buf:
|
|
80
|
-
self._maxlen = maxlen
|
|
82
|
+
self._buf: collections.deque = collections.deque(maxlen=maxlen)
|
|
81
83
|
self._lock = threading.Lock()
|
|
82
84
|
|
|
83
85
|
def append(self, entry: dict) -> None:
|
|
84
86
|
with self._lock:
|
|
85
87
|
self._buf.append(entry)
|
|
86
|
-
if len(self._buf) > self._maxlen:
|
|
87
|
-
self._buf.pop(0)
|
|
88
88
|
|
|
89
89
|
def get_all(self) -> list[dict]:
|
|
90
90
|
with self._lock:
|
|
@@ -110,10 +110,22 @@ class DashboardState:
|
|
|
110
110
|
self.events = EventBus()
|
|
111
111
|
self.logs = LogBuffer(maxlen=200)
|
|
112
112
|
self._lock = threading.Lock()
|
|
113
|
+
# Debounce tracking for agent updates
|
|
114
|
+
self._agent_last_publish: dict[str, float] = {}
|
|
115
|
+
self._agent_debounce_ms: float = 0.05 # 50ms debounce for same agent
|
|
113
116
|
|
|
114
117
|
def update_agent(self, name: str, info: dict) -> None:
|
|
118
|
+
now = time.monotonic()
|
|
115
119
|
with self._lock:
|
|
116
120
|
self.agents[name] = info
|
|
121
|
+
last = self._agent_last_publish.get(name, 0)
|
|
122
|
+
# Coalesce rapid agent updates (< 50ms apart) unless state changed
|
|
123
|
+
prev_state = self._agent_last_publish.get(f"{name}_state")
|
|
124
|
+
cur_state = info.get("state")
|
|
125
|
+
if cur_state == prev_state and (now - last) < self._agent_debounce_ms:
|
|
126
|
+
return
|
|
127
|
+
self._agent_last_publish[name] = now
|
|
128
|
+
self._agent_last_publish[f"{name}_state"] = cur_state
|
|
117
129
|
self.events.publish("agent", {"name": name, **info})
|
|
118
130
|
|
|
119
131
|
def update_cycle(self, iteration: int, phase: str, state: str) -> None:
|
|
@@ -124,6 +136,7 @@ class DashboardState:
|
|
|
124
136
|
# Reset agents
|
|
125
137
|
for name in list(self.agents.keys()):
|
|
126
138
|
self.agents[name] = {"state": "idle", "detail": "idle", "elapsed": 0}
|
|
139
|
+
self._agent_last_publish.clear()
|
|
127
140
|
self.events.publish("cycle_start", {"iteration": iteration, "phase": phase, "state": state})
|
|
128
141
|
self.events.publish("state", {"cycle": iteration, "phase": phase, "goal": self.goal})
|
|
129
142
|
|
|
@@ -212,7 +225,7 @@ class DashboardHandler(BaseHTTPRequestHandler):
|
|
|
212
225
|
self.send_error(404)
|
|
213
226
|
|
|
214
227
|
def _handle_sse(self, ds: DashboardState) -> None:
|
|
215
|
-
"""Stream Server-Sent Events to the client."""
|
|
228
|
+
"""Stream Server-Sent Events to the client with batched flushing."""
|
|
216
229
|
self.send_response(200)
|
|
217
230
|
self.send_header("Content-Type", "text/event-stream")
|
|
218
231
|
self.send_header("Cache-Control", "no-cache")
|
|
@@ -228,20 +241,52 @@ class DashboardHandler(BaseHTTPRequestHandler):
|
|
|
228
241
|
client_q = ds.events.subscribe()
|
|
229
242
|
try:
|
|
230
243
|
while True:
|
|
244
|
+
# Batch: collect all available events within a short window
|
|
245
|
+
batch: list[str] = []
|
|
231
246
|
try:
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
247
|
+
# Block up to 100ms for first event, then drain
|
|
248
|
+
payload = client_q.get(timeout=0.1)
|
|
249
|
+
batch.append(payload)
|
|
235
250
|
except queue.Empty:
|
|
236
|
-
|
|
237
|
-
|
|
251
|
+
pass
|
|
252
|
+
|
|
253
|
+
# Drain any additional queued events (non-blocking)
|
|
254
|
+
while not client_q.empty() and len(batch) < 50:
|
|
255
|
+
try:
|
|
256
|
+
batch.append(client_q.get_nowait())
|
|
257
|
+
except queue.Empty:
|
|
258
|
+
break
|
|
259
|
+
|
|
260
|
+
if batch:
|
|
261
|
+
# Write all events in one I/O call
|
|
262
|
+
combined = "".join(batch)
|
|
263
|
+
self.wfile.write(combined.encode("utf-8"))
|
|
238
264
|
self.wfile.flush()
|
|
265
|
+
else:
|
|
266
|
+
# Send keepalive every ~15s (150 empty 100ms cycles)
|
|
267
|
+
# Use a counter to avoid frequent keepalives
|
|
268
|
+
if not hasattr(self, '_keepalive_counter'):
|
|
269
|
+
self._keepalive_counter = 0
|
|
270
|
+
self._keepalive_counter += 1
|
|
271
|
+
if self._keepalive_counter >= 150: # ~15 seconds
|
|
272
|
+
self.wfile.write(": keepalive\n\n".encode("utf-8"))
|
|
273
|
+
self.wfile.flush()
|
|
274
|
+
self._keepalive_counter = 0
|
|
275
|
+
|
|
239
276
|
except (BrokenPipeError, ConnectionResetError, OSError):
|
|
240
277
|
pass
|
|
241
278
|
finally:
|
|
242
279
|
ds.events.unsubscribe(client_q)
|
|
243
280
|
|
|
244
281
|
|
|
282
|
+
# ── Threading HTTP Server ────────────────────────────────────────────────────
|
|
283
|
+
|
|
284
|
+
class ThreadingDashboardHTTPServer(socketserver.ThreadingMixIn, HTTPServer):
|
|
285
|
+
"""Multi-threaded HTTP server so SSE connections don't block API requests."""
|
|
286
|
+
daemon_threads = True
|
|
287
|
+
allow_reuse_address = True
|
|
288
|
+
|
|
289
|
+
|
|
245
290
|
# ── Dashboard Server ─────────────────────────────────────────────────────────
|
|
246
291
|
|
|
247
292
|
class DashboardServer:
|
|
@@ -258,13 +303,13 @@ class DashboardServer:
|
|
|
258
303
|
def __init__(self, state: DashboardState, port: int = 5042):
|
|
259
304
|
self.state = state
|
|
260
305
|
self.port = port
|
|
261
|
-
self._httpd: Optional[
|
|
306
|
+
self._httpd: Optional[ThreadingDashboardHTTPServer] = None
|
|
262
307
|
self._thread: Optional[threading.Thread] = None
|
|
263
308
|
|
|
264
309
|
def start(self) -> bool:
|
|
265
310
|
"""Start the dashboard server in a background thread. Returns True on success."""
|
|
266
311
|
try:
|
|
267
|
-
self._httpd =
|
|
312
|
+
self._httpd = ThreadingDashboardHTTPServer(("0.0.0.0", self.port), DashboardHandler)
|
|
268
313
|
self._httpd._dashboard_state = self.state # type: ignore
|
|
269
314
|
self._httpd.timeout = 0.5
|
|
270
315
|
self._thread = threading.Thread(
|
package/adelie/dashboard_html.py
CHANGED
|
@@ -3,6 +3,12 @@ adelie/dashboard_html.py
|
|
|
3
3
|
|
|
4
4
|
Embedded HTML/CSS/JS template for the Adelie real-time web dashboard.
|
|
5
5
|
Served as a single self-contained page — no external assets needed.
|
|
6
|
+
|
|
7
|
+
Performance-optimized:
|
|
8
|
+
- requestAnimationFrame DOM batching
|
|
9
|
+
- DocumentFragment for bulk log insertion
|
|
10
|
+
- Event throttling for rapid agent updates
|
|
11
|
+
- Phase timeline class-toggle (no full re-render)
|
|
6
12
|
"""
|
|
7
13
|
|
|
8
14
|
DASHBOARD_HTML = r"""<!DOCTYPE html>
|
|
@@ -49,8 +55,8 @@ body{font-family:'Inter',system-ui,sans-serif;background:var(--bg);color:var(--f
|
|
|
49
55
|
/* ── Agent Grid ─────────────────────── */
|
|
50
56
|
.section-title{font-size:13px;font-weight:600;color:var(--fg2);text-transform:uppercase;letter-spacing:1px;margin-bottom:10px}
|
|
51
57
|
.agents-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:10px;margin-bottom:16px}
|
|
52
|
-
.agent-card{background:var(--glass);border:1px solid var(--border);border-radius:10px;padding:14px;transition:
|
|
53
|
-
.agent-card:hover{border-color:var(--fg3)
|
|
58
|
+
.agent-card{background:var(--glass);border:1px solid var(--border);border-radius:10px;padding:14px;transition:border-color .3s ease,box-shadow .3s ease;position:relative;overflow:hidden;contain:layout style}
|
|
59
|
+
.agent-card:hover{border-color:var(--fg3)}
|
|
54
60
|
.agent-card.running{border-color:var(--cyan);box-shadow:0 0 20px rgba(88,166,255,.1)}
|
|
55
61
|
.agent-card.running::before{content:'';position:absolute;top:0;left:-100%;width:100%;height:2px;background:linear-gradient(90deg,transparent,var(--cyan),transparent);animation:scan 2s linear infinite}
|
|
56
62
|
@keyframes scan{to{left:100%}}
|
|
@@ -128,7 +134,7 @@ body{font-family:'Inter',system-ui,sans-serif;background:var(--bg);color:var(--f
|
|
|
128
134
|
<svg viewBox="0 0 32 32" fill="none"><circle cx="16" cy="12" r="10" fill="#58a6ff" opacity=".15" stroke="#58a6ff" stroke-width="1.5"/><circle cx="13" cy="10" r="1.5" fill="#e6edf3"/><circle cx="19" cy="10" r="1.5" fill="#e6edf3"/><ellipse cx="16" cy="14" rx="3" ry="2" fill="#f0883e"/><path d="M10 20 C10 26 22 26 22 20" stroke="#58a6ff" stroke-width="1.5" fill="none"/><path d="M6 14 C4 18 8 22 10 20" stroke="#58a6ff" stroke-width="1.5" fill="none"/><path d="M26 14 C28 18 24 22 22 20" stroke="#58a6ff" stroke-width="1.5" fill="none"/></svg>
|
|
129
135
|
Adelie Dashboard
|
|
130
136
|
</div>
|
|
131
|
-
<span class="version" id="version">v0.
|
|
137
|
+
<span class="version" id="version">v0.2.0</span>
|
|
132
138
|
<div class="spacer"></div>
|
|
133
139
|
<div class="status-dot" id="statusDot"></div>
|
|
134
140
|
<span class="status-label" id="statusLabel">Connected</span>
|
|
@@ -199,7 +205,6 @@ body{font-family:'Inter',system-ui,sans-serif;background:var(--bg);color:var(--f
|
|
|
199
205
|
|
|
200
206
|
// ── Agents ──────────────────────────
|
|
201
207
|
const AGENTS = ["Writer","Expert","Scanner","Coder","Reviewer","Tester","Runner","Monitor","Analyst","Research"];
|
|
202
|
-
const AGENT_COLORS = {Writer:"#58a6ff",Expert:"#58a6ff",Scanner:"#bc8cff",Coder:"#3fb950","Coder:0":"#3fb950","Coder:1":"#3fb950","Coder:2":"#3fb950",Reviewer:"#d29922",Tester:"#f85149",Runner:"#3fb950",Monitor:"#58a6ff",Analyst:"#bc8cff",Inform:"#58a6ff",Research:"#d29922"};
|
|
203
208
|
|
|
204
209
|
// ── Phases ──────────────────────────
|
|
205
210
|
const PHASES = [
|
|
@@ -213,7 +218,7 @@ body{font-family:'Inter',system-ui,sans-serif;background:var(--bg);color:var(--f
|
|
|
213
218
|
|
|
214
219
|
// ── State ───────────────────────────
|
|
215
220
|
let state = {agents:{},cycle:0,phase:"initial",goal:"",workspace:"",metrics:{}};
|
|
216
|
-
let
|
|
221
|
+
let logCount = 0;
|
|
217
222
|
const MAX_LOGS = 300;
|
|
218
223
|
let autoScroll = true;
|
|
219
224
|
|
|
@@ -221,11 +226,33 @@ body{font-family:'Inter',system-ui,sans-serif;background:var(--bg);color:var(--f
|
|
|
221
226
|
const $ = id => document.getElementById(id);
|
|
222
227
|
const agentsGrid = $("agentsGrid");
|
|
223
228
|
const logBody = $("logBody");
|
|
224
|
-
const
|
|
229
|
+
const logCountEl = $("logCount");
|
|
230
|
+
|
|
231
|
+
// ── RAF Batch Queue ─────────────────
|
|
232
|
+
// All DOM mutations are queued and applied in a single rAF frame
|
|
233
|
+
let pendingUpdates = [];
|
|
234
|
+
let rafScheduled = false;
|
|
235
|
+
|
|
236
|
+
function scheduleUpdate(fn) {
|
|
237
|
+
pendingUpdates.push(fn);
|
|
238
|
+
if (!rafScheduled) {
|
|
239
|
+
rafScheduled = true;
|
|
240
|
+
requestAnimationFrame(flushUpdates);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function flushUpdates() {
|
|
245
|
+
const updates = pendingUpdates;
|
|
246
|
+
pendingUpdates = [];
|
|
247
|
+
rafScheduled = false;
|
|
248
|
+
for (let i = 0; i < updates.length; i++) {
|
|
249
|
+
updates[i]();
|
|
250
|
+
}
|
|
251
|
+
}
|
|
225
252
|
|
|
226
253
|
// ── Init agents grid ────────────────
|
|
227
254
|
function initAgents(){
|
|
228
|
-
|
|
255
|
+
const frag = document.createDocumentFragment();
|
|
229
256
|
AGENTS.forEach(name => {
|
|
230
257
|
const card = document.createElement("div");
|
|
231
258
|
card.className = "agent-card";
|
|
@@ -234,79 +261,157 @@ body{font-family:'Inter',system-ui,sans-serif;background:var(--bg);color:var(--f
|
|
|
234
261
|
<div class="agent-name"><span class="dot idle" id="dot-${name}"></span>${name}</div>
|
|
235
262
|
<div class="agent-detail" id="detail-${name}">idle</div>
|
|
236
263
|
<div class="agent-elapsed" id="elapsed-${name}"></div>`;
|
|
237
|
-
|
|
264
|
+
frag.appendChild(card);
|
|
238
265
|
});
|
|
266
|
+
agentsGrid.appendChild(frag);
|
|
239
267
|
}
|
|
240
268
|
|
|
241
|
-
// ── Update agent card
|
|
269
|
+
// ── Update agent card (throttled) ──
|
|
270
|
+
const agentThrottleMap = {};
|
|
271
|
+
const AGENT_THROTTLE_MS = 80;
|
|
272
|
+
|
|
242
273
|
function updateAgent(name, info){
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
const
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
274
|
+
const now = performance.now();
|
|
275
|
+
const lastUpdate = agentThrottleMap[name] || 0;
|
|
276
|
+
const prevState = agentThrottleMap[name + "_state"];
|
|
277
|
+
const curState = info.state || "idle";
|
|
278
|
+
|
|
279
|
+
// Skip if same state and within throttle window
|
|
280
|
+
if (curState === prevState && (now - lastUpdate) < AGENT_THROTTLE_MS) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
agentThrottleMap[name] = now;
|
|
284
|
+
agentThrottleMap[name + "_state"] = curState;
|
|
285
|
+
|
|
286
|
+
scheduleUpdate(() => {
|
|
287
|
+
const card = $("agent-"+name);
|
|
288
|
+
if(!card) return;
|
|
289
|
+
const dot = $("dot-"+name);
|
|
290
|
+
const detail = $("detail-"+name);
|
|
291
|
+
const elapsed = $("elapsed-"+name);
|
|
292
|
+
const st = info.state || "idle";
|
|
293
|
+
card.className = "agent-card " + st;
|
|
294
|
+
dot.className = "dot " + st;
|
|
295
|
+
detail.textContent = info.detail || st;
|
|
296
|
+
if(info.elapsed > 0) elapsed.textContent = info.elapsed.toFixed(1)+"s";
|
|
297
|
+
else elapsed.textContent = st === "running" ? "…" : "";
|
|
298
|
+
});
|
|
254
299
|
}
|
|
255
300
|
|
|
256
|
-
// ──
|
|
257
|
-
|
|
301
|
+
// ── Init phase timeline (once) ─────
|
|
302
|
+
let phaseElements = [];
|
|
303
|
+
function initPhaseTimeline() {
|
|
258
304
|
const tl = $("phaseTimeline");
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
PHASES.forEach(p => {
|
|
305
|
+
const frag = document.createDocumentFragment();
|
|
306
|
+
PHASES.forEach((p, i) => {
|
|
262
307
|
const item = document.createElement("div");
|
|
263
308
|
item.className = "phase-item";
|
|
264
|
-
|
|
265
|
-
else if(!found){ item.classList.add("completed"); }
|
|
309
|
+
item.dataset.phase = p.value;
|
|
266
310
|
item.innerHTML = `<span class="phase-name">${p.label}</span>`;
|
|
267
|
-
|
|
311
|
+
frag.appendChild(item);
|
|
312
|
+
phaseElements.push(item);
|
|
313
|
+
});
|
|
314
|
+
tl.appendChild(frag);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ── Update phase timeline (class toggle only) ──
|
|
318
|
+
let currentPhase = "";
|
|
319
|
+
function updatePhase(current){
|
|
320
|
+
if (current === currentPhase) return;
|
|
321
|
+
currentPhase = current;
|
|
322
|
+
|
|
323
|
+
scheduleUpdate(() => {
|
|
324
|
+
let found = false;
|
|
325
|
+
for (let i = 0; i < phaseElements.length; i++) {
|
|
326
|
+
const el = phaseElements[i];
|
|
327
|
+
el.classList.remove("active", "completed");
|
|
328
|
+
if (el.dataset.phase === current) {
|
|
329
|
+
el.classList.add("active");
|
|
330
|
+
found = true;
|
|
331
|
+
} else if (!found) {
|
|
332
|
+
el.classList.add("completed");
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
$("phaseBadge").textContent = (PHASES.find(p=>p.value===current)||{}).label || current;
|
|
268
336
|
});
|
|
269
|
-
$("phaseBadge").textContent = (PHASES.find(p=>p.value===current)||{}).label || current;
|
|
270
337
|
}
|
|
271
338
|
|
|
272
339
|
// ── Update metrics panel ───────────
|
|
273
340
|
function updateMetrics(m){
|
|
274
341
|
if(!m) return;
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
342
|
+
scheduleUpdate(() => {
|
|
343
|
+
$("mTokens").textContent = (m.total_tokens||0).toLocaleString();
|
|
344
|
+
$("mCalls").textContent = m.llm_calls || m.calls || 0;
|
|
345
|
+
$("mFiles").textContent = m.files_written || 0;
|
|
346
|
+
$("mTime").textContent = (m.cycle_time||0).toFixed(1)+"s";
|
|
347
|
+
if(m.tests_total > 0) $("mTests").textContent = m.tests_passed+"/"+m.tests_total;
|
|
348
|
+
if(m.review_score > 0) $("mReview").textContent = m.review_score.toFixed(0)+"/10";
|
|
349
|
+
});
|
|
281
350
|
}
|
|
282
351
|
|
|
283
352
|
// ── Update cycle chart ─────────────
|
|
284
353
|
function updateChart(history){
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
354
|
+
scheduleUpdate(() => {
|
|
355
|
+
const bars = $("chartBars");
|
|
356
|
+
if(!history||!history.length){ bars.innerHTML="<span style='color:var(--fg3);font-size:11px'>No data yet</span>"; return; }
|
|
357
|
+
const slice = history.slice(-30);
|
|
358
|
+
const maxTime = Math.max(...slice.map(h=>h.cycle_time||1), 1);
|
|
359
|
+
const frag = document.createDocumentFragment();
|
|
360
|
+
slice.forEach(h => {
|
|
361
|
+
const pct = Math.max(5, ((h.cycle_time||0)/maxTime)*100);
|
|
362
|
+
const bar = document.createElement("div");
|
|
363
|
+
bar.className = "chart-bar";
|
|
364
|
+
bar.style.height = pct+"%";
|
|
365
|
+
bar.innerHTML = `<div class="tooltip">#${h.cycle} · ${(h.cycle_time||0).toFixed(1)}s · ${((h.tokens||{}).total||0).toLocaleString()} tok</div>`;
|
|
366
|
+
frag.appendChild(bar);
|
|
367
|
+
});
|
|
368
|
+
bars.innerHTML = "";
|
|
369
|
+
bars.appendChild(frag);
|
|
296
370
|
});
|
|
297
371
|
}
|
|
298
372
|
|
|
299
|
-
// ── Add log
|
|
373
|
+
// ── Add log entries (batched) ──────
|
|
300
374
|
const CAT_ICONS = {agent_start:"▶",agent_end:"✓",error:"✕",warning:"⚠",phase_change:"◆",info:"·",debug:"·",cycle_header:"─",cycle_summary:"📊",progress:"→"};
|
|
375
|
+
let pendingLogs = [];
|
|
376
|
+
let logRafScheduled = false;
|
|
377
|
+
|
|
301
378
|
function addLog(entry){
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
379
|
+
pendingLogs.push(entry);
|
|
380
|
+
logCount++;
|
|
381
|
+
if (!logRafScheduled) {
|
|
382
|
+
logRafScheduled = true;
|
|
383
|
+
requestAnimationFrame(flushLogs);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function flushLogs() {
|
|
388
|
+
logRafScheduled = false;
|
|
389
|
+
const entries = pendingLogs;
|
|
390
|
+
pendingLogs = [];
|
|
391
|
+
if (!entries.length) return;
|
|
392
|
+
|
|
393
|
+
// Trim excess before adding
|
|
394
|
+
const totalAfter = logBody.childElementCount + entries.length;
|
|
395
|
+
if (totalAfter > MAX_LOGS) {
|
|
396
|
+
const toRemove = totalAfter - MAX_LOGS;
|
|
397
|
+
for (let i = 0; i < toRemove && logBody.firstChild; i++) {
|
|
398
|
+
logBody.removeChild(logBody.firstChild);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Build all new entries in a DocumentFragment
|
|
403
|
+
const frag = document.createDocumentFragment();
|
|
404
|
+
for (let i = 0; i < entries.length; i++) {
|
|
405
|
+
const entry = entries[i];
|
|
406
|
+
const div = document.createElement("div");
|
|
407
|
+
div.className = "log-entry " + (entry.category||"info");
|
|
408
|
+
const ts = entry.timestamp ? new Date(entry.timestamp).toLocaleTimeString() : "";
|
|
409
|
+
div.innerHTML = `<span class="log-ts">${ts}</span><span class="log-cat">${CAT_ICONS[entry.category]||"·"}</span><span class="log-msg">${escHtml(entry.message||"")}</span>`;
|
|
410
|
+
frag.appendChild(div);
|
|
411
|
+
}
|
|
412
|
+
logBody.appendChild(frag);
|
|
413
|
+
|
|
414
|
+
logCountEl.textContent = Math.min(logCount, MAX_LOGS) + " entries";
|
|
310
415
|
if(autoScroll) logBody.scrollTop = logBody.scrollHeight;
|
|
311
416
|
}
|
|
312
417
|
|
|
@@ -316,7 +421,7 @@ body{font-family:'Inter',system-ui,sans-serif;background:var(--bg);color:var(--f
|
|
|
316
421
|
logBody.addEventListener("scroll", () => {
|
|
317
422
|
const gap = logBody.scrollHeight - logBody.scrollTop - logBody.clientHeight;
|
|
318
423
|
autoScroll = gap < 40;
|
|
319
|
-
});
|
|
424
|
+
}, {passive: true});
|
|
320
425
|
|
|
321
426
|
// ── Load initial state ──────────────
|
|
322
427
|
async function loadState(){
|
|
@@ -354,9 +459,18 @@ body{font-family:'Inter',system-ui,sans-serif;background:var(--bg);color:var(--f
|
|
|
354
459
|
// ── SSE Connection ─────────────────
|
|
355
460
|
let evtSource = null;
|
|
356
461
|
let reconnectDelay = 1000;
|
|
462
|
+
let reconnectTimer = null;
|
|
357
463
|
|
|
358
464
|
function connectSSE(){
|
|
359
|
-
if(evtSource)
|
|
465
|
+
if(evtSource){
|
|
466
|
+
evtSource.close();
|
|
467
|
+
evtSource = null;
|
|
468
|
+
}
|
|
469
|
+
if(reconnectTimer){
|
|
470
|
+
clearTimeout(reconnectTimer);
|
|
471
|
+
reconnectTimer = null;
|
|
472
|
+
}
|
|
473
|
+
|
|
360
474
|
evtSource = new EventSource("/events");
|
|
361
475
|
|
|
362
476
|
evtSource.onopen = () => {
|
|
@@ -368,9 +482,9 @@ body{font-family:'Inter',system-ui,sans-serif;background:var(--bg);color:var(--f
|
|
|
368
482
|
|
|
369
483
|
evtSource.addEventListener("state", (e) => {
|
|
370
484
|
const d = JSON.parse(e.data);
|
|
371
|
-
if(d.cycle) $("cycleNum").textContent = "#"+d.cycle;
|
|
485
|
+
if(d.cycle) scheduleUpdate(() => { $("cycleNum").textContent = "#"+d.cycle; });
|
|
372
486
|
if(d.phase) updatePhase(d.phase);
|
|
373
|
-
if(d.goal) $("goal").textContent = d.goal;
|
|
487
|
+
if(d.goal) scheduleUpdate(() => { $("goal").textContent = d.goal; });
|
|
374
488
|
});
|
|
375
489
|
|
|
376
490
|
evtSource.addEventListener("agent", (e) => {
|
|
@@ -390,7 +504,9 @@ body{font-family:'Inter',system-ui,sans-serif;background:var(--bg);color:var(--f
|
|
|
390
504
|
|
|
391
505
|
evtSource.addEventListener("cycle_start", (e) => {
|
|
392
506
|
const d = JSON.parse(e.data);
|
|
393
|
-
|
|
507
|
+
scheduleUpdate(() => {
|
|
508
|
+
$("cycleNum").textContent = "#"+(d.iteration||0);
|
|
509
|
+
});
|
|
394
510
|
// Reset agents
|
|
395
511
|
AGENTS.forEach(name => updateAgent(name, {state:"idle",detail:"idle",elapsed:0}));
|
|
396
512
|
});
|
|
@@ -406,21 +522,20 @@ body{font-family:'Inter',system-ui,sans-serif;background:var(--bg);color:var(--f
|
|
|
406
522
|
$("statusDot").style.background = "var(--red)";
|
|
407
523
|
$("statusLabel").textContent = "Disconnected";
|
|
408
524
|
evtSource.close();
|
|
409
|
-
|
|
410
|
-
|
|
525
|
+
evtSource = null;
|
|
526
|
+
reconnectTimer = setTimeout(connectSSE, reconnectDelay);
|
|
527
|
+
reconnectDelay = Math.min(reconnectDelay * 1.5, 15000);
|
|
411
528
|
};
|
|
412
529
|
}
|
|
413
530
|
|
|
414
531
|
// ── Init ────────────────────────────
|
|
415
532
|
initAgents();
|
|
533
|
+
initPhaseTimeline();
|
|
416
534
|
loadState().then(()=>{
|
|
417
535
|
loadLogs();
|
|
418
536
|
loadHistory();
|
|
419
537
|
connectSSE();
|
|
420
538
|
});
|
|
421
|
-
|
|
422
|
-
// Refresh history every 30s
|
|
423
|
-
setInterval(loadHistory, 30000);
|
|
424
539
|
})();
|
|
425
540
|
</script>
|
|
426
541
|
</body>
|
package/adelie/interactive.py
CHANGED
|
@@ -62,14 +62,14 @@ AGENT_COLORS = {
|
|
|
62
62
|
# ── Header ────────────────────────────────────────────────────────────────────
|
|
63
63
|
|
|
64
64
|
def print_header(goal: str, phase: str, model: str, workspace: str):
|
|
65
|
-
"""Print the startup header —
|
|
65
|
+
"""Print the startup header — penguin ASCII icon + info."""
|
|
66
|
+
from adelie import __version__
|
|
66
67
|
width = shutil.get_terminal_size((80, 24)).columns
|
|
67
68
|
|
|
68
69
|
console.print()
|
|
69
|
-
console.print("
|
|
70
|
-
console.print(
|
|
71
|
-
console.print(
|
|
72
|
-
console.print(f" [cyan]▝▀[/cyan]")
|
|
70
|
+
console.print(" [cyan] (o_ [/cyan] [bold]Adelie[/bold] [dim]v" + __version__ + "[/dim]")
|
|
71
|
+
console.print(" [cyan] //\\\\ [/cyan] [dim]" + model + "[/dim]")
|
|
72
|
+
console.print(" [cyan] V_/_ [/cyan] [dim]Phase:[/dim] " + phase)
|
|
73
73
|
console.print()
|
|
74
74
|
|
|
75
75
|
# Goal
|