pocketcoder-a1 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.
- a1/__init__.py +6 -0
- a1/checkpoint.py +146 -0
- a1/cli.py +368 -0
- a1/config.py +126 -0
- a1/dashboard.py +2589 -0
- a1/loop.py +1151 -0
- a1/tasks.py +211 -0
- a1/tester/__init__.py +6 -0
- a1/tester/analyzer.py +142 -0
- a1/tester/browser.py +124 -0
- a1/tester/report.py +193 -0
- a1/tester/runner.py +419 -0
- a1/tester/scenarios.py +203 -0
- a1/validator.py +361 -0
- pocketcoder_a1-0.1.0.dist-info/METADATA +230 -0
- pocketcoder_a1-0.1.0.dist-info/RECORD +20 -0
- pocketcoder_a1-0.1.0.dist-info/WHEEL +5 -0
- pocketcoder_a1-0.1.0.dist-info/entry_points.txt +2 -0
- pocketcoder_a1-0.1.0.dist-info/licenses/LICENSE +21 -0
- pocketcoder_a1-0.1.0.dist-info/top_level.txt +1 -0
a1/dashboard.py
ADDED
|
@@ -0,0 +1,2589 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
PocketCoder-A1 Dashboard — Full-featured Web UI
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import html as html_mod
|
|
7
|
+
import json
|
|
8
|
+
import socket
|
|
9
|
+
import threading
|
|
10
|
+
import webbrowser
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from string import Template
|
|
15
|
+
from urllib.parse import parse_qs, urlparse
|
|
16
|
+
|
|
17
|
+
from .checkpoint import CheckpointManager
|
|
18
|
+
from .tasks import TaskManager
|
|
19
|
+
|
|
20
|
+
PROJECT_DIR = None
|
|
21
|
+
AGENT_RUNNING = False
|
|
22
|
+
AGENT_LOOP = None # Reference to SessionLoop for stop control
|
|
23
|
+
ACTIVITY_LOG = [] # Live activity log
|
|
24
|
+
AGENT_LOG_BUFFER = [] # Live agent output lines for dashboard
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def esc(text: str) -> str:
|
|
28
|
+
"""Escape HTML to prevent XSS"""
|
|
29
|
+
return html_mod.escape(str(text)) if text else ""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def log_activity(action: str, details: str = "", status: str = "info"):
|
|
33
|
+
"""Log activity for dashboard"""
|
|
34
|
+
ACTIVITY_LOG.append({
|
|
35
|
+
"time": datetime.now().strftime("%H:%M:%S"),
|
|
36
|
+
"action": action,
|
|
37
|
+
"details": details,
|
|
38
|
+
"status": status # info, success, error, warning
|
|
39
|
+
})
|
|
40
|
+
# Keep last 100 entries
|
|
41
|
+
if len(ACTIVITY_LOG) > 100:
|
|
42
|
+
ACTIVITY_LOG.pop(0)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _classify_line(line: str) -> str:
|
|
46
|
+
"""Classify a Claude output line into a tool type for icon display"""
|
|
47
|
+
lower = line.lower().strip()
|
|
48
|
+
if any(kw in lower for kw in ["read(", "reading file", "read file", "reading ", "read "]):
|
|
49
|
+
return "read"
|
|
50
|
+
elif any(kw in lower for kw in ["edit(", "editing file", "edit file", "editing "]):
|
|
51
|
+
return "edit"
|
|
52
|
+
elif any(kw in lower for kw in ["write(", "writing file", "write file", "creating file", "created "]):
|
|
53
|
+
return "write"
|
|
54
|
+
elif any(kw in lower for kw in ["bash(", "running:", "$ ", "command", "terminal", "pytest", "ruff "]):
|
|
55
|
+
return "bash"
|
|
56
|
+
elif any(kw in lower for kw in ["let me", "i'll", "i need", "thinking", "analyzing", "looking"]):
|
|
57
|
+
return "thinking"
|
|
58
|
+
return "text"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _on_agent_line(line: str, event_type: str = None):
|
|
62
|
+
"""Parse agent output line and add to live buffer.
|
|
63
|
+
event_type: pre-classified type from stream-json parser
|
|
64
|
+
(read/edit/write/bash/thinking/text/metric/verify)
|
|
65
|
+
If not provided, falls back to heuristic classifier."""
|
|
66
|
+
stripped = line.rstrip("\n")
|
|
67
|
+
if not stripped:
|
|
68
|
+
return
|
|
69
|
+
entry = {
|
|
70
|
+
"time": datetime.now().strftime("%H:%M:%S"),
|
|
71
|
+
"line": stripped,
|
|
72
|
+
"type": event_type if event_type else _classify_line(line),
|
|
73
|
+
}
|
|
74
|
+
AGENT_LOG_BUFFER.append(entry)
|
|
75
|
+
if len(AGENT_LOG_BUFFER) > 500:
|
|
76
|
+
AGENT_LOG_BUFFER.pop(0)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
CSS = '''
|
|
80
|
+
:root {
|
|
81
|
+
--bg-primary: #f8f9fa;
|
|
82
|
+
--bg-secondary: #ffffff;
|
|
83
|
+
--bg-tertiary: #e9ecef;
|
|
84
|
+
--text-primary: #212529;
|
|
85
|
+
--text-secondary: #6c757d;
|
|
86
|
+
--border-color: #dee2e6;
|
|
87
|
+
--accent: #6366f1;
|
|
88
|
+
--accent-light: #818cf8;
|
|
89
|
+
--success: #10b981;
|
|
90
|
+
--warning: #f59e0b;
|
|
91
|
+
--danger: #ef4444;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
[data-theme="dark"] {
|
|
95
|
+
--bg-primary: #1a1a2e;
|
|
96
|
+
--bg-secondary: #16213e;
|
|
97
|
+
--bg-tertiary: #0f0f23;
|
|
98
|
+
--text-primary: #f8f9fa;
|
|
99
|
+
--text-secondary: #9ca3af;
|
|
100
|
+
--border-color: #374151;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
104
|
+
|
|
105
|
+
body {
|
|
106
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
107
|
+
background: var(--bg-primary);
|
|
108
|
+
color: var(--text-primary);
|
|
109
|
+
min-height: 100vh;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.layout {
|
|
113
|
+
display: grid;
|
|
114
|
+
grid-template-columns: 240px 1fr;
|
|
115
|
+
min-height: 100vh;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/* Sidebar */
|
|
119
|
+
.sidebar {
|
|
120
|
+
background: var(--bg-secondary);
|
|
121
|
+
border-right: 1px solid var(--border-color);
|
|
122
|
+
padding: 20px;
|
|
123
|
+
position: sticky;
|
|
124
|
+
top: 0;
|
|
125
|
+
height: 100vh;
|
|
126
|
+
overflow-y: auto;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.logo {
|
|
130
|
+
font-size: 18px;
|
|
131
|
+
font-weight: bold;
|
|
132
|
+
margin-bottom: 8px;
|
|
133
|
+
display: flex;
|
|
134
|
+
align-items: center;
|
|
135
|
+
gap: 8px;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.logo-sub {
|
|
139
|
+
font-size: 12px;
|
|
140
|
+
color: var(--text-secondary);
|
|
141
|
+
margin-bottom: 30px;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.nav-section {
|
|
145
|
+
margin-bottom: 24px;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.nav-title {
|
|
149
|
+
font-size: 11px;
|
|
150
|
+
text-transform: uppercase;
|
|
151
|
+
letter-spacing: 1px;
|
|
152
|
+
color: var(--text-secondary);
|
|
153
|
+
margin-bottom: 12px;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.nav-item {
|
|
157
|
+
display: flex;
|
|
158
|
+
align-items: center;
|
|
159
|
+
gap: 10px;
|
|
160
|
+
padding: 10px 12px;
|
|
161
|
+
border-radius: 8px;
|
|
162
|
+
color: var(--text-primary);
|
|
163
|
+
text-decoration: none;
|
|
164
|
+
margin-bottom: 4px;
|
|
165
|
+
transition: background 0.2s;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.nav-item:hover {
|
|
169
|
+
background: var(--bg-tertiary);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.nav-item.active {
|
|
173
|
+
background: var(--accent);
|
|
174
|
+
color: white;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.nav-item i {
|
|
178
|
+
width: 18px;
|
|
179
|
+
text-align: center;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/* Main content */
|
|
183
|
+
.main {
|
|
184
|
+
padding: 24px 32px;
|
|
185
|
+
overflow-y: auto;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.header {
|
|
189
|
+
display: flex;
|
|
190
|
+
justify-content: space-between;
|
|
191
|
+
align-items: center;
|
|
192
|
+
margin-bottom: 24px;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.page-title {
|
|
196
|
+
font-size: 24px;
|
|
197
|
+
font-weight: 600;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.header-actions {
|
|
201
|
+
display: flex;
|
|
202
|
+
gap: 12px;
|
|
203
|
+
align-items: center;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/* Status badge */
|
|
207
|
+
.status {
|
|
208
|
+
display: inline-flex;
|
|
209
|
+
align-items: center;
|
|
210
|
+
gap: 6px;
|
|
211
|
+
padding: 6px 14px;
|
|
212
|
+
border-radius: 20px;
|
|
213
|
+
font-size: 13px;
|
|
214
|
+
font-weight: 500;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.status-running { background: var(--success); color: white; }
|
|
218
|
+
.status-stopped { background: var(--danger); color: white; }
|
|
219
|
+
.status-completed { background: var(--accent); color: white; }
|
|
220
|
+
|
|
221
|
+
/* Cards */
|
|
222
|
+
.cards {
|
|
223
|
+
display: grid;
|
|
224
|
+
grid-template-columns: repeat(3, 1fr);
|
|
225
|
+
gap: 16px;
|
|
226
|
+
margin-bottom: 24px;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.card {
|
|
230
|
+
background: var(--bg-secondary);
|
|
231
|
+
border: 1px solid var(--border-color);
|
|
232
|
+
border-radius: 12px;
|
|
233
|
+
padding: 20px;
|
|
234
|
+
transition: transform 0.2s, box-shadow 0.2s;
|
|
235
|
+
}
|
|
236
|
+
.card:hover {
|
|
237
|
+
transform: translateY(-2px);
|
|
238
|
+
box-shadow: 0 8px 25px rgba(0,0,0,0.08);
|
|
239
|
+
}
|
|
240
|
+
[data-theme="dark"] .card:hover {
|
|
241
|
+
box-shadow: 0 8px 25px rgba(0,0,0,0.3);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.card-title {
|
|
245
|
+
font-size: 12px;
|
|
246
|
+
text-transform: uppercase;
|
|
247
|
+
letter-spacing: 0.5px;
|
|
248
|
+
color: var(--text-secondary);
|
|
249
|
+
margin-bottom: 8px;
|
|
250
|
+
display: flex;
|
|
251
|
+
align-items: center;
|
|
252
|
+
gap: 6px;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.card-value {
|
|
256
|
+
font-size: 32px;
|
|
257
|
+
font-weight: 600;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.card-sub {
|
|
261
|
+
font-size: 13px;
|
|
262
|
+
color: var(--text-secondary);
|
|
263
|
+
margin-top: 4px;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/* Progress bar */
|
|
267
|
+
.progress {
|
|
268
|
+
height: 6px;
|
|
269
|
+
background: var(--bg-tertiary);
|
|
270
|
+
border-radius: 3px;
|
|
271
|
+
margin-top: 12px;
|
|
272
|
+
overflow: hidden;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.progress-fill {
|
|
276
|
+
height: 100%;
|
|
277
|
+
background: linear-gradient(90deg, var(--accent), var(--success));
|
|
278
|
+
border-radius: 3px;
|
|
279
|
+
transition: width 0.3s;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/* Tasks list */
|
|
283
|
+
.task-list {
|
|
284
|
+
background: var(--bg-secondary);
|
|
285
|
+
border: 1px solid var(--border-color);
|
|
286
|
+
border-radius: 12px;
|
|
287
|
+
overflow: hidden;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.task-header {
|
|
291
|
+
padding: 16px 20px;
|
|
292
|
+
border-bottom: 1px solid var(--border-color);
|
|
293
|
+
display: flex;
|
|
294
|
+
justify-content: space-between;
|
|
295
|
+
align-items: center;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
.task-header h3 {
|
|
299
|
+
font-size: 16px;
|
|
300
|
+
font-weight: 600;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.task {
|
|
304
|
+
display: flex;
|
|
305
|
+
align-items: center;
|
|
306
|
+
padding: 14px 20px;
|
|
307
|
+
border-bottom: 1px solid var(--border-color);
|
|
308
|
+
transition: background 0.2s;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.task:last-child {
|
|
312
|
+
border-bottom: none;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
.task:hover {
|
|
316
|
+
background: var(--bg-tertiary);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.task-check {
|
|
320
|
+
width: 22px;
|
|
321
|
+
height: 22px;
|
|
322
|
+
border-radius: 50%;
|
|
323
|
+
border: 2px solid var(--border-color);
|
|
324
|
+
margin-right: 14px;
|
|
325
|
+
display: flex;
|
|
326
|
+
align-items: center;
|
|
327
|
+
justify-content: center;
|
|
328
|
+
font-size: 12px;
|
|
329
|
+
flex-shrink: 0;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
.task-check.done {
|
|
333
|
+
background: var(--success);
|
|
334
|
+
border-color: var(--success);
|
|
335
|
+
color: white;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
.task-check.progress {
|
|
339
|
+
background: var(--warning);
|
|
340
|
+
border-color: var(--warning);
|
|
341
|
+
color: white;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
.task-content {
|
|
345
|
+
flex: 1;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.task-title {
|
|
349
|
+
font-weight: 500;
|
|
350
|
+
margin-bottom: 4px;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
.task-meta {
|
|
354
|
+
font-size: 12px;
|
|
355
|
+
color: var(--text-secondary);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.task-phase {
|
|
359
|
+
background: var(--bg-tertiary);
|
|
360
|
+
padding: 4px 10px;
|
|
361
|
+
border-radius: 12px;
|
|
362
|
+
font-size: 11px;
|
|
363
|
+
color: var(--text-secondary);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/* Activity log */
|
|
367
|
+
.activity {
|
|
368
|
+
background: var(--bg-secondary);
|
|
369
|
+
border: 1px solid var(--border-color);
|
|
370
|
+
border-radius: 12px;
|
|
371
|
+
margin-top: 24px;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
.activity-header {
|
|
375
|
+
padding: 16px 20px;
|
|
376
|
+
border-bottom: 1px solid var(--border-color);
|
|
377
|
+
font-weight: 600;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
.activity-list {
|
|
381
|
+
max-height: 300px;
|
|
382
|
+
overflow-y: auto;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
.activity-item {
|
|
386
|
+
display: flex;
|
|
387
|
+
gap: 12px;
|
|
388
|
+
padding: 12px 20px;
|
|
389
|
+
border-bottom: 1px solid var(--border-color);
|
|
390
|
+
font-size: 13px;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
.activity-item:last-child {
|
|
394
|
+
border-bottom: none;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
.activity-time {
|
|
398
|
+
color: var(--text-secondary);
|
|
399
|
+
font-family: monospace;
|
|
400
|
+
flex-shrink: 0;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
.activity-dot {
|
|
404
|
+
width: 8px;
|
|
405
|
+
height: 8px;
|
|
406
|
+
border-radius: 50%;
|
|
407
|
+
margin-top: 5px;
|
|
408
|
+
flex-shrink: 0;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
.activity-dot.info { background: var(--accent); }
|
|
412
|
+
.activity-dot.success { background: var(--success); }
|
|
413
|
+
.activity-dot.warning { background: var(--warning); }
|
|
414
|
+
.activity-dot.error { background: var(--danger); }
|
|
415
|
+
|
|
416
|
+
.activity-text {
|
|
417
|
+
flex: 1;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
.activity-details {
|
|
421
|
+
color: var(--text-secondary);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/* Forms */
|
|
425
|
+
.form-section {
|
|
426
|
+
background: var(--bg-secondary);
|
|
427
|
+
border: 1px solid var(--border-color);
|
|
428
|
+
border-radius: 12px;
|
|
429
|
+
padding: 20px;
|
|
430
|
+
margin-top: 24px;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
.form-section h3 {
|
|
434
|
+
font-size: 16px;
|
|
435
|
+
margin-bottom: 16px;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
.form-row {
|
|
439
|
+
display: flex;
|
|
440
|
+
gap: 12px;
|
|
441
|
+
margin-bottom: 12px;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
input[type="text"] {
|
|
445
|
+
flex: 1;
|
|
446
|
+
padding: 10px 14px;
|
|
447
|
+
border: 1px solid var(--border-color);
|
|
448
|
+
border-radius: 8px;
|
|
449
|
+
background: var(--bg-primary);
|
|
450
|
+
color: var(--text-primary);
|
|
451
|
+
font-size: 14px;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
input[type="text"]:focus {
|
|
455
|
+
outline: none;
|
|
456
|
+
border-color: var(--accent);
|
|
457
|
+
box-shadow: 0 0 0 3px rgba(99,102,241,0.15);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
input[type="text"]::placeholder {
|
|
461
|
+
color: var(--text-secondary);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
textarea {
|
|
465
|
+
width: 100%;
|
|
466
|
+
padding: 10px 14px;
|
|
467
|
+
border: 1px solid var(--border-color);
|
|
468
|
+
border-radius: 8px;
|
|
469
|
+
background: var(--bg-primary);
|
|
470
|
+
color: var(--text-primary);
|
|
471
|
+
font-size: 14px;
|
|
472
|
+
font-family: inherit;
|
|
473
|
+
resize: vertical;
|
|
474
|
+
min-height: 60px;
|
|
475
|
+
box-sizing: border-box;
|
|
476
|
+
}
|
|
477
|
+
textarea:focus {
|
|
478
|
+
outline: none;
|
|
479
|
+
border-color: var(--accent);
|
|
480
|
+
box-shadow: 0 0 0 3px rgba(99,102,241,0.15);
|
|
481
|
+
}
|
|
482
|
+
textarea::placeholder {
|
|
483
|
+
color: var(--text-secondary);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
.priority-badge {
|
|
487
|
+
display: inline-block;
|
|
488
|
+
background: var(--accent);
|
|
489
|
+
color: white;
|
|
490
|
+
padding: 1px 7px;
|
|
491
|
+
border-radius: 10px;
|
|
492
|
+
font-size: 11px;
|
|
493
|
+
font-weight: 600;
|
|
494
|
+
margin-left: 4px;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
.task[draggable="true"] {
|
|
498
|
+
cursor: grab;
|
|
499
|
+
}
|
|
500
|
+
.task[draggable="true"]:active {
|
|
501
|
+
cursor: grabbing;
|
|
502
|
+
}
|
|
503
|
+
.task.drag-over {
|
|
504
|
+
border-top: 2px solid var(--accent);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/* Terminal-style log panel */
|
|
508
|
+
.log-panel {
|
|
509
|
+
background: #1e1e2e;
|
|
510
|
+
border-radius: 12px;
|
|
511
|
+
margin-top: 24px;
|
|
512
|
+
overflow: hidden;
|
|
513
|
+
box-shadow: 0 4px 24px rgba(0,0,0,0.3);
|
|
514
|
+
border: 1px solid #313244;
|
|
515
|
+
}
|
|
516
|
+
.log-panel-header {
|
|
517
|
+
padding: 10px 16px;
|
|
518
|
+
background: #181825;
|
|
519
|
+
border-bottom: 1px solid #313244;
|
|
520
|
+
display: flex;
|
|
521
|
+
justify-content: space-between;
|
|
522
|
+
align-items: center;
|
|
523
|
+
}
|
|
524
|
+
.terminal-dots {
|
|
525
|
+
display: flex;
|
|
526
|
+
gap: 6px;
|
|
527
|
+
align-items: center;
|
|
528
|
+
}
|
|
529
|
+
.terminal-dots span {
|
|
530
|
+
width: 12px;
|
|
531
|
+
height: 12px;
|
|
532
|
+
border-radius: 50%;
|
|
533
|
+
display: inline-block;
|
|
534
|
+
}
|
|
535
|
+
.terminal-dots .dot-red { background: #f38ba8; }
|
|
536
|
+
.terminal-dots .dot-yellow { background: #f9e2af; }
|
|
537
|
+
.terminal-dots .dot-green { background: #a6e3a1; }
|
|
538
|
+
.terminal-title {
|
|
539
|
+
font-family: 'SF Mono', 'Fira Code', 'JetBrains Mono', monospace;
|
|
540
|
+
font-size: 12px;
|
|
541
|
+
color: #6c7086;
|
|
542
|
+
letter-spacing: 0.5px;
|
|
543
|
+
}
|
|
544
|
+
.log-feed {
|
|
545
|
+
max-height: 340px;
|
|
546
|
+
overflow-y: auto;
|
|
547
|
+
padding: 4px 0;
|
|
548
|
+
scrollbar-width: thin;
|
|
549
|
+
scrollbar-color: #45475a #1e1e2e;
|
|
550
|
+
}
|
|
551
|
+
.log-feed::-webkit-scrollbar { width: 6px; }
|
|
552
|
+
.log-feed::-webkit-scrollbar-track { background: #1e1e2e; }
|
|
553
|
+
.log-feed::-webkit-scrollbar-thumb { background: #45475a; border-radius: 3px; }
|
|
554
|
+
.log-entry {
|
|
555
|
+
display: flex;
|
|
556
|
+
gap: 8px;
|
|
557
|
+
padding: 3px 16px;
|
|
558
|
+
font-family: 'SF Mono', 'Fira Code', 'JetBrains Mono', monospace;
|
|
559
|
+
font-size: 12px;
|
|
560
|
+
align-items: flex-start;
|
|
561
|
+
line-height: 1.5;
|
|
562
|
+
transition: background 0.15s;
|
|
563
|
+
}
|
|
564
|
+
.log-entry:hover {
|
|
565
|
+
background: rgba(69, 71, 90, 0.3);
|
|
566
|
+
}
|
|
567
|
+
.log-entry i {
|
|
568
|
+
width: 16px;
|
|
569
|
+
text-align: center;
|
|
570
|
+
flex-shrink: 0;
|
|
571
|
+
font-size: 11px;
|
|
572
|
+
margin-top: 2px;
|
|
573
|
+
}
|
|
574
|
+
.log-time {
|
|
575
|
+
color: #585b70;
|
|
576
|
+
font-size: 11px;
|
|
577
|
+
flex-shrink: 0;
|
|
578
|
+
user-select: none;
|
|
579
|
+
}
|
|
580
|
+
.log-text {
|
|
581
|
+
flex: 1;
|
|
582
|
+
overflow: hidden;
|
|
583
|
+
text-overflow: ellipsis;
|
|
584
|
+
white-space: nowrap;
|
|
585
|
+
color: #cdd6f4;
|
|
586
|
+
}
|
|
587
|
+
.log-prompt {
|
|
588
|
+
color: #a6e3a1;
|
|
589
|
+
flex-shrink: 0;
|
|
590
|
+
user-select: none;
|
|
591
|
+
}
|
|
592
|
+
.log-cursor {
|
|
593
|
+
display: inline-block;
|
|
594
|
+
width: 7px;
|
|
595
|
+
height: 14px;
|
|
596
|
+
background: #a6e3a1;
|
|
597
|
+
animation: blink 1s step-end infinite;
|
|
598
|
+
margin-left: 4px;
|
|
599
|
+
vertical-align: text-bottom;
|
|
600
|
+
}
|
|
601
|
+
@keyframes blink {
|
|
602
|
+
0%, 100% { opacity: 1; }
|
|
603
|
+
50% { opacity: 0; }
|
|
604
|
+
}
|
|
605
|
+
.log-empty {
|
|
606
|
+
padding: 20px 16px;
|
|
607
|
+
color: #585b70;
|
|
608
|
+
font-family: 'SF Mono', 'Fira Code', 'JetBrains Mono', monospace;
|
|
609
|
+
font-size: 12px;
|
|
610
|
+
text-align: center;
|
|
611
|
+
}
|
|
612
|
+
.raw-log {
|
|
613
|
+
max-height: 300px;
|
|
614
|
+
overflow-y: auto;
|
|
615
|
+
padding: 12px 16px;
|
|
616
|
+
font-family: 'SF Mono', 'Fira Code', 'JetBrains Mono', monospace;
|
|
617
|
+
font-size: 11px;
|
|
618
|
+
white-space: pre-wrap;
|
|
619
|
+
word-break: break-all;
|
|
620
|
+
background: #11111b;
|
|
621
|
+
color: #a6adc8;
|
|
622
|
+
border-top: 1px solid #313244;
|
|
623
|
+
}
|
|
624
|
+
.log-toggle {
|
|
625
|
+
font-family: 'SF Mono', 'Fira Code', 'JetBrains Mono', monospace;
|
|
626
|
+
font-size: 11px;
|
|
627
|
+
color: #89b4fa;
|
|
628
|
+
cursor: pointer;
|
|
629
|
+
background: none;
|
|
630
|
+
border: none;
|
|
631
|
+
padding: 2px 8px;
|
|
632
|
+
border-radius: 4px;
|
|
633
|
+
transition: background 0.15s;
|
|
634
|
+
}
|
|
635
|
+
.log-toggle:hover {
|
|
636
|
+
background: rgba(137, 180, 250, 0.1);
|
|
637
|
+
}
|
|
638
|
+
/* Pixel art indicator */
|
|
639
|
+
.px-icon {
|
|
640
|
+
display: inline-block;
|
|
641
|
+
width: 6px;
|
|
642
|
+
height: 6px;
|
|
643
|
+
border-radius: 1px;
|
|
644
|
+
flex-shrink: 0;
|
|
645
|
+
margin-top: 5px;
|
|
646
|
+
image-rendering: pixelated;
|
|
647
|
+
box-shadow: 1px 0 0 0 currentColor, 0 1px 0 0 currentColor;
|
|
648
|
+
}
|
|
649
|
+
.log-label {
|
|
650
|
+
font-family: 'SF Mono', 'Fira Code', 'JetBrains Mono', monospace;
|
|
651
|
+
font-size: 10px;
|
|
652
|
+
font-weight: 600;
|
|
653
|
+
letter-spacing: 0.5px;
|
|
654
|
+
min-width: 44px;
|
|
655
|
+
flex-shrink: 0;
|
|
656
|
+
text-transform: uppercase;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
button {
|
|
660
|
+
padding: 10px 20px;
|
|
661
|
+
border: none;
|
|
662
|
+
border-radius: 8px;
|
|
663
|
+
font-size: 14px;
|
|
664
|
+
font-weight: 500;
|
|
665
|
+
cursor: pointer;
|
|
666
|
+
display: inline-flex;
|
|
667
|
+
align-items: center;
|
|
668
|
+
gap: 6px;
|
|
669
|
+
transition: transform 0.1s, opacity 0.2s;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
button:hover { filter: brightness(1.1); }
|
|
673
|
+
button:active { transform: scale(0.98); }
|
|
674
|
+
|
|
675
|
+
.btn-primary { background: var(--accent); color: white; }
|
|
676
|
+
.btn-primary:hover { box-shadow: 0 4px 12px rgba(99,102,241,0.3); }
|
|
677
|
+
.btn-success { background: var(--success); color: white; }
|
|
678
|
+
.btn-success:hover { box-shadow: 0 4px 12px rgba(16,185,129,0.3); }
|
|
679
|
+
.btn-danger { background: var(--danger); color: white; }
|
|
680
|
+
.btn-secondary { background: var(--bg-tertiary); color: var(--text-primary); }
|
|
681
|
+
|
|
682
|
+
/* Global select styling */
|
|
683
|
+
select {
|
|
684
|
+
padding: 10px 14px;
|
|
685
|
+
border: 1px solid var(--border-color);
|
|
686
|
+
border-radius: 8px;
|
|
687
|
+
background: var(--bg-primary);
|
|
688
|
+
color: var(--text-primary);
|
|
689
|
+
font-size: 14px;
|
|
690
|
+
cursor: pointer;
|
|
691
|
+
width: 100%;
|
|
692
|
+
transition: border-color 0.2s, box-shadow 0.2s;
|
|
693
|
+
}
|
|
694
|
+
select:focus {
|
|
695
|
+
outline: none;
|
|
696
|
+
border-color: var(--accent);
|
|
697
|
+
box-shadow: 0 0 0 3px rgba(99,102,241,0.15);
|
|
698
|
+
}
|
|
699
|
+
input[type="number"] {
|
|
700
|
+
padding: 10px 14px;
|
|
701
|
+
border: 1px solid var(--border-color);
|
|
702
|
+
border-radius: 8px;
|
|
703
|
+
background: var(--bg-primary);
|
|
704
|
+
color: var(--text-primary);
|
|
705
|
+
font-size: 14px;
|
|
706
|
+
width: 100%;
|
|
707
|
+
transition: border-color 0.2s, box-shadow 0.2s;
|
|
708
|
+
}
|
|
709
|
+
input[type="number"]:focus {
|
|
710
|
+
outline: none;
|
|
711
|
+
border-color: var(--accent);
|
|
712
|
+
box-shadow: 0 0 0 3px rgba(99,102,241,0.15);
|
|
713
|
+
}
|
|
714
|
+
input[type="password"] {
|
|
715
|
+
padding: 10px 14px;
|
|
716
|
+
border: 1px solid var(--border-color);
|
|
717
|
+
border-radius: 8px;
|
|
718
|
+
background: var(--bg-primary);
|
|
719
|
+
color: var(--text-primary);
|
|
720
|
+
font-size: 14px;
|
|
721
|
+
font-family: monospace;
|
|
722
|
+
width: 100%;
|
|
723
|
+
transition: border-color 0.2s, box-shadow 0.2s;
|
|
724
|
+
}
|
|
725
|
+
input[type="password"]:focus {
|
|
726
|
+
outline: none;
|
|
727
|
+
border-color: var(--accent);
|
|
728
|
+
box-shadow: 0 0 0 3px rgba(99,102,241,0.15);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/* Styled scrollbars */
|
|
732
|
+
.activity-list::-webkit-scrollbar { width: 6px; }
|
|
733
|
+
.activity-list::-webkit-scrollbar-track { background: transparent; }
|
|
734
|
+
.activity-list::-webkit-scrollbar-thumb { background: var(--border-color); border-radius: 3px; }
|
|
735
|
+
|
|
736
|
+
/* Toast notification */
|
|
737
|
+
.toast {
|
|
738
|
+
position: fixed;
|
|
739
|
+
bottom: 24px;
|
|
740
|
+
right: 24px;
|
|
741
|
+
padding: 12px 20px;
|
|
742
|
+
background: var(--success);
|
|
743
|
+
color: white;
|
|
744
|
+
border-radius: 8px;
|
|
745
|
+
font-size: 14px;
|
|
746
|
+
font-weight: 500;
|
|
747
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
|
|
748
|
+
z-index: 1000;
|
|
749
|
+
animation: toast-in 0.3s ease, toast-out 0.3s ease 2s forwards;
|
|
750
|
+
}
|
|
751
|
+
@keyframes toast-in { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
|
|
752
|
+
@keyframes toast-out { from { opacity: 1; } to { opacity: 0; transform: translateY(10px); } }
|
|
753
|
+
|
|
754
|
+
/* Settings-specific styles */
|
|
755
|
+
.settings-label {
|
|
756
|
+
font-size: 12px;
|
|
757
|
+
color: var(--text-secondary);
|
|
758
|
+
margin-bottom: 4px;
|
|
759
|
+
display: block;
|
|
760
|
+
}
|
|
761
|
+
.settings-grid {
|
|
762
|
+
display: grid;
|
|
763
|
+
grid-template-columns: 1fr 1fr;
|
|
764
|
+
gap: 12px;
|
|
765
|
+
margin-top: 8px;
|
|
766
|
+
}
|
|
767
|
+
.settings-hint {
|
|
768
|
+
color: var(--text-secondary);
|
|
769
|
+
font-size: 12px;
|
|
770
|
+
margin-top: 6px;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/* Commit styling */
|
|
774
|
+
.commit-hash {
|
|
775
|
+
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
776
|
+
font-size: 12px;
|
|
777
|
+
color: var(--text-secondary);
|
|
778
|
+
background: var(--bg-tertiary);
|
|
779
|
+
padding: 2px 8px;
|
|
780
|
+
border-radius: 4px;
|
|
781
|
+
}
|
|
782
|
+
.commit-msg {
|
|
783
|
+
font-weight: 500;
|
|
784
|
+
}
|
|
785
|
+
.commit-time {
|
|
786
|
+
font-size: 12px;
|
|
787
|
+
color: var(--text-secondary);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/* Control buttons */
|
|
791
|
+
.controls {
|
|
792
|
+
display: flex;
|
|
793
|
+
gap: 12px;
|
|
794
|
+
margin-top: 24px;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
.controls form {
|
|
798
|
+
flex: 1;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
.controls button {
|
|
802
|
+
width: 100%;
|
|
803
|
+
padding: 14px;
|
|
804
|
+
font-size: 15px;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/* Theme toggle */
|
|
808
|
+
.theme-toggle {
|
|
809
|
+
background: none;
|
|
810
|
+
border: 1px solid var(--border-color);
|
|
811
|
+
padding: 8px 12px;
|
|
812
|
+
cursor: pointer;
|
|
813
|
+
border-radius: 8px;
|
|
814
|
+
color: var(--text-primary);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/* Session details */
|
|
818
|
+
.session-card {
|
|
819
|
+
background: var(--bg-secondary);
|
|
820
|
+
border: 1px solid var(--border-color);
|
|
821
|
+
border-radius: 12px;
|
|
822
|
+
padding: 20px;
|
|
823
|
+
margin-bottom: 16px;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
.session-header {
|
|
827
|
+
display: flex;
|
|
828
|
+
justify-content: space-between;
|
|
829
|
+
align-items: center;
|
|
830
|
+
margin-bottom: 16px;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
.session-title {
|
|
834
|
+
font-weight: 600;
|
|
835
|
+
font-size: 16px;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
.session-meta {
|
|
839
|
+
display: grid;
|
|
840
|
+
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
841
|
+
gap: 16px;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
.meta-item {
|
|
845
|
+
display: flex;
|
|
846
|
+
flex-direction: column;
|
|
847
|
+
gap: 4px;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
.meta-label {
|
|
851
|
+
font-size: 11px;
|
|
852
|
+
text-transform: uppercase;
|
|
853
|
+
color: var(--text-secondary);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
.meta-value {
|
|
857
|
+
font-weight: 500;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/* Empty state */
|
|
861
|
+
.empty {
|
|
862
|
+
text-align: center;
|
|
863
|
+
padding: 40px;
|
|
864
|
+
color: var(--text-secondary);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
.empty i {
|
|
868
|
+
font-size: 48px;
|
|
869
|
+
margin-bottom: 16px;
|
|
870
|
+
opacity: 0.5;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/* Pulsing indicator for running state */
|
|
874
|
+
.pulse-dot {
|
|
875
|
+
display: inline-block;
|
|
876
|
+
width: 8px;
|
|
877
|
+
height: 8px;
|
|
878
|
+
border-radius: 50%;
|
|
879
|
+
background: #fff;
|
|
880
|
+
margin: 0 2px;
|
|
881
|
+
animation: pulse-anim 1.5s ease-in-out infinite;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
@keyframes pulse-anim {
|
|
885
|
+
0%, 100% { opacity: 1; transform: scale(1); }
|
|
886
|
+
50% { opacity: 0.4; transform: scale(0.7); }
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/* Live timer styling */
|
|
890
|
+
.live-timer {
|
|
891
|
+
font-family: monospace;
|
|
892
|
+
font-variant-numeric: tabular-nums;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
@media (max-width: 1200px) {
|
|
896
|
+
.cards {
|
|
897
|
+
grid-template-columns: repeat(2, 1fr);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
@media (max-width: 768px) {
|
|
902
|
+
.layout {
|
|
903
|
+
grid-template-columns: 1fr;
|
|
904
|
+
}
|
|
905
|
+
.sidebar {
|
|
906
|
+
position: fixed;
|
|
907
|
+
left: -260px;
|
|
908
|
+
z-index: 100;
|
|
909
|
+
transition: left 0.3s;
|
|
910
|
+
width: 240px;
|
|
911
|
+
}
|
|
912
|
+
.sidebar.open {
|
|
913
|
+
left: 0;
|
|
914
|
+
}
|
|
915
|
+
.hamburger {
|
|
916
|
+
display: block !important;
|
|
917
|
+
}
|
|
918
|
+
.cards {
|
|
919
|
+
grid-template-columns: 1fr;
|
|
920
|
+
}
|
|
921
|
+
.log-feed {
|
|
922
|
+
max-height: none;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
.hamburger {
|
|
927
|
+
display: none;
|
|
928
|
+
background: none;
|
|
929
|
+
border: 1px solid var(--border-color);
|
|
930
|
+
padding: 8px 12px;
|
|
931
|
+
cursor: pointer;
|
|
932
|
+
border-radius: 8px;
|
|
933
|
+
color: var(--text-primary);
|
|
934
|
+
font-size: 18px;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
/* Task detail expandable — slide transition */
|
|
938
|
+
.task-detail {
|
|
939
|
+
max-height: 0;
|
|
940
|
+
overflow: hidden;
|
|
941
|
+
padding: 0 20px 0 56px;
|
|
942
|
+
border-bottom: 1px solid var(--border-color);
|
|
943
|
+
transition: max-height 0.3s ease, padding 0.3s ease;
|
|
944
|
+
}
|
|
945
|
+
.task-detail.open {
|
|
946
|
+
max-height: 400px;
|
|
947
|
+
padding: 12px 20px 16px 56px;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
.task-stages {
|
|
951
|
+
display: flex;
|
|
952
|
+
gap: 4px;
|
|
953
|
+
margin-bottom: 12px;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
.stage-step {
|
|
957
|
+
flex: 1;
|
|
958
|
+
height: 6px;
|
|
959
|
+
border-radius: 3px;
|
|
960
|
+
background: var(--bg-tertiary);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
.stage-step.active {
|
|
964
|
+
background: var(--warning);
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
.stage-step.done {
|
|
968
|
+
background: var(--success);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
.task-detail-meta {
|
|
972
|
+
display: grid;
|
|
973
|
+
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
|
974
|
+
gap: 8px;
|
|
975
|
+
font-size: 12px;
|
|
976
|
+
color: var(--text-secondary);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
.task-detail-meta dt {
|
|
980
|
+
font-weight: 600;
|
|
981
|
+
color: var(--text-primary);
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
.task-criteria {
|
|
985
|
+
margin-top: 8px;
|
|
986
|
+
padding: 8px 12px;
|
|
987
|
+
background: var(--bg-tertiary);
|
|
988
|
+
border-radius: 8px;
|
|
989
|
+
font-size: 12px;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
.task-clickable {
|
|
993
|
+
cursor: pointer;
|
|
994
|
+
}
|
|
995
|
+
'''
|
|
996
|
+
|
|
997
|
+
HTML_TEMPLATE = Template('''<!DOCTYPE html>
|
|
998
|
+
<html lang="en" data-theme="$theme">
|
|
999
|
+
<head>
|
|
1000
|
+
<meta charset="UTF-8">
|
|
1001
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1002
|
+
<title>PocketCoder-A1 Dashboard</title>
|
|
1003
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
|
1004
|
+
<style>''' + CSS + '''</style>
|
|
1005
|
+
</head>
|
|
1006
|
+
<body>
|
|
1007
|
+
<div class="layout">
|
|
1008
|
+
<aside class="sidebar">
|
|
1009
|
+
<div class="logo">
|
|
1010
|
+
<i class="bi bi-robot"></i> PocketCoder-A1
|
|
1011
|
+
</div>
|
|
1012
|
+
<div class="logo-sub">Autonomous Gnome v0.1.0</div>
|
|
1013
|
+
|
|
1014
|
+
<nav>
|
|
1015
|
+
<div class="nav-section">
|
|
1016
|
+
<div class="nav-title">Overview</div>
|
|
1017
|
+
<a href="/" class="nav-item $nav_dashboard">
|
|
1018
|
+
<i class="bi bi-speedometer2"></i> Dashboard
|
|
1019
|
+
</a>
|
|
1020
|
+
<a href="/tasks" class="nav-item $nav_tasks">
|
|
1021
|
+
<i class="bi bi-list-check"></i> Tasks
|
|
1022
|
+
</a>
|
|
1023
|
+
<a href="/sessions" class="nav-item $nav_sessions">
|
|
1024
|
+
<i class="bi bi-terminal"></i> Sessions
|
|
1025
|
+
</a>
|
|
1026
|
+
</div>
|
|
1027
|
+
|
|
1028
|
+
<div class="nav-section">
|
|
1029
|
+
<div class="nav-title">Activity</div>
|
|
1030
|
+
<a href="/log" class="nav-item $nav_log">
|
|
1031
|
+
<i class="bi bi-journal-text"></i> Activity Log
|
|
1032
|
+
</a>
|
|
1033
|
+
<a href="/commits" class="nav-item $nav_commits">
|
|
1034
|
+
<i class="bi bi-git"></i> Commits
|
|
1035
|
+
</a>
|
|
1036
|
+
<a href="/transform" class="nav-item $nav_transform">
|
|
1037
|
+
<i class="bi bi-magic"></i> Transform
|
|
1038
|
+
</a>
|
|
1039
|
+
</div>
|
|
1040
|
+
|
|
1041
|
+
<div class="nav-section">
|
|
1042
|
+
<div class="nav-title">Settings</div>
|
|
1043
|
+
<a href="/settings" class="nav-item $nav_settings">
|
|
1044
|
+
<i class="bi bi-gear"></i> Settings
|
|
1045
|
+
</a>
|
|
1046
|
+
</div>
|
|
1047
|
+
</nav>
|
|
1048
|
+
</aside>
|
|
1049
|
+
|
|
1050
|
+
<main class="main">
|
|
1051
|
+
<button class="hamburger" onclick="document.querySelector('.sidebar').classList.toggle('open')">
|
|
1052
|
+
<i class="bi bi-list"></i>
|
|
1053
|
+
</button>
|
|
1054
|
+
$content
|
|
1055
|
+
</main>
|
|
1056
|
+
</div>
|
|
1057
|
+
|
|
1058
|
+
<script>
|
|
1059
|
+
// Theme toggle
|
|
1060
|
+
function toggleTheme() {
|
|
1061
|
+
const html = document.documentElement;
|
|
1062
|
+
const current = html.getAttribute('data-theme');
|
|
1063
|
+
const next = current === 'dark' ? 'light' : 'dark';
|
|
1064
|
+
html.setAttribute('data-theme', next);
|
|
1065
|
+
localStorage.setItem('theme', next);
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// Load saved theme
|
|
1069
|
+
const saved = localStorage.getItem('theme');
|
|
1070
|
+
if (saved) {
|
|
1071
|
+
document.documentElement.setAttribute('data-theme', saved);
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// Escape HTML
|
|
1075
|
+
function escHtml(s) {
|
|
1076
|
+
const d = document.createElement('div');
|
|
1077
|
+
d.textContent = s;
|
|
1078
|
+
return d.innerHTML;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// --- AJAX polling (dashboard page only) ---
|
|
1082
|
+
const pageName = '$page_name';
|
|
1083
|
+
let logIndex = 0;
|
|
1084
|
+
|
|
1085
|
+
function fmtTokens(n) {
|
|
1086
|
+
if (!n || n === 0) return '0';
|
|
1087
|
+
if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
|
|
1088
|
+
if (n >= 1000) return (n / 1000).toFixed(1) + 'K';
|
|
1089
|
+
return n.toString();
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
function fmtDuration(s) {
|
|
1093
|
+
if (!s || s <= 0) return '0s';
|
|
1094
|
+
if (s < 60) return s + 's';
|
|
1095
|
+
const m = Math.floor(s / 60);
|
|
1096
|
+
const sec = s % 60;
|
|
1097
|
+
return m + 'm ' + sec + 's';
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
function estimateCost(tokensIn, tokensOut, cacheRead) {
|
|
1101
|
+
// Claude Opus pricing (approximate): 15/M input, 75/M output, 1.5/M cache read
|
|
1102
|
+
const costIn = (tokensIn - (cacheRead || 0)) * 15 / 1000000;
|
|
1103
|
+
const costCache = (cacheRead || 0) * 1.5 / 1000000;
|
|
1104
|
+
const costOut = tokensOut * 75 / 1000000;
|
|
1105
|
+
return Math.max(0, costIn + costCache + costOut);
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
function updateStatus() {
|
|
1109
|
+
fetch('/api/status')
|
|
1110
|
+
.then(r => r.json())
|
|
1111
|
+
.then(data => {
|
|
1112
|
+
const badge = document.getElementById('status-badge');
|
|
1113
|
+
if (badge) {
|
|
1114
|
+
if (data.running) {
|
|
1115
|
+
badge.className = 'status status-running';
|
|
1116
|
+
badge.innerHTML = '<i class="bi bi-play-circle-fill"></i> <span class="pulse-dot"></span> Running';
|
|
1117
|
+
} else if (data.checkpoint && data.checkpoint.status === 'COMPLETED') {
|
|
1118
|
+
badge.className = 'status status-completed';
|
|
1119
|
+
badge.innerHTML = '<i class="bi bi-check-circle-fill"></i> Completed';
|
|
1120
|
+
} else {
|
|
1121
|
+
badge.className = 'status status-stopped';
|
|
1122
|
+
badge.innerHTML = '<i class="bi bi-stop-circle-fill"></i> Stopped';
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
const tc = document.getElementById('task-count');
|
|
1126
|
+
if (tc && data.progress) {
|
|
1127
|
+
tc.textContent = data.progress[0] + '/' + data.progress[1];
|
|
1128
|
+
}
|
|
1129
|
+
const sc = document.getElementById('session-count');
|
|
1130
|
+
if (sc && data.checkpoint) {
|
|
1131
|
+
sc.textContent = '#' + data.checkpoint.session;
|
|
1132
|
+
}
|
|
1133
|
+
const fc = document.getElementById('files-count');
|
|
1134
|
+
if (fc && data.checkpoint) {
|
|
1135
|
+
fc.textContent = (data.checkpoint.files_modified || []).length;
|
|
1136
|
+
}
|
|
1137
|
+
// Token metrics
|
|
1138
|
+
const m = data.metrics || {};
|
|
1139
|
+
const tokC = document.getElementById('tokens-count');
|
|
1140
|
+
if (tokC) {
|
|
1141
|
+
tokC.textContent = fmtTokens(m.tokens_in || 0) + ' / ' + fmtTokens(m.tokens_out || 0);
|
|
1142
|
+
}
|
|
1143
|
+
const tokSub = document.getElementById('tokens-sub');
|
|
1144
|
+
if (tokSub && m.cache_read) {
|
|
1145
|
+
tokSub.textContent = 'cache: ' + fmtTokens(m.cache_read);
|
|
1146
|
+
}
|
|
1147
|
+
const tokBar = document.getElementById('tokens-bar');
|
|
1148
|
+
if (tokBar) {
|
|
1149
|
+
const pct = Math.min(100, (m.context_percent || 0) * 100);
|
|
1150
|
+
tokBar.style.width = pct + '%';
|
|
1151
|
+
// Change color at threshold
|
|
1152
|
+
if (pct >= 70) tokBar.style.background = '#ef4444';
|
|
1153
|
+
else if (pct >= 50) tokBar.style.background = '#f59e0b';
|
|
1154
|
+
else tokBar.style.background = 'var(--accent)';
|
|
1155
|
+
}
|
|
1156
|
+
// Context percent text
|
|
1157
|
+
const ctxPct = document.getElementById('context-pct');
|
|
1158
|
+
if (ctxPct) {
|
|
1159
|
+
const pct = Math.round((m.context_percent || 0) * 100);
|
|
1160
|
+
ctxPct.textContent = pct + '%';
|
|
1161
|
+
}
|
|
1162
|
+
// Cost
|
|
1163
|
+
const costEl = document.getElementById('cost-value');
|
|
1164
|
+
if (costEl) {
|
|
1165
|
+
const cost = estimateCost(m.tokens_in || 0, m.tokens_out || 0, m.cache_read || 0);
|
|
1166
|
+
costEl.textContent = '$$' + cost.toFixed(3);
|
|
1167
|
+
}
|
|
1168
|
+
// Duration
|
|
1169
|
+
const durEl = document.getElementById('duration-value');
|
|
1170
|
+
if (durEl) {
|
|
1171
|
+
durEl.textContent = fmtDuration(m.session_duration || 0);
|
|
1172
|
+
}
|
|
1173
|
+
// Queue message section
|
|
1174
|
+
const qms = document.getElementById('queue-msg-section');
|
|
1175
|
+
if (qms) {
|
|
1176
|
+
qms.style.display = data.running ? 'block' : 'none';
|
|
1177
|
+
}
|
|
1178
|
+
})
|
|
1179
|
+
.catch(() => {});
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
function updateLog() {
|
|
1183
|
+
fetch('/api/log?since=' + logIndex)
|
|
1184
|
+
.then(r => r.json())
|
|
1185
|
+
.then(data => {
|
|
1186
|
+
if (data.entries && data.entries.length > 0) {
|
|
1187
|
+
const feed = document.getElementById('action-feed');
|
|
1188
|
+
const rawlog = document.getElementById('raw-log');
|
|
1189
|
+
if (feed && rawlog) {
|
|
1190
|
+
data.entries.forEach(e => {
|
|
1191
|
+
const labelMap = {read:'READ', edit:'EDIT', write:'WRITE', bash:'BASH', thinking:'THINK', text:'OUT', metric:'METRIC', verify:'CHECK'};
|
|
1192
|
+
const colorMap = {read:'#89b4fa', edit:'#fab387', write:'#a6e3a1', bash:'#cba6f7', thinking:'#f9e2af', text:'#6c7086', metric:'#89dceb', verify:'#a6e3a1'};
|
|
1193
|
+
const label = labelMap[e.type] || 'LOG';
|
|
1194
|
+
const color = colorMap[e.type] || '#6c7086';
|
|
1195
|
+
const pixelIcon = '<span class="px-icon" style="background:' + color + '"></span>';
|
|
1196
|
+
feed.innerHTML += '<div class="log-entry">' + pixelIcon + '<span class="log-time">' + e.time + '</span><span class="log-label" style="color:' + color + '">' + label + '</span><span class="log-text">' + escHtml(e.line.substring(0,150)) + '</span></div>';
|
|
1197
|
+
rawlog.textContent += e.line + '\\n';
|
|
1198
|
+
});
|
|
1199
|
+
feed.scrollTop = feed.scrollHeight;
|
|
1200
|
+
rawlog.scrollTop = rawlog.scrollHeight;
|
|
1201
|
+
}
|
|
1202
|
+
logIndex = data.total;
|
|
1203
|
+
}
|
|
1204
|
+
})
|
|
1205
|
+
.catch(() => {});
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
function sendQueueMessage(e) {
|
|
1209
|
+
e.preventDefault();
|
|
1210
|
+
const input = document.getElementById('queue-msg-input');
|
|
1211
|
+
const status = document.getElementById('queue-msg-status');
|
|
1212
|
+
if (!input.value.trim()) return;
|
|
1213
|
+
fetch('/queue-message', {
|
|
1214
|
+
method: 'POST',
|
|
1215
|
+
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
|
1216
|
+
body: 'message=' + encodeURIComponent(input.value)
|
|
1217
|
+
}).then(r => r.json()).then(data => {
|
|
1218
|
+
status.textContent = 'Message queued at ' + new Date().toLocaleTimeString();
|
|
1219
|
+
input.value = '';
|
|
1220
|
+
}).catch(() => { status.textContent = 'Failed to send'; });
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
// Live timer tick (updates every second when running)
|
|
1224
|
+
let sessionStartTime = 0;
|
|
1225
|
+
let agentRunning = false;
|
|
1226
|
+
|
|
1227
|
+
function tickTimer() {
|
|
1228
|
+
if (!agentRunning || !sessionStartTime) return;
|
|
1229
|
+
const elapsed = Math.floor(Date.now() / 1000 - sessionStartTime);
|
|
1230
|
+
const durEl = document.getElementById('duration-value');
|
|
1231
|
+
if (durEl) durEl.textContent = fmtDuration(elapsed);
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
// Toggle task detail on tasks page
|
|
1235
|
+
function toggleTaskDetail(taskId) {
|
|
1236
|
+
const el = document.getElementById('detail-' + taskId);
|
|
1237
|
+
if (el) el.classList.toggle('open');
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
if (pageName === 'dashboard') {
|
|
1241
|
+
// Wrap updateStatus to also track timer state
|
|
1242
|
+
const _origUpdateStatus = updateStatus;
|
|
1243
|
+
updateStatus = function() {
|
|
1244
|
+
fetch('/api/status')
|
|
1245
|
+
.then(r => r.json())
|
|
1246
|
+
.then(data => {
|
|
1247
|
+
agentRunning = data.running;
|
|
1248
|
+
if (data.metrics && data.metrics.session_start) {
|
|
1249
|
+
sessionStartTime = data.metrics.session_start;
|
|
1250
|
+
}
|
|
1251
|
+
}).catch(() => {});
|
|
1252
|
+
_origUpdateStatus();
|
|
1253
|
+
};
|
|
1254
|
+
setInterval(updateStatus, 3000);
|
|
1255
|
+
setInterval(updateLog, 2000);
|
|
1256
|
+
setInterval(tickTimer, 1000);
|
|
1257
|
+
updateStatus();
|
|
1258
|
+
} else if (pageName === 'tasks') {
|
|
1259
|
+
// No auto-reload on tasks page (drag-drop needs stable DOM)
|
|
1260
|
+
} else {
|
|
1261
|
+
setTimeout(() => location.reload(), 5000);
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
// --- Drag and Drop (tasks page) ---
|
|
1265
|
+
if (pageName === 'tasks') {
|
|
1266
|
+
let dragSrc = null;
|
|
1267
|
+
document.querySelectorAll('.task[draggable]').forEach(el => {
|
|
1268
|
+
el.addEventListener('dragstart', e => {
|
|
1269
|
+
dragSrc = el;
|
|
1270
|
+
el.style.opacity = '0.4';
|
|
1271
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
1272
|
+
});
|
|
1273
|
+
el.addEventListener('dragover', e => {
|
|
1274
|
+
e.preventDefault();
|
|
1275
|
+
e.dataTransfer.dropEffect = 'move';
|
|
1276
|
+
el.classList.add('drag-over');
|
|
1277
|
+
});
|
|
1278
|
+
el.addEventListener('dragleave', () => {
|
|
1279
|
+
el.classList.remove('drag-over');
|
|
1280
|
+
});
|
|
1281
|
+
el.addEventListener('drop', e => {
|
|
1282
|
+
e.preventDefault();
|
|
1283
|
+
el.classList.remove('drag-over');
|
|
1284
|
+
if (dragSrc && dragSrc !== el) {
|
|
1285
|
+
el.parentNode.insertBefore(dragSrc, el);
|
|
1286
|
+
const order = [...document.querySelectorAll('.task[data-task-id]')]
|
|
1287
|
+
.map(t => t.dataset.taskId);
|
|
1288
|
+
fetch('/api/reorder', {
|
|
1289
|
+
method: 'POST',
|
|
1290
|
+
headers: {'Content-Type': 'application/json'},
|
|
1291
|
+
body: JSON.stringify({order})
|
|
1292
|
+
}).then(() => location.reload());
|
|
1293
|
+
}
|
|
1294
|
+
});
|
|
1295
|
+
el.addEventListener('dragend', () => {
|
|
1296
|
+
el.style.opacity = '';
|
|
1297
|
+
});
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1300
|
+
</script>
|
|
1301
|
+
</body>
|
|
1302
|
+
</html>
|
|
1303
|
+
''')
|
|
1304
|
+
|
|
1305
|
+
|
|
1306
|
+
class DashboardHandler(BaseHTTPRequestHandler):
|
|
1307
|
+
def log_message(self, format, *args):
|
|
1308
|
+
pass
|
|
1309
|
+
|
|
1310
|
+
def do_GET(self):
|
|
1311
|
+
path = urlparse(self.path).path
|
|
1312
|
+
|
|
1313
|
+
if path == '/' or path == '/index.html':
|
|
1314
|
+
self.send_page('dashboard')
|
|
1315
|
+
elif path == '/tasks':
|
|
1316
|
+
self.send_page('tasks')
|
|
1317
|
+
elif path == '/sessions':
|
|
1318
|
+
self.send_page('sessions')
|
|
1319
|
+
elif path == '/log':
|
|
1320
|
+
self.send_page('log')
|
|
1321
|
+
elif path == '/commits':
|
|
1322
|
+
self.send_page('commits')
|
|
1323
|
+
elif path == '/settings':
|
|
1324
|
+
self.send_page('settings')
|
|
1325
|
+
elif path == '/transform':
|
|
1326
|
+
self.send_page('transform')
|
|
1327
|
+
elif path == '/api/status':
|
|
1328
|
+
self.send_json_status()
|
|
1329
|
+
elif path.startswith('/api/log'):
|
|
1330
|
+
self.send_json_log()
|
|
1331
|
+
elif path == '/api/config':
|
|
1332
|
+
self.send_json_config()
|
|
1333
|
+
else:
|
|
1334
|
+
self.send_error(404)
|
|
1335
|
+
|
|
1336
|
+
def do_POST(self):
|
|
1337
|
+
content_length = int(self.headers.get('Content-Length', 0))
|
|
1338
|
+
post_data = self.rfile.read(content_length).decode('utf-8')
|
|
1339
|
+
params = parse_qs(post_data)
|
|
1340
|
+
|
|
1341
|
+
if self.path == '/add-task':
|
|
1342
|
+
task_text = params.get('task', [''])[0]
|
|
1343
|
+
task_desc = params.get('description', [''])[0]
|
|
1344
|
+
if task_text:
|
|
1345
|
+
tasks = TaskManager(PROJECT_DIR)
|
|
1346
|
+
tasks.add_task(task_text, description=task_desc)
|
|
1347
|
+
log_activity("Task added", task_text, "success")
|
|
1348
|
+
self.redirect('/')
|
|
1349
|
+
|
|
1350
|
+
elif self.path == '/add-thought':
|
|
1351
|
+
thought = params.get('thought', [''])[0]
|
|
1352
|
+
if thought:
|
|
1353
|
+
tasks = TaskManager(PROJECT_DIR)
|
|
1354
|
+
tasks.add_raw_thought(thought)
|
|
1355
|
+
log_activity("Thought added", thought, "info")
|
|
1356
|
+
self.redirect('/')
|
|
1357
|
+
|
|
1358
|
+
elif self.path == '/start':
|
|
1359
|
+
self.start_agent()
|
|
1360
|
+
self.redirect('/')
|
|
1361
|
+
|
|
1362
|
+
elif self.path == '/stop':
|
|
1363
|
+
self.stop_agent()
|
|
1364
|
+
self.redirect('/')
|
|
1365
|
+
|
|
1366
|
+
elif self.path == '/toggle-theme':
|
|
1367
|
+
self.redirect('/')
|
|
1368
|
+
elif self.path == '/transform':
|
|
1369
|
+
raw_text = params.get('text', [''])[0]
|
|
1370
|
+
if raw_text:
|
|
1371
|
+
result = self._transform_text(raw_text)
|
|
1372
|
+
self.send_response(200)
|
|
1373
|
+
self.send_header('Content-Type', 'application/json')
|
|
1374
|
+
self.end_headers()
|
|
1375
|
+
self.wfile.write(json.dumps(result).encode('utf-8'))
|
|
1376
|
+
else:
|
|
1377
|
+
self.send_response(400)
|
|
1378
|
+
self.end_headers()
|
|
1379
|
+
|
|
1380
|
+
elif self.path == '/transform-confirm':
|
|
1381
|
+
try:
|
|
1382
|
+
body = json.loads(post_data)
|
|
1383
|
+
tasks_list = body.get('tasks', [])
|
|
1384
|
+
tasks_mgr = TaskManager(PROJECT_DIR)
|
|
1385
|
+
added = 0
|
|
1386
|
+
for t in tasks_list:
|
|
1387
|
+
if t.get('title'):
|
|
1388
|
+
tasks_mgr.add_task(t['title'], description=t.get('description', ''))
|
|
1389
|
+
added += 1
|
|
1390
|
+
log_activity("Transform confirmed", f"{added} tasks added", "success")
|
|
1391
|
+
self.send_response(200)
|
|
1392
|
+
self.send_header('Content-Type', 'application/json')
|
|
1393
|
+
self.end_headers()
|
|
1394
|
+
self.wfile.write(json.dumps({"ok": True, "added": added}).encode('utf-8'))
|
|
1395
|
+
except Exception:
|
|
1396
|
+
self.send_response(400)
|
|
1397
|
+
self.end_headers()
|
|
1398
|
+
|
|
1399
|
+
elif self.path == '/queue-message':
|
|
1400
|
+
msg_text = params.get('message', [''])[0]
|
|
1401
|
+
if msg_text:
|
|
1402
|
+
queue_file = PROJECT_DIR / '.a1' / 'queue.json'
|
|
1403
|
+
queue_data = {"messages": []}
|
|
1404
|
+
if queue_file.exists():
|
|
1405
|
+
try:
|
|
1406
|
+
queue_data = json.loads(queue_file.read_text())
|
|
1407
|
+
except (json.JSONDecodeError, IOError):
|
|
1408
|
+
pass
|
|
1409
|
+
queue_data["messages"].append({
|
|
1410
|
+
"text": msg_text,
|
|
1411
|
+
"added_at": datetime.now().isoformat(),
|
|
1412
|
+
"read": False,
|
|
1413
|
+
})
|
|
1414
|
+
queue_file.write_text(json.dumps(queue_data, indent=2, ensure_ascii=False))
|
|
1415
|
+
log_activity("Message queued", msg_text[:50], "info")
|
|
1416
|
+
self.send_response(200)
|
|
1417
|
+
self.send_header('Content-Type', 'application/json')
|
|
1418
|
+
self.end_headers()
|
|
1419
|
+
self.wfile.write(b'{"ok": true}')
|
|
1420
|
+
|
|
1421
|
+
elif self.path == '/api/config':
|
|
1422
|
+
try:
|
|
1423
|
+
body = json.loads(post_data)
|
|
1424
|
+
from .config import Config
|
|
1425
|
+
config = Config(PROJECT_DIR)
|
|
1426
|
+
for key, value in body.items():
|
|
1427
|
+
config.set(key, value)
|
|
1428
|
+
log_activity("Config updated", ", ".join(body.keys()), "info")
|
|
1429
|
+
self.send_response(200)
|
|
1430
|
+
self.send_header('Content-Type', 'application/json')
|
|
1431
|
+
self.end_headers()
|
|
1432
|
+
self.wfile.write(json.dumps({"ok": True}).encode('utf-8'))
|
|
1433
|
+
except Exception as e:
|
|
1434
|
+
self.send_response(400)
|
|
1435
|
+
self.send_header('Content-Type', 'application/json')
|
|
1436
|
+
self.end_headers()
|
|
1437
|
+
self.wfile.write(json.dumps({"error": str(e)}).encode('utf-8'))
|
|
1438
|
+
|
|
1439
|
+
elif self.path == '/api/reorder':
|
|
1440
|
+
try:
|
|
1441
|
+
body = json.loads(post_data)
|
|
1442
|
+
task_ids = body.get('order', [])
|
|
1443
|
+
if task_ids:
|
|
1444
|
+
tasks_mgr = TaskManager(PROJECT_DIR)
|
|
1445
|
+
tasks_mgr.reorder_tasks(task_ids)
|
|
1446
|
+
log_activity("Tasks reordered", f"{len(task_ids)} tasks", "info")
|
|
1447
|
+
self.send_response(200)
|
|
1448
|
+
self.send_header('Content-Type', 'application/json')
|
|
1449
|
+
self.end_headers()
|
|
1450
|
+
self.wfile.write(b'{"ok": true}')
|
|
1451
|
+
except Exception:
|
|
1452
|
+
self.send_response(400)
|
|
1453
|
+
self.end_headers()
|
|
1454
|
+
else:
|
|
1455
|
+
self.send_error(404)
|
|
1456
|
+
|
|
1457
|
+
def redirect(self, location):
|
|
1458
|
+
self.send_response(302)
|
|
1459
|
+
self.send_header('Location', location)
|
|
1460
|
+
self.end_headers()
|
|
1461
|
+
|
|
1462
|
+
def send_page(self, page):
|
|
1463
|
+
theme = 'light' # Default light theme
|
|
1464
|
+
content = self.build_content(page)
|
|
1465
|
+
|
|
1466
|
+
nav_active = {
|
|
1467
|
+
'nav_dashboard': 'active' if page == 'dashboard' else '',
|
|
1468
|
+
'nav_tasks': 'active' if page == 'tasks' else '',
|
|
1469
|
+
'nav_sessions': 'active' if page == 'sessions' else '',
|
|
1470
|
+
'nav_log': 'active' if page == 'log' else '',
|
|
1471
|
+
'nav_commits': 'active' if page == 'commits' else '',
|
|
1472
|
+
'nav_transform': 'active' if page == 'transform' else '',
|
|
1473
|
+
'nav_settings': 'active' if page == 'settings' else '',
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
html = HTML_TEMPLATE.substitute(
|
|
1477
|
+
theme=theme,
|
|
1478
|
+
content=content,
|
|
1479
|
+
page_name=page,
|
|
1480
|
+
**nav_active
|
|
1481
|
+
)
|
|
1482
|
+
|
|
1483
|
+
self.send_response(200)
|
|
1484
|
+
self.send_header('Content-Type', 'text/html; charset=utf-8')
|
|
1485
|
+
self.end_headers()
|
|
1486
|
+
self.wfile.write(html.encode('utf-8'))
|
|
1487
|
+
|
|
1488
|
+
def build_content(self, page):
|
|
1489
|
+
if page == 'dashboard':
|
|
1490
|
+
return self.build_dashboard()
|
|
1491
|
+
elif page == 'tasks':
|
|
1492
|
+
return self.build_tasks_page()
|
|
1493
|
+
elif page == 'sessions':
|
|
1494
|
+
return self.build_sessions_page()
|
|
1495
|
+
elif page == 'log':
|
|
1496
|
+
return self.build_log_page()
|
|
1497
|
+
elif page == 'commits':
|
|
1498
|
+
return self.build_commits_page()
|
|
1499
|
+
elif page == 'settings':
|
|
1500
|
+
return self.build_settings_page()
|
|
1501
|
+
elif page == 'transform':
|
|
1502
|
+
return self.build_transform_page()
|
|
1503
|
+
return ''
|
|
1504
|
+
|
|
1505
|
+
def build_dashboard(self):
|
|
1506
|
+
checkpoint = CheckpointManager(PROJECT_DIR)
|
|
1507
|
+
tasks_mgr = TaskManager(PROJECT_DIR)
|
|
1508
|
+
|
|
1509
|
+
cp = checkpoint.load()
|
|
1510
|
+
all_tasks = tasks_mgr.get_tasks()
|
|
1511
|
+
done, total = tasks_mgr.get_progress()
|
|
1512
|
+
progress = int((done / total * 100)) if total > 0 else 0
|
|
1513
|
+
|
|
1514
|
+
# Status
|
|
1515
|
+
if AGENT_RUNNING:
|
|
1516
|
+
status_class = 'status-running'
|
|
1517
|
+
status_text = '<i class="bi bi-play-circle-fill"></i> Running'
|
|
1518
|
+
elif cp.get('status') == 'COMPLETED':
|
|
1519
|
+
status_class = 'status-completed'
|
|
1520
|
+
status_text = '<i class="bi bi-check-circle-fill"></i> Completed'
|
|
1521
|
+
else:
|
|
1522
|
+
status_class = 'status-stopped'
|
|
1523
|
+
status_text = '<i class="bi bi-stop-circle-fill"></i> Stopped'
|
|
1524
|
+
|
|
1525
|
+
# Tasks HTML (last 5, sorted by priority)
|
|
1526
|
+
sorted_tasks = sorted(all_tasks, key=lambda t: (t.status == 'done', t.priority))
|
|
1527
|
+
tasks_html = ''
|
|
1528
|
+
for t in sorted_tasks[:5]:
|
|
1529
|
+
if t.status == 'done':
|
|
1530
|
+
check_class = 'done'
|
|
1531
|
+
check_icon = '<i class="bi bi-check"></i>'
|
|
1532
|
+
elif t.status == 'in_progress':
|
|
1533
|
+
check_class = 'progress'
|
|
1534
|
+
check_icon = '<i class="bi bi-arrow-repeat"></i>'
|
|
1535
|
+
else:
|
|
1536
|
+
check_class = ''
|
|
1537
|
+
check_icon = ''
|
|
1538
|
+
|
|
1539
|
+
pri = getattr(t, 'priority', 0)
|
|
1540
|
+
pri_badge = f'<span class="priority-badge">#{pri}</span>' if pri and t.status != 'done' else ''
|
|
1541
|
+
|
|
1542
|
+
tasks_html += f'''
|
|
1543
|
+
<div class="task">
|
|
1544
|
+
<div class="task-check {check_class}">{check_icon}</div>
|
|
1545
|
+
<div class="task-content">
|
|
1546
|
+
<div class="task-title">{esc(t.title)}</div>
|
|
1547
|
+
<div class="task-meta">{esc(t.id)} {pri_badge}</div>
|
|
1548
|
+
</div>
|
|
1549
|
+
</div>
|
|
1550
|
+
'''
|
|
1551
|
+
|
|
1552
|
+
if not tasks_html:
|
|
1553
|
+
tasks_html = '<div class="empty"><i class="bi bi-inbox"></i><p>No tasks yet</p></div>'
|
|
1554
|
+
|
|
1555
|
+
# Activity HTML (last 5) with status icons
|
|
1556
|
+
status_icons = {
|
|
1557
|
+
'success': '<i class="bi bi-check-circle-fill" style="color:var(--success)"></i>',
|
|
1558
|
+
'error': '<i class="bi bi-x-circle-fill" style="color:var(--danger)"></i>',
|
|
1559
|
+
'warning': '<i class="bi bi-exclamation-triangle-fill" style="color:var(--warning)"></i>',
|
|
1560
|
+
'info': '<i class="bi bi-info-circle-fill" style="color:var(--accent)"></i>',
|
|
1561
|
+
}
|
|
1562
|
+
activity_html = ''
|
|
1563
|
+
for a in reversed(ACTIVITY_LOG[-5:]):
|
|
1564
|
+
icon = status_icons.get(a['status'], status_icons['info'])
|
|
1565
|
+
activity_html += f'''
|
|
1566
|
+
<div class="activity-item">
|
|
1567
|
+
<span class="activity-time">{esc(a['time'])}</span>
|
|
1568
|
+
<span style="flex-shrink:0">{icon}</span>
|
|
1569
|
+
<span class="activity-text"><strong>{esc(a['action'])}</strong> <span class="activity-details">{esc(a['details'])}</span></span>
|
|
1570
|
+
</div>
|
|
1571
|
+
'''
|
|
1572
|
+
|
|
1573
|
+
if not activity_html:
|
|
1574
|
+
activity_html = '<div class="empty" style="padding:20px"><i class="bi bi-clock-history"></i><p>No activity yet</p></div>'
|
|
1575
|
+
|
|
1576
|
+
# Control buttons
|
|
1577
|
+
if AGENT_RUNNING:
|
|
1578
|
+
control_html = '''
|
|
1579
|
+
<form method="POST" action="/stop">
|
|
1580
|
+
<button type="submit" class="btn-danger"><i class="bi bi-stop-fill"></i> Stop Agent</button>
|
|
1581
|
+
</form>
|
|
1582
|
+
'''
|
|
1583
|
+
else:
|
|
1584
|
+
control_html = '''
|
|
1585
|
+
<form method="POST" action="/start">
|
|
1586
|
+
<button type="submit" class="btn-success"><i class="bi bi-play-fill"></i> Start Agent</button>
|
|
1587
|
+
</form>
|
|
1588
|
+
'''
|
|
1589
|
+
|
|
1590
|
+
return f'''
|
|
1591
|
+
<div class="header">
|
|
1592
|
+
<h1 class="page-title">Dashboard</h1>
|
|
1593
|
+
<div class="header-actions">
|
|
1594
|
+
<span class="status {status_class}" id="status-badge">{status_text}</span>
|
|
1595
|
+
<button class="theme-toggle" onclick="toggleTheme()">
|
|
1596
|
+
<i class="bi bi-moon-stars"></i>
|
|
1597
|
+
</button>
|
|
1598
|
+
</div>
|
|
1599
|
+
</div>
|
|
1600
|
+
|
|
1601
|
+
<div class="cards">
|
|
1602
|
+
<div class="card">
|
|
1603
|
+
<div class="card-title"><i class="bi bi-check2-square"></i> Tasks</div>
|
|
1604
|
+
<div class="card-value" id="task-count">{done}/{total}</div>
|
|
1605
|
+
<div class="card-sub">completed</div>
|
|
1606
|
+
<div class="progress">
|
|
1607
|
+
<div class="progress-fill" style="width: {progress}%"></div>
|
|
1608
|
+
</div>
|
|
1609
|
+
</div>
|
|
1610
|
+
<div class="card">
|
|
1611
|
+
<div class="card-title"><i class="bi bi-terminal"></i> Session</div>
|
|
1612
|
+
<div class="card-value" id="session-count">#{cp.get('session', 0)}</div>
|
|
1613
|
+
<div class="card-sub">{cp.get('status', 'Not started')}</div>
|
|
1614
|
+
</div>
|
|
1615
|
+
<div class="card">
|
|
1616
|
+
<div class="card-title"><i class="bi bi-lightning-charge" style="color:#f59e0b"></i> Tokens</div>
|
|
1617
|
+
<div class="card-value" id="tokens-count" style="font-size:22px">0 / 0</div>
|
|
1618
|
+
<div class="card-sub" id="tokens-sub">in / out</div>
|
|
1619
|
+
<div class="progress">
|
|
1620
|
+
<div class="progress-fill" id="tokens-bar" style="width: 0%"></div>
|
|
1621
|
+
</div>
|
|
1622
|
+
<div class="card-sub" style="margin-top:4px">Context: <strong id="context-pct">0%</strong> <span style="color:var(--muted);font-size:11px">(auto-save at 70%)</span></div>
|
|
1623
|
+
</div>
|
|
1624
|
+
<div class="card">
|
|
1625
|
+
<div class="card-title"><i class="bi bi-currency-dollar" style="color:#10b981"></i> Cost</div>
|
|
1626
|
+
<div class="card-value" id="cost-value" style="font-size:22px">$$0.00</div>
|
|
1627
|
+
<div class="card-sub">this session</div>
|
|
1628
|
+
</div>
|
|
1629
|
+
<div class="card">
|
|
1630
|
+
<div class="card-title"><i class="bi bi-stopwatch" style="color:#6366f1"></i> Duration</div>
|
|
1631
|
+
<div class="card-value" id="duration-value">0s</div>
|
|
1632
|
+
<div class="card-sub" id="duration-sub">current session</div>
|
|
1633
|
+
</div>
|
|
1634
|
+
<div class="card">
|
|
1635
|
+
<div class="card-title"><i class="bi bi-file-earmark-code"></i> Files</div>
|
|
1636
|
+
<div class="card-value" id="files-count">{len(cp.get('files_modified', []))}</div>
|
|
1637
|
+
<div class="card-sub">modified</div>
|
|
1638
|
+
</div>
|
|
1639
|
+
</div>
|
|
1640
|
+
|
|
1641
|
+
<div class="task-list">
|
|
1642
|
+
<div class="task-header">
|
|
1643
|
+
<h3>Recent Tasks</h3>
|
|
1644
|
+
<a href="/tasks" class="btn-secondary" style="text-decoration:none">View All</a>
|
|
1645
|
+
</div>
|
|
1646
|
+
{tasks_html}
|
|
1647
|
+
</div>
|
|
1648
|
+
|
|
1649
|
+
<div class="form-section">
|
|
1650
|
+
<h3>Quick Add</h3>
|
|
1651
|
+
<form method="POST" action="/add-task">
|
|
1652
|
+
<div class="form-row">
|
|
1653
|
+
<input type="text" name="task" placeholder="Task title..." required>
|
|
1654
|
+
<button type="submit" class="btn-primary"><i class="bi bi-plus"></i> Add</button>
|
|
1655
|
+
</div>
|
|
1656
|
+
<textarea name="description" placeholder="Description (optional)..." rows="2"></textarea>
|
|
1657
|
+
</form>
|
|
1658
|
+
</div>
|
|
1659
|
+
|
|
1660
|
+
<div class="controls">
|
|
1661
|
+
{control_html}
|
|
1662
|
+
</div>
|
|
1663
|
+
|
|
1664
|
+
<div class="form-section" id="queue-msg-section" style="{'display:block' if AGENT_RUNNING else 'display:none'}">
|
|
1665
|
+
<h3><i class="bi bi-envelope"></i> Message to Agent</h3>
|
|
1666
|
+
<form onsubmit="sendQueueMessage(event)">
|
|
1667
|
+
<div class="form-row">
|
|
1668
|
+
<input type="text" id="queue-msg-input" placeholder="Send instruction to agent (next session)..." required>
|
|
1669
|
+
<button type="submit" class="btn-primary"><i class="bi bi-send"></i> Send</button>
|
|
1670
|
+
</div>
|
|
1671
|
+
</form>
|
|
1672
|
+
<div id="queue-msg-status" style="font-size:12px;color:var(--text-secondary);margin-top:6px"></div>
|
|
1673
|
+
</div>
|
|
1674
|
+
|
|
1675
|
+
<div class="log-panel">
|
|
1676
|
+
<div class="log-panel-header">
|
|
1677
|
+
<div style="display:flex;align-items:center;gap:12px">
|
|
1678
|
+
<div class="terminal-dots">
|
|
1679
|
+
<span class="dot-red"></span>
|
|
1680
|
+
<span class="dot-yellow"></span>
|
|
1681
|
+
<span class="dot-green"></span>
|
|
1682
|
+
</div>
|
|
1683
|
+
<span class="terminal-title">agent@pocketcoder ~ live-log</span>
|
|
1684
|
+
</div>
|
|
1685
|
+
<button class="log-toggle" onclick="document.getElementById('raw-log-wrap').style.display = document.getElementById('raw-log-wrap').style.display === 'none' ? 'block' : 'none'">raw</button>
|
|
1686
|
+
</div>
|
|
1687
|
+
<div class="log-feed" id="action-feed">
|
|
1688
|
+
{self._render_log_entries()}
|
|
1689
|
+
</div>
|
|
1690
|
+
<div id="raw-log-wrap" style="display:none">
|
|
1691
|
+
<div class="raw-log" id="raw-log">{self._render_raw_log()}</div>
|
|
1692
|
+
</div>
|
|
1693
|
+
</div>
|
|
1694
|
+
|
|
1695
|
+
<div class="activity">
|
|
1696
|
+
<div class="activity-header">Recent Activity</div>
|
|
1697
|
+
<div class="activity-list">
|
|
1698
|
+
{activity_html}
|
|
1699
|
+
</div>
|
|
1700
|
+
</div>
|
|
1701
|
+
'''
|
|
1702
|
+
|
|
1703
|
+
def build_tasks_page(self):
|
|
1704
|
+
tasks_mgr = TaskManager(PROJECT_DIR)
|
|
1705
|
+
all_tasks = tasks_mgr.get_tasks()
|
|
1706
|
+
all_tasks.sort(key=lambda t: (t.status == 'done', t.priority))
|
|
1707
|
+
thoughts = tasks_mgr.get_raw_thoughts()
|
|
1708
|
+
|
|
1709
|
+
tasks_html = ''
|
|
1710
|
+
for t in all_tasks:
|
|
1711
|
+
if t.status == 'done':
|
|
1712
|
+
check_class = 'done'
|
|
1713
|
+
check_icon = '<i class="bi bi-check"></i>'
|
|
1714
|
+
elif t.status == 'in_progress':
|
|
1715
|
+
check_class = 'progress'
|
|
1716
|
+
check_icon = '<i class="bi bi-arrow-repeat"></i>'
|
|
1717
|
+
else:
|
|
1718
|
+
check_class = ''
|
|
1719
|
+
check_icon = ''
|
|
1720
|
+
|
|
1721
|
+
desc = t.description[:100] if t.description else ''
|
|
1722
|
+
pri = getattr(t, 'priority', 0)
|
|
1723
|
+
pri_badge = f'<span class="priority-badge">#{pri}</span>' if pri and t.status != 'done' else ''
|
|
1724
|
+
draggable = 'draggable="true"' if t.status != 'done' else ''
|
|
1725
|
+
|
|
1726
|
+
# Stage progress bar
|
|
1727
|
+
stage_steps = ''
|
|
1728
|
+
if t.status == 'done':
|
|
1729
|
+
stage_steps = '<div class="stage-step done"></div><div class="stage-step done"></div><div class="stage-step done"></div>'
|
|
1730
|
+
elif t.status == 'in_progress':
|
|
1731
|
+
stage_steps = '<div class="stage-step done"></div><div class="stage-step active"></div><div class="stage-step"></div>'
|
|
1732
|
+
else:
|
|
1733
|
+
stage_steps = '<div class="stage-step"></div><div class="stage-step"></div><div class="stage-step"></div>'
|
|
1734
|
+
|
|
1735
|
+
# Task detail metadata
|
|
1736
|
+
phase_text = getattr(t, 'phase', '') or ''
|
|
1737
|
+
criteria = getattr(t, 'success_criteria', '') or ''
|
|
1738
|
+
created = getattr(t, 'created_at', '') or ''
|
|
1739
|
+
completed = getattr(t, 'completed_at', '') or ''
|
|
1740
|
+
|
|
1741
|
+
detail_html = f'''
|
|
1742
|
+
<div class="task-detail" id="detail-{t.id}">
|
|
1743
|
+
<div class="task-stages">{stage_steps}</div>
|
|
1744
|
+
<div class="task-detail-meta">
|
|
1745
|
+
<div><dt>Status</dt><dd>{esc(t.status)}</dd></div>
|
|
1746
|
+
<div><dt>Phase</dt><dd>{esc(phase_text) if phase_text else 'N/A'}</dd></div>
|
|
1747
|
+
<div><dt>Created</dt><dd>{esc(created[:10]) if created else 'N/A'}</dd></div>
|
|
1748
|
+
<div><dt>Completed</dt><dd>{esc(completed[:10]) if completed else '—'}</dd></div>
|
|
1749
|
+
</div>
|
|
1750
|
+
{f'<div class="task-criteria"><strong>Criteria:</strong> {esc(criteria)}</div>' if criteria else ''}
|
|
1751
|
+
{f'<div style="margin-top:6px;font-size:12px;color:var(--text-secondary)">{esc(desc)}</div>' if desc else ''}
|
|
1752
|
+
</div>
|
|
1753
|
+
'''
|
|
1754
|
+
|
|
1755
|
+
tasks_html += f'''
|
|
1756
|
+
<div class="task task-clickable" {draggable} data-task-id="{t.id}" onclick="toggleTaskDetail('{t.id}')">
|
|
1757
|
+
<div class="task-check {check_class}">{check_icon}</div>
|
|
1758
|
+
<div class="task-content">
|
|
1759
|
+
<div class="task-title">{esc(t.title)}</div>
|
|
1760
|
+
<div class="task-meta">{esc(t.id)} {pri_badge}</div>
|
|
1761
|
+
</div>
|
|
1762
|
+
<i class="bi bi-chevron-down" style="color:var(--text-secondary);font-size:14px"></i>
|
|
1763
|
+
</div>
|
|
1764
|
+
{detail_html}
|
|
1765
|
+
'''
|
|
1766
|
+
|
|
1767
|
+
thoughts_html = ''
|
|
1768
|
+
if thoughts:
|
|
1769
|
+
for th in thoughts:
|
|
1770
|
+
thoughts_html += f'''
|
|
1771
|
+
<div class="task">
|
|
1772
|
+
<div class="task-check"><i class="bi bi-lightbulb"></i></div>
|
|
1773
|
+
<div class="task-content">
|
|
1774
|
+
<div class="task-title">{esc(th['text'])}</div>
|
|
1775
|
+
<div class="task-meta">Raw thought</div>
|
|
1776
|
+
</div>
|
|
1777
|
+
</div>
|
|
1778
|
+
'''
|
|
1779
|
+
|
|
1780
|
+
return f'''
|
|
1781
|
+
<div class="header">
|
|
1782
|
+
<h1 class="page-title">Tasks</h1>
|
|
1783
|
+
<button class="theme-toggle" onclick="toggleTheme()">
|
|
1784
|
+
<i class="bi bi-moon-stars"></i>
|
|
1785
|
+
</button>
|
|
1786
|
+
</div>
|
|
1787
|
+
|
|
1788
|
+
<div class="task-list">
|
|
1789
|
+
<div class="task-header">
|
|
1790
|
+
<h3>All Tasks ({len(all_tasks)})</h3>
|
|
1791
|
+
</div>
|
|
1792
|
+
{tasks_html if tasks_html else '<div class="empty"><i class="bi bi-inbox"></i><p>No tasks yet</p></div>'}
|
|
1793
|
+
</div>
|
|
1794
|
+
|
|
1795
|
+
{('<div class="task-list" style="margin-top:24px"><div class="task-header"><h3>Raw Thoughts</h3></div>' + thoughts_html + '</div>') if thoughts_html else ''}
|
|
1796
|
+
|
|
1797
|
+
<div class="form-section">
|
|
1798
|
+
<h3>Add Task</h3>
|
|
1799
|
+
<form method="POST" action="/add-task">
|
|
1800
|
+
<div class="form-row">
|
|
1801
|
+
<input type="text" name="task" placeholder="Task title..." required>
|
|
1802
|
+
<button type="submit" class="btn-primary"><i class="bi bi-plus"></i> Add Task</button>
|
|
1803
|
+
</div>
|
|
1804
|
+
<textarea name="description" placeholder="Description (optional)..." rows="3"></textarea>
|
|
1805
|
+
</form>
|
|
1806
|
+
<form method="POST" action="/add-thought" style="margin-top:12px">
|
|
1807
|
+
<div class="form-row">
|
|
1808
|
+
<input type="text" name="thought" placeholder="Quick thought or idea...">
|
|
1809
|
+
<button type="submit" class="btn-secondary"><i class="bi bi-lightbulb"></i> Add Thought</button>
|
|
1810
|
+
</div>
|
|
1811
|
+
</form>
|
|
1812
|
+
</div>
|
|
1813
|
+
'''
|
|
1814
|
+
|
|
1815
|
+
def build_sessions_page(self):
|
|
1816
|
+
checkpoint = CheckpointManager(PROJECT_DIR)
|
|
1817
|
+
cp = checkpoint.load()
|
|
1818
|
+
|
|
1819
|
+
sessions_html = ''
|
|
1820
|
+
checkpoints_dir = PROJECT_DIR / '.a1' / 'checkpoints'
|
|
1821
|
+
if checkpoints_dir.exists():
|
|
1822
|
+
for f in sorted(checkpoints_dir.glob('session_*.json'), reverse=True)[:10]:
|
|
1823
|
+
try:
|
|
1824
|
+
data = json.loads(f.read_text())
|
|
1825
|
+
session_num = data.get('session', '?')
|
|
1826
|
+
status = data.get('status', 'Unknown')
|
|
1827
|
+
files = len(data.get('files_modified', []))
|
|
1828
|
+
|
|
1829
|
+
# Status badge color
|
|
1830
|
+
if status == 'COMPLETED':
|
|
1831
|
+
badge_cls = 'status-completed'
|
|
1832
|
+
elif status == 'WORKING':
|
|
1833
|
+
badge_cls = 'status-running'
|
|
1834
|
+
else:
|
|
1835
|
+
badge_cls = 'status-stopped'
|
|
1836
|
+
|
|
1837
|
+
# Extract metrics from checkpoint data
|
|
1838
|
+
sm = data.get('session_metrics', {})
|
|
1839
|
+
tok_in = sm.get('tokens_in', 0)
|
|
1840
|
+
tok_out = sm.get('tokens_out', 0)
|
|
1841
|
+
duration = sm.get('session_duration', 0)
|
|
1842
|
+
tools = sm.get('tools_used', 0)
|
|
1843
|
+
|
|
1844
|
+
# Format duration
|
|
1845
|
+
dur_str = f'{duration // 60}m {duration % 60}s' if duration >= 60 else f'{duration}s'
|
|
1846
|
+
|
|
1847
|
+
# Format tokens
|
|
1848
|
+
def _fmt_tok(n):
|
|
1849
|
+
if n >= 1000000: return f'{n/1000000:.1f}M'
|
|
1850
|
+
if n >= 1000: return f'{n/1000:.1f}K'
|
|
1851
|
+
return str(n)
|
|
1852
|
+
|
|
1853
|
+
sessions_html += f'''
|
|
1854
|
+
<div class="session-card">
|
|
1855
|
+
<div class="session-header">
|
|
1856
|
+
<span class="session-title">Session #{session_num}</span>
|
|
1857
|
+
<span class="status {badge_cls}">{esc(status)}</span>
|
|
1858
|
+
</div>
|
|
1859
|
+
<div class="session-meta">
|
|
1860
|
+
<div class="meta-item">
|
|
1861
|
+
<span class="meta-label"><i class="bi bi-file-earmark-code"></i> Files</span>
|
|
1862
|
+
<span class="meta-value">{files}</span>
|
|
1863
|
+
</div>
|
|
1864
|
+
<div class="meta-item">
|
|
1865
|
+
<span class="meta-label"><i class="bi bi-check2-square"></i> Task</span>
|
|
1866
|
+
<span class="meta-value">{esc(data.get('current_task', 'N/A'))}</span>
|
|
1867
|
+
</div>
|
|
1868
|
+
<div class="meta-item">
|
|
1869
|
+
<span class="meta-label"><i class="bi bi-lightning-charge"></i> Tokens</span>
|
|
1870
|
+
<span class="meta-value">{_fmt_tok(tok_in)} / {_fmt_tok(tok_out)}</span>
|
|
1871
|
+
</div>
|
|
1872
|
+
<div class="meta-item">
|
|
1873
|
+
<span class="meta-label"><i class="bi bi-stopwatch"></i> Duration</span>
|
|
1874
|
+
<span class="meta-value">{dur_str}</span>
|
|
1875
|
+
</div>
|
|
1876
|
+
<div class="meta-item">
|
|
1877
|
+
<span class="meta-label"><i class="bi bi-tools"></i> Tool Calls</span>
|
|
1878
|
+
<span class="meta-value">{tools}</span>
|
|
1879
|
+
</div>
|
|
1880
|
+
</div>
|
|
1881
|
+
</div>
|
|
1882
|
+
'''
|
|
1883
|
+
except Exception:
|
|
1884
|
+
pass
|
|
1885
|
+
|
|
1886
|
+
# Current session status badge
|
|
1887
|
+
if AGENT_RUNNING:
|
|
1888
|
+
cur_badge = 'status-running'
|
|
1889
|
+
cur_label = '<i class="bi bi-play-circle-fill"></i> Running'
|
|
1890
|
+
elif cp.get('status') == 'COMPLETED':
|
|
1891
|
+
cur_badge = 'status-completed'
|
|
1892
|
+
cur_label = '<i class="bi bi-check-circle-fill"></i> Completed'
|
|
1893
|
+
elif cp.get('status') == 'WORKING':
|
|
1894
|
+
cur_badge = 'status-running'
|
|
1895
|
+
cur_label = '<i class="bi bi-arrow-repeat"></i> Working'
|
|
1896
|
+
else:
|
|
1897
|
+
cur_badge = 'status-stopped'
|
|
1898
|
+
cur_label = '<i class="bi bi-stop-circle-fill"></i> Idle'
|
|
1899
|
+
|
|
1900
|
+
return f'''
|
|
1901
|
+
<div class="header">
|
|
1902
|
+
<h1 class="page-title"><i class="bi bi-terminal"></i> Sessions</h1>
|
|
1903
|
+
<button class="theme-toggle" onclick="toggleTheme()">
|
|
1904
|
+
<i class="bi bi-moon-stars"></i>
|
|
1905
|
+
</button>
|
|
1906
|
+
</div>
|
|
1907
|
+
|
|
1908
|
+
<div class="session-card" style="border-left: 3px solid var(--accent)">
|
|
1909
|
+
<div class="session-header">
|
|
1910
|
+
<span class="session-title">Current Session #{cp.get('session', 0)}</span>
|
|
1911
|
+
<span class="status {cur_badge}">{cur_label}</span>
|
|
1912
|
+
</div>
|
|
1913
|
+
<div class="session-meta">
|
|
1914
|
+
<div class="meta-item">
|
|
1915
|
+
<span class="meta-label"><i class="bi bi-flag"></i> Status</span>
|
|
1916
|
+
<span class="meta-value">{esc(cp.get('status', 'Not started'))}</span>
|
|
1917
|
+
</div>
|
|
1918
|
+
<div class="meta-item">
|
|
1919
|
+
<span class="meta-label"><i class="bi bi-pie-chart"></i> Context</span>
|
|
1920
|
+
<span class="meta-value">{cp.get('context_percent', 0)}%</span>
|
|
1921
|
+
</div>
|
|
1922
|
+
<div class="meta-item">
|
|
1923
|
+
<span class="meta-label"><i class="bi bi-check2-square"></i> Current Task</span>
|
|
1924
|
+
<span class="meta-value">{esc(cp.get('current_task', 'None'))}</span>
|
|
1925
|
+
</div>
|
|
1926
|
+
<div class="meta-item">
|
|
1927
|
+
<span class="meta-label"><i class="bi bi-file-earmark-code"></i> Files Modified</span>
|
|
1928
|
+
<span class="meta-value">{len(cp.get('files_modified', []))}</span>
|
|
1929
|
+
</div>
|
|
1930
|
+
</div>
|
|
1931
|
+
</div>
|
|
1932
|
+
|
|
1933
|
+
<h3 style="margin: 24px 0 16px; color: var(--text-secondary); font-size: 14px; text-transform: uppercase; letter-spacing: 1px">Previous Sessions</h3>
|
|
1934
|
+
{sessions_html if sessions_html else '<div class="empty"><i class="bi bi-clock-history"></i><p>No previous sessions</p></div>'}
|
|
1935
|
+
'''
|
|
1936
|
+
|
|
1937
|
+
def build_log_page(self):
|
|
1938
|
+
# Icons per status type
|
|
1939
|
+
status_icons = {
|
|
1940
|
+
'success': '<i class="bi bi-check-circle-fill" style="color:var(--success)"></i>',
|
|
1941
|
+
'error': '<i class="bi bi-x-circle-fill" style="color:var(--danger)"></i>',
|
|
1942
|
+
'warning': '<i class="bi bi-exclamation-triangle-fill" style="color:var(--warning)"></i>',
|
|
1943
|
+
'info': '<i class="bi bi-info-circle-fill" style="color:var(--accent)"></i>',
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
activity_html = ''
|
|
1947
|
+
for a in reversed(ACTIVITY_LOG):
|
|
1948
|
+
icon = status_icons.get(a['status'], status_icons['info'])
|
|
1949
|
+
activity_html += f'''
|
|
1950
|
+
<div class="activity-item">
|
|
1951
|
+
<span class="activity-time">{esc(a['time'])}</span>
|
|
1952
|
+
<span style="flex-shrink:0">{icon}</span>
|
|
1953
|
+
<span class="activity-text">
|
|
1954
|
+
<strong>{esc(a['action'])}</strong>
|
|
1955
|
+
<span class="activity-details">{esc(a['details'])}</span>
|
|
1956
|
+
</span>
|
|
1957
|
+
</div>
|
|
1958
|
+
'''
|
|
1959
|
+
|
|
1960
|
+
return f'''
|
|
1961
|
+
<div class="header">
|
|
1962
|
+
<h1 class="page-title"><i class="bi bi-journal-text"></i> Activity Log</h1>
|
|
1963
|
+
<button class="theme-toggle" onclick="toggleTheme()">
|
|
1964
|
+
<i class="bi bi-moon-stars"></i>
|
|
1965
|
+
</button>
|
|
1966
|
+
</div>
|
|
1967
|
+
|
|
1968
|
+
<div class="activity">
|
|
1969
|
+
<div class="activity-header">
|
|
1970
|
+
All Activity
|
|
1971
|
+
<span style="font-weight:400;color:var(--text-secondary);font-size:13px;margin-left:8px">{len(ACTIVITY_LOG)} entries</span>
|
|
1972
|
+
</div>
|
|
1973
|
+
<div class="activity-list" style="max-height:none">
|
|
1974
|
+
{activity_html if activity_html else '<div class="empty"><i class="bi bi-clock-history"></i><p>No activity yet</p></div>'}
|
|
1975
|
+
</div>
|
|
1976
|
+
</div>
|
|
1977
|
+
'''
|
|
1978
|
+
|
|
1979
|
+
def build_commits_page(self):
|
|
1980
|
+
# Get git log with dates and full formatting
|
|
1981
|
+
import subprocess
|
|
1982
|
+
commits_html = ''
|
|
1983
|
+
try:
|
|
1984
|
+
result = subprocess.run(
|
|
1985
|
+
['git', 'log', '--format=%h|%s|%cr|%an', '-20'],
|
|
1986
|
+
cwd=PROJECT_DIR,
|
|
1987
|
+
capture_output=True,
|
|
1988
|
+
text=True
|
|
1989
|
+
)
|
|
1990
|
+
if result.returncode == 0:
|
|
1991
|
+
for line in result.stdout.strip().split('\n'):
|
|
1992
|
+
if line and '|' in line:
|
|
1993
|
+
parts = line.split('|', 3)
|
|
1994
|
+
hash_short = parts[0] if len(parts) > 0 else ''
|
|
1995
|
+
msg = parts[1] if len(parts) > 1 else ''
|
|
1996
|
+
rel_time = parts[2] if len(parts) > 2 else ''
|
|
1997
|
+
author = parts[3] if len(parts) > 3 else ''
|
|
1998
|
+
|
|
1999
|
+
# Split commit message: first line = title, rest = body
|
|
2000
|
+
msg_title = esc(msg)
|
|
2001
|
+
|
|
2002
|
+
# Icon based on conventional commit prefix
|
|
2003
|
+
if msg.startswith('feat'):
|
|
2004
|
+
icon_cls = 'bi-plus-circle'
|
|
2005
|
+
icon_color = 'var(--success)'
|
|
2006
|
+
elif msg.startswith('fix'):
|
|
2007
|
+
icon_cls = 'bi-bug'
|
|
2008
|
+
icon_color = 'var(--danger)'
|
|
2009
|
+
elif msg.startswith('docs') or msg.startswith('doc'):
|
|
2010
|
+
icon_cls = 'bi-file-text'
|
|
2011
|
+
icon_color = 'var(--accent)'
|
|
2012
|
+
elif msg.startswith('refactor') or msg.startswith('chore'):
|
|
2013
|
+
icon_cls = 'bi-arrow-repeat'
|
|
2014
|
+
icon_color = 'var(--warning)'
|
|
2015
|
+
elif msg.startswith('test'):
|
|
2016
|
+
icon_cls = 'bi-check2-circle'
|
|
2017
|
+
icon_color = '#89dceb'
|
|
2018
|
+
else:
|
|
2019
|
+
icon_cls = 'bi-git'
|
|
2020
|
+
icon_color = 'var(--text-secondary)'
|
|
2021
|
+
|
|
2022
|
+
commits_html += f'''
|
|
2023
|
+
<div class="task">
|
|
2024
|
+
<div class="task-check" style="border-color:{icon_color};color:{icon_color}"><i class="bi {icon_cls}"></i></div>
|
|
2025
|
+
<div class="task-content">
|
|
2026
|
+
<div class="commit-msg">{msg_title}</div>
|
|
2027
|
+
<div style="display:flex;gap:12px;align-items:center;margin-top:4px">
|
|
2028
|
+
<span class="commit-hash">{esc(hash_short)}</span>
|
|
2029
|
+
<span class="commit-time">{esc(rel_time)}</span>
|
|
2030
|
+
<span class="commit-time">{esc(author)}</span>
|
|
2031
|
+
</div>
|
|
2032
|
+
</div>
|
|
2033
|
+
</div>
|
|
2034
|
+
'''
|
|
2035
|
+
except Exception:
|
|
2036
|
+
pass
|
|
2037
|
+
|
|
2038
|
+
# Branch info
|
|
2039
|
+
branch = ''
|
|
2040
|
+
try:
|
|
2041
|
+
result = subprocess.run(
|
|
2042
|
+
['git', 'branch', '--show-current'],
|
|
2043
|
+
cwd=PROJECT_DIR, capture_output=True, text=True
|
|
2044
|
+
)
|
|
2045
|
+
if result.returncode == 0:
|
|
2046
|
+
branch = result.stdout.strip()
|
|
2047
|
+
except Exception:
|
|
2048
|
+
pass
|
|
2049
|
+
|
|
2050
|
+
return f'''
|
|
2051
|
+
<div class="header">
|
|
2052
|
+
<h1 class="page-title"><i class="bi bi-git"></i> Git Commits</h1>
|
|
2053
|
+
<div class="header-actions">
|
|
2054
|
+
{f'<span style="font-size:13px;color:var(--text-secondary)"><i class="bi bi-diagram-2"></i> {esc(branch)}</span>' if branch else ''}
|
|
2055
|
+
<button class="theme-toggle" onclick="toggleTheme()">
|
|
2056
|
+
<i class="bi bi-moon-stars"></i>
|
|
2057
|
+
</button>
|
|
2058
|
+
</div>
|
|
2059
|
+
</div>
|
|
2060
|
+
|
|
2061
|
+
<div class="task-list">
|
|
2062
|
+
<div class="task-header">
|
|
2063
|
+
<h3>Recent Commits</h3>
|
|
2064
|
+
</div>
|
|
2065
|
+
{commits_html if commits_html else '<div class="empty"><i class="bi bi-git"></i><p>No commits found</p></div>'}
|
|
2066
|
+
</div>
|
|
2067
|
+
'''
|
|
2068
|
+
|
|
2069
|
+
def build_settings_page(self):
|
|
2070
|
+
from .config import Config
|
|
2071
|
+
config = Config(PROJECT_DIR)
|
|
2072
|
+
data = config.get_all()
|
|
2073
|
+
provider = esc(data.get("provider", "claude-max"))
|
|
2074
|
+
api_key_masked = esc(config.mask_api_key(data.get("api_key")) or "")
|
|
2075
|
+
ollama_host = esc(data.get("ollama_host", "http://localhost:11434"))
|
|
2076
|
+
ollama_model = esc(data.get("ollama_model", "qwen3:30b-a3b"))
|
|
2077
|
+
max_sessions = data.get("max_sessions", 100)
|
|
2078
|
+
max_turns = data.get("max_turns", 25)
|
|
2079
|
+
session_delay = data.get("session_delay", 5)
|
|
2080
|
+
context_threshold = data.get("context_threshold", 0.70)
|
|
2081
|
+
|
|
2082
|
+
# Provider options with selected state
|
|
2083
|
+
providers = [
|
|
2084
|
+
("claude-max", "claude-max (Claude Code CLI)"),
|
|
2085
|
+
("claude-api", "claude-api [EXPERIMENTAL]"),
|
|
2086
|
+
("ollama", "ollama [EXPERIMENTAL]"),
|
|
2087
|
+
]
|
|
2088
|
+
options_html = ""
|
|
2089
|
+
for val, label in providers:
|
|
2090
|
+
sel = ' selected' if val == provider else ''
|
|
2091
|
+
options_html += f'<option value="{val}"{sel}>{label}</option>'
|
|
2092
|
+
|
|
2093
|
+
return f'''
|
|
2094
|
+
<div class="header">
|
|
2095
|
+
<h1 class="page-title"><i class="bi bi-gear"></i> Settings</h1>
|
|
2096
|
+
<button class="theme-toggle" onclick="toggleTheme()">
|
|
2097
|
+
<i class="bi bi-moon-stars"></i>
|
|
2098
|
+
</button>
|
|
2099
|
+
</div>
|
|
2100
|
+
|
|
2101
|
+
<div class="card" style="margin-bottom:16px">
|
|
2102
|
+
<div class="card-title"><i class="bi bi-cpu"></i> Provider</div>
|
|
2103
|
+
<select id="cfg-provider" onchange="onProviderChange(this.value)" style="margin-top:8px">
|
|
2104
|
+
{options_html}
|
|
2105
|
+
</select>
|
|
2106
|
+
<div id="provider-badge" class="settings-hint">
|
|
2107
|
+
{self._provider_badge(provider)}
|
|
2108
|
+
</div>
|
|
2109
|
+
</div>
|
|
2110
|
+
|
|
2111
|
+
<div class="card" id="card-apikey" style="margin-bottom:16px;{'display:none' if provider != 'claude-api' else ''}">
|
|
2112
|
+
<div class="card-title"><i class="bi bi-key"></i> API Key</div>
|
|
2113
|
+
<div style="display:flex;gap:8px;margin-top:8px">
|
|
2114
|
+
<input type="password" id="cfg-apikey" placeholder="sk-ant-api03-..."
|
|
2115
|
+
value="{api_key_masked}" style="flex:1">
|
|
2116
|
+
<button onclick="saveApiKey()" class="btn-primary" style="white-space:nowrap">
|
|
2117
|
+
<i class="bi bi-check-lg"></i> Save
|
|
2118
|
+
</button>
|
|
2119
|
+
</div>
|
|
2120
|
+
<div class="settings-hint">
|
|
2121
|
+
Or set <code style="background:var(--bg-tertiary);padding:2px 6px;border-radius:4px">ANTHROPIC_API_KEY</code> environment variable
|
|
2122
|
+
</div>
|
|
2123
|
+
</div>
|
|
2124
|
+
|
|
2125
|
+
<div class="card" id="card-ollama" style="margin-bottom:16px;{'display:none' if provider != 'ollama' else ''}">
|
|
2126
|
+
<div class="card-title"><i class="bi bi-hdd-network"></i> Ollama</div>
|
|
2127
|
+
<div style="margin-top:8px">
|
|
2128
|
+
<label class="settings-label">Host URL</label>
|
|
2129
|
+
<input type="text" id="cfg-ollama-host" value="{ollama_host}" style="font-family:monospace;width:100%">
|
|
2130
|
+
</div>
|
|
2131
|
+
<div style="margin-top:12px">
|
|
2132
|
+
<label class="settings-label">Model</label>
|
|
2133
|
+
<input type="text" id="cfg-ollama-model" value="{ollama_model}" style="font-family:monospace;width:100%">
|
|
2134
|
+
</div>
|
|
2135
|
+
<button onclick="saveOllamaConfig()" class="btn-primary" style="margin-top:12px">
|
|
2136
|
+
<i class="bi bi-check-lg"></i> Save
|
|
2137
|
+
</button>
|
|
2138
|
+
</div>
|
|
2139
|
+
|
|
2140
|
+
<div class="card" style="margin-bottom:16px">
|
|
2141
|
+
<div class="card-title"><i class="bi bi-sliders"></i> Session</div>
|
|
2142
|
+
<div class="settings-grid">
|
|
2143
|
+
<div>
|
|
2144
|
+
<label class="settings-label">Max Sessions</label>
|
|
2145
|
+
<input type="number" id="cfg-max-sessions" value="{max_sessions}" min="1" max="1000">
|
|
2146
|
+
</div>
|
|
2147
|
+
<div>
|
|
2148
|
+
<label class="settings-label">Max Turns</label>
|
|
2149
|
+
<input type="number" id="cfg-max-turns" value="{max_turns}" min="1" max="100">
|
|
2150
|
+
</div>
|
|
2151
|
+
<div>
|
|
2152
|
+
<label class="settings-label">Session Delay (sec)</label>
|
|
2153
|
+
<input type="number" id="cfg-delay" value="{session_delay}" min="0" max="60">
|
|
2154
|
+
</div>
|
|
2155
|
+
<div>
|
|
2156
|
+
<label class="settings-label">Context Threshold</label>
|
|
2157
|
+
<input type="number" id="cfg-threshold" value="{context_threshold}" min="0.1" max="0.95" step="0.05">
|
|
2158
|
+
</div>
|
|
2159
|
+
</div>
|
|
2160
|
+
<button onclick="saveSessionConfig()" class="btn-primary" style="margin-top:12px">
|
|
2161
|
+
<i class="bi bi-check-lg"></i> Save
|
|
2162
|
+
</button>
|
|
2163
|
+
</div>
|
|
2164
|
+
|
|
2165
|
+
<div class="card" style="margin-bottom:16px">
|
|
2166
|
+
<div class="card-title"><i class="bi bi-moon-stars"></i> Theme</div>
|
|
2167
|
+
<button onclick="toggleTheme()" class="btn-secondary" style="margin-top:8px">
|
|
2168
|
+
<i class="bi bi-moon-stars"></i> Toggle Dark/Light
|
|
2169
|
+
</button>
|
|
2170
|
+
</div>
|
|
2171
|
+
|
|
2172
|
+
<div class="card" style="margin-bottom:16px">
|
|
2173
|
+
<div class="card-title"><i class="bi bi-file-earmark-code"></i> Config File</div>
|
|
2174
|
+
<p style="margin-top:8px;font-family:monospace" class="commit-hash">
|
|
2175
|
+
{esc(str(config.path))}
|
|
2176
|
+
</p>
|
|
2177
|
+
</div>
|
|
2178
|
+
|
|
2179
|
+
<script>
|
|
2180
|
+
function showToast(msg) {{
|
|
2181
|
+
const t = document.createElement('div');
|
|
2182
|
+
t.className = 'toast';
|
|
2183
|
+
t.textContent = msg;
|
|
2184
|
+
document.body.appendChild(t);
|
|
2185
|
+
setTimeout(() => t.remove(), 2500);
|
|
2186
|
+
}}
|
|
2187
|
+
|
|
2188
|
+
function onProviderChange(val) {{
|
|
2189
|
+
document.getElementById('card-apikey').style.display = val === 'claude-api' ? 'block' : 'none';
|
|
2190
|
+
document.getElementById('card-ollama').style.display = val === 'ollama' ? 'block' : 'none';
|
|
2191
|
+
fetch('/api/config', {{
|
|
2192
|
+
method: 'POST',
|
|
2193
|
+
headers: {{'Content-Type': 'application/json'}},
|
|
2194
|
+
body: JSON.stringify({{provider: val}})
|
|
2195
|
+
}}).then(r => r.json()).then(d => {{
|
|
2196
|
+
if (d.ok) {{
|
|
2197
|
+
let badge = document.getElementById('provider-badge');
|
|
2198
|
+
if (val === 'claude-max') badge.innerHTML = '<span style="color:var(--success)">Active</span>';
|
|
2199
|
+
else badge.innerHTML = '<span style="color:var(--warning)">EXPERIMENTAL</span>';
|
|
2200
|
+
showToast('Provider saved: ' + val);
|
|
2201
|
+
}}
|
|
2202
|
+
}});
|
|
2203
|
+
}}
|
|
2204
|
+
|
|
2205
|
+
function saveApiKey() {{
|
|
2206
|
+
let key = document.getElementById('cfg-apikey').value;
|
|
2207
|
+
if (!key || key.includes('...')) {{ showToast('Enter the full API key'); return; }}
|
|
2208
|
+
fetch('/api/config', {{
|
|
2209
|
+
method: 'POST',
|
|
2210
|
+
headers: {{'Content-Type': 'application/json'}},
|
|
2211
|
+
body: JSON.stringify({{api_key: key}})
|
|
2212
|
+
}}).then(r => r.json()).then(d => {{
|
|
2213
|
+
if (d.ok) showToast('API key saved');
|
|
2214
|
+
}});
|
|
2215
|
+
}}
|
|
2216
|
+
|
|
2217
|
+
function saveOllamaConfig() {{
|
|
2218
|
+
let host = document.getElementById('cfg-ollama-host').value;
|
|
2219
|
+
let model = document.getElementById('cfg-ollama-model').value;
|
|
2220
|
+
fetch('/api/config', {{
|
|
2221
|
+
method: 'POST',
|
|
2222
|
+
headers: {{'Content-Type': 'application/json'}},
|
|
2223
|
+
body: JSON.stringify({{ollama_host: host, ollama_model: model}})
|
|
2224
|
+
}}).then(r => r.json()).then(d => {{
|
|
2225
|
+
if (d.ok) showToast('Ollama config saved');
|
|
2226
|
+
}});
|
|
2227
|
+
}}
|
|
2228
|
+
|
|
2229
|
+
function saveSessionConfig() {{
|
|
2230
|
+
let cfg = {{
|
|
2231
|
+
max_sessions: parseInt(document.getElementById('cfg-max-sessions').value),
|
|
2232
|
+
max_turns: parseInt(document.getElementById('cfg-max-turns').value),
|
|
2233
|
+
session_delay: parseInt(document.getElementById('cfg-delay').value),
|
|
2234
|
+
context_threshold: parseFloat(document.getElementById('cfg-threshold').value),
|
|
2235
|
+
}};
|
|
2236
|
+
fetch('/api/config', {{
|
|
2237
|
+
method: 'POST',
|
|
2238
|
+
headers: {{'Content-Type': 'application/json'}},
|
|
2239
|
+
body: JSON.stringify(cfg)
|
|
2240
|
+
}}).then(r => r.json()).then(d => {{
|
|
2241
|
+
if (d.ok) showToast('Session config saved');
|
|
2242
|
+
}});
|
|
2243
|
+
}}
|
|
2244
|
+
</script>
|
|
2245
|
+
'''
|
|
2246
|
+
|
|
2247
|
+
def _provider_badge(self, provider: str) -> str:
|
|
2248
|
+
"""Generate provider status badge HTML"""
|
|
2249
|
+
if provider == "claude-max":
|
|
2250
|
+
return '<span style="color:#a6e3a1">Active</span>'
|
|
2251
|
+
return '<span style="color:#f9e2af">EXPERIMENTAL</span> — untested'
|
|
2252
|
+
|
|
2253
|
+
def build_transform_page(self):
|
|
2254
|
+
return '''
|
|
2255
|
+
<div class="header">
|
|
2256
|
+
<h1 class="page-title"><i class="bi bi-magic"></i> Transform</h1>
|
|
2257
|
+
<button class="theme-toggle" onclick="toggleTheme()">
|
|
2258
|
+
<i class="bi bi-moon-stars"></i>
|
|
2259
|
+
</button>
|
|
2260
|
+
</div>
|
|
2261
|
+
|
|
2262
|
+
<div class="cards" style="grid-template-columns:repeat(3,1fr);margin-bottom:24px">
|
|
2263
|
+
<div class="card" style="text-align:center;padding:16px">
|
|
2264
|
+
<div style="font-size:24px;margin-bottom:8px"><i class="bi bi-pencil-square" style="color:var(--accent)"></i></div>
|
|
2265
|
+
<div style="font-size:13px;font-weight:600">1. Write</div>
|
|
2266
|
+
<div style="font-size:12px;color:var(--text-secondary)">Enter raw text or ideas</div>
|
|
2267
|
+
</div>
|
|
2268
|
+
<div class="card" style="text-align:center;padding:16px">
|
|
2269
|
+
<div style="font-size:24px;margin-bottom:8px"><i class="bi bi-magic" style="color:var(--warning)"></i></div>
|
|
2270
|
+
<div style="font-size:13px;font-weight:600">2. Transform</div>
|
|
2271
|
+
<div style="font-size:12px;color:var(--text-secondary)">AI breaks into tasks</div>
|
|
2272
|
+
</div>
|
|
2273
|
+
<div class="card" style="text-align:center;padding:16px">
|
|
2274
|
+
<div style="font-size:24px;margin-bottom:8px"><i class="bi bi-check2-all" style="color:var(--success)"></i></div>
|
|
2275
|
+
<div style="font-size:13px;font-weight:600">3. Confirm</div>
|
|
2276
|
+
<div style="font-size:12px;color:var(--text-secondary)">Review and add to queue</div>
|
|
2277
|
+
</div>
|
|
2278
|
+
</div>
|
|
2279
|
+
|
|
2280
|
+
<div class="form-section">
|
|
2281
|
+
<h3>Raw Text to Tasks</h3>
|
|
2282
|
+
<p style="color: var(--text-secondary); margin-bottom:16px; font-size:13px">
|
|
2283
|
+
Enter raw text, notes, or ideas — AI will break them into structured tasks.
|
|
2284
|
+
</p>
|
|
2285
|
+
<textarea id="transform-input" rows="6" placeholder="Example: Add login page with email/password fields Registration form with validation Password reset flow via email Write unit tests for auth module"></textarea>
|
|
2286
|
+
<div style="margin-top:12px;display:flex;align-items:center;gap:12px">
|
|
2287
|
+
<button class="btn-primary" onclick="doTransform()" id="transform-btn">
|
|
2288
|
+
<i class="bi bi-magic"></i> AI Transform
|
|
2289
|
+
</button>
|
|
2290
|
+
<span id="transform-status" style="font-size:13px;color:var(--text-secondary)"></span>
|
|
2291
|
+
</div>
|
|
2292
|
+
</div>
|
|
2293
|
+
|
|
2294
|
+
<div id="transform-preview" style="display:none;margin-top:24px">
|
|
2295
|
+
<div class="task-list">
|
|
2296
|
+
<div class="task-header">
|
|
2297
|
+
<h3>Preview Tasks</h3>
|
|
2298
|
+
<button class="btn-success" onclick="confirmTransform()">
|
|
2299
|
+
<i class="bi bi-check-all"></i> Add Selected
|
|
2300
|
+
</button>
|
|
2301
|
+
</div>
|
|
2302
|
+
<div id="preview-tasks"></div>
|
|
2303
|
+
</div>
|
|
2304
|
+
</div>
|
|
2305
|
+
|
|
2306
|
+
<script>
|
|
2307
|
+
let transformedTasks = [];
|
|
2308
|
+
|
|
2309
|
+
function doTransform() {
|
|
2310
|
+
const text = document.getElementById('transform-input').value.trim();
|
|
2311
|
+
if (!text) return;
|
|
2312
|
+
const btn = document.getElementById('transform-btn');
|
|
2313
|
+
const status = document.getElementById('transform-status');
|
|
2314
|
+
btn.disabled = true;
|
|
2315
|
+
status.textContent = 'Thinking...';
|
|
2316
|
+
|
|
2317
|
+
fetch('/transform', {
|
|
2318
|
+
method: 'POST',
|
|
2319
|
+
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
|
2320
|
+
body: 'text=' + encodeURIComponent(text)
|
|
2321
|
+
})
|
|
2322
|
+
.then(r => r.json())
|
|
2323
|
+
.then(data => {
|
|
2324
|
+
btn.disabled = false;
|
|
2325
|
+
if (data.tasks && data.tasks.length > 0) {
|
|
2326
|
+
transformedTasks = data.tasks;
|
|
2327
|
+
status.textContent = data.tasks.length + ' tasks generated';
|
|
2328
|
+
renderPreview(data.tasks);
|
|
2329
|
+
} else {
|
|
2330
|
+
status.textContent = data.error || 'No tasks generated';
|
|
2331
|
+
}
|
|
2332
|
+
})
|
|
2333
|
+
.catch(err => {
|
|
2334
|
+
btn.disabled = false;
|
|
2335
|
+
status.textContent = 'Error: ' + err.message;
|
|
2336
|
+
});
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
function renderPreview(tasks) {
|
|
2340
|
+
const container = document.getElementById('preview-tasks');
|
|
2341
|
+
container.innerHTML = '';
|
|
2342
|
+
tasks.forEach((t, i) => {
|
|
2343
|
+
container.innerHTML += '<div class="task"><div class="task-check"><input type="checkbox" checked data-idx="' + i + '" style="width:18px;height:18px;cursor:pointer"></div><div class="task-content"><div class="task-title">' + escHtml(t.title) + '</div><div class="task-meta">' + escHtml(t.description || '') + '</div></div></div>';
|
|
2344
|
+
});
|
|
2345
|
+
document.getElementById('transform-preview').style.display = 'block';
|
|
2346
|
+
}
|
|
2347
|
+
|
|
2348
|
+
function confirmTransform() {
|
|
2349
|
+
const checks = document.querySelectorAll('#preview-tasks input[type=checkbox]');
|
|
2350
|
+
const selected = [];
|
|
2351
|
+
checks.forEach(cb => {
|
|
2352
|
+
if (cb.checked) selected.push(transformedTasks[parseInt(cb.dataset.idx)]);
|
|
2353
|
+
});
|
|
2354
|
+
if (selected.length === 0) return;
|
|
2355
|
+
fetch('/transform-confirm', {
|
|
2356
|
+
method: 'POST',
|
|
2357
|
+
headers: {'Content-Type': 'application/json'},
|
|
2358
|
+
body: JSON.stringify({tasks: selected})
|
|
2359
|
+
})
|
|
2360
|
+
.then(r => r.json())
|
|
2361
|
+
.then(data => {
|
|
2362
|
+
if (data.ok) {
|
|
2363
|
+
document.getElementById('transform-status').textContent = data.added + ' tasks added!';
|
|
2364
|
+
document.getElementById('transform-preview').style.display = 'none';
|
|
2365
|
+
document.getElementById('transform-input').value = '';
|
|
2366
|
+
}
|
|
2367
|
+
});
|
|
2368
|
+
}
|
|
2369
|
+
</script>
|
|
2370
|
+
'''
|
|
2371
|
+
|
|
2372
|
+
def _transform_text(self, raw_text: str) -> dict:
|
|
2373
|
+
"""Use Claude to break raw text into structured tasks"""
|
|
2374
|
+
import os
|
|
2375
|
+
import subprocess
|
|
2376
|
+
|
|
2377
|
+
prompt = f'''Break the following text into structured tasks for a software project.
|
|
2378
|
+
Return ONLY valid JSON array, no other text. Each task object must have:
|
|
2379
|
+
- "title": short task title (imperative, e.g. "Add login page")
|
|
2380
|
+
- "description": 1-2 sentence description
|
|
2381
|
+
|
|
2382
|
+
Text to transform:
|
|
2383
|
+
{raw_text}
|
|
2384
|
+
|
|
2385
|
+
Return format: [{{"title": "...", "description": "..."}}, ...]'''
|
|
2386
|
+
|
|
2387
|
+
try:
|
|
2388
|
+
env = os.environ.copy()
|
|
2389
|
+
env.pop("CLAUDECODE", None)
|
|
2390
|
+
result = subprocess.run(
|
|
2391
|
+
["claude", "-p", prompt, "--max-turns", "1", "--no-session-persistence", "--dangerously-skip-permissions"],
|
|
2392
|
+
cwd=str(PROJECT_DIR),
|
|
2393
|
+
env=env,
|
|
2394
|
+
capture_output=True,
|
|
2395
|
+
text=True,
|
|
2396
|
+
timeout=300,
|
|
2397
|
+
)
|
|
2398
|
+
output = result.stdout.strip()
|
|
2399
|
+
# Try to extract JSON from output
|
|
2400
|
+
start = output.find('[')
|
|
2401
|
+
end = output.rfind(']')
|
|
2402
|
+
if start >= 0 and end > start:
|
|
2403
|
+
tasks = json.loads(output[start:end+1])
|
|
2404
|
+
return {"tasks": tasks}
|
|
2405
|
+
return {"tasks": [], "error": "Could not parse AI response"}
|
|
2406
|
+
except subprocess.TimeoutExpired:
|
|
2407
|
+
return {"tasks": [], "error": "AI request timed out (5 min)"}
|
|
2408
|
+
except FileNotFoundError:
|
|
2409
|
+
return {"tasks": [], "error": "Claude CLI not found"}
|
|
2410
|
+
except Exception as e:
|
|
2411
|
+
return {"tasks": [], "error": str(e)}
|
|
2412
|
+
|
|
2413
|
+
def send_json_status(self):
|
|
2414
|
+
checkpoint = CheckpointManager(PROJECT_DIR)
|
|
2415
|
+
tasks = TaskManager(PROJECT_DIR)
|
|
2416
|
+
|
|
2417
|
+
# Get live metrics from agent loop if running
|
|
2418
|
+
metrics = {}
|
|
2419
|
+
if AGENT_LOOP and hasattr(AGENT_LOOP, 'get_session_metrics'):
|
|
2420
|
+
metrics = AGENT_LOOP.get_session_metrics()
|
|
2421
|
+
else:
|
|
2422
|
+
# Try to load from checkpoint
|
|
2423
|
+
cp = checkpoint.load()
|
|
2424
|
+
metrics = cp.get('session_metrics', {})
|
|
2425
|
+
|
|
2426
|
+
data = {
|
|
2427
|
+
'checkpoint': checkpoint.load(),
|
|
2428
|
+
'tasks': [t.to_dict() for t in tasks.get_tasks()],
|
|
2429
|
+
'progress': tasks.get_progress(),
|
|
2430
|
+
'running': AGENT_RUNNING,
|
|
2431
|
+
'activity': ACTIVITY_LOG[-10:],
|
|
2432
|
+
'metrics': metrics,
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
self.send_response(200)
|
|
2436
|
+
self.send_header('Content-Type', 'application/json')
|
|
2437
|
+
self.end_headers()
|
|
2438
|
+
self.wfile.write(json.dumps(data).encode('utf-8'))
|
|
2439
|
+
|
|
2440
|
+
def send_json_log(self):
|
|
2441
|
+
"""Return agent log entries since a given index"""
|
|
2442
|
+
query = urlparse(self.path).query
|
|
2443
|
+
params = parse_qs(query)
|
|
2444
|
+
since = int(params.get('since', ['0'])[0])
|
|
2445
|
+
|
|
2446
|
+
entries = AGENT_LOG_BUFFER[since:]
|
|
2447
|
+
data = {
|
|
2448
|
+
'entries': entries,
|
|
2449
|
+
'total': len(AGENT_LOG_BUFFER),
|
|
2450
|
+
'since': since,
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
self.send_response(200)
|
|
2454
|
+
self.send_header('Content-Type', 'application/json')
|
|
2455
|
+
self.end_headers()
|
|
2456
|
+
self.wfile.write(json.dumps(data).encode('utf-8'))
|
|
2457
|
+
|
|
2458
|
+
def send_json_config(self):
|
|
2459
|
+
"""Return current config (API key masked)"""
|
|
2460
|
+
from .config import Config, DEFAULTS
|
|
2461
|
+
config = Config(PROJECT_DIR)
|
|
2462
|
+
data = config.get_all()
|
|
2463
|
+
# Mask API key for security
|
|
2464
|
+
if data.get("api_key"):
|
|
2465
|
+
data["api_key"] = config.mask_api_key(data["api_key"])
|
|
2466
|
+
data["_defaults"] = DEFAULTS
|
|
2467
|
+
self.send_response(200)
|
|
2468
|
+
self.send_header('Content-Type', 'application/json')
|
|
2469
|
+
self.end_headers()
|
|
2470
|
+
self.wfile.write(json.dumps(data).encode('utf-8'))
|
|
2471
|
+
|
|
2472
|
+
def _render_log_entries(self):
|
|
2473
|
+
"""Render existing log buffer entries as HTML (terminal style)"""
|
|
2474
|
+
label_map = {
|
|
2475
|
+
'read': 'READ', 'edit': 'EDIT', 'write': 'WRITE',
|
|
2476
|
+
'bash': 'BASH', 'thinking': 'THINK', 'text': 'OUT',
|
|
2477
|
+
'metric': 'METRIC', 'verify': 'CHECK',
|
|
2478
|
+
}
|
|
2479
|
+
color_map = {
|
|
2480
|
+
'read': '#89b4fa', 'edit': '#fab387', 'write': '#a6e3a1',
|
|
2481
|
+
'bash': '#cba6f7', 'thinking': '#f9e2af', 'text': '#6c7086',
|
|
2482
|
+
'metric': '#89dceb', 'verify': '#a6e3a1',
|
|
2483
|
+
}
|
|
2484
|
+
html = ''
|
|
2485
|
+
for e in AGENT_LOG_BUFFER[-50:]:
|
|
2486
|
+
etype = e.get('type', 'text')
|
|
2487
|
+
label = label_map.get(etype, 'LOG')
|
|
2488
|
+
color = color_map.get(etype, '#6c7086')
|
|
2489
|
+
html += f'<div class="log-entry"><span class="px-icon" style="background:{color}"></span><span class="log-time">{esc(e["time"])}</span><span class="log-label" style="color:{color}">{label}</span><span class="log-text">{esc(e["line"][:150])}</span></div>'
|
|
2490
|
+
if not AGENT_LOG_BUFFER:
|
|
2491
|
+
html = '<div class="log-empty">Waiting for agent output...<span class="log-cursor"></span></div>'
|
|
2492
|
+
return html
|
|
2493
|
+
|
|
2494
|
+
def _render_raw_log(self):
|
|
2495
|
+
"""Render raw log text for initial page load"""
|
|
2496
|
+
return esc('\n'.join(e['line'] for e in AGENT_LOG_BUFFER[-50:]))
|
|
2497
|
+
|
|
2498
|
+
def start_agent(self):
|
|
2499
|
+
global AGENT_RUNNING, AGENT_LOOP
|
|
2500
|
+
if not AGENT_RUNNING:
|
|
2501
|
+
AGENT_LOG_BUFFER.clear()
|
|
2502
|
+
log_activity("Agent started", "", "success")
|
|
2503
|
+
|
|
2504
|
+
def run():
|
|
2505
|
+
global AGENT_RUNNING, AGENT_LOOP
|
|
2506
|
+
AGENT_RUNNING = True
|
|
2507
|
+
try:
|
|
2508
|
+
from .config import Config
|
|
2509
|
+
from .loop import SessionLoop
|
|
2510
|
+
config = Config(PROJECT_DIR)
|
|
2511
|
+
resolved = config.resolve()
|
|
2512
|
+
loop = SessionLoop(project_dir=PROJECT_DIR, **resolved)
|
|
2513
|
+
loop._log_callback = _on_agent_line
|
|
2514
|
+
AGENT_LOOP = loop
|
|
2515
|
+
loop.start()
|
|
2516
|
+
finally:
|
|
2517
|
+
AGENT_RUNNING = False
|
|
2518
|
+
AGENT_LOOP = None
|
|
2519
|
+
log_activity("Agent stopped", "", "warning")
|
|
2520
|
+
|
|
2521
|
+
thread = threading.Thread(target=run, daemon=True)
|
|
2522
|
+
thread.start()
|
|
2523
|
+
|
|
2524
|
+
def stop_agent(self):
|
|
2525
|
+
global AGENT_RUNNING, AGENT_LOOP
|
|
2526
|
+
if AGENT_LOOP:
|
|
2527
|
+
AGENT_LOOP.stop()
|
|
2528
|
+
AGENT_RUNNING = False
|
|
2529
|
+
log_activity("Agent stop requested", "", "warning")
|
|
2530
|
+
|
|
2531
|
+
|
|
2532
|
+
def find_free_port(start_port: int = 7331, max_attempts: int = 20) -> int:
|
|
2533
|
+
for offset in range(max_attempts):
|
|
2534
|
+
port = start_port + offset
|
|
2535
|
+
try:
|
|
2536
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
2537
|
+
s.bind(('localhost', port))
|
|
2538
|
+
return port
|
|
2539
|
+
except OSError:
|
|
2540
|
+
continue
|
|
2541
|
+
raise RuntimeError(f"No free port in range {start_port}-{start_port + max_attempts}")
|
|
2542
|
+
|
|
2543
|
+
|
|
2544
|
+
def run_dashboard(project_dir: Path, port: int = None, open_browser: bool = True):
|
|
2545
|
+
global PROJECT_DIR
|
|
2546
|
+
PROJECT_DIR = Path(project_dir).resolve()
|
|
2547
|
+
|
|
2548
|
+
a1_dir = PROJECT_DIR / '.a1'
|
|
2549
|
+
if not a1_dir.exists():
|
|
2550
|
+
a1_dir.mkdir(parents=True)
|
|
2551
|
+
(a1_dir / 'sessions').mkdir()
|
|
2552
|
+
(a1_dir / 'checkpoints').mkdir()
|
|
2553
|
+
|
|
2554
|
+
if port is None:
|
|
2555
|
+
port = find_free_port(7331)
|
|
2556
|
+
else:
|
|
2557
|
+
try:
|
|
2558
|
+
port = find_free_port(port, max_attempts=1)
|
|
2559
|
+
except RuntimeError:
|
|
2560
|
+
port = find_free_port(7331)
|
|
2561
|
+
|
|
2562
|
+
server = HTTPServer(('localhost', port), DashboardHandler)
|
|
2563
|
+
|
|
2564
|
+
url = f'http://localhost:{port}'
|
|
2565
|
+
print()
|
|
2566
|
+
print(' PocketCoder-A1 Dashboard')
|
|
2567
|
+
print(' -------------------------')
|
|
2568
|
+
print(f' URL: {url}')
|
|
2569
|
+
print(f' Project: {PROJECT_DIR}')
|
|
2570
|
+
print()
|
|
2571
|
+
print(' Press Ctrl+C to stop')
|
|
2572
|
+
print()
|
|
2573
|
+
|
|
2574
|
+
log_activity("Dashboard started", f"Port {port}", "info")
|
|
2575
|
+
|
|
2576
|
+
if open_browser:
|
|
2577
|
+
webbrowser.open(url)
|
|
2578
|
+
|
|
2579
|
+
try:
|
|
2580
|
+
server.serve_forever()
|
|
2581
|
+
except KeyboardInterrupt:
|
|
2582
|
+
print('\\nDashboard stopped')
|
|
2583
|
+
server.shutdown()
|
|
2584
|
+
|
|
2585
|
+
|
|
2586
|
+
if __name__ == '__main__':
|
|
2587
|
+
import sys
|
|
2588
|
+
project = sys.argv[1] if len(sys.argv) > 1 else '.'
|
|
2589
|
+
run_dashboard(project)
|