autodevloop 0.1.0__py3-none-any.whl

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.
autodevloop/webapp.py ADDED
@@ -0,0 +1,1184 @@
1
+ """A dependency-free local web dashboard for AutoDevLoop.
2
+
3
+ Bind to localhost only. It can create, start, and stop runs, exposes live run
4
+ state (progress, per-agent timers, token usage, per-agent output), and an
5
+ editable settings page (config + prompt templates) that is locked while a run
6
+ is active. It never asks for API keys; provider switching is a CLI command swap.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import os
13
+ import subprocess
14
+ import sys
15
+ import threading
16
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
17
+ from pathlib import Path
18
+ from typing import Any
19
+ from urllib.parse import parse_qs, urlparse
20
+
21
+ from . import prompts, registry
22
+ from .config import default_config, deep_merge, load_config, save_config, deep_get
23
+ from .util import (
24
+ APP_DIR, PROGRESS_FILE, STATE_FILE, STOP_FILE,
25
+ load_json, now_text, read_text, restore_working_dir, save_json, write_text,
26
+ )
27
+
28
+ _RUNS: dict[str, subprocess.Popen] = {}
29
+ _LOCK = threading.Lock()
30
+
31
+
32
+ # --------------------------------------------------------------------------- #
33
+ # HTTP helpers
34
+ # --------------------------------------------------------------------------- #
35
+ def _json_response(handler: BaseHTTPRequestHandler, data: Any, status: int = 200) -> None:
36
+ body = json.dumps(data, ensure_ascii=False).encode("utf-8")
37
+ handler.send_response(status)
38
+ handler.send_header("Content-Type", "application/json; charset=utf-8")
39
+ handler.send_header("Content-Length", str(len(body)))
40
+ handler.end_headers()
41
+ handler.wfile.write(body)
42
+
43
+
44
+ def _text_response(handler: BaseHTTPRequestHandler, text: str,
45
+ content_type: str = "text/plain; charset=utf-8", status: int = 200) -> None:
46
+ body = text.encode("utf-8")
47
+ handler.send_response(status)
48
+ handler.send_header("Content-Type", content_type)
49
+ handler.send_header("Content-Length", str(len(body)))
50
+ handler.end_headers()
51
+ handler.wfile.write(body)
52
+
53
+
54
+ def _safe_within(root: Path, candidate: Path) -> bool:
55
+ try:
56
+ candidate.resolve().relative_to(root.resolve())
57
+ return True
58
+ except ValueError:
59
+ return False
60
+
61
+
62
+ # --------------------------------------------------------------------------- #
63
+ # Run process tracking
64
+ # --------------------------------------------------------------------------- #
65
+ def _is_running(dir_str: str) -> bool:
66
+ with _LOCK:
67
+ proc = _RUNS.get(str(Path(dir_str).resolve()))
68
+ return proc is not None and proc.poll() is None
69
+
70
+
71
+ def _kill(proc: subprocess.Popen | None) -> None:
72
+ if proc is None:
73
+ return
74
+ try:
75
+ if os.name == "nt":
76
+ subprocess.run(["taskkill", "/F", "/T", "/PID", str(proc.pid)], capture_output=True)
77
+ else:
78
+ proc.terminate()
79
+ except Exception: # noqa: BLE001
80
+ pass
81
+ try:
82
+ proc.kill()
83
+ except Exception: # noqa: BLE001
84
+ pass
85
+
86
+
87
+ # --------------------------------------------------------------------------- #
88
+ # Project summaries / config
89
+ # --------------------------------------------------------------------------- #
90
+ def _project_summary(entry: dict[str, Any]) -> dict[str, Any]:
91
+ root = Path(entry["dir"])
92
+ state = load_json(root / APP_DIR / STATE_FILE, {})
93
+ config = load_config(root)
94
+ running = _is_running(str(root))
95
+ status = "running" if running else (state.get("status") or "initialized")
96
+ return {
97
+ "dir": str(root),
98
+ "name": entry.get("name") or deep_get(config, "project.name", "") or root.name,
99
+ "status": status,
100
+ "phase": state.get("phase", "build"),
101
+ "current_version": state.get("current_version", 0),
102
+ "max_versions": state.get("max_versions") or deep_get(config, "project.max_versions", 0),
103
+ "goal_progress": state.get("goal_progress", 0),
104
+ "running": running,
105
+ }
106
+
107
+
108
+ def _create_project(payload: dict[str, Any]) -> dict[str, Any]:
109
+ raw_dir = str(payload.get("dir") or "").strip()
110
+ goal = str(payload.get("goal") or "").strip()
111
+ if not raw_dir:
112
+ return {"ok": False, "error": "project directory is required"}
113
+ if not goal:
114
+ return {"ok": False, "error": "goal is required"}
115
+ root = Path(raw_dir).expanduser().resolve()
116
+ root.mkdir(parents=True, exist_ok=True)
117
+
118
+ config = load_config(root)
119
+ overrides: dict[str, Any] = {"project": {"goal": goal}, "provider": {}, "pipeline": {}}
120
+ if payload.get("name"):
121
+ overrides["project"]["name"] = payload["name"]
122
+ if payload.get("max_versions"):
123
+ overrides["project"]["max_versions"] = int(payload["max_versions"])
124
+ if payload.get("arch_hint"):
125
+ overrides["project"]["arch_hint"] = payload["arch_hint"]
126
+ if payload.get("mode"):
127
+ overrides["pipeline"]["mode"] = payload["mode"]
128
+ if payload.get("provider"):
129
+ overrides["provider"]["name"] = payload["provider"]
130
+ if payload.get("provider_command"):
131
+ overrides["provider"]["command"] = payload["provider_command"]
132
+ if payload.get("model"):
133
+ overrides["provider"]["model"] = payload["model"]
134
+ save_config(root, deep_merge(config, {k: v for k, v in overrides.items() if v}))
135
+ (root / APP_DIR).mkdir(parents=True, exist_ok=True)
136
+ prompts.ensure_templates(root / APP_DIR)
137
+ registry.register(root, str(payload.get("name") or root.name))
138
+ return {"ok": True, "dir": str(root)}
139
+
140
+
141
+ def _start_run(payload: dict[str, Any]) -> dict[str, Any]:
142
+ raw_dir = str(payload.get("dir") or "").strip()
143
+ if not raw_dir:
144
+ return {"ok": False, "error": "project directory is required"}
145
+ root = Path(raw_dir).expanduser().resolve()
146
+ if _is_running(str(root)):
147
+ return {"ok": False, "error": "a run is already active for this project"}
148
+
149
+ config = load_config(root)
150
+ state = load_json(root / APP_DIR / STATE_FILE, {})
151
+ goal = deep_get(config, "project.goal", "") or state.get("goal", "")
152
+ if not goal:
153
+ return {"ok": False, "error": "no goal configured; create the project first"}
154
+
155
+ cmd = [sys.executable, "-u", "-m", "autodevloop", "run", "--project-dir", str(root), "--non-interactive"]
156
+ if payload.get("reset"):
157
+ cmd += ["--reset"]
158
+
159
+ (root / APP_DIR).mkdir(parents=True, exist_ok=True)
160
+ log_file = (root / APP_DIR / "web_run.log").open("a", encoding="utf-8")
161
+ log_file.write(f"\n=== run started {now_text()} ===\n")
162
+ log_file.flush()
163
+ pkg_root = str(Path(__file__).resolve().parents[1])
164
+ env = dict(os.environ)
165
+ env["PYTHONPATH"] = pkg_root + os.pathsep + env.get("PYTHONPATH", "")
166
+ env["PYTHONIOENCODING"] = "utf-8"
167
+ proc = subprocess.Popen(cmd, stdout=log_file, stderr=subprocess.STDOUT, cwd=pkg_root, env=env)
168
+ with _LOCK:
169
+ _RUNS[str(root)] = proc
170
+ return {"ok": True, "dir": str(root), "pid": proc.pid}
171
+
172
+
173
+ def _rollback_current(root: Path) -> tuple[bool, int]:
174
+ """Restore current/ to the last completed version. Returns (rolled, version)."""
175
+ state = load_json(root / APP_DIR / STATE_FILE, {})
176
+ cur = int(state.get("current_version", 0) or 0)
177
+ target = root / "current"
178
+ before = root / APP_DIR / "work" / f"v{cur + 1}" / "_before"
179
+ if before.exists():
180
+ restore_working_dir(before, target)
181
+ return True, cur
182
+ snap = root / "versions" / f"v{cur}"
183
+ if cur > 0 and snap.exists():
184
+ restore_working_dir(snap, target)
185
+ return True, cur
186
+ return False, cur
187
+
188
+
189
+ def _stop_run(payload: dict[str, Any]) -> dict[str, Any]:
190
+ raw_dir = str(payload.get("dir") or "").strip()
191
+ mode = str(payload.get("mode") or "graceful")
192
+ if not raw_dir:
193
+ return {"ok": False, "error": "dir required"}
194
+ root = Path(raw_dir).expanduser().resolve()
195
+ app_dir = root / APP_DIR
196
+ app_dir.mkdir(parents=True, exist_ok=True)
197
+
198
+ if mode == "immediate":
199
+ with _LOCK:
200
+ proc = _RUNS.pop(str(root), None)
201
+ if proc is None or proc.poll() is not None:
202
+ # Not tracked (e.g. server restarted). Fall back to a graceful stop.
203
+ write_text(app_dir / STOP_FILE, f"stop requested at {now_text()}\n")
204
+ return {"ok": True, "mode": "graceful_fallback",
205
+ "note": "process not tracked; requested graceful stop instead"}
206
+ _kill(proc)
207
+ rolled, rolled_to = _rollback_current(root)
208
+ # Patch state + progress so the UI reflects the abort.
209
+ state = load_json(app_dir / STATE_FILE, {})
210
+ if state:
211
+ state["status"] = "stopped"
212
+ state["stop_reason"] = "Immediate stop: current version discarded and rolled back"
213
+ save_json(app_dir / STATE_FILE, state)
214
+ prog = load_json(app_dir / PROGRESS_FILE, {})
215
+ if prog:
216
+ prog["status"] = "stopped"
217
+ prog["active"] = []
218
+ prog["run_ended_at"] = now_text()
219
+ prog.setdefault("events", []).append(
220
+ {"time": now_text(), "step": "STOP", "agent": "", "message": "immediate stop; version discarded"})
221
+ save_json(app_dir / PROGRESS_FILE, prog, stamp=False)
222
+ return {"ok": True, "mode": "immediate", "rolled_back": rolled, "rolled_to": rolled_to}
223
+
224
+ # graceful
225
+ write_text(app_dir / STOP_FILE, f"stop requested at {now_text()}\n")
226
+ return {"ok": True, "mode": "graceful"}
227
+
228
+
229
+ def _get_config(dir_str: str) -> dict[str, Any]:
230
+ root = Path(dir_str).expanduser().resolve()
231
+ config = load_config(root)
232
+ app_dir = root / APP_DIR
233
+ prompts.ensure_templates(app_dir)
234
+ templates = {name: prompts.load_template(app_dir, name) for name in prompts.TEMPLATE_NAMES}
235
+ return {"config": config, "templates": templates,
236
+ "template_names": prompts.TEMPLATE_NAMES,
237
+ "required_tokens": prompts.REQUIRED_TOKENS,
238
+ "running": _is_running(str(root))}
239
+
240
+
241
+ def _save_config(dir_str: str, payload: dict[str, Any]) -> dict[str, Any]:
242
+ root = Path(dir_str).expanduser().resolve()
243
+ if _is_running(str(root)):
244
+ return {"ok": False, "error": "cannot edit settings while a run is active"}
245
+ templates = payload.get("templates") or {}
246
+ # Refuse to save prompts that dropped tokens the engine depends on, so an
247
+ # edited prompt can't silently break the pipeline.
248
+ invalid: dict[str, list[str]] = {}
249
+ for name, body in templates.items():
250
+ if name in prompts.TEMPLATE_NAMES and isinstance(body, str):
251
+ missing = prompts.validate_template(name, body)
252
+ if missing:
253
+ invalid[name] = missing
254
+ if invalid:
255
+ return {"ok": False, "error": "invalid_templates", "invalid": invalid}
256
+
257
+ config = payload.get("config")
258
+ if isinstance(config, dict):
259
+ save_config(root, deep_merge(default_config(), config))
260
+ base = prompts.templates_dir(root / APP_DIR)
261
+ base.mkdir(parents=True, exist_ok=True)
262
+ for name, body in templates.items():
263
+ if name in prompts.TEMPLATE_NAMES and isinstance(body, str):
264
+ write_text(base / f"{name}.md", body.rstrip() + "\n")
265
+ return {"ok": True}
266
+
267
+
268
+ # --------------------------------------------------------------------------- #
269
+ # HTTP handler
270
+ # --------------------------------------------------------------------------- #
271
+ class Handler(BaseHTTPRequestHandler):
272
+ def log_message(self, *_args: Any) -> None:
273
+ pass
274
+
275
+ def _query(self) -> dict[str, str]:
276
+ return {k: v[0] for k, v in parse_qs(urlparse(self.path).query).items()}
277
+
278
+ def _read_body(self) -> dict[str, Any]:
279
+ length = int(self.headers.get("Content-Length", 0) or 0)
280
+ if not length:
281
+ return {}
282
+ try:
283
+ return json.loads(self.rfile.read(length).decode("utf-8"))
284
+ except (json.JSONDecodeError, UnicodeDecodeError):
285
+ return {}
286
+
287
+ def do_GET(self) -> None: # noqa: N802
288
+ path = urlparse(self.path).path
289
+ if path in ("/", "/index.html"):
290
+ return _text_response(self, INDEX_HTML, "text/html; charset=utf-8")
291
+ if path == "/api/projects":
292
+ return _json_response(self, {"projects": [_project_summary(e) for e in registry.load()]})
293
+ if path == "/api/state":
294
+ root = Path(self._query().get("dir", ""))
295
+ return _json_response(self, load_json(root / APP_DIR / STATE_FILE, {}))
296
+ if path == "/api/progress":
297
+ root = Path(self._query().get("dir", ""))
298
+ prog = load_json(root / APP_DIR / PROGRESS_FILE, {})
299
+ if isinstance(prog, dict):
300
+ prog["running"] = _is_running(str(root))
301
+ return _json_response(self, prog)
302
+ if path == "/api/config":
303
+ return _json_response(self, _get_config(self._query().get("dir", "")))
304
+ if path == "/api/log":
305
+ return self._serve_log()
306
+ if path == "/api/doc":
307
+ return self._serve_doc()
308
+ return _text_response(self, "not found", status=404)
309
+
310
+ def do_POST(self) -> None: # noqa: N802
311
+ path = urlparse(self.path).path
312
+ body = self._read_body()
313
+ if path == "/api/create":
314
+ return _json_response(self, _create_project(body))
315
+ if path == "/api/start":
316
+ return _json_response(self, _start_run(body))
317
+ if path == "/api/stop":
318
+ return _json_response(self, _stop_run(body))
319
+ if path == "/api/config":
320
+ return _json_response(self, _save_config(self._query().get("dir", ""), body))
321
+ return _text_response(self, "not found", status=404)
322
+
323
+ def _serve_log(self) -> None:
324
+ q = self._query()
325
+ root = Path(q.get("dir", "")).resolve()
326
+ name = q.get("name", "")
327
+ target = root / APP_DIR / "logs" / name
328
+ if not name or not _safe_within(root / APP_DIR / "logs", target) or not target.exists():
329
+ return _text_response(self, "(log not found)")
330
+ return _text_response(self, read_text(target))
331
+
332
+ def _serve_doc(self) -> None:
333
+ q = self._query()
334
+ root = Path(q.get("dir", "")).resolve()
335
+ which = q.get("name", "changelog")
336
+ mapping = {"changelog": root / "CHANGELOG.md", "features": root / "FEATURES.md",
337
+ "report": root / APP_DIR / "final_report.md", "weblog": root / APP_DIR / "web_run.log"}
338
+ target = mapping.get(which)
339
+ if target is None or not target.exists():
340
+ return _text_response(self, "(not available yet)")
341
+ return _text_response(self, read_text(target))
342
+
343
+
344
+ def serve(host: str = "127.0.0.1", port: int = 8787) -> None:
345
+ server = ThreadingHTTPServer((host, port), Handler)
346
+ print(f"[AutoDevLoop] Web dashboard: http://{host}:{port}")
347
+ print("[AutoDevLoop] Press Ctrl+C to stop the server (running projects keep going).")
348
+ try:
349
+ server.serve_forever()
350
+ except KeyboardInterrupt:
351
+ print("\n[AutoDevLoop] Web server stopped.")
352
+ finally:
353
+ server.server_close()
354
+
355
+
356
+ INDEX_HTML = r"""<!doctype html>
357
+ <html lang="en">
358
+ <head>
359
+ <meta charset="utf-8">
360
+ <meta name="viewport" content="width=device-width, initial-scale=1">
361
+ <title>AutoDevLoop</title>
362
+ <style>
363
+ :root{
364
+ --bg:#f4f6fb; --panel:#ffffff; --panel2:#f0f3f9; --line:#e2e7f0; --line2:#d4dbe8;
365
+ --text:#1f2733; --muted:#6b7686; --brand:#3b6fe0; --brand-soft:#e8f0ff;
366
+ --ok:#1f9d57; --ok-soft:#e3f6ec; --bad:#d8472b; --bad-soft:#fdeae6;
367
+ --warn:#c07a13; --warn-soft:#fdf2dd; --run:#3b6fe0; --run-soft:#e8f0ff;
368
+ --shadow:0 1px 3px rgba(20,30,60,.07),0 1px 2px rgba(20,30,60,.04);
369
+ }
370
+ *{box-sizing:border-box}
371
+ html,body{height:100%}
372
+ body{margin:0;font-family:-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"PingFang SC","Microsoft YaHei",sans-serif;background:var(--bg);color:var(--text);font-size:14px;display:flex;flex-direction:column;height:100vh;overflow:hidden}
373
+ header{display:flex;align-items:center;gap:10px;padding:10px 18px;background:var(--panel);border-bottom:1px solid var(--line);box-shadow:var(--shadow);flex-wrap:nowrap;flex-shrink:0}
374
+ header .brand{font-size:16px;font-weight:700;letter-spacing:.2px;white-space:nowrap}
375
+ header .brand b{color:var(--brand)}
376
+ header .sub{color:var(--muted);font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
377
+ header .spacer{flex:1}
378
+ button{background:var(--brand);color:#fff;border:0;border-radius:8px;padding:8px 14px;cursor:pointer;font-size:13px;font-weight:600;transition:.15s}
379
+ button:hover{filter:brightness(.96)}
380
+ button.sec{background:var(--panel2);border:1px solid var(--line2);color:var(--text)}
381
+ button.ok{background:var(--ok)}
382
+ button.warn{background:var(--warn)}
383
+ button.danger{background:var(--bad)}
384
+ button:disabled{opacity:.45;cursor:not-allowed;filter:none}
385
+ #newBtn{white-space:nowrap}
386
+ .iconbtn{background:var(--panel2);border:1px solid var(--line2);color:var(--text);border-radius:8px;padding:7px 10px;font-weight:600;font-size:13px;white-space:nowrap;display:inline-flex;align-items:center;gap:6px}
387
+ .iconbtn:hover{background:var(--brand-soft);border-color:var(--brand)}
388
+ .langwrap{position:relative}
389
+ .menu{position:absolute;right:0;top:calc(100% + 6px);background:var(--panel);border:1px solid var(--line2);border-radius:10px;box-shadow:0 12px 30px rgba(20,30,60,.18);min-width:140px;z-index:30;display:none;overflow:hidden}
390
+ .menu.show{display:block}
391
+ .menu div{padding:9px 14px;cursor:pointer;font-size:13px}
392
+ .menu div:hover{background:var(--brand-soft)}
393
+ .menu div.sel{color:var(--brand);font-weight:700}
394
+ .layout{display:flex;flex:1;min-height:0}
395
+ .sidebar{width:264px;background:var(--panel);border-right:1px solid var(--line);overflow:auto;padding:14px}
396
+ .sidebar .h{font-size:12px;text-transform:uppercase;letter-spacing:.6px;color:var(--muted);margin-bottom:10px}
397
+ .main{flex:1;overflow:auto;padding:20px 24px}
398
+ .proj{padding:10px 12px;border:1px solid var(--line);border-radius:10px;margin-bottom:9px;cursor:pointer;background:var(--panel)}
399
+ .proj:hover{border-color:var(--line2)}
400
+ .proj.active{border-color:var(--brand);background:var(--brand-soft)}
401
+ .proj .nm{font-weight:600}
402
+ .proj .meta{color:var(--muted);font-size:12px;margin-top:4px;display:flex;gap:6px;align-items:center;flex-wrap:wrap}
403
+ .tabs{display:flex;gap:6px;margin-bottom:18px}
404
+ .tabs .t{padding:8px 14px;border-radius:9px;cursor:pointer;color:var(--muted);font-weight:600}
405
+ .tabs .t:hover{background:var(--panel2)}
406
+ .tabs .t.active{background:var(--brand);color:#fff}
407
+ .toolbar{display:flex;align-items:center;gap:10px;margin-bottom:16px;flex-wrap:wrap}
408
+ .toolbar .rt{margin-left:auto;color:var(--muted);font-size:13px}
409
+ .toolbar .rt b{color:var(--text);font-variant-numeric:tabular-nums}
410
+ .cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:14px;margin-bottom:18px}
411
+ .card{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:14px;box-shadow:var(--shadow)}
412
+ .card .lbl{color:var(--muted);font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:.4px}
413
+ .card .val{font-size:22px;font-weight:700;margin-top:6px;font-variant-numeric:tabular-nums}
414
+ .card .sub{color:var(--muted);font-size:12px;margin-top:4px}
415
+ .bar{height:7px;background:var(--panel2);border-radius:6px;overflow:hidden;margin-top:9px}
416
+ .bar>i{display:block;height:100%;background:var(--brand);transition:width .4s}
417
+ .panel{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:16px;margin-bottom:16px;box-shadow:var(--shadow)}
418
+ .panel h3{margin:0 0 12px;font-size:14px;display:flex;align-items:center;gap:8px}
419
+ table{width:100%;border-collapse:collapse;font-size:13px}
420
+ th,td{text-align:left;padding:8px 9px;border-bottom:1px solid var(--line);vertical-align:top}
421
+ th{color:var(--muted);font-weight:600}
422
+ .vtable{border:1px solid var(--line);border-radius:10px;overflow:hidden}
423
+ .vtable th{white-space:nowrap;background:var(--panel2);border-bottom:1px solid var(--line2)}
424
+ .vtable td.nowrap,.vtable th{white-space:nowrap}
425
+ .vtable tbody tr:nth-child(even){background:var(--panel2)}
426
+ .vtable tbody tr:hover{background:var(--brand-soft)}
427
+ .badge{display:inline-flex;align-items:center;gap:4px;padding:2px 9px;border-radius:7px;font-size:12px;font-weight:700;background:var(--panel2);color:var(--muted);border:1px solid var(--line2)}
428
+ .badge.ok{color:var(--ok);background:var(--ok-soft);border-color:transparent}
429
+ .badge.bad{color:var(--bad);background:var(--bad-soft);border-color:transparent}
430
+ .badge.warn{color:var(--warn);background:var(--warn-soft);border-color:transparent}
431
+ .pill{display:inline-flex;align-items:center;gap:5px;padding:3px 10px;border-radius:20px;font-size:12px;font-weight:600;border:1px solid var(--line2);background:var(--panel2);color:var(--muted)}
432
+ .pill.ok{color:var(--ok);background:var(--ok-soft);border-color:transparent}
433
+ .pill.bad{color:var(--bad);background:var(--bad-soft);border-color:transparent}
434
+ .pill.run{color:var(--run);background:var(--run-soft);border-color:transparent}
435
+ .pill.warn{color:var(--warn);background:var(--warn-soft);border-color:transparent}
436
+ .pill .dot{width:7px;height:7px;border-radius:50%;background:currentColor}
437
+ .pill.run .dot{animation:blink 1s infinite}
438
+ @keyframes blink{50%{opacity:.3}}
439
+ .agents{display:flex;flex-direction:column;gap:8px}
440
+ .agent-row{display:flex;align-items:center;gap:10px;padding:9px 12px;border:1px solid var(--line);border-radius:10px;background:var(--panel2)}
441
+ .agent-row .nm{font-weight:600}
442
+ .agent-row .st{color:var(--muted);font-size:12px;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
443
+ .agent-row .tm{font-variant-numeric:tabular-nums;color:var(--brand);font-weight:700}
444
+ .events{max-height:360px;overflow:auto;display:flex;flex-direction:column;gap:2px}
445
+ .ev{padding:6px 8px;border-radius:8px;font-size:12.5px;display:flex;gap:8px;align-items:baseline;flex-wrap:wrap}
446
+ .ev:hover{background:var(--panel2)}
447
+ .ev .tm{color:var(--muted);font-variant-numeric:tabular-nums;font-size:11px}
448
+ .ev .stp{font-weight:700;color:var(--brand)}
449
+ .ev .ag{color:var(--warn);font-weight:600}
450
+ .ev .msg{color:var(--text)}
451
+ .ev .snip{flex-basis:100%;color:var(--muted);margin:2px 0 0 0;padding-left:8px;border-left:2px solid var(--line2)}
452
+ .ev-div{display:flex;align-items:center;gap:10px;margin:8px 2px;color:var(--brand);font-weight:700;font-size:12px}
453
+ .ev-div:before,.ev-div:after{content:"";flex:1;height:2px;background:linear-gradient(90deg,transparent,var(--brand-soft),var(--brand))}
454
+ .ev-div:after{background:linear-gradient(90deg,var(--brand),var(--brand-soft),transparent)}
455
+ .logbtn{background:var(--brand-soft);color:var(--brand);border:1px solid var(--brand);border-radius:7px;padding:2px 9px;font-size:11.5px;font-weight:700;cursor:pointer}
456
+ .logbtn:hover{background:var(--brand);color:#fff}
457
+ pre{white-space:pre-wrap;background:var(--panel2);padding:12px;border-radius:10px;max-height:380px;overflow:auto;font-size:12.5px;font-family:ui-monospace,Consolas,monospace;margin:0}
458
+ .viewer-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}
459
+ input,select,textarea{width:100%;background:var(--panel);border:1px solid var(--line2);color:var(--text);border-radius:9px;padding:9px;font-size:13px;font-family:inherit}
460
+ input:disabled,select:disabled,textarea:disabled{background:var(--panel2);color:var(--muted)}
461
+ textarea{min-height:150px;font-family:ui-monospace,Consolas,monospace}
462
+ label{display:block;margin:11px 0 5px;color:var(--muted);font-size:12px;font-weight:600}
463
+ .row{display:grid;grid-template-columns:1fr 1fr;gap:14px}
464
+ .banner{padding:10px 12px;border-radius:10px;background:var(--warn-soft);color:var(--warn);font-weight:600;margin-bottom:12px}
465
+ .modal{position:fixed;inset:0;background:rgba(20,30,60,.45);display:none;align-items:center;justify-content:center;z-index:40}
466
+ .modal.show{display:flex}
467
+ .modal .box{background:var(--panel);border-radius:16px;padding:22px;width:580px;max-height:92vh;overflow:auto;box-shadow:0 20px 60px rgba(20,30,60,.25)}
468
+ .modal .box.wide{width:760px}
469
+ .modal h3{margin:0 0 6px}
470
+ .muted{color:var(--muted)}
471
+ .hint{font-size:12px;color:var(--muted);margin-top:7px}
472
+ .help{display:inline-flex;align-items:center;justify-content:center;width:15px;height:15px;border-radius:50%;background:var(--panel2);border:1px solid var(--line2);color:var(--muted);font-size:10px;font-weight:700;cursor:help;margin-left:5px;vertical-align:middle}
473
+ .help:hover{background:var(--brand);color:#fff;border-color:var(--brand)}
474
+ .tip{position:fixed;z-index:90;max-width:340px;background:#1f2733;color:#fff;padding:8px 11px;border-radius:8px;font-size:12px;line-height:1.5;box-shadow:0 8px 24px rgba(0,0,0,.25);display:none;pointer-events:none}
475
+ .tmpl-tabs{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:10px}
476
+ .tmpl-tabs span{padding:5px 10px;border:1px solid var(--line2);border-radius:8px;cursor:pointer;font-size:12px;font-weight:600;color:var(--muted)}
477
+ .tmpl-tabs span.active{border-color:var(--brand);color:var(--brand);background:var(--brand-soft)}
478
+ .tmpl-tabs span.bad{border-color:var(--bad);color:var(--bad)}
479
+ .tmpl-tabs span.inactive{opacity:.4}
480
+ .linklike{color:var(--brand);cursor:pointer;text-decoration:underline;font-size:12px}
481
+ .tmplnote{font-size:12px;color:var(--warn);margin-top:6px;display:none}
482
+ .chips{display:flex;flex-wrap:wrap;gap:6px;margin:6px 0}
483
+ .chip{font-family:ui-monospace,Consolas,monospace;font-size:11px;padding:2px 7px;border-radius:6px;background:var(--panel2);border:1px solid var(--line2);color:var(--text)}
484
+ .chip.miss{background:var(--bad-soft);border-color:transparent;color:var(--bad)}
485
+ .steptable{border:1px solid var(--line);border-radius:10px;overflow:hidden;margin-top:6px}
486
+ .grouphdr{background:var(--panel2);padding:7px 12px;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:var(--muted);border-bottom:1px solid var(--line)}
487
+ .step-row{display:flex;align-items:center;gap:10px;padding:9px 12px;border-bottom:1px solid var(--line)}
488
+ .step-row:last-child{border-bottom:0}
489
+ .step-row input{width:auto;margin:0}
490
+ .step-row .sa{font-weight:700;min-width:148px}
491
+ .step-row .sd{color:var(--muted);font-size:12px;flex:1}
492
+ .step-row .stmpl{font-family:ui-monospace,Consolas,monospace;font-size:11px;color:var(--brand);background:var(--brand-soft);padding:2px 7px;border-radius:6px;white-space:nowrap}
493
+ .step-row.req{background:var(--panel2)}
494
+ .step-row.req .sa{color:var(--muted)}
495
+ .step-row.off{opacity:.45}
496
+ .lockicon{font-size:11px;color:var(--muted)}
497
+ .helpsec{margin:14px 0}
498
+ .helpsec h4{margin:0 0 6px;font-size:14px;color:var(--brand)}
499
+ .helpsec p{margin:4px 0;color:var(--text);font-size:13px;line-height:1.6}
500
+ .helpsec table{margin-top:8px}
501
+ .helpsec td,.helpsec th{font-size:12.5px}
502
+ </style>
503
+ </head>
504
+ <body>
505
+ <header>
506
+ <div class="brand">Auto<b>Dev</b>Loop</div>
507
+ <div class="sub" id="brandSub"></div>
508
+ <div class="spacer"></div>
509
+ <button class="iconbtn" id="helpBtn" data-tip="" onclick="showHelp()">? <span id="helpLbl"></span></button>
510
+ <div class="langwrap">
511
+ <button class="iconbtn" id="langBtn" onclick="toggleLang(event)">🌐 <span id="langCur"></span> ▾</button>
512
+ <div class="menu" id="langMenu">
513
+ <div data-l="en" onclick="setLang('en')">English</div>
514
+ <div data-l="zh" onclick="setLang('zh')">简体中文</div>
515
+ <div data-l="ja" onclick="setLang('ja')">日本語</div>
516
+ </div>
517
+ </div>
518
+ <button onclick="openNew()" id="newBtn">+ New</button>
519
+ </header>
520
+ <div class="layout">
521
+ <div class="sidebar">
522
+ <div class="h" id="sideProjects">Projects</div>
523
+ <div id="projects"></div>
524
+ </div>
525
+ <div class="main" id="main"></div>
526
+ </div>
527
+
528
+ <div class="modal" id="newModal">
529
+ <div class="box">
530
+ <h3 id="mTitle"></h3>
531
+ <div class="muted" id="mDesc" style="font-size:12px;margin-bottom:8px"></div>
532
+ <label id="lDir"></label><input id="f_dir" placeholder="E:\path\to\my-app">
533
+ <label id="lName"></label><input id="f_name">
534
+ <label id="lGoal"></label><textarea id="f_goal"></textarea>
535
+ <div class="row">
536
+ <div><label id="lVer"></label><input id="f_versions" type="number" value="6" min="1"></div>
537
+ <div><label id="lMode"></label><select id="f_mode"><option value="advanced">advanced</option><option value="simple">simple</option></select></div>
538
+ </div>
539
+ <div class="row">
540
+ <div><label id="lProv"></label><select id="f_provider"><option>claude</option><option>codex</option><option>gemini</option></select></div>
541
+ <div><label id="lPcmd"></label><input id="f_pcmd" placeholder="claude"></div>
542
+ </div>
543
+ <label id="lArch"></label><input id="f_hint">
544
+ <div class="hint" id="mTip"></div>
545
+ <div style="margin-top:18px;display:flex;gap:8px;justify-content:flex-end">
546
+ <button class="sec" onclick="closeNew()" id="bCancel"></button>
547
+ <button onclick="createProject()" id="bCreate"></button>
548
+ </div>
549
+ </div>
550
+ </div>
551
+
552
+ <div class="modal" id="infoModal">
553
+ <div class="box" id="infoBox">
554
+ <h3 id="infoTitle"></h3>
555
+ <div id="infoBody"></div>
556
+ <div style="margin-top:18px;display:flex;justify-content:flex-end">
557
+ <button onclick="closeInfo()" id="infoClose"></button>
558
+ </div>
559
+ </div>
560
+ </div>
561
+
562
+ <div class="tip" id="tip"></div>
563
+
564
+ <script>
565
+ const I18N = {
566
+ en:{brandSub:"autonomous AI iteration · local dashboard",projects:"Projects",noProjects:"No projects yet.",
567
+ selectHint:"Select a project, or create a new one.",newBtn:"+ New project",helpLbl:"Help",close:"Close",
568
+ tabDash:"Dashboard",tabVer:"Versions",tabDocs:"Docs",tabSet:"Settings",
569
+ run:"▶ Run",stopG:"Stop (graceful)",stopI:"Stop (discard)",
570
+ status:"Status",version:"Version",phase:"Phase",goalProg:"Goal progress",calls:"Agent calls",tokens:"Tokens",runtime:"Run time",
571
+ activeAgents:"Running now",noActive:"No agent running.",liveProgress:"Activity log",noEvents:"No activity yet.",
572
+ outputViewer:"Agent output",outputHint:"Click an output button in the log to view an agent's full output here.",
573
+ cVer:"Version",cPhase:"Phase",cScore:"Score",cTests:"Tests",cGoal:"Goal",cSummary:"Summary",cNew:"What's new",pass:"pass",fail:"fail",
574
+ backlog:"Accepted feature backlog",backlogEmpty:"Empty (fills after the goal is met).",
575
+ featuresTitle:"FEATURES.md (overview table)",changelogTitle:"CHANGELOG.md",
576
+ setTitle:"Run configuration",mode:"Mode",maxVer:"Max versions (default)",provider:"Provider",providerCmd:"Provider command",
577
+ model:"Model (optional)",reviewTh:"Review threshold",valueTh:"Value threshold (feature gate)",fixRetries:"Fix retries",
578
+ maxPar:"Max parallel agents",retries:"Provider call retries",testCmd:"Test command override",gitVer:"Git versioning",
579
+ steps:"Pipeline steps",stepsHint:"Each step is an agent with an editable prompt template. Required agents are always on.",
580
+ grpReq:"Required agents (always on)",grpOpt:"Optional steps",reqd:"required",
581
+ promptTpl:"Prompt templates",promptHint:"Edit the wording in any language. Keep the {{placeholders}} and JSON field names listed below — they are how the engine injects context and reads the reply.",
582
+ reqTokens:"Must keep these tokens:",reqOk:"format OK ✓",reqBad:"missing required tokens",
583
+ save:"Save settings",saved:"saved ✓",locked:"A run is active. Settings are read-only until it stops. Saved settings take effect on the next run.",
584
+ mTitle:"Create a project",mDesc:"This only creates the project. Edit settings first, then press Run on the dashboard.",
585
+ lDir:"Project directory (absolute path)",lName:"Project name",lGoal:"Goal / requirement",lVer:"Max versions",lMode:"Mode",
586
+ lProv:"Provider",lPcmd:"Provider command (optional)",lArch:"Architecture hint (optional)",
587
+ mTip:"The provider CLI must already be installed and authenticated locally. No API key is entered here.",
588
+ cancel:"Cancel",create:"Create",on:"on",off:"off",confirmImmediate:"Discard the current unfinished version and roll back to the last completed version?",
589
+ st_running:"running",st_completed:"completed",st_failed:"failed",st_stopped:"stopped",st_initialized:"not started",st_unknown:"unknown",
590
+ stopGT:"Graceful stop requested",stopGB:"The current version will finish, then the run stops — nothing is discarded. Any settings you saved apply to the next run.",
591
+ stopIT:"Stopped immediately",stopIB1:"The unfinished version was discarded and current/ was rolled back to the last completed version (v{n}).",
592
+ stopIB0:"The run was stopped and the unfinished version discarded. There was no earlier completed version to roll back to, so current/ was reset.",
593
+ stopFB:"This server wasn't tracking the process (it may have been restarted), so a graceful stop was requested instead.",
594
+ reqMissTitle:"Prompt format incomplete",reqMissBody:"These templates are missing tokens the engine needs. Add them back, then save again:",
595
+ helpTitle:"AutoDevLoop — Help",
596
+ tip_help:"Open the help guide: what every setting, agent and button does.",tip_lang:"Change the interface language.",
597
+ tip_mode:"simple = a lean loop (plan → develop → test → review) that saves tokens. advanced = adds an architect, goal check, an AI test planner, docs, feature scouting and the value gate.",
598
+ tip_maxVer:"How many versions to build before stopping. The loop keeps going to this number even after the goal is met — it then adds extra useful features.",
599
+ tip_provider:"Which coding-agent CLI to drive: claude, codex or gemini. It must already be installed and logged in on this machine.",
600
+ tip_providerCmd:"Override the command that gets run (a full path or a wrapper). Leave blank to use the provider's default command.",
601
+ tip_model:"Optional model name/alias passed to the CLI. Leave blank to use the CLI's own default model.",
602
+ tip_reviewTh:"If a version's review score (0-100) is below this, a fix pass runs before the version is accepted.",
603
+ tip_valueTh:"In the expand phase, a proposed feature is only accepted when its value score (0-100) reaches this number.",
604
+ tip_fixRetries:"Maximum fix → re-test rounds when tests fail or the score is too low.",
605
+ tip_maxPar:"How many development agents may run at the same time when the planner splits work across files.",
606
+ tip_retries:"Automatic retries when a provider call fails transiently (network / timeout).",
607
+ tip_testCmd:"Force one specific test/build command for every version (e.g. npm test). Leave blank to auto-detect or let the AI choose.",
608
+ tip_gitVer:"When on, each version is committed and tagged inside current/. Falls back to folder snapshots if git is unavailable.",
609
+ tip_run:"Start the loop with the saved settings. Disabled while a run is active.",
610
+ tip_stopG:"Graceful: let the current version finish, then stop. Nothing is discarded.",
611
+ tip_stopI:"Discard: kill the agent now, throw away the unfinished version, and roll current/ back to the last completed version.",
612
+ tip_plan:"Decides what this version delivers and how many dev agents to spawn.",
613
+ tip_dev:"Writes the code for the version (one or more agents working in parallel).",
614
+ tip_test:"Runs the tests / build for the version and reports pass or fail.",
615
+ tip_review:"Scores quality, flags blockers, judges goal completeness and writes the 'what's new' summary.",
616
+ tip_fix:"Repairs the version when tests fail or the score is below the review threshold.",
617
+ tip_arch:"One-time architect: picks the stack, layout and test strategy at the very start of the project.",
618
+ tip_goal_check:"A second, independent agent that confirms whether the original goal is genuinely met.",
619
+ tip_test_agent:"Let an AI choose the test commands (advanced). Off = built-in test detection only, no extra call.",
620
+ tip_doc:"Keeps README / design docs accurate on every version.",
621
+ tip_scout:"After the goal is met, proposes genuinely valuable new features to add next.",
622
+ tip_evaluate:"Independently scores the scouted features; only high-value ones enter the backlog.",
623
+ tip_features_doc:"Writes the FEATURES.md overview table (no AI call).",
624
+ h_modes:"Modes",hb_modes:"simple keeps a lean, cheap loop (plan → develop → test → review). advanced turns on the architect, a separate goal check, an AI test planner, a docs agent, plus feature scouting and the value gate after your goal is reached. You can also toggle individual optional steps below the mode selector.",
625
+ h_pipe:"Pipeline & agents",hb_pipe:"Every version runs a fixed sequence of agents. Required agents always run; optional ones are toggled in Settings. Each agent is driven by an editable prompt template of the same name.",
626
+ h_set:"Settings glossary",hb_set:"Hover the ? next to any field for a one-line explanation. Settings are locked while a run is active and take effect on the next run.",
627
+ h_stop:"Stopping a run",hb_stop:"Graceful stop lets the current version finish and keeps it. Discard stop kills the agent immediately, throws away the half-built version, and rolls the working copy back to the last completed version.",
628
+ h_score:"Scores & goal progress",hb_score:"Score (0-100) is the reviewer's quality rating of a version. Goal progress (0-100%) is how much of your ORIGINAL request is done. Once goal progress reaches 100% / goal is met, the run switches from the build phase to the expand phase and starts proposing extra features.",
629
+ h_sec:"Safety",hb_sec:"AutoDevLoop runs AI-generated code and shell test commands on your machine, unattended. Use a dedicated folder or VM, and keep this dashboard on localhost only — it has no login.",
630
+ h_ph:"Prompt placeholders",hb_ph:"A prompt template is the instruction sent to an agent. Before sending, the engine replaces each {{placeholder}} with real data, and afterwards reads specific JSON fields back. Keep every placeholder its template requires; put it on its own line, introduced by a short label so the model knows what follows. Below: what each one is, which templates use it, and a suggested way to introduce it.",
631
+ colAgent:"Agent",colKind:"Kind",colTmpl:"Prompt",colWhat:"What it does",colPh:"Placeholder",colUsed:"Used in",colEx:"Suggested phrasing",
632
+ tmplInactive:"This template's step is turned off in the current mode/config, so it is not used right now and is read-only. Enable its step (or switch to advanced) to edit it.",
633
+ phHelpLink:"Not sure what a placeholder means? See the Help guide →"},
634
+
635
+ zh:{brandSub:"自治 AI 迭代 · 本地面板",projects:"项目",noProjects:"还没有项目。",
636
+ selectHint:"选择一个项目,或新建一个。",newBtn:"+ 新建项目",helpLbl:"帮助",close:"关闭",
637
+ tabDash:"总览",tabVer:"版本",tabDocs:"文档",tabSet:"设置",
638
+ run:"▶ 运行",stopG:"停止(优雅)",stopI:"停止(废弃本版)",
639
+ status:"状态",version:"版本",phase:"阶段",goalProg:"目标进度",calls:"Agent 调用",tokens:"Tokens",runtime:"运行时长",
640
+ activeAgents:"正在运行",noActive:"当前没有 agent 在运行。",liveProgress:"活动日志",noEvents:"暂无活动。",
641
+ outputViewer:"Agent 输出",outputHint:"点击日志中的「输出」按钮,可在此查看该 agent 的完整输出。",
642
+ cVer:"版本",cPhase:"阶段",cScore:"评分",cTests:"测试",cGoal:"目标",cSummary:"摘要",cNew:"新增/变化",pass:"通过",fail:"失败",
643
+ backlog:"已接受的功能待办",backlogEmpty:"空(目标达成后才会填充)。",
644
+ featuresTitle:"FEATURES.md(一览表)",changelogTitle:"CHANGELOG.md",
645
+ setTitle:"运行配置",mode:"模式",maxVer:"最大版本数(默认)",provider:"Provider",providerCmd:"Provider 命令",
646
+ model:"模型(可选)",reviewTh:"评审阈值",valueTh:"价值阈值(功能闸门)",fixRetries:"修复重试次数",
647
+ maxPar:"最大并行 agent 数",retries:"Provider 调用重试",testCmd:"测试命令覆盖",gitVer:"Git 版本管理",
648
+ steps:"流程步骤",stepsHint:"每个步骤都是一个 agent,对应一个可编辑的 prompt 模板。必需的 agent 始终开启。",
649
+ grpReq:"必需 agent(始终开启)",grpOpt:"可选步骤",reqd:"必需",
650
+ promptTpl:"Prompt 模板",promptHint:"可用任意语言修改文字,但请保留 {{占位符}} 和下方列出的 JSON 字段名——引擎靠它们注入上下文并解析回复。",
651
+ reqTokens:"必须保留这些标记:",reqOk:"格式正确 ✓",reqBad:"缺少必需标记",
652
+ save:"保存设置",saved:"已保存 ✓",locked:"项目运行中,设置为只读,停止后方可修改。保存的设置在下次运行时生效。",
653
+ mTitle:"新建项目",mDesc:"这里只会创建项目,不会立即运行。先去设置里调整参数,再到总览页点击「运行」。",
654
+ lDir:"项目目录(绝对路径)",lName:"项目名称",lGoal:"目标 / 需求",lVer:"最大版本数",lMode:"模式",
655
+ lProv:"Provider",lPcmd:"Provider 命令(可选)",lArch:"架构提示(可选)",
656
+ mTip:"所选 provider 的 CLI 必须已在本地安装并登录。此处不需要填写任何 API key。",
657
+ cancel:"取消",create:"创建",on:"开",off:"关",confirmImmediate:"废弃当前未完成的版本,并回退到上一个已完成的版本?",
658
+ st_running:"运行中",st_completed:"已完成",st_failed:"失败",st_stopped:"已停止",st_initialized:"未开始",st_unknown:"未知",
659
+ stopGT:"已请求优雅停止",stopGB:"当前版本会先跑完,然后停止——不会废弃任何内容。你保存的设置将在下次运行时生效。",
660
+ stopIT:"已立即停止",stopIB1:"未完成的版本已被废弃,current/ 已回退到上一个已完成版本(v{n})。",
661
+ stopIB0:"运行已停止,未完成的版本已废弃。没有更早的已完成版本可回退,current/ 已被重置。",
662
+ stopFB:"本服务没有在跟踪该进程(可能服务重启过),因此改为请求优雅停止。",
663
+ reqMissTitle:"Prompt 格式不完整",reqMissBody:"以下模板缺少引擎需要的标记。请补回这些标记后再保存:",
664
+ helpTitle:"AutoDevLoop — 帮助",
665
+ tip_help:"打开帮助:解释每个设置、agent 和按钮的作用。",tip_lang:"切换界面语言。",
666
+ tip_mode:"simple = 精简循环(计划 → 开发 → 测试 → 评审),省 token。advanced = 额外加入架构师、目标检查、AI 测试规划、文档、功能发掘和价值闸门。",
667
+ tip_maxVer:"停止前要构建多少个版本。即使目标已达成,循环也会一直跑到这个数——之后会继续添加有用的周边功能。",
668
+ tip_provider:"驱动哪个编码 agent 的 CLI:claude、codex 或 gemini。它必须已在本机安装并登录。",
669
+ tip_providerCmd:"覆盖实际运行的命令(完整路径或包装命令)。留空则使用该 provider 的默认命令。",
670
+ tip_model:"可选,传给 CLI 的模型名/别名。留空则使用 CLI 自己的默认模型。",
671
+ tip_reviewTh:"某版本的评审分(0-100)低于此值时,会先跑一轮修复再接受该版本。",
672
+ tip_valueTh:"扩展阶段,提议的功能只有价值分(0-100)达到此值才会被接受。",
673
+ tip_fixRetries:"测试失败或评分过低时,最多进行多少轮「修复 → 重测」。",
674
+ tip_maxPar:"当计划把工作拆给多个开发 agent 时,最多允许多少个同时运行。",
675
+ tip_retries:"provider 调用因网络/超时等瞬时失败时,自动重试的次数。",
676
+ tip_testCmd:"为每个版本强制使用某条测试/构建命令(如 npm test)。留空则自动检测或由 AI 决定。",
677
+ tip_gitVer:"开启后,每个版本都会在 current/ 内 commit 并打 tag。没有 git 时回退为文件夹快照。",
678
+ tip_run:"用已保存的设置启动循环。运行中时禁用。",
679
+ tip_stopG:"优雅:让当前版本跑完再停,不废弃任何内容。",
680
+ tip_stopI:"废弃:立即终止 agent,丢弃未完成的版本,并把 current/ 回退到上一个已完成版本。",
681
+ tip_plan:"决定本版本要交付什么,以及开几个开发 agent。",
682
+ tip_dev:"编写本版本的代码(一个或多个 agent 并行)。",
683
+ tip_test:"运行本版本的测试/构建,并报告通过或失败。",
684
+ tip_review:"打分、标记阻断问题、判断目标完成度,并写出「本版新增」摘要。",
685
+ tip_fix:"当测试失败或评分低于评审阈值时,修复该版本。",
686
+ tip_arch:"一次性架构师:在项目最开始选定技术栈、目录结构和测试策略。",
687
+ tip_goal_check:"第二个独立 agent,确认原始目标是否真正达成。",
688
+ tip_test_agent:"由 AI 选择测试命令(高级)。关闭则只用内置测试检测,不额外调用。",
689
+ tip_doc:"每个版本维护 README / 设计文档的准确性。",
690
+ tip_scout:"目标达成后,发掘真正有价值的、可继续添加的新功能。",
691
+ tip_evaluate:"独立给发掘到的功能打分;只有高价值的才进入待办清单。",
692
+ tip_features_doc:"生成 FEATURES.md 一览表(不调用 AI)。",
693
+ h_modes:"模式",hb_modes:"simple 保持精简省钱的循环(计划 → 开发 → 测试 → 评审)。advanced 会开启架构师、独立的目标检查、AI 测试规划、文档 agent,以及目标达成后的功能发掘和价值闸门。你也可以在模式选择下方单独开关可选步骤。",
694
+ h_pipe:"流程与 agent",hb_pipe:"每个版本都会按固定顺序运行一串 agent。必需 agent 总会运行;可选的在设置里开关。每个 agent 由同名的、可编辑的 prompt 模板驱动。",
695
+ h_set:"设置词条",hb_set:"把鼠标悬停在字段旁的 ? 上即可看到一行解释。运行中设置为只读,保存的设置在下次运行时生效。",
696
+ h_stop:"停止运行",hb_stop:"优雅停止会让当前版本跑完并保留它。废弃停止会立即终止 agent,丢弃这个还没建完的版本,并把工作目录回退到上一个已完成版本。",
697
+ h_score:"评分与目标进度",hb_score:"评分(0-100)是评审对某版本质量的打分。目标进度(0-100%)是你最初需求完成了多少。一旦目标进度达到 100% / 目标达成,运行会从 build 阶段切到 expand 阶段,并开始提议额外功能。",
698
+ h_sec:"安全",hb_sec:"AutoDevLoop 会在你机器上无人值守地运行 AI 生成的代码和 shell 测试命令。请使用专门的文件夹或虚拟机,并让本面板只绑定 localhost——它没有登录鉴权。",
699
+ h_ph:"Prompt 占位符",hb_ph:"prompt 模板就是发给某个 agent 的指令。发送前,引擎会把每个 {{占位符}} 替换成真实数据;返回后,再从中读取特定的 JSON 字段。请保留该模板要求的所有占位符;把占位符单独放一行,前面加一句简短说明,让模型知道下面是什么内容。下表列出每个占位符的含义、用在哪些模板、以及建议的引导写法。",
700
+ colAgent:"Agent",colKind:"类型",colTmpl:"Prompt",colWhat:"作用",colPh:"占位符",colUsed:"用于模板",colEx:"建议写法",
701
+ tmplInactive:"该模板对应的步骤在当前模式/配置下是关闭的,所以现在用不到,处于只读状态。开启它对应的步骤(或切到 advanced 模式)即可编辑。",
702
+ phHelpLink:"不清楚占位符含义?查看帮助文档 →"},
703
+
704
+ ja:{brandSub:"自律型 AI 反復 · ローカルダッシュボード",projects:"プロジェクト",noProjects:"プロジェクトがありません。",
705
+ selectHint:"プロジェクトを選ぶか、新規作成してください。",newBtn:"+ 新規作成",helpLbl:"ヘルプ",close:"閉じる",
706
+ tabDash:"ダッシュボード",tabVer:"バージョン",tabDocs:"ドキュメント",tabSet:"設定",
707
+ run:"▶ 実行",stopG:"停止(安全)",stopI:"停止(破棄)",
708
+ status:"状態",version:"バージョン",phase:"フェーズ",goalProg:"目標達成度",calls:"エージェント呼出",tokens:"トークン",runtime:"実行時間",
709
+ activeAgents:"実行中",noActive:"実行中のエージェントはありません。",liveProgress:"アクティビティログ",noEvents:"まだアクティビティはありません。",
710
+ outputViewer:"エージェント出力",outputHint:"ログ内の「出力」ボタンをクリックすると、ここで全文を表示します。",
711
+ cVer:"バージョン",cPhase:"フェーズ",cScore:"スコア",cTests:"テスト",cGoal:"目標",cSummary:"概要",cNew:"変更点",pass:"成功",fail:"失敗",
712
+ backlog:"承認済み機能バックログ",backlogEmpty:"空(目標達成後に追加されます)。",
713
+ featuresTitle:"FEATURES.md(一覧表)",changelogTitle:"CHANGELOG.md",
714
+ setTitle:"実行設定",mode:"モード",maxVer:"最大バージョン数(既定)",provider:"プロバイダ",providerCmd:"プロバイダコマンド",
715
+ model:"モデル(任意)",reviewTh:"レビュー閾値",valueTh:"価値閾値(機能ゲート)",fixRetries:"修正リトライ回数",
716
+ maxPar:"最大並列エージェント数",retries:"プロバイダ呼出リトライ",testCmd:"テストコマンド上書き",gitVer:"Git バージョン管理",
717
+ steps:"パイプライン手順",stepsHint:"各手順は編集可能なプロンプトテンプレートを持つエージェントです。必須エージェントは常に有効です。",
718
+ grpReq:"必須エージェント(常時オン)",grpOpt:"任意の手順",reqd:"必須",
719
+ promptTpl:"プロンプトテンプレート",promptHint:"文言は任意の言語で編集できますが、{{プレースホルダ}} と下記の JSON フィールド名は残してください。エンジンがそれらでコンテキストを注入し、応答を解析します。",
720
+ reqTokens:"次のトークンは必須です:",reqOk:"形式OK ✓",reqBad:"必須トークンが不足",
721
+ save:"設定を保存",saved:"保存しました ✓",locked:"実行中のため設定は読み取り専用です。停止後に編集できます。保存した設定は次回実行時に反映されます。",
722
+ mTitle:"プロジェクト作成",mDesc:"ここでは作成のみ行い、すぐには実行しません。先に設定を調整し、ダッシュボードで「実行」を押してください。",
723
+ lDir:"プロジェクトディレクトリ(絶対パス)",lName:"プロジェクト名",lGoal:"目標 / 要件",lVer:"最大バージョン数",lMode:"モード",
724
+ lProv:"プロバイダ",lPcmd:"プロバイダコマンド(任意)",lArch:"アーキテクチャのヒント(任意)",
725
+ mTip:"選択したプロバイダの CLI は事前にインストール・認証済みである必要があります。API キーの入力は不要です。",
726
+ cancel:"キャンセル",create:"作成",on:"オン",off:"オフ",confirmImmediate:"未完成の現在のバージョンを破棄し、最後に完了したバージョンに戻しますか?",
727
+ st_running:"実行中",st_completed:"完了",st_failed:"失敗",st_stopped:"停止",st_initialized:"未開始",st_unknown:"不明",
728
+ stopGT:"安全な停止を要求しました",stopGB:"現在のバージョンが完了してから停止します。破棄はされません。保存した設定は次回実行時に反映されます。",
729
+ stopIT:"即座に停止しました",stopIB1:"未完成のバージョンは破棄され、current/ は最後に完了したバージョン(v{n})に戻されました。",
730
+ stopIB0:"実行を停止し、未完成のバージョンを破棄しました。戻せる完了済みバージョンが無かったため current/ はリセットされました。",
731
+ stopFB:"このサーバーはプロセスを追跡していなかった(再起動された可能性)ため、代わりに安全な停止を要求しました。",
732
+ reqMissTitle:"プロンプト形式が不完全です",reqMissBody:"次のテンプレートにエンジンが必要とするトークンがありません。追加してから保存してください:",
733
+ helpTitle:"AutoDevLoop — ヘルプ",
734
+ tip_help:"ヘルプを開く:各設定・エージェント・ボタンの役割を説明します。",tip_lang:"表示言語を変更します。",
735
+ tip_mode:"simple = 軽量ループ(計画 → 開発 → テスト → レビュー)でトークン節約。advanced = アーキテクト、目標チェック、AIテスト計画、ドキュメント、機能探索、価値ゲートを追加。",
736
+ tip_maxVer:"停止までに作るバージョン数。目標達成後もこの数まで続き、その後は有用な追加機能を作ります。",
737
+ tip_provider:"駆動する CLI:claude / codex / gemini。本機にインストール・ログイン済みである必要があります。",
738
+ tip_providerCmd:"実際に実行されるコマンドを上書き(フルパスやラッパー)。空ならプロバイダ既定のコマンド。",
739
+ tip_model:"任意。CLI に渡すモデル名/別名。空なら CLI 既定のモデル。",
740
+ tip_reviewTh:"バージョンのレビュースコア(0-100)がこれ未満なら、採用前に修正を行います。",
741
+ tip_valueTh:"拡張フェーズでは、提案機能の価値スコア(0-100)がこの値に達した場合のみ採用されます。",
742
+ tip_fixRetries:"テスト失敗やスコア不足時の「修正 → 再テスト」の最大回数。",
743
+ tip_maxPar:"計画が作業を分割した際に同時実行できる開発エージェントの数。",
744
+ tip_retries:"ネットワーク/タイムアウト等の一時的失敗時に自動リトライする回数。",
745
+ tip_testCmd:"全バージョンで使うテスト/ビルドコマンドを固定(例: npm test)。空なら自動検出かAIが選択。",
746
+ tip_gitVer:"オンにすると各バージョンを current/ 内で commit してタグ付け。git が無ければフォルダスナップショットに退避。",
747
+ tip_run:"保存済み設定でループを開始。実行中は無効。",
748
+ tip_stopG:"安全:現在のバージョンを完了させてから停止。破棄しません。",
749
+ tip_stopI:"破棄:エージェントを即停止し、未完成バージョンを捨て、current/ を最後の完了版に戻します。",
750
+ tip_plan:"このバージョンで何を提供するか、開発エージェントを何体にするかを決定。",
751
+ tip_dev:"バージョンのコードを記述(複数体が並列で動作)。",
752
+ tip_test:"バージョンのテスト/ビルドを実行し、成功か失敗かを報告。",
753
+ tip_review:"品質を採点し、阻害要因を指摘、目標達成度を判定、変更点の要約を作成。",
754
+ tip_fix:"テスト失敗やスコアが閾値未満のとき、バージョンを修復。",
755
+ tip_arch:"一度きりのアーキテクト:開始時にスタック・構成・テスト戦略を決定。",
756
+ tip_goal_check:"元の目標が本当に達成されたかを確認する独立エージェント。",
757
+ tip_test_agent:"AI にテストコマンドを選ばせる(advanced)。オフなら組み込み検出のみ。",
758
+ tip_doc:"各バージョンで README / 設計文書を正確に保つ。",
759
+ tip_scout:"目標達成後、本当に価値ある新機能を提案。",
760
+ tip_evaluate:"探索した機能を独立して採点。高価値のものだけバックログ入り。",
761
+ tip_features_doc:"FEATURES.md 一覧表を作成(AI呼出なし)。",
762
+ h_modes:"モード",hb_modes:"simple は軽量・低コストなループ(計画 → 開発 → テスト → レビュー)。advanced はアーキテクト、独立した目標チェック、AIテスト計画、ドキュメント、目標達成後の機能探索と価値ゲートを有効化します。モード選択の下で各任意手順を個別に切替もできます。",
763
+ h_pipe:"パイプラインとエージェント",hb_pipe:"各バージョンは固定順のエージェント列を実行します。必須は常に実行、任意は設定で切替。各エージェントは同名の編集可能なプロンプトで駆動します。",
764
+ h_set:"設定用語",hb_set:"各項目の ? にカーソルを合わせると一行説明が出ます。実行中は読み取り専用、保存内容は次回実行時に反映。",
765
+ h_stop:"実行の停止",hb_stop:"安全停止は現在のバージョンを完了させて保持します。破棄停止はエージェントを即停止し、作りかけのバージョンを捨て、作業コピーを最後の完了版に戻します。",
766
+ h_score:"スコアと目標達成度",hb_score:"スコア(0-100)はレビューによる品質評価。目標達成度(0-100%)は元の要件の達成割合。100%/目標達成になると build フェーズから expand フェーズへ移り、追加機能の提案を始めます。",
767
+ h_sec:"安全性",hb_sec:"AutoDevLoop は AI 生成コードと shell テストを無人で実行します。専用フォルダや VM を使い、本ダッシュボードは localhost のみに——ログイン認証はありません。",
768
+ h_ph:"プロンプトのプレースホルダ",hb_ph:"プロンプトテンプレートはエージェントへ送る指示です。送信前にエンジンが各 {{プレースホルダ}} を実データに置換し、返答から特定の JSON フィールドを読み取ります。テンプレートが要求するプレースホルダは必ず残し、独立した行に置き、短いラベルで何が続くか示してください。下表に各プレースホルダの意味・使用テンプレート・推奨の導入文を示します。",
769
+ colAgent:"エージェント",colKind:"種別",colTmpl:"プロンプト",colWhat:"役割",colPh:"プレースホルダ",colUsed:"使用テンプレート",colEx:"推奨の書き方",
770
+ tmplInactive:"このテンプレートの手順は現在のモード/設定でオフのため、今は使われず読み取り専用です。対応手順を有効化(または advanced に切替)すると編集できます。",
771
+ phHelpLink:"プレースホルダの意味が不明ですか?ヘルプを参照 →"}
772
+ };
773
+ const LANG_LABEL={en:"EN",zh:"中文",ja:"日本語"};
774
+ const REQUIRED_STEPS=[{key:"plan",agent:"AgentPLAN",tmpl:"plan"},{key:"dev",agent:"AgentDEV",tmpl:"dev"},
775
+ {key:"test",agent:"AgentTEST",tmpl:"test"},{key:"review",agent:"AgentREVIEW",tmpl:"review"},{key:"fix",agent:"AgentFIX",tmpl:"fix"}];
776
+ const OPTIONAL_STEPS=[{key:"arch",agent:"AgentARCH",tmpl:"arch"},{key:"goal_check",agent:"AgentGOALCHECK",tmpl:"goal_check"},
777
+ {key:"test_agent",agent:"AgentTEST",tmpl:"test"},{key:"doc",agent:"AgentDOC",tmpl:"doc"},{key:"scout",agent:"AgentSCOUT",tmpl:"scout"},
778
+ {key:"evaluate",agent:"AgentEVALUATE",tmpl:"evaluate"},{key:"features_doc",agent:"—",tmpl:"—"}];
779
+ // Mirror config.py: which optional steps each mode runs by default.
780
+ const SIMPLE_STEPS={arch:true,goal_check:false,test_agent:false,doc:false,scout:false,evaluate:false,features_doc:true};
781
+ const ADVANCED_STEPS={arch:true,goal_check:true,test_agent:true,doc:true,scout:true,evaluate:true,features_doc:true};
782
+ // Which optional step gates each prompt template (null = always used / required).
783
+ const TMPL_STEP={arch:"arch",plan:null,dev:null,doc:"doc",test:"test_agent",review:null,fix:null,scout:"scout",evaluate:"evaluate",goal_check:"goal_check"};
784
+ // Per-placeholder docs for the Help guide: where used + meaning + a suggested
785
+ // lead-in phrase, in each language. [desc, example phrasing].
786
+ const PH={
787
+ goal:{used:"all",en:["The user's overall goal/requirement for the whole project; present in almost every template.","User goal:"],zh:["用户对整个项目的总目标/需求,几乎每个模板都会用到。","用户目标:"],ja:["プロジェクト全体の目標/要件。ほぼ全テンプレートで使用。","ユーザー目標:"]},
788
+ arch_hint:{used:"arch",en:["Optional architecture hint the user typed when creating the project (may be empty). Only AgentARCH sees it.","Extra architecture hints from the user (may be empty):"],zh:["创建项目时用户填写的可选架构提示(可能为空)。仅 AgentARCH 用到。","用户提供的额外架构提示(可能为空):"],ja:["作成時に入力した任意のアーキテクチャヒント(空の場合あり)。AgentARCH のみ使用。","ユーザーからの追加アーキテクチャヒント(空の場合あり):"]},
789
+ version:{used:"plan, dev, doc, test, review, fix, scout",en:["The version number currently being built (1, 2, 3…).","for version v{{version}}"],zh:["当前正在构建的版本号(1、2、3……)。","针对版本 v{{version}}"],ja:["現在構築中のバージョン番号(1, 2, 3…)。","バージョン v{{version}} 向け"]},
790
+ phase:{used:"plan, review",en:["Which phase the loop is in: build (driving at the goal) or expand (adding features after it is met).","Phase: {{phase}}"],zh:["循环所处阶段:build(冲目标)或 expand(达成后加功能)。","当前阶段:{{phase}}"],ja:["ループの段階:build(目標へ)か expand(達成後の機能追加)。","フェーズ:{{phase}}"]},
791
+ architecture:{used:"plan, dev",en:["The architecture report AgentARCH produced (stack, layout, test strategy). Stay consistent with it.","Architecture contract (stay consistent with this):"],zh:["AgentARCH 产出的架构报告(技术栈、目录、测试策略)。请与之保持一致。","架构约定(请与此保持一致):"],ja:["AgentARCH 作成のアーキテクチャ報告(スタック・構成・テスト戦略)。これに従う。","アーキテクチャ規約(これに従う):"]},
792
+ phase_guidance:{used:"plan",en:["Auto-generated guidance telling the planner whether to complete the goal (build) or extend it (expand).","Guidance for this version:"],zh:["自动生成的指引,告诉计划 agent 是冲目标(build)还是扩展(expand)。","本版本指引:"],ja:["計画に build か expand かを伝える自動生成の指針。","本バージョンの指針:"]},
793
+ backlog:{used:"plan, scout",en:["The list of accepted future features (filled during the expand phase). Pick from here when extending.","Accepted feature backlog:"],zh:["已接受的待开发功能清单(扩展阶段填充)。扩展时从这里挑。","已接受的功能待办:"],ja:["承認済みの今後の機能リスト(拡張フェーズで蓄積)。拡張時はここから選ぶ。","承認済み機能バックログ:"]},
794
+ previous:{used:"plan",en:["A summary of the previous iteration: last review, last test result, recent version summaries.","Previous iteration context:"],zh:["上一轮迭代的摘要:上次评审、上次测试结果、最近版本摘要。","上一轮迭代上下文:"],ja:["前回反復の要約:前回レビュー・テスト結果・直近バージョン概要。","前回反復のコンテキスト:"]},
795
+ context:{used:"plan, test, review, scout, goal_check",en:["A snapshot of the current project's files/contents so the agent can see the real code.","Current project context:"],zh:["当前项目文件/内容的快照,让 agent 能看到真实代码。","当前项目上下文:"],ja:["現在のプロジェクトのファイル/内容のスナップショット。実コードを参照可能に。","現在のプロジェクトコンテキスト:"]},
796
+ agent_name:{used:"dev",en:["The name of this specific dev agent (the planner may spawn several).","You are {{agent_name}}"],zh:["当前这个开发 agent 的名字(计划可能会开多个)。","你是 {{agent_name}}"],ja:["この開発エージェントの名前(計画が複数生成する場合あり)。","あなたは {{agent_name}} です"]},
797
+ plan:{used:"dev, doc, test, review, fix",en:["The JSON plan for this version (version goal, acceptance criteria, dev agents, test focus).","Version plan:"],zh:["本版本的 JSON 计划(版本目标、验收标准、开发 agent、测试重点)。","版本计划:"],ja:["本バージョンの JSON 計画(目標・受入基準・開発エージェント・テスト重点)。","バージョン計画:"]},
798
+ task:{used:"dev",en:["This dev agent's specific assignment for the version.","Your specific task:"],zh:["该开发 agent 在本版本中的具体任务。","你的具体任务:"],ja:["この開発エージェントの具体的な担当。","あなたの具体的タスク:"]},
799
+ owns:{used:"dev",en:["The files/paths this agent should edit, to avoid clobbering peers.","Files you own (edit only these):"],zh:["该 agent 应编辑的文件/路径,避免覆盖其他 agent。","你负责的文件(只改这些):"],ja:["このエージェントが編集すべきファイル/パス(他者の上書き回避)。","担当ファイル(これのみ編集):"]},
800
+ candidates:{used:"test, evaluate",en:["In the test template: detected test/build commands. In evaluate: the scouted feature ideas to score.","Candidates:"],zh:["在 test 模板里:检测到的测试/构建命令;在 evaluate 里:待打分的功能点子。","候选项:"],ja:["test では検出したテスト/ビルドコマンド。evaluate では採点対象の機能案。","候補:"]},
801
+ test_result:{used:"review, fix",en:["The JSON result of the test run (success flag, commands, output).","Test result:"],zh:["测试运行的 JSON 结果(是否成功、命令、输出)。","测试结果:"],ja:["テスト実行の JSON 結果(成否・コマンド・出力)。","テスト結果:"]},
802
+ dev_summaries:{used:"review",en:["What each dev agent reported doing this version.","Development agent summaries:"],zh:["本版本各开发 agent 报告自己做了什么。","开发 agent 摘要:"],ja:["本バージョンで各開発エージェントが報告した作業内容。","開発エージェント概要:"]},
803
+ review:{used:"fix, scout, goal_check",en:["The reviewer's JSON verdict (score, issues, goal progress).","Review:"],zh:["评审的 JSON 结论(评分、问题、目标进度)。","评审结果:"],ja:["レビューの JSON 判定(スコア・問題・目標達成度)。","レビュー:"]},
804
+ attempt:{used:"fix",en:["Which fix attempt this is (1, 2, …).","fix attempt {{attempt}}"],zh:["这是第几次修复尝试(1、2……)。","第 {{attempt}} 次修复"],ja:["何回目の修正試行か(1, 2…)。","{{attempt}} 回目の修正"]},
805
+ threshold:{used:"evaluate",en:["The minimum value score a feature needs to be accepted by the gate.","A feature is accepted only when value >= {{threshold}}."],zh:["功能被闸门接受所需的最低价值分。","只有价值 >= {{threshold}} 的功能才被接受。"],ja:["機能がゲートに承認されるための最低価値スコア。","価値 >= {{threshold}} の機能のみ承認。"]}
806
+ };
807
+
808
+ let lang = localStorage.getItem('adl_lang') || 'en';
809
+ let current=null, tab='dashboard', projects=[], tmplActive=null, cfgCache=null;
810
+ let curEvents=[], renderedCount=0, selectedLog=null, lastProgress=null, dashBuilt=false;
811
+
812
+ function t(k){return (I18N[lang] && I18N[lang][k]) || (I18N.en[k]) || k;}
813
+ function h(s){return (s===null||s===undefined)?'':String(s).replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));}
814
+ async function api(p,o){const r=await fetch(p,o);const ct=r.headers.get('content-type')||'';return ct.includes('json')?r.json():r.text();}
815
+ function val(id){const e=document.getElementById(id);return e?e.value.trim():'';}
816
+ function statusLabel(s){return t('st_'+(s||'unknown'))||s;}
817
+ function fmtDur(sec){sec=Math.max(0,Math.floor(sec));const h=Math.floor(sec/3600),m=Math.floor((sec%3600)/60),s=sec%60;return (h?h+'h ':'')+(h||m?m+'m ':'')+s+'s';}
818
+ function helpDot(key){return '<span class="help" data-tip="'+h(t(key))+'">?</span>';}
819
+
820
+ // ---- floating tooltip ----
821
+ const tipEl=document.getElementById('tip');
822
+ function posTip(e){const pad=14;let x=e.clientX+pad,y=e.clientY+pad;const w=tipEl.offsetWidth,hh=tipEl.offsetHeight;
823
+ if(x+w>window.innerWidth-8)x=window.innerWidth-w-8;if(y+hh>window.innerHeight-8)y=e.clientY-hh-pad;
824
+ tipEl.style.left=Math.max(8,x)+'px';tipEl.style.top=Math.max(8,y)+'px';}
825
+ document.addEventListener('mouseover',e=>{const el=e.target.closest('[data-tip]');if(el&&el.getAttribute('data-tip')){tipEl.textContent=el.getAttribute('data-tip');tipEl.style.display='block';posTip(e);}});
826
+ document.addEventListener('mousemove',e=>{if(tipEl.style.display==='block'){const el=e.target.closest('[data-tip]');if(el)posTip(e);else tipEl.style.display='none';}});
827
+ document.addEventListener('mouseout',e=>{const el=e.target.closest('[data-tip]');if(el)tipEl.style.display='none';});
828
+
829
+ // ---- language menu ----
830
+ function toggleLang(e){e.stopPropagation();document.getElementById('langMenu').classList.toggle('show');}
831
+ document.addEventListener('click',()=>document.getElementById('langMenu').classList.remove('show'));
832
+ function setLang(l){lang=l;localStorage.setItem('adl_lang',l);document.getElementById('langMenu').classList.remove('show');applyStatic();dashBuilt=false;render();loadProjects();}
833
+ function applyStatic(){
834
+ document.getElementById('brandSub').textContent=t('brandSub');
835
+ document.getElementById('sideProjects').textContent=t('projects');
836
+ document.getElementById('newBtn').textContent=t('newBtn');
837
+ document.getElementById('helpLbl').textContent=t('helpLbl');
838
+ document.getElementById('helpBtn').setAttribute('data-tip',t('tip_help'));
839
+ document.getElementById('langBtn').setAttribute('data-tip',t('tip_lang'));
840
+ document.getElementById('langCur').textContent=LANG_LABEL[lang];
841
+ document.querySelectorAll('#langMenu div').forEach(d=>d.classList.toggle('sel',d.getAttribute('data-l')===lang));
842
+ }
843
+
844
+ // ---- generic info / help modal ----
845
+ function showInfo(title,bodyHtml,wide){
846
+ document.getElementById('infoTitle').textContent=title;
847
+ document.getElementById('infoBody').innerHTML=bodyHtml;
848
+ document.getElementById('infoClose').textContent=t('close');
849
+ document.getElementById('infoBox').classList.toggle('wide',!!wide);
850
+ document.getElementById('infoModal').classList.add('show');
851
+ }
852
+ function closeInfo(){document.getElementById('infoModal').classList.remove('show');}
853
+ function showHelp(){
854
+ const secs=[['h_modes','hb_modes'],['h_pipe','hb_pipe'],['h_set','hb_set'],['h_stop','hb_stop'],['h_score','hb_score'],['h_sec','hb_sec']];
855
+ let html='';
856
+ html+='<div class="helpsec"><h4>'+h(t('h_modes'))+'</h4><p>'+h(t('hb_modes'))+'</p></div>';
857
+ // pipeline section + auto table
858
+ let rows='';
859
+ REQUIRED_STEPS.forEach(s=>{rows+=pipeRow(s,true);});
860
+ OPTIONAL_STEPS.forEach(s=>{rows+=pipeRow(s,false);});
861
+ html+='<div class="helpsec"><h4>'+h(t('h_pipe'))+'</h4><p>'+h(t('hb_pipe'))+'</p>'
862
+ +'<table><thead><tr><th>'+h(t('colAgent'))+'</th><th>'+h(t('colKind'))+'</th><th>'+h(t('colTmpl'))+'</th><th>'+h(t('colWhat'))+'</th></tr></thead><tbody>'+rows+'</tbody></table></div>';
863
+ // prompt placeholders section
864
+ html+='<div class="helpsec" id="help_ph"><h4>'+h(t('h_ph'))+'</h4><p>'+h(t('hb_ph'))+'</p>'
865
+ +'<table><thead><tr><th>'+h(t('colPh'))+'</th><th>'+h(t('colUsed'))+'</th><th>'+h(t('colWhat'))+'</th><th>'+h(t('colEx'))+'</th></tr></thead><tbody>'+phRows()+'</tbody></table></div>';
866
+ secs.slice(2).forEach(s=>{html+='<div class="helpsec"><h4>'+h(t(s[0]))+'</h4><p>'+h(t(s[1]))+'</p></div>';});
867
+ showInfo(t('helpTitle'),html,true);
868
+ }
869
+ function phRows(){
870
+ return Object.keys(PH).map(k=>{const e=PH[k][lang]||PH[k].en;
871
+ return '<tr><td><code>{{'+h(k)+'}}</code></td><td class="muted">'+h(PH[k].used)+'</td><td>'+h(e[0])+'</td><td><code>'+h(e[1])+'</code></td></tr>';
872
+ }).join('');
873
+ }
874
+ function pipeRow(s,req){
875
+ const kind=req?('<span class="badge">'+h(t('reqd'))+'</span>'):('<span class="badge warn">'+h(t('grpOpt'))+'</span>');
876
+ return '<tr><td><b>'+h(s.agent)+'</b></td><td>'+kind+'</td><td><code>'+h(s.tmpl)+'</code></td><td>'+h(t('tip_'+s.key))+'</td></tr>';
877
+ }
878
+
879
+ function openNew(){
880
+ const m=document.getElementById('newModal');m.classList.add('show');
881
+ document.getElementById('mTitle').textContent=t('mTitle');
882
+ document.getElementById('mDesc').textContent=t('mDesc');
883
+ document.getElementById('lDir').textContent=t('lDir');document.getElementById('lName').textContent=t('lName');
884
+ document.getElementById('lGoal').textContent=t('lGoal');document.getElementById('lVer').textContent=t('lVer');
885
+ document.getElementById('lMode').textContent=t('lMode');document.getElementById('lProv').textContent=t('lProv');
886
+ document.getElementById('lPcmd').textContent=t('lPcmd');document.getElementById('lArch').textContent=t('lArch');
887
+ document.getElementById('mTip').textContent=t('mTip');
888
+ document.getElementById('bCancel').textContent=t('cancel');document.getElementById('bCreate').textContent=t('create');
889
+ }
890
+ function closeNew(){document.getElementById('newModal').classList.remove('show');}
891
+ async function createProject(){
892
+ const payload={dir:val('f_dir'),name:val('f_name'),goal:val('f_goal'),max_versions:val('f_versions'),
893
+ mode:val('f_mode'),provider:val('f_provider'),provider_command:val('f_pcmd'),arch_hint:val('f_hint')};
894
+ const res=await api('/api/create',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});
895
+ if(res.ok){closeNew();await loadProjects();selectDir(res.dir);tab='settings';dashBuilt=false;render();}
896
+ else alert(res.error||'failed');
897
+ }
898
+
899
+ async function loadProjects(){
900
+ const d=await api('/api/projects');projects=d.projects||[];
901
+ const box=document.getElementById('projects');
902
+ if(!projects.length){box.innerHTML='<div class="muted">'+t('noProjects')+'</div>';return;}
903
+ box.innerHTML=projects.map((p,i)=>{
904
+ const cls=p.running?'run':(p.status&&p.status.startsWith('completed')?'ok':(p.status==='failed'?'bad':(p.status==='stopped'?'warn':'')));
905
+ return '<div class="proj '+(p.dir===current?'active':'')+'" onclick="selectIdx('+i+')">'
906
+ +'<div class="nm">'+h(p.name)+'</div><div class="meta">'
907
+ +'<span class="pill '+cls+'">'+(p.running?'<span class="dot"></span>':'')+statusLabel(p.status)+'</span>'
908
+ +'<span>v'+p.current_version+'/'+p.max_versions+'</span><span>'+(p.goal_progress||0)+'%</span></div></div>';
909
+ }).join('');
910
+ }
911
+ function selectIdx(i){selectDir(projects[i].dir);}
912
+ function selectDir(dir){current=dir;tab='dashboard';dashBuilt=false;curEvents=[];renderedCount=0;selectedLog=null;loadProjects();render();}
913
+ function setTab(tt){tab=tt;dashBuilt=false;render();}
914
+ function curProj(){return projects.find(p=>p.dir===current);}
915
+
916
+ async function render(){
917
+ const main=document.getElementById('main');
918
+ if(!current){main.innerHTML='<div class="muted">'+t('selectHint')+'</div>';return;}
919
+ const tabs='<div class="tabs">'
920
+ +['dashboard','versions','docs','settings'].map(x=>'<div class="t '+(tab===x?'active':'')+'" onclick="setTab(\''+x+'\')">'+t(x==='dashboard'?'tabDash':x==='versions'?'tabVer':x==='docs'?'tabDocs':'tabSet')+'</div>').join('')
921
+ +'</div>';
922
+ if(tab==='dashboard'){ if(!dashBuilt){ main.innerHTML=tabs+dashSkeleton(); dashBuilt=true; curEvents=[];renderedCount=0; } await pollDashboard(true); }
923
+ else if(tab==='versions'){ main.innerHTML=tabs+await renderVersions(); }
924
+ else if(tab==='docs'){ main.innerHTML=tabs+await renderDocs(); }
925
+ else if(tab==='settings'){ main.innerHTML=tabs+await renderSettings(); bindTemplate(); }
926
+ }
927
+
928
+ function dashSkeleton(){
929
+ return '<div class="toolbar">'
930
+ +'<button class="ok" id="btnRun" data-tip="'+h(t('tip_run'))+'" onclick="doRun()">'+t('run')+'</button>'
931
+ +'<button class="warn" id="btnStopG" data-tip="'+h(t('tip_stopG'))+'" onclick="doStop(\'graceful\')">'+t('stopG')+'</button>'
932
+ +'<button class="danger" id="btnStopI" data-tip="'+h(t('tip_stopI'))+'" onclick="doStop(\'immediate\')">'+t('stopI')+'</button>'
933
+ +'<span class="rt">'+t('runtime')+': <b id="rtVal">-</b></span></div>'
934
+ +'<div class="cards" id="cards"></div>'
935
+ +'<div class="panel"><h3>'+t('activeAgents')+'</h3><div class="agents" id="agents"></div></div>'
936
+ +'<div class="panel"><h3>'+t('liveProgress')+'</h3><div class="events" id="events"></div></div>'
937
+ +'<div class="panel"><div class="viewer-head"><h3 style="margin:0">'+t('outputViewer')+'</h3><span class="muted" id="viewerName"></span></div>'
938
+ +'<pre id="logview" class="muted">'+t('outputHint')+'</pre></div>';
939
+ }
940
+
941
+ async function pollDashboard(force){
942
+ if(tab!=='dashboard'||!current) return;
943
+ const p=await api('/api/progress?dir='+encodeURIComponent(current));
944
+ lastProgress=p; const running=!!p.running;
945
+ const br=document.getElementById('btnRun'),bg=document.getElementById('btnStopG'),bi=document.getElementById('btnStopI');
946
+ if(br){br.disabled=running;bg.disabled=!running;bi.disabled=!running;}
947
+ const cls=running?'run':((p.status||'').startsWith('completed')?'ok':(p.status==='failed'?'bad':(p.status==='stopped'?'warn':'')));
948
+ const gp=p.goal_progress||0,tk=p.tokens||{};
949
+ const cards=document.getElementById('cards');
950
+ if(cards) cards.innerHTML=
951
+ card(t('status'),'<span class="pill '+cls+'">'+(running?'<span class="dot"></span>':'')+statusLabel(p.status)+'</span>','')
952
+ +card(t('version'),'v'+(p.current_version||0)+'/'+(p.max_versions||0),'')
953
+ +card(t('phase'),p.phase==='expand'?'expand':'build',(p.goal_completed_version?('🎯 v'+p.goal_completed_version):''))
954
+ +card(t('goalProg'),gp+'%','<div class="bar"><i style="width:'+gp+'%"></i></div>')
955
+ +card(t('calls'),(p.calls||0),'')
956
+ +card(t('tokens'),(tk.input||0),'↓in · '+(tk.output||0)+' ↑out');
957
+ const agbox=document.getElementById('agents');
958
+ const active=p.active||[];
959
+ if(agbox){
960
+ agbox.innerHTML=active.length?active.map(a=>
961
+ '<div class="agent-row"><span class="nm">'+h(a.agent)+'</span><span class="st">'+h(a.message||'')+'</span>'
962
+ +'<span class="tm" data-start="'+(a.started_ts||0)+'">0s</span></div>').join('')
963
+ :'<div class="muted">'+t('noActive')+'</div>';
964
+ }
965
+ const evs=p.events||[]; const ebox=document.getElementById('events');
966
+ if(ebox){
967
+ if(evs.length<curEvents.length){curEvents=[];renderedCount=0;ebox.innerHTML='';}
968
+ curEvents=evs;
969
+ const atBottom=ebox.scrollHeight-ebox.scrollTop-ebox.clientHeight<40;
970
+ let html='';
971
+ for(let i=renderedCount;i<evs.length;i++){ html+=evHtml(evs[i],i); }
972
+ if(html){ ebox.insertAdjacentHTML('beforeend',html); renderedCount=evs.length; if(atBottom||force) ebox.scrollTop=ebox.scrollHeight; }
973
+ if(!evs.length) ebox.innerHTML='<div class="muted">'+t('noEvents')+'</div>';
974
+ }
975
+ tickTimers();
976
+ }
977
+ function card(l,v,s){return '<div class="card"><div class="lbl">'+l+'</div><div class="val">'+v+'</div><div class="sub">'+(s||'')+'</div></div>';}
978
+ function evHtml(e,i){
979
+ if(e.step==='VERSION_START'){
980
+ const vno=e.vno||e.version||'';
981
+ return '<div class="ev-div">v'+h(vno)+' · '+h(e.phase||'')+'</div>';
982
+ }
983
+ const log = e.log? '<button class="logbtn" onclick="viewLog('+i+')">📄 '+h(e.agent||'output')+'</button>' : '';
984
+ const snip = e.snippet? '<div class="snip">'+h(e.snippet)+'</div>' : '';
985
+ return '<div class="ev"><span class="tm">'+h((e.time||'').slice(11))+'</span>'
986
+ +'<span class="stp">'+h(e.step)+'</span>'+(e.agent?'<span class="ag">'+h(e.agent)+'</span>':'')
987
+ +'<span class="msg">'+h(e.message)+'</span>'+log+snip+'</div>';
988
+ }
989
+ async function viewLog(i){
990
+ const e=curEvents[i]; if(!e||!e.log) return; selectedLog=e.log;
991
+ const txt=await api('/api/log?dir='+encodeURIComponent(current)+'&name='+encodeURIComponent(e.log));
992
+ const v=document.getElementById('logview'); if(v){v.classList.remove('muted');v.textContent=txt;}
993
+ const vn=document.getElementById('viewerName'); if(vn) vn.textContent=(e.agent||'')+' · '+e.log;
994
+ }
995
+ function tickTimers(){
996
+ const now=Date.now()/1000;
997
+ document.querySelectorAll('.tm[data-start]').forEach(el=>{const s=parseFloat(el.getAttribute('data-start'))||0;if(s>0)el.textContent=fmtDur(now-s);});
998
+ const rt=document.getElementById('rtVal');
999
+ if(rt&&lastProgress){const st=lastProgress.run_started_ts,en=lastProgress.run_ended_ts;
1000
+ if(st){const end=(lastProgress.running||!en)?now:en;rt.textContent=fmtDur(end-st);}else rt.textContent='-';}
1001
+ }
1002
+
1003
+ async function doRun(){const r=await api('/api/start',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({dir:current})});if(!r.ok)alert(r.error||'failed');setTimeout(()=>pollDashboard(true),600);}
1004
+ async function doStop(mode){
1005
+ if(mode==='immediate'&&!confirm(t('confirmImmediate')))return;
1006
+ const r=await api('/api/stop',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({dir:current,mode:mode})});
1007
+ setTimeout(()=>{pollDashboard(true);loadProjects();},600);
1008
+ if(r&&r.ok){
1009
+ if(r.mode==='graceful'){ showInfo(t('stopGT'),'<p>'+h(t('stopGB'))+'</p>'); }
1010
+ else if(r.mode==='graceful_fallback'){ showInfo(t('stopGT'),'<p>'+h(t('stopFB'))+'</p>'); }
1011
+ else if(r.mode==='immediate'){ const body=r.rolled_back?t('stopIB1').replace('{n}',r.rolled_to):t('stopIB0'); showInfo(t('stopIT'),'<p>'+h(body)+'</p>'); }
1012
+ } else if(r&&r.error){ alert(r.error); }
1013
+ }
1014
+
1015
+ async function renderVersions(){
1016
+ const s=await api('/api/state?dir='+encodeURIComponent(current));
1017
+ const gv=s.goal_completed_version;
1018
+ const rows=(s.versions||[]).map(v=>{
1019
+ const tests=(v.test_result&&v.test_result.success)?'<span class="badge ok">✅ '+t('pass')+'</span>':'<span class="badge bad">❌ '+t('fail')+'</span>';
1020
+ const gp=v.goal_progress||0;
1021
+ const nw=(v.whats_new||[]).slice(0,3).map(x=>'• '+h(x)).join('<br>');
1022
+ return '<tr><td class="nowrap"><b>v'+v.version+'</b>'+(v.version===gv?' 🎯':'')+'</td>'
1023
+ +'<td class="nowrap"><span class="badge '+(v.phase==='expand'?'warn':'')+'">'+h(v.phase)+'</span></td>'
1024
+ +'<td class="nowrap">'+scoreBadge(v.review_score)+'</td><td class="nowrap">'+tests+'</td>'
1025
+ +'<td class="nowrap">'+gp+'%</td><td>'+h(v.feature_summary)+'</td><td class="muted">'+nw+'</td></tr>';
1026
+ }).join('');
1027
+ const bl=(s.backlog||[]).filter(b=>b.status==='accepted').map(b=>'<tr><td class="nowrap">'+scoreBadge100(b.value)+'</td><td><b>'+h(b.title)+'</b><div class="muted">'+h(b.description)+'</div></td></tr>').join('');
1028
+ return '<div class="panel"><h3>'+t('tabVer')+'</h3><table class="vtable"><thead><tr><th>'+t('cVer')+'</th><th>'+t('cPhase')+'</th><th>'+t('cScore')+'</th><th>'+t('cTests')+'</th><th>'+t('cGoal')+'</th><th>'+t('cSummary')+'</th><th>'+t('cNew')+'</th></tr></thead><tbody>'+(rows||'<tr><td colspan=7 class="muted">-</td></tr>')+'</tbody></table></div>'
1029
+ +'<div class="panel"><h3>'+t('backlog')+'</h3><table class="vtable"><thead><tr><th>'+t('cScore')+'</th><th>'+t('cSummary')+'</th></tr></thead><tbody>'+(bl||'<tr><td colspan=2 class="muted">'+t('backlogEmpty')+'</td></tr>')+'</tbody></table></div>';
1030
+ }
1031
+ function scoreBadge(n){n=parseInt(n)||0;const c=n>=80?'ok':(n>=60?'warn':'bad');return '<span class="badge '+c+'">'+n+'/100</span>';}
1032
+ function scoreBadge100(n){n=parseInt(n)||0;const c=n>=80?'ok':(n>=60?'warn':'');return '<span class="badge '+c+'">'+n+'</span>';}
1033
+
1034
+ async function renderDocs(){
1035
+ const ft=await api('/api/doc?dir='+encodeURIComponent(current)+'&name=features');
1036
+ const cl=await api('/api/doc?dir='+encodeURIComponent(current)+'&name=changelog');
1037
+ return '<div class="panel"><h3>'+t('featuresTitle')+'</h3><pre>'+h(ft)+'</pre></div><div class="panel"><h3>'+t('changelogTitle')+'</h3><pre>'+h(cl)+'</pre></div>';
1038
+ }
1039
+
1040
+ async function renderSettings(){
1041
+ cfgCache=await api('/api/config?dir='+encodeURIComponent(current));
1042
+ const c=cfgCache.config,names=cfgCache.template_names,locked=cfgCache.running;
1043
+ if(!tmplActive||names.indexOf(tmplActive)<0)tmplActive=names[0];
1044
+ const dis=locked?'disabled':'';
1045
+ const banner=locked?'<div class="banner">'+t('locked')+'</div>':'';
1046
+ const tabsHtml=names.map(n=>'<span class="'+(n===tmplActive?'active':'')+'" id="tt_'+n+'" onclick="pickTmpl(\''+n+'\')">'+n+'</span>').join('');
1047
+ const lf=(key,inner)=>'<div><label>'+t(key)+helpDot('tip_'+key)+'</label>'+inner+'</div>';
1048
+ return banner+'<div class="panel"><h3>'+t('setTitle')+'</h3>'
1049
+ +'<div class="row">'
1050
+ +lf('mode','<select id="c_mode" onchange="applyMode(this.value)" '+dis+'><option value="advanced" '+(c.pipeline.mode==='advanced'?'selected':'')+'>advanced</option><option value="simple" '+(c.pipeline.mode==='simple'?'selected':'')+'>simple</option></select>')
1051
+ +lf('maxVer','<input id="c_versions" type="number" value="'+c.project.max_versions+'" '+dis+'>')+'</div>'
1052
+ +'<div class="row">'+lf('provider','<input id="c_provider" value="'+h(c.provider.name)+'" '+dis+'>')
1053
+ +lf('providerCmd','<input id="c_pcmd" value="'+h(c.provider.command)+'" placeholder="claude / codex / gemini" '+dis+'>')+'</div>'
1054
+ +'<div class="row">'+lf('model','<input id="c_model" value="'+h(c.provider.model)+'" '+dis+'>')
1055
+ +lf('reviewTh','<input id="c_review" type="number" value="'+c.review.threshold+'" '+dis+'>')+'</div>'
1056
+ +'<div class="row">'+lf('valueTh','<input id="c_value" type="number" value="'+c.value.threshold+'" '+dis+'>')
1057
+ +lf('fixRetries','<input id="c_fix" type="number" value="'+c.fix.retries+'" '+dis+'>')+'</div>'
1058
+ +'<div class="row">'+lf('maxPar','<input id="c_par" type="number" value="'+c.agents.max_parallel+'" '+dis+'>')
1059
+ +lf('retries','<input id="c_retries" type="number" value="'+c.agents.retries+'" '+dis+'>')+'</div>'
1060
+ +'<div class="row">'+lf('testCmd','<input id="c_test" value="'+h(c.tests.command)+'" '+dis+'>')
1061
+ +lf('gitVer','<select id="c_git" '+dis+'><option value="true" '+(c.vcs.git?'selected':'')+'>'+t('on')+'</option><option value="false" '+(!c.vcs.git?'selected':'')+'>'+t('off')+'</option></select>')+'</div>'
1062
+ +'<label>'+t('steps')+helpDot('stepsHint')+'</label><div class="hint">'+t('stepsHint')+'</div>'
1063
+ +stepsHtml(c,locked)+'</div>'
1064
+ +'<div class="panel"><h3>'+t('promptTpl')+'</h3><div class="tmpl-tabs">'+tabsHtml+'</div>'
1065
+ +'<textarea id="tmpl_text" oninput="validateActive()" '+dis+'></textarea>'
1066
+ +'<div class="tmplnote" id="tmplNote">'+t('tmplInactive')+'</div>'
1067
+ +'<div class="hint">'+t('promptHint')+'</div>'
1068
+ +'<div style="margin-top:6px"><span class="muted" style="font-size:12px">'+t('reqTokens')+'</span><div class="chips" id="reqChips"></div><div id="reqStatus" class="hint"></div>'
1069
+ +'<div style="margin-top:6px"><span class="linklike" onclick="showHelp()">'+t('phHelpLink')+'</span></div></div></div>'
1070
+ +(locked?'':'<div style="display:flex;gap:10px;align-items:center"><button id="saveBtn" onclick="saveSettings()">'+t('save')+'</button><span id="savemsg" class="muted"></span></div>');
1071
+ }
1072
+ function stepsHtml(c,locked){
1073
+ const ov=(c.pipeline&&c.pipeline.steps)||{};
1074
+ const mode=(c.pipeline&&c.pipeline.mode)||'advanced';
1075
+ const base=mode==='simple'?SIMPLE_STEPS:ADVANCED_STEPS;
1076
+ const row=(s,req)=>{
1077
+ let attrs,off='';
1078
+ if(req){ attrs='checked disabled'; }
1079
+ else {
1080
+ const supported=base[s.key]!==false; // does this mode run it at all?
1081
+ const forceOff=(mode==='simple'&&!supported); // advanced-only step under simple
1082
+ const resolved=(ov[s.key]===true||ov[s.key]===false)?ov[s.key]:base[s.key];
1083
+ const checked=forceOff?false:resolved;
1084
+ const disabled=locked||forceOff;
1085
+ attrs='id="step_'+s.key+'" onchange="refreshTmplState()"'+(checked?' checked':'')+(disabled?' disabled':'');
1086
+ if(forceOff)off=' off';
1087
+ }
1088
+ const lock=req?'<span class="lockicon" data-tip="'+h(t('reqd'))+'">🔒</span>':'';
1089
+ return '<div class="step-row'+(req?' req':'')+off+'">'
1090
+ +'<input type="checkbox" '+attrs+'>'
1091
+ +'<span class="sa">'+h(s.agent)+' '+lock+'</span>'
1092
+ +'<span class="stmpl">'+h(s.tmpl)+'</span>'
1093
+ +'<span class="sd">'+h(t('tip_'+s.key))+'</span></div>';
1094
+ };
1095
+ let html='<div class="steptable"><div class="grouphdr">'+t('grpReq')+'</div>';
1096
+ REQUIRED_STEPS.forEach(s=>html+=row(s,true));
1097
+ html+='<div class="grouphdr">'+t('grpOpt')+'</div>';
1098
+ OPTIONAL_STEPS.forEach(s=>html+=row(s,false));
1099
+ return html+'</div>';
1100
+ }
1101
+ // Live: switching mode resets the optional steps to that mode's defaults and
1102
+ // disables/greys the advanced-only ones when simple is selected.
1103
+ function applyMode(mode){
1104
+ const base=mode==='simple'?SIMPLE_STEPS:ADVANCED_STEPS;
1105
+ const locked=cfgCache&&cfgCache.running;
1106
+ OPTIONAL_STEPS.forEach(s=>{
1107
+ const el=document.getElementById('step_'+s.key);if(!el)return;
1108
+ const forceOff=(mode==='simple'&&base[s.key]===false);
1109
+ el.checked=forceOff?false:!!base[s.key];
1110
+ el.disabled=!!locked||forceOff;
1111
+ const r=el.closest('.step-row');if(r)r.classList.toggle('off',forceOff);
1112
+ });
1113
+ refreshTmplState();
1114
+ }
1115
+ function pickTmpl(n){
1116
+ // swap in place (do NOT re-render: that would re-fetch config and drop unsaved edits)
1117
+ if(cfgCache){const ta=document.getElementById('tmpl_text');if(ta)cfgCache.templates[tmplActive]=ta.value;}
1118
+ tmplActive=n;
1119
+ document.querySelectorAll('.tmpl-tabs span').forEach(sp=>sp.classList.toggle('active',sp.id==='tt_'+n));
1120
+ bindTemplate();
1121
+ }
1122
+ function bindTemplate(){
1123
+ if(!cfgCache)return;
1124
+ const ta=document.getElementById('tmpl_text');if(ta)ta.value=cfgCache.templates[tmplActive]||'';
1125
+ validateActive();
1126
+ refreshTmplState();
1127
+ }
1128
+ function tmplStepOn(name){const k=TMPL_STEP[name];if(!k)return true;const el=document.getElementById('step_'+k);return el?el.checked:true;}
1129
+ // Grey the prompt tabs whose step is off, and make the editor read-only for the
1130
+ // active template when its step is off — so prompts follow the mode like agents.
1131
+ function refreshTmplState(){
1132
+ if(!cfgCache)return;
1133
+ (cfgCache.template_names||[]).forEach(n=>{const sp=document.getElementById('tt_'+n);if(sp)sp.classList.toggle('inactive',!tmplStepOn(n));});
1134
+ const inactive=!tmplStepOn(tmplActive);
1135
+ const ta=document.getElementById('tmpl_text');if(ta)ta.disabled=(!!cfgCache.running)||inactive;
1136
+ const note=document.getElementById('tmplNote');if(note)note.style.display=inactive?'block':'none';
1137
+ }
1138
+ function missingTokens(name,body){const req=(cfgCache.required_tokens||{})[name]||[];body=body||'';return req.filter(x=>body.indexOf(x)<0);}
1139
+ function validateActive(){
1140
+ if(!cfgCache)return;
1141
+ const ta=document.getElementById('tmpl_text');const body=ta?ta.value:(cfgCache.templates[tmplActive]||'');
1142
+ const req=(cfgCache.required_tokens||{})[tmplActive]||[];
1143
+ const miss=missingTokens(tmplActive,body);
1144
+ const chips=document.getElementById('reqChips');
1145
+ if(chips)chips.innerHTML=req.map(x=>'<span class="chip'+(miss.indexOf(x)>=0?' miss':'')+'">'+h(x)+'</span>').join('')||'<span class="muted" style="font-size:12px">—</span>';
1146
+ const st=document.getElementById('reqStatus');
1147
+ if(st){st.textContent=miss.length?(t('reqBad')+': '+miss.join(', ')):t('reqOk');st.style.color=miss.length?'var(--bad)':'var(--ok)';}
1148
+ const tabSpan=document.getElementById('tt_'+tmplActive);if(tabSpan)tabSpan.classList.toggle('bad',miss.length>0);
1149
+ }
1150
+ async function saveSettings(){
1151
+ if(cfgCache){const ta=document.getElementById('tmpl_text');if(ta)cfgCache.templates[tmplActive]=ta.value;}
1152
+ // client-side format check across all templates
1153
+ const bad={};(cfgCache.template_names||[]).forEach(n=>{const m=missingTokens(n,cfgCache.templates[n]);if(m.length)bad[n]=m;});
1154
+ if(Object.keys(bad).length){
1155
+ showInfo(t('reqMissTitle'),'<p>'+h(t('reqMissBody'))+'</p>'+Object.keys(bad).map(n=>'<p><b>'+h(n)+'</b>: <code>'+bad[n].map(h).join('</code> <code>')+'</code></p>').join(''));
1156
+ return;
1157
+ }
1158
+ const steps={};['arch','goal_check','test_agent','doc','scout','evaluate','features_doc'].forEach(s=>{const el=document.getElementById('step_'+s);if(el)steps[s]=el.checked;});
1159
+ const config={project:{max_versions:parseInt(val('c_versions')||'6')},
1160
+ provider:{name:val('c_provider'),command:val('c_pcmd'),model:val('c_model')},
1161
+ pipeline:{mode:document.getElementById('c_mode').value,steps:steps},
1162
+ agents:{max_parallel:parseInt(val('c_par')||'3'),retries:parseInt(val('c_retries')||'3')},
1163
+ review:{threshold:parseInt(val('c_review')||'80')},value:{threshold:parseInt(val('c_value')||'65')},
1164
+ fix:{retries:parseInt(val('c_fix')||'2')},tests:{command:val('c_test')},vcs:{git:document.getElementById('c_git').value==='true'}};
1165
+ const btn=document.getElementById('saveBtn');if(btn)btn.disabled=true;
1166
+ const r=await api('/api/config?dir='+encodeURIComponent(current),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({config:config,templates:cfgCache.templates})});
1167
+ const m=document.getElementById('savemsg');
1168
+ if(r.ok){if(btn)btn.textContent=t('saved');if(m)m.textContent=t('saved');
1169
+ // reload settings from disk so the user sees the page visibly refresh
1170
+ setTimeout(()=>{cfgCache=null;render();},700);}
1171
+ else{ if(btn)btn.disabled=false;
1172
+ if(r.error==='invalid_templates'){showInfo(t('reqMissTitle'),'<p>'+h(t('reqMissBody'))+'</p>'+Object.keys(r.invalid||{}).map(n=>'<p><b>'+h(n)+'</b>: <code>'+(r.invalid[n]||[]).map(h).join('</code> <code>')+'</code></p>').join(''));}
1173
+ else if(m)m.textContent=r.error||'error'; }
1174
+ }
1175
+
1176
+ // pollers
1177
+ setInterval(()=>{ if(current&&tab==='dashboard') pollDashboard(false).catch(()=>{}); loadProjects().catch(()=>{}); },1800);
1178
+ setInterval(tickTimers,1000);
1179
+
1180
+ applyStatic();loadProjects();render();
1181
+ </script>
1182
+ </body>
1183
+ </html>
1184
+ """