local-coding-agent 1.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.
Files changed (101) hide show
  1. local_coding_agent/__init__.py +314 -0
  2. local_coding_agent/__main__.py +5 -0
  3. local_coding_agent/acp_server/__init__.py +38 -0
  4. local_coding_agent/acp_server/_codec.py +95 -0
  5. local_coding_agent/acp_server/_protocol.py +13 -0
  6. local_coding_agent/acp_server/_server.py +663 -0
  7. local_coding_agent/acp_server/_session.py +24 -0
  8. local_coding_agent/acp_server/_tools.py +126 -0
  9. local_coding_agent/ast_compactor.py +130 -0
  10. local_coding_agent/atomizer.py +77 -0
  11. local_coding_agent/benchmark/__init__.py +57 -0
  12. local_coding_agent/benchmark/_cases.py +307 -0
  13. local_coding_agent/benchmark/_judge.py +175 -0
  14. local_coding_agent/benchmark/_oracles.py +157 -0
  15. local_coding_agent/benchmark/_runner.py +259 -0
  16. local_coding_agent/benchmark/_stats.py +22 -0
  17. local_coding_agent/benchmark/_summary.py +143 -0
  18. local_coding_agent/benchmark_oracle_worker.py +169 -0
  19. local_coding_agent/calibration.py +154 -0
  20. local_coding_agent/capability.py +450 -0
  21. local_coding_agent/chat_compaction.py +276 -0
  22. local_coding_agent/cli/__init__.py +12 -0
  23. local_coding_agent/cli/_dispatch.py +377 -0
  24. local_coding_agent/cli/_handlers.py +320 -0
  25. local_coding_agent/cli/_handlers2.py +725 -0
  26. local_coding_agent/cli/_input.py +61 -0
  27. local_coding_agent/cli/_main.py +159 -0
  28. local_coding_agent/cli/_parser.py +327 -0
  29. local_coding_agent/context_manager.py +155 -0
  30. local_coding_agent/controller/__init__.py +18 -0
  31. local_coding_agent/controller/_constants.py +108 -0
  32. local_coding_agent/controller/_controller.py +1008 -0
  33. local_coding_agent/controller/_post_apply.py +216 -0
  34. local_coding_agent/delegator.py +180 -0
  35. local_coding_agent/desktop/__init__.py +8 -0
  36. local_coding_agent/desktop/app.py +100 -0
  37. local_coding_agent/desktop/assets/lucide.min.js +12 -0
  38. local_coding_agent/desktop/assets/tailwind.css +1 -0
  39. local_coding_agent/desktop/client_js.py +1271 -0
  40. local_coding_agent/desktop/components.py +555 -0
  41. local_coding_agent/desktop/server/__init__.py +37 -0
  42. local_coding_agent/desktop/server/_handlers.py +2178 -0
  43. local_coding_agent/desktop/server/_models.py +204 -0
  44. local_coding_agent/desktop/server/_server.py +347 -0
  45. local_coding_agent/desktop/server/_telemetry.py +87 -0
  46. local_coding_agent/desktop/styles.py +76 -0
  47. local_coding_agent/desktop/ui.py +53 -0
  48. local_coding_agent/doctor.py +413 -0
  49. local_coding_agent/hooks/__init__.py +19 -0
  50. local_coding_agent/hooks/_adapters.py +458 -0
  51. local_coding_agent/hooks/_bridge.py +346 -0
  52. local_coding_agent/hooks/_decision.py +26 -0
  53. local_coding_agent/lsp/__init__.py +60 -0
  54. local_coding_agent/lsp/_client.py +322 -0
  55. local_coding_agent/lsp/_fallback.py +297 -0
  56. local_coding_agent/lsp/_manager.py +173 -0
  57. local_coding_agent/lsp/_mock.py +200 -0
  58. local_coding_agent/lsp/_types.py +398 -0
  59. local_coding_agent/mcp_config.py +398 -0
  60. local_coding_agent/mcp_server.py +377 -0
  61. local_coding_agent/memory.py +140 -0
  62. local_coding_agent/mode_router.py +200 -0
  63. local_coding_agent/model_scanner.py +525 -0
  64. local_coding_agent/monitor.py +635 -0
  65. local_coding_agent/observation_policy.py +184 -0
  66. local_coding_agent/ollama_adapter.py +736 -0
  67. local_coding_agent/plan_mode.py +653 -0
  68. local_coding_agent/prescriptions.py +278 -0
  69. local_coding_agent/profiles.py +239 -0
  70. local_coding_agent/repository_tools.py +782 -0
  71. local_coding_agent/ripgrep.py +326 -0
  72. local_coding_agent/semantic_linter.py +122 -0
  73. local_coding_agent/service.py +405 -0
  74. local_coding_agent/session_events.py +684 -0
  75. local_coding_agent/session_query.py +578 -0
  76. local_coding_agent/skill_config.py +426 -0
  77. local_coding_agent/smoke.py +268 -0
  78. local_coding_agent/speculative_racing.py +85 -0
  79. local_coding_agent/spill.py +258 -0
  80. local_coding_agent/stats.py +255 -0
  81. local_coding_agent/stdio.py +132 -0
  82. local_coding_agent/subagent.py +516 -0
  83. local_coding_agent/task.py +90 -0
  84. local_coding_agent/task_store.py +142 -0
  85. local_coding_agent/tasks.py +240 -0
  86. local_coding_agent/terminal/__init__.py +35 -0
  87. local_coding_agent/terminal/_manager.py +156 -0
  88. local_coding_agent/terminal/_process.py +148 -0
  89. local_coding_agent/terminal/_session.py +452 -0
  90. local_coding_agent/terminal/_tools.py +233 -0
  91. local_coding_agent/terminal/_types.py +60 -0
  92. local_coding_agent/tool_call_parser.py +178 -0
  93. local_coding_agent/validators.py +563 -0
  94. local_coding_agent/vram_fit.py +240 -0
  95. local_coding_agent/worker_pool.py +516 -0
  96. local_coding_agent-1.1.0.dist-info/METADATA +202 -0
  97. local_coding_agent-1.1.0.dist-info/RECORD +101 -0
  98. local_coding_agent-1.1.0.dist-info/WHEEL +5 -0
  99. local_coding_agent-1.1.0.dist-info/entry_points.txt +3 -0
  100. local_coding_agent-1.1.0.dist-info/licenses/LICENSE +21 -0
  101. local_coding_agent-1.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,635 @@
1
+ """Lightweight stdlib-only HTTP monitoring server and HTML dashboard.
2
+
3
+ Provides real-time visibility into delegation performance, worker pool load,
4
+ queue depth, latency distributions, and task outcomes.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import html
10
+ import json
11
+ import threading
12
+ import time
13
+ from http import HTTPStatus
14
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
15
+ from typing import Any
16
+ from urllib.parse import urlparse
17
+
18
+ from .stats import DelegationStats, append_stats, load_stats, merge_stats_snapshots
19
+ from .worker_pool import BoundedWorkerPool
20
+
21
+
22
+ class MonitorRequestHandler(BaseHTTPRequestHandler):
23
+ """Handler for HTTP metrics and live dashboard."""
24
+
25
+ @property
26
+ def monitor(self) -> Any:
27
+ return getattr(self.server, "monitor_server", self.server)
28
+
29
+ def do_GET(self) -> None:
30
+ parsed = urlparse(self.path)
31
+ path = parsed.path.rstrip("/")
32
+
33
+ if path in {"", "/dashboard"}:
34
+ self._handle_dashboard()
35
+ elif path == "/workbench":
36
+ self._handle_workbench()
37
+ elif path == "/health":
38
+ self._handle_health()
39
+ elif path == "/stats":
40
+ self._handle_stats()
41
+ elif path == "/tasks":
42
+ self._handle_tasks()
43
+ elif path in {"/api/events", "/events"}:
44
+ self._handle_events()
45
+ else:
46
+ self._send_response(HTTPStatus.NOT_FOUND, "text/plain; charset=utf-8", b"404 Not Found\n")
47
+
48
+ def do_POST(self) -> None:
49
+ parsed = urlparse(self.path)
50
+ path = parsed.path.rstrip("/")
51
+ if path in {"/api/delegate", "/delegate"}:
52
+ self._handle_api_delegate()
53
+ else:
54
+ self._send_response(HTTPStatus.NOT_FOUND, "text/plain; charset=utf-8", b"404 Not Found\n")
55
+
56
+ def _handle_api_delegate(self) -> None:
57
+ content_length = int(self.headers.get("Content-Length", 0))
58
+ body = self.rfile.read(content_length).decode("utf-8") if content_length > 0 else "{}"
59
+ try:
60
+ data = json.loads(body)
61
+ raw_task = data.get("task", {})
62
+ profile_name = data.get("profile", "qwen2.5-1.5b")
63
+ apply_flag = bool(data.get("apply", False))
64
+
65
+ from pathlib import Path
66
+ from .controller import Controller
67
+ from .ollama_adapter import build_client
68
+ from .profiles import get_profile
69
+ from .task import TaskEnvelope
70
+
71
+ task = TaskEnvelope.from_mapping(raw_task)
72
+ profile = get_profile(profile_name)
73
+ client = build_client(profile)
74
+ controller = Controller(client, str(Path.cwd()))
75
+ started_ns = time.monotonic_ns()
76
+ result = controller.run(task, apply=apply_flag)
77
+ if getattr(self.monitor, "stats_path", None):
78
+ append_stats(
79
+ self.monitor.stats_path,
80
+ result,
81
+ model=profile_name,
82
+ latency_ns=time.monotonic_ns() - started_ns,
83
+ )
84
+ self._send_json(result)
85
+ except Exception as error:
86
+ self._send_json({"status": "failed", "error": str(error)})
87
+
88
+ def _handle_events(self) -> None:
89
+ events = self.monitor.get_recent_events()
90
+ payload = {
91
+ "uptime_seconds": round(time.monotonic() - self.monitor.started_at, 2),
92
+ "events": events,
93
+ "count": len(events),
94
+ }
95
+ self._send_json(payload)
96
+
97
+ def _handle_health(self) -> None:
98
+ payload = {
99
+ "status": "ok",
100
+ "uptime_seconds": round(time.monotonic() - self.monitor.started_at, 2),
101
+ }
102
+ self._send_json(payload)
103
+
104
+ def _stats_snapshot(self) -> dict[str, Any]:
105
+ """Aggregate in-process stats with the cross-process JSONL journal."""
106
+ if self.monitor.stats is None:
107
+ return {}
108
+ snapshot = self.monitor.stats.snapshot()
109
+ stats_path = getattr(self.monitor, "stats_path", None)
110
+ if stats_path:
111
+ snapshot = merge_stats_snapshots(load_stats(stats_path).snapshot(), snapshot)
112
+ return snapshot
113
+
114
+ def _handle_stats(self) -> None:
115
+ pool_data = self.monitor.worker_pool.status() if self.monitor.worker_pool is not None else {}
116
+ payload = {
117
+ "uptime_seconds": round(time.monotonic() - self.monitor.started_at, 2),
118
+ "stats": self._stats_snapshot(),
119
+ "worker_pool": pool_data,
120
+ }
121
+ self._send_json(payload)
122
+
123
+ def _handle_tasks(self) -> None:
124
+ pool_data = self.monitor.worker_pool.status() if self.monitor.worker_pool is not None else {}
125
+ payload = {
126
+ "jobs": pool_data.get("jobs", []),
127
+ "total": pool_data.get("total_jobs", 0),
128
+ "queued": pool_data.get("queued_jobs", 0),
129
+ "active": pool_data.get("active_workers", 0),
130
+ }
131
+ self._send_json(payload)
132
+
133
+ def _handle_dashboard(self) -> None:
134
+ stats_data = self._stats_snapshot()
135
+ pool_data = self.monitor.worker_pool.status() if self.monitor.worker_pool is not None else {}
136
+ dashboard_html = self._render_dashboard_html(stats_data, pool_data)
137
+ self._send_response(HTTPStatus.OK, "text/html; charset=utf-8", dashboard_html.encode("utf-8"))
138
+
139
+
140
+ def _render_dashboard_html(self, stats: dict[str, Any], pool: dict[str, Any]) -> str:
141
+ total = stats.get("total", 0)
142
+ by_status = stats.get("by_status", {})
143
+ accepted = by_status.get("accepted", 0)
144
+ rejected = by_status.get("rejected", 0)
145
+ failed = by_status.get("failed", 0)
146
+ escaped = by_status.get("escalated", 0)
147
+ success_rate = f"{(accepted / total * 100):.1f}%" if total > 0 else "N/A"
148
+
149
+ active_workers = pool.get("active_workers", 0)
150
+ max_workers = pool.get("max_workers", 1)
151
+ queued_jobs = pool.get("queued_jobs", 0)
152
+ max_queue = pool.get("max_queue", 16)
153
+ stopping = pool.get("stopping", False)
154
+ pool_status = "Stopping" if stopping else ("Busy" if active_workers >= max_workers else "Healthy")
155
+
156
+ latency = stats.get("latency", {})
157
+ avg_latency = f"{latency.get('avg_ms', 0):.1f} ms" if latency.get("avg_ms") is not None else "N/A"
158
+ min_latency = f"{latency.get('min_ms', 0):.1f} ms" if latency.get("min_ms") is not None else "N/A"
159
+ max_latency = f"{latency.get('max_ms', 0):.1f} ms" if latency.get("max_ms") is not None else "N/A"
160
+
161
+ model_calls = stats.get("model_calls", 0)
162
+ tool_calls = stats.get("tool_calls", 0)
163
+
164
+ jobs = pool.get("jobs", [])
165
+ jobs_rows = ""
166
+ for job in jobs[-15:]:
167
+ state = html.escape(str(job.get("state", "")))
168
+ badge_class = "badge-success" if state == "completed" else ("badge-warning" if state == "running" else "badge-info")
169
+ jobs_rows += f"""
170
+ <tr>
171
+ <td class="mono">{html.escape(str(job.get('job_id', '')))}</td>
172
+ <td>{html.escape(str(job.get('caller_id', '')))}</td>
173
+ <td><span class="badge {badge_class}">{state}</span></td>
174
+ <td class="mono small">{html.escape(str(job.get('created_at', '')))}</td>
175
+ <td class="mono small">{html.escape(str(job.get('updated_at', '')))}</td>
176
+ </tr>
177
+ """
178
+ if not jobs:
179
+ jobs_rows = "<tr><td colspan='5' class='empty'>No delegation jobs recorded yet.</td></tr>"
180
+
181
+ errors = stats.get("by_error_kind", {})
182
+ errors_rows = ""
183
+ for kind, count in sorted(errors.items(), key=lambda x: x[1], reverse=True):
184
+ errors_rows += f"<tr><td class='mono'>{html.escape(str(kind))}</td><td class='numeric'>{count}</td></tr>"
185
+ if not errors:
186
+ errors_rows = "<tr><td colspan='2' class='empty'>No errors recorded.</td></tr>"
187
+
188
+ return f"""<!DOCTYPE html>
189
+ <html lang="en">
190
+ <head>
191
+ <meta charset="UTF-8">
192
+ <meta http-equiv="refresh" content="3">
193
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
194
+ <title>Local Coding Agent Monitor</title>
195
+ <style>
196
+ :root {{
197
+ --bg: #0f172a;
198
+ --card-bg: #1e293b;
199
+ --border: #334155;
200
+ --text: #f8fafc;
201
+ --text-muted: #94a3b8;
202
+ --accent: #38bdf8;
203
+ --success: #4ade80;
204
+ --warning: #facc15;
205
+ --danger: #f87171;
206
+ }}
207
+ * {{ box-sizing: border-box; margin: 0; padding: 0; }}
208
+ body {{
209
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
210
+ background: var(--bg);
211
+ color: var(--text);
212
+ line-height: 1.5;
213
+ padding: 24px;
214
+ }}
215
+ header {{
216
+ display: flex;
217
+ justify-content: space-between;
218
+ align-items: center;
219
+ margin-bottom: 24px;
220
+ padding-bottom: 16px;
221
+ border-bottom: 1px solid var(--border);
222
+ }}
223
+ h1 {{ font-size: 1.5rem; font-weight: 700; color: var(--accent); }}
224
+ .status-badge {{
225
+ display: inline-flex;
226
+ align-items: center;
227
+ padding: 4px 12px;
228
+ border-radius: 9999px;
229
+ font-size: 0.875rem;
230
+ font-weight: 600;
231
+ background: #064e3b;
232
+ color: var(--success);
233
+ }}
234
+ .grid {{
235
+ display: grid;
236
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
237
+ gap: 16px;
238
+ margin-bottom: 24px;
239
+ }}
240
+ .card {{
241
+ background: var(--card-bg);
242
+ border: 1px solid var(--border);
243
+ border-radius: 8px;
244
+ padding: 16px;
245
+ }}
246
+ .card-title {{ font-size: 0.8125rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-muted); }}
247
+ .card-value {{ font-size: 1.75rem; font-weight: 700; margin-top: 4px; }}
248
+ .card-sub {{ font-size: 0.75rem; color: var(--text-muted); margin-top: 4px; }}
249
+ table {{
250
+ width: 100%;
251
+ border-collapse: collapse;
252
+ font-size: 0.875rem;
253
+ }}
254
+ th, td {{
255
+ padding: 10px 14px;
256
+ text-align: left;
257
+ border-bottom: 1px solid var(--border);
258
+ }}
259
+ th {{ background: #182234; color: var(--text-muted); font-weight: 600; }}
260
+ .mono {{ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 0.8125rem; }}
261
+ .small {{ font-size: 0.75rem; color: var(--text-muted); }}
262
+ .numeric {{ text-align: right; }}
263
+ .empty {{ text-align: center; color: var(--text-muted); padding: 20px; }}
264
+ .badge {{
265
+ padding: 2px 8px;
266
+ border-radius: 4px;
267
+ font-size: 0.75rem;
268
+ font-weight: 600;
269
+ text-transform: uppercase;
270
+ }}
271
+ .badge-success {{ background: #065f46; color: #a7f3d0; }}
272
+ .badge-warning {{ background: #713f12; color: #fef08a; }}
273
+ .badge-info {{ background: #1e3a8a; color: #bfdbfe; }}
274
+ .section-title {{ font-size: 1.125rem; font-weight: 600; margin-bottom: 12px; }}
275
+ .two-col {{ display: grid; grid-template-columns: 2fr 1fr; gap: 16px; }}
276
+ @media (max-width: 900px) {{ .two-col {{ grid-template-columns: 1fr; }} }}
277
+ </style>
278
+ </head>
279
+ <body>
280
+ <header>
281
+ <div>
282
+ <h1>Local Coding Agent Monitor</h1>
283
+ <p class="card-sub">Ollama Autonomous Delegation Service &amp; Bounded Worker Pool</p>
284
+ </div>
285
+ <span class="status-badge">{pool_status}</span>
286
+ </header>
287
+
288
+ <div class="grid">
289
+ <div class="card">
290
+ <div class="card-title">Worker Capacity</div>
291
+ <div class="card-value">{active_workers} / {max_workers}</div>
292
+ <div class="card-sub">Active workers running jobs</div>
293
+ </div>
294
+ <div class="card">
295
+ <div class="card-title">Queue Depth</div>
296
+ <div class="card-value">{queued_jobs} / {max_queue}</div>
297
+ <div class="card-sub">Pending delegation requests</div>
298
+ </div>
299
+ <div class="card">
300
+ <div class="card-title">Success Rate</div>
301
+ <div class="card-value" style="color: var(--success);">{success_rate}</div>
302
+ <div class="card-sub">{accepted} accepted / {total} total</div>
303
+ </div>
304
+ <div class="card">
305
+ <div class="card-title">Avg Latency</div>
306
+ <div class="card-value">{avg_latency}</div>
307
+ <div class="card-sub">Min: {min_latency} | Max: {max_latency}</div>
308
+ </div>
309
+ <div class="card">
310
+ <div class="card-title">Calls &amp; Tools</div>
311
+ <div class="card-value">{model_calls} / {tool_calls}</div>
312
+ <div class="card-sub">Model inferences / Tool executions</div>
313
+ </div>
314
+ </div>
315
+
316
+ <div class="two-col">
317
+ <div class="card">
318
+ <div class="section-title">Recent Delegation Jobs</div>
319
+ <table>
320
+ <thead>
321
+ <tr>
322
+ <th>Job ID</th>
323
+ <th>Caller</th>
324
+ <th>State</th>
325
+ <th>Created</th>
326
+ <th>Updated</th>
327
+ </tr>
328
+ </thead>
329
+ <tbody>
330
+ {jobs_rows}
331
+ </tbody>
332
+ </table>
333
+ </div>
334
+ <div class="card">
335
+ <div class="section-title">Failure Categories</div>
336
+ <table>
337
+ <thead>
338
+ <tr>
339
+ <th>Error Kind</th>
340
+ <th class="numeric">Count</th>
341
+ </tr>
342
+ </thead>
343
+ <tbody>
344
+ {errors_rows}
345
+ </tbody>
346
+ </table>
347
+ </div>
348
+ </div>
349
+ </body>
350
+ </html>
351
+ """
352
+
353
+ def _handle_workbench(self) -> None:
354
+ workbench_html = self._render_workbench_html()
355
+ self._send_response(HTTPStatus.OK, "text/html; charset=utf-8", workbench_html.encode("utf-8"))
356
+
357
+ def _render_workbench_html(self) -> str:
358
+ return """<!DOCTYPE html>
359
+ <html lang="en">
360
+ <head>
361
+ <meta charset="UTF-8">
362
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
363
+ <title>Local Coding Agent — Interactive Coding Workbench</title>
364
+ <style>
365
+ :root {
366
+ --bg: #0f172a;
367
+ --card-bg: #1e293b;
368
+ --border: #334155;
369
+ --text: #f8fafc;
370
+ --text-muted: #94a3b8;
371
+ --accent: #38bdf8;
372
+ --success: #4ade80;
373
+ --warning: #facc15;
374
+ --danger: #f87171;
375
+ }
376
+ * { box-sizing: border-box; margin: 0; padding: 0; }
377
+ body {
378
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
379
+ background: var(--bg);
380
+ color: var(--text);
381
+ line-height: 1.5;
382
+ padding: 24px;
383
+ }
384
+ header {
385
+ display: flex;
386
+ justify-content: space-between;
387
+ align-items: center;
388
+ margin-bottom: 24px;
389
+ padding-bottom: 16px;
390
+ border-bottom: 1px solid var(--border);
391
+ }
392
+ h1 { font-size: 1.5rem; font-weight: 700; color: var(--accent); }
393
+ nav a {
394
+ color: var(--text-muted);
395
+ text-decoration: none;
396
+ margin-left: 16px;
397
+ font-size: 0.875rem;
398
+ font-weight: 500;
399
+ }
400
+ nav a.active { color: var(--accent); border-bottom: 2px solid var(--accent); padding-bottom: 4px; }
401
+ .two-col {
402
+ display: grid;
403
+ grid-template-columns: 1fr 1fr;
404
+ gap: 20px;
405
+ }
406
+ @media (max-width: 900px) { .two-col { grid-template-columns: 1fr; } }
407
+ .card {
408
+ background: var(--card-bg);
409
+ border: 1px solid var(--border);
410
+ border-radius: 8px;
411
+ padding: 20px;
412
+ }
413
+ .form-group { margin-bottom: 16px; }
414
+ label { display: block; font-size: 0.8125rem; text-transform: uppercase; color: var(--text-muted); margin-bottom: 6px; }
415
+ input, textarea, select {
416
+ width: 100%;
417
+ background: #0b1120;
418
+ border: 1px solid var(--border);
419
+ border-radius: 6px;
420
+ color: var(--text);
421
+ padding: 8px 12px;
422
+ font-family: inherit;
423
+ font-size: 0.875rem;
424
+ }
425
+ input:focus, textarea:focus, select:focus { outline: none; border-color: var(--accent); }
426
+ button {
427
+ background: var(--accent);
428
+ color: #0f172a;
429
+ border: none;
430
+ border-radius: 6px;
431
+ padding: 10px 18px;
432
+ font-weight: 600;
433
+ cursor: pointer;
434
+ font-size: 0.875rem;
435
+ }
436
+ button:hover { opacity: 0.9; }
437
+ pre {
438
+ background: #0b1120;
439
+ border: 1px solid var(--border);
440
+ border-radius: 6px;
441
+ padding: 12px;
442
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
443
+ font-size: 0.8125rem;
444
+ overflow-x: auto;
445
+ max-height: 480px;
446
+ }
447
+ </style>
448
+ </head>
449
+ <body>
450
+ <header>
451
+ <div>
452
+ <div style="display: flex; align-items: center; gap: 10px;">
453
+ <h1>Local Coding Agent</h1>
454
+ <span style="background: #854d0e; color: #fef08a; font-size: 0.7rem; font-weight: 700; padding: 2px 8px; border-radius: 4px; text-transform: uppercase;">Experimental Preview</span>
455
+ </div>
456
+ <p style="color: var(--text-muted); font-size: 0.8125rem;">Interactive Coding Workbench (Standalone Desktop Harness Redesign Underway)</p>
457
+ </div>
458
+ <nav>
459
+ <a href="/dashboard">Dashboard</a>
460
+ <a href="/workbench" class="active">Workbench</a>
461
+ </nav>
462
+ </header>
463
+
464
+ <div class="two-col">
465
+ <div class="card">
466
+ <h2 style="font-size: 1.1rem; margin-bottom: 16px;">Task Envelope</h2>
467
+ <form id="taskForm">
468
+ <div class="form-group">
469
+ <label>Task ID</label>
470
+ <input type="text" id="taskId" value="atomic-fix-1" required>
471
+ </div>
472
+ <div class="form-group">
473
+ <label>Goal</label>
474
+ <input type="text" id="taskGoal" placeholder="Describe the single atomic change" required>
475
+ </div>
476
+ <div class="form-group">
477
+ <label>Target Files (comma-separated)</label>
478
+ <input type="text" id="taskFiles" placeholder="src/module.py" required>
479
+ </div>
480
+ <div class="form-group">
481
+ <label>Target Checks (comma-separated)</label>
482
+ <input type="text" id="taskChecks" placeholder="pytest tests/test_module.py">
483
+ </div>
484
+ <div class="form-group">
485
+ <label>Model Profile</label>
486
+ <select id="taskProfile">
487
+ <option value="qwen2.5-1.5b">qwen2.5-1.5b</option>
488
+ <option value="ling-3.0-tiny-q6k">ling-3.0-tiny-q6k (llama-server:8080)</option>
489
+ <option value="qwen3-8b-q6k">qwen3-8b-q6k</option>
490
+ <option value="qwen3.8-27b-q4">qwen3.8-27b-q4</option>
491
+ </select>
492
+ </div>
493
+ <button type="submit" id="btnSubmit">Delegate Task</button>
494
+ </form>
495
+ </div>
496
+
497
+ <div class="card">
498
+ <h2 style="font-size: 1.1rem; margin-bottom: 16px;">Execution Result &amp; Diff</h2>
499
+ <div id="statusBadge" style="margin-bottom: 12px; font-size: 0.875rem; color: var(--text-muted);">Ready. Submit a task envelope to run.</div>
500
+ <pre id="outputPre">// Result will appear here...</pre>
501
+ </div>
502
+ </div>
503
+
504
+ <script>
505
+ document.getElementById('taskForm').addEventListener('submit', async (e) => {
506
+ e.preventDefault();
507
+ const btn = document.getElementById('btnSubmit');
508
+ const badge = document.getElementById('statusBadge');
509
+ const output = document.getElementById('outputPre');
510
+
511
+ btn.disabled = true;
512
+ btn.textContent = 'Running...';
513
+ badge.textContent = 'Delegating task to local model executor...';
514
+
515
+ const task = {
516
+ id: document.getElementById('taskId').value,
517
+ goal: document.getElementById('taskGoal').value,
518
+ files: document.getElementById('taskFiles').value.split(',').map(s => s.trim()).filter(Boolean),
519
+ checks: document.getElementById('taskChecks').value.split(',').map(s => s.trim()).filter(Boolean)
520
+ };
521
+
522
+ try {
523
+ const res = await fetch('/api/delegate', {
524
+ method: 'POST',
525
+ headers: { 'Content-Type': 'application/json' },
526
+ body: JSON.stringify({ task, profile: document.getElementById('taskProfile').value })
527
+ });
528
+ const data = await res.json();
529
+ badge.textContent = 'Status: ' + (data.status || 'done');
530
+ output.textContent = JSON.stringify(data, null, 2);
531
+ } catch (err) {
532
+ badge.textContent = 'Error executing task';
533
+ output.textContent = String(err);
534
+ } finally {
535
+ btn.disabled = false;
536
+ btn.textContent = 'Delegate Task';
537
+ }
538
+ });
539
+ </script>
540
+ </body>
541
+ </html>"""
542
+
543
+ def _send_json(self, data: Any) -> None:
544
+ body = json.dumps(data, ensure_ascii=False).encode("utf-8")
545
+ self._send_response(HTTPStatus.OK, "application/json; charset=utf-8", body)
546
+
547
+ def _send_response(self, status: HTTPStatus, content_type: str, body: bytes) -> None:
548
+ self.send_response(status)
549
+ self.send_header("Content-Type", content_type)
550
+ self.send_header("Content-Length", str(len(body)))
551
+ self.send_header("Cache-Control", "no-cache, no-store, must-revalidate")
552
+ self.end_headers()
553
+ self.wfile.write(body)
554
+
555
+ def log_message(self, format: str, *args: Any) -> None:
556
+ # Ponytail / minimal: suppress default stderr request logging
557
+ del format, args
558
+
559
+
560
+ class MonitorServer:
561
+ """Thread-safe, daemonized monitoring HTTP server."""
562
+
563
+ def __init__(
564
+ self,
565
+ host: str = "127.0.0.1",
566
+ port: int = 0,
567
+ *,
568
+ worker_pool: BoundedWorkerPool | None = None,
569
+ stats: DelegationStats | None = None,
570
+ stats_path: str | Path | None = None,
571
+ ) -> None:
572
+ self.host = host
573
+ self.port = port
574
+ self.worker_pool = worker_pool
575
+ self.stats = stats
576
+ self.stats_path = stats_path
577
+ self.started_at = time.monotonic()
578
+ self._events: list[dict[str, Any]] = []
579
+ self._events_lock = threading.Lock()
580
+ self._max_events = 200
581
+ self._httpd = ThreadingHTTPServer((host, port), MonitorRequestHandler)
582
+ self._httpd.monitor_server = self # type: ignore[attr-defined]
583
+ self._thread: threading.Thread | None = None
584
+
585
+ def emit_event(self, event: dict[str, Any]) -> None:
586
+ """Emit a telemetry or lifecycle event."""
587
+ with self._events_lock:
588
+ event_with_ts = {"timestamp": time.time(), **event}
589
+ self._events.append(event_with_ts)
590
+ if len(self._events) > self._max_events:
591
+ self._events = self._events[-self._max_events :]
592
+
593
+ def get_recent_events(self) -> list[dict[str, Any]]:
594
+ """Return a copy of recent telemetry events."""
595
+ with self._events_lock:
596
+ return list(self._events)
597
+
598
+ @property
599
+ def server_address(self) -> tuple[str, int]:
600
+ return self._httpd.server_address
601
+
602
+ @property
603
+ def actual_port(self) -> int:
604
+ return self._httpd.server_address[1]
605
+
606
+ @property
607
+ def url(self) -> str:
608
+ return f"http://{self.host}:{self.actual_port}"
609
+
610
+ def start(self) -> None:
611
+ """Start serving in a background daemon thread."""
612
+ if self._thread is not None:
613
+ return
614
+ self._thread = threading.Thread(
615
+ target=self._httpd.serve_forever,
616
+ name="local-agent-monitor-http",
617
+ daemon=True,
618
+ )
619
+ self._thread.start()
620
+
621
+ def stop(self) -> None:
622
+ """Shut down the HTTP server and release port."""
623
+ if self._thread is None:
624
+ return
625
+ self._httpd.shutdown()
626
+ self._httpd.server_close()
627
+ self._thread.join(timeout=3.0)
628
+ self._thread = None
629
+
630
+ def __enter__(self) -> MonitorServer:
631
+ self.start()
632
+ return self
633
+
634
+ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
635
+ self.stop()