tlc-claude-code 0.6.3 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,708 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>TLC Dev Server</title>
7
+ <style>
8
+ * { box-sizing: border-box; margin: 0; padding: 0; }
9
+ body {
10
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
11
+ background: #0d1117;
12
+ color: #e6edf3;
13
+ height: 100vh;
14
+ overflow: hidden;
15
+ }
16
+
17
+ .header {
18
+ background: #161b22;
19
+ padding: 12px 20px;
20
+ display: flex;
21
+ justify-content: space-between;
22
+ align-items: center;
23
+ border-bottom: 1px solid #30363d;
24
+ }
25
+ .header h1 {
26
+ font-size: 16px;
27
+ color: #58a6ff;
28
+ display: flex;
29
+ align-items: center;
30
+ gap: 10px;
31
+ }
32
+ .header h1 .logo {
33
+ font-family: monospace;
34
+ font-size: 14px;
35
+ color: #7ee787;
36
+ }
37
+ .header .status {
38
+ display: flex;
39
+ gap: 12px;
40
+ align-items: center;
41
+ }
42
+ .header .status .dot {
43
+ width: 8px;
44
+ height: 8px;
45
+ border-radius: 50%;
46
+ }
47
+ .header .status .dot.running { background: #3fb950; }
48
+ .header .status .dot.stopped { background: #f85149; }
49
+ .header button {
50
+ padding: 6px 14px;
51
+ background: #21262d;
52
+ border: 1px solid #30363d;
53
+ border-radius: 6px;
54
+ color: #e6edf3;
55
+ cursor: pointer;
56
+ font-size: 13px;
57
+ }
58
+ .header button:hover {
59
+ background: #30363d;
60
+ }
61
+
62
+ .main {
63
+ display: grid;
64
+ grid-template-columns: 1fr 380px;
65
+ grid-template-rows: 1fr 1fr;
66
+ height: calc(100vh - 50px);
67
+ }
68
+
69
+ .preview {
70
+ grid-row: 1 / 3;
71
+ border-right: 1px solid #30363d;
72
+ display: flex;
73
+ flex-direction: column;
74
+ }
75
+ .preview-header {
76
+ padding: 8px 12px;
77
+ background: #161b22;
78
+ display: flex;
79
+ justify-content: space-between;
80
+ align-items: center;
81
+ border-bottom: 1px solid #30363d;
82
+ gap: 10px;
83
+ }
84
+ .preview-header .label {
85
+ font-size: 12px;
86
+ color: #8b949e;
87
+ text-transform: uppercase;
88
+ }
89
+ .preview-header input {
90
+ flex: 1;
91
+ padding: 6px 10px;
92
+ background: #0d1117;
93
+ border: 1px solid #30363d;
94
+ border-radius: 6px;
95
+ color: #e6edf3;
96
+ font-size: 13px;
97
+ }
98
+ .preview-header button {
99
+ padding: 6px 12px;
100
+ background: transparent;
101
+ border: 1px solid #30363d;
102
+ border-radius: 6px;
103
+ color: #8b949e;
104
+ cursor: pointer;
105
+ font-size: 12px;
106
+ }
107
+ .preview-header button:hover {
108
+ color: #e6edf3;
109
+ border-color: #58a6ff;
110
+ }
111
+ .preview iframe {
112
+ flex: 1;
113
+ border: none;
114
+ background: white;
115
+ }
116
+ .preview .loading {
117
+ flex: 1;
118
+ display: flex;
119
+ align-items: center;
120
+ justify-content: center;
121
+ background: #1a1a2e;
122
+ color: #8b949e;
123
+ }
124
+
125
+ .logs {
126
+ border-bottom: 1px solid #30363d;
127
+ display: flex;
128
+ flex-direction: column;
129
+ }
130
+ .logs-header {
131
+ padding: 8px 12px;
132
+ background: #161b22;
133
+ display: flex;
134
+ gap: 8px;
135
+ border-bottom: 1px solid #30363d;
136
+ }
137
+ .logs-header button {
138
+ padding: 4px 12px;
139
+ background: transparent;
140
+ border: 1px solid #30363d;
141
+ border-radius: 4px;
142
+ color: #8b949e;
143
+ cursor: pointer;
144
+ font-size: 12px;
145
+ }
146
+ .logs-header button.active {
147
+ background: #21262d;
148
+ color: #e6edf3;
149
+ border-color: #58a6ff;
150
+ }
151
+ .logs-header button:hover:not(.active) {
152
+ background: #21262d;
153
+ }
154
+ .logs-content {
155
+ flex: 1;
156
+ overflow-y: auto;
157
+ padding: 10px;
158
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
159
+ font-size: 12px;
160
+ background: #010409;
161
+ line-height: 1.5;
162
+ }
163
+ .log-line { padding: 1px 0; white-space: pre-wrap; word-break: break-word; }
164
+ .log-line.error { color: #f85149; }
165
+ .log-line.success { color: #3fb950; }
166
+ .log-line.info { color: #58a6ff; }
167
+ .log-line.warn { color: #d29922; }
168
+
169
+ .sidebar {
170
+ display: flex;
171
+ flex-direction: column;
172
+ }
173
+
174
+ .tasks {
175
+ flex: 1;
176
+ overflow-y: auto;
177
+ padding: 12px;
178
+ }
179
+ .tasks-header {
180
+ display: flex;
181
+ justify-content: space-between;
182
+ align-items: center;
183
+ margin-bottom: 12px;
184
+ }
185
+ .tasks h2 {
186
+ font-size: 11px;
187
+ text-transform: uppercase;
188
+ color: #8b949e;
189
+ letter-spacing: 0.5px;
190
+ }
191
+ .tasks .phase-name {
192
+ font-size: 13px;
193
+ color: #e6edf3;
194
+ font-weight: 500;
195
+ }
196
+ .task {
197
+ padding: 10px 12px;
198
+ background: #161b22;
199
+ border-radius: 6px;
200
+ margin-bottom: 8px;
201
+ border-left: 3px solid transparent;
202
+ cursor: default;
203
+ }
204
+ .task.done {
205
+ border-left-color: #3fb950;
206
+ opacity: 0.6;
207
+ }
208
+ .task.working {
209
+ border-left-color: #58a6ff;
210
+ background: #1c2128;
211
+ }
212
+ .task.available {
213
+ border-left-color: #484f58;
214
+ }
215
+ .task .title {
216
+ font-weight: 500;
217
+ font-size: 13px;
218
+ display: flex;
219
+ align-items: center;
220
+ gap: 8px;
221
+ }
222
+ .task .title .icon {
223
+ font-size: 12px;
224
+ }
225
+ .task.done .icon { color: #3fb950; }
226
+ .task.working .icon { color: #58a6ff; }
227
+ .task.available .icon { color: #484f58; }
228
+ .task .meta {
229
+ font-size: 11px;
230
+ color: #8b949e;
231
+ margin-top: 4px;
232
+ margin-left: 20px;
233
+ }
234
+
235
+ .bug-form {
236
+ padding: 12px;
237
+ background: #161b22;
238
+ border-top: 1px solid #30363d;
239
+ }
240
+ .bug-form h2 {
241
+ font-size: 11px;
242
+ text-transform: uppercase;
243
+ color: #8b949e;
244
+ margin-bottom: 10px;
245
+ letter-spacing: 0.5px;
246
+ }
247
+ .bug-form textarea {
248
+ width: 100%;
249
+ height: 60px;
250
+ padding: 8px;
251
+ background: #0d1117;
252
+ border: 1px solid #30363d;
253
+ border-radius: 6px;
254
+ color: #e6edf3;
255
+ resize: none;
256
+ margin-bottom: 8px;
257
+ font-family: inherit;
258
+ font-size: 13px;
259
+ }
260
+ .bug-form textarea:focus {
261
+ outline: none;
262
+ border-color: #58a6ff;
263
+ }
264
+ .bug-form .actions {
265
+ display: flex;
266
+ gap: 8px;
267
+ }
268
+ .bug-form button {
269
+ padding: 8px 14px;
270
+ border-radius: 6px;
271
+ border: none;
272
+ cursor: pointer;
273
+ font-size: 12px;
274
+ font-weight: 500;
275
+ }
276
+ .bug-form .screenshot {
277
+ background: #21262d;
278
+ color: #e6edf3;
279
+ flex: 1;
280
+ border: 1px solid #30363d;
281
+ }
282
+ .bug-form .screenshot:hover {
283
+ background: #30363d;
284
+ }
285
+ .bug-form .submit {
286
+ background: #238636;
287
+ color: white;
288
+ padding-left: 20px;
289
+ padding-right: 20px;
290
+ }
291
+ .bug-form .submit:hover {
292
+ background: #2ea043;
293
+ }
294
+
295
+ .stats {
296
+ display: grid;
297
+ grid-template-columns: repeat(3, 1fr);
298
+ gap: 8px;
299
+ padding: 12px;
300
+ background: #161b22;
301
+ border-top: 1px solid #30363d;
302
+ }
303
+ .stat {
304
+ text-align: center;
305
+ padding: 10px;
306
+ background: #0d1117;
307
+ border-radius: 6px;
308
+ }
309
+ .stat-value {
310
+ font-size: 22px;
311
+ font-weight: bold;
312
+ font-variant-numeric: tabular-nums;
313
+ }
314
+ .stat-value.green { color: #3fb950; }
315
+ .stat-value.red { color: #f85149; }
316
+ .stat-value.blue { color: #58a6ff; }
317
+ .stat-label {
318
+ font-size: 10px;
319
+ color: #8b949e;
320
+ text-transform: uppercase;
321
+ margin-top: 4px;
322
+ }
323
+
324
+ /* Responsive */
325
+ @media (max-width: 900px) {
326
+ .main {
327
+ grid-template-columns: 1fr;
328
+ grid-template-rows: 1fr auto auto;
329
+ }
330
+ .preview {
331
+ grid-row: auto;
332
+ min-height: 300px;
333
+ }
334
+ .logs {
335
+ max-height: 200px;
336
+ }
337
+ }
338
+ </style>
339
+ </head>
340
+ <body>
341
+ <div class="header">
342
+ <h1>
343
+ <span class="logo">TLC</span>
344
+ Dev Server
345
+ </h1>
346
+ <div class="status">
347
+ <span class="dot" id="status-dot"></span>
348
+ <span id="status-text">Connecting...</span>
349
+ <button onclick="runTests()">Run Tests</button>
350
+ <button onclick="restartApp()">Restart App</button>
351
+ </div>
352
+ </div>
353
+
354
+ <div class="main">
355
+ <div class="preview">
356
+ <div class="preview-header">
357
+ <span class="label">Preview</span>
358
+ <input type="text" id="url-bar" value="http://localhost:3000/" onkeypress="if(event.key==='Enter')navigateTo(this.value)">
359
+ <button onclick="refreshPreview()">Refresh</button>
360
+ <button onclick="openInTab()">Open in Tab</button>
361
+ </div>
362
+ <iframe id="app-frame" src="/app/"></iframe>
363
+ </div>
364
+
365
+ <div class="logs">
366
+ <div class="logs-header">
367
+ <button class="active" data-type="app" onclick="showLogs('app')">App</button>
368
+ <button data-type="test" onclick="showLogs('test')">Tests</button>
369
+ <button data-type="git" onclick="showLogs('git')">Git</button>
370
+ </div>
371
+ <div class="logs-content" id="logs"></div>
372
+ </div>
373
+
374
+ <div class="sidebar">
375
+ <div class="tasks" id="tasks">
376
+ <div class="tasks-header">
377
+ <h2>Tasks</h2>
378
+ <span class="phase-name" id="phase-name">Loading...</span>
379
+ </div>
380
+ </div>
381
+
382
+ <div class="bug-form">
383
+ <h2>Report Bug</h2>
384
+ <textarea id="bug-desc" placeholder="Describe the issue..."></textarea>
385
+ <div class="actions">
386
+ <button class="screenshot" onclick="takeScreenshot()">Screenshot</button>
387
+ <button class="submit" onclick="submitBug()">Submit Bug</button>
388
+ </div>
389
+ </div>
390
+
391
+ <div class="stats">
392
+ <div class="stat">
393
+ <div class="stat-value green" id="tests-pass">-</div>
394
+ <div class="stat-label">Passing</div>
395
+ </div>
396
+ <div class="stat">
397
+ <div class="stat-value red" id="tests-fail">-</div>
398
+ <div class="stat-label">Failing</div>
399
+ </div>
400
+ <div class="stat">
401
+ <div class="stat-value blue" id="bugs-open">-</div>
402
+ <div class="stat-label">Open Bugs</div>
403
+ </div>
404
+ </div>
405
+ </div>
406
+ </div>
407
+
408
+ <script>
409
+ let ws;
410
+ const logs = { app: [], test: [], git: [] };
411
+ let currentLogType = 'app';
412
+ let appPort = 3000;
413
+ let reconnectAttempts = 0;
414
+
415
+ function connect() {
416
+ ws = new WebSocket(`ws://${location.host}`);
417
+
418
+ ws.onopen = () => {
419
+ console.log('Connected to TLC server');
420
+ reconnectAttempts = 0;
421
+ updateStatus(true);
422
+ addLog('app', 'Connected to TLC Dev Server', 'success');
423
+ };
424
+
425
+ ws.onclose = () => {
426
+ console.log('Disconnected from TLC server');
427
+ updateStatus(false);
428
+ addLog('app', 'Disconnected from server', 'error');
429
+
430
+ // Reconnect with backoff
431
+ if (reconnectAttempts < 10) {
432
+ const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
433
+ setTimeout(connect, delay);
434
+ reconnectAttempts++;
435
+ }
436
+ };
437
+
438
+ ws.onerror = (err) => {
439
+ console.error('WebSocket error:', err);
440
+ };
441
+
442
+ ws.onmessage = (event) => {
443
+ const msg = JSON.parse(event.data);
444
+ handleMessage(msg);
445
+ };
446
+ }
447
+
448
+ function handleMessage(msg) {
449
+ switch(msg.type) {
450
+ case 'init':
451
+ // Restore logs from server
452
+ Object.assign(logs, msg.data.logs || {});
453
+ appPort = msg.data.appPort || 3000;
454
+ document.getElementById('url-bar').value = `http://localhost:${appPort}/`;
455
+ renderLogs();
456
+ break;
457
+
458
+ case 'app-log':
459
+ addLog('app', msg.data.data, msg.data.level || detectLogLevel(msg.data.data));
460
+ break;
461
+
462
+ case 'test-output':
463
+ addLog('test', msg.data.data, msg.data.stream === 'stderr' ? 'error' : '');
464
+ break;
465
+
466
+ case 'test-complete':
467
+ const result = msg.data.exitCode === 0 ? 'passed' : 'failed';
468
+ addLog('test', `Tests ${result}`, msg.data.exitCode === 0 ? 'success' : 'error');
469
+ refreshStats();
470
+ break;
471
+
472
+ case 'git-activity':
473
+ addLog('git', msg.data.entry, 'info');
474
+ break;
475
+
476
+ case 'app-restart':
477
+ addLog('app', '--- App restarting ---', 'warn');
478
+ setTimeout(refreshPreview, 2000);
479
+ break;
480
+
481
+ case 'app-start':
482
+ appPort = msg.data.port;
483
+ document.getElementById('url-bar').value = `http://localhost:${appPort}/`;
484
+ setTimeout(refreshPreview, 1000);
485
+ break;
486
+
487
+ case 'file-change':
488
+ addLog('app', `File changed: ${msg.data.path}`, 'info');
489
+ break;
490
+
491
+ case 'task-update':
492
+ refreshTasks();
493
+ break;
494
+
495
+ case 'bug-created':
496
+ addLog('app', `Bug ${msg.data.bugId} created`, 'warn');
497
+ refreshStats();
498
+ break;
499
+
500
+ case 'bug-update':
501
+ refreshStats();
502
+ break;
503
+ }
504
+ }
505
+
506
+ function updateStatus(connected) {
507
+ const dot = document.getElementById('status-dot');
508
+ const text = document.getElementById('status-text');
509
+
510
+ if (connected) {
511
+ dot.className = 'dot running';
512
+ text.textContent = 'Running';
513
+ } else {
514
+ dot.className = 'dot stopped';
515
+ text.textContent = 'Disconnected';
516
+ }
517
+ }
518
+
519
+ function detectLogLevel(text) {
520
+ if (/error|fail|exception|ECONNREFUSED/i.test(text)) return 'error';
521
+ if (/warn/i.test(text)) return 'warn';
522
+ if (/success|✓|pass|ready|started|listening/i.test(text)) return 'success';
523
+ if (/info|GET|POST|PUT|DELETE/i.test(text)) return 'info';
524
+ return '';
525
+ }
526
+
527
+ function addLog(type, text, level = '') {
528
+ if (!text || !text.trim()) return;
529
+
530
+ const lines = text.split('\n');
531
+ for (const line of lines) {
532
+ if (line.trim()) {
533
+ logs[type].push({ text: line, level: level || detectLogLevel(line), time: new Date() });
534
+ }
535
+ }
536
+
537
+ // Keep last 1000 lines per type
538
+ while (logs[type].length > 1000) {
539
+ logs[type].shift();
540
+ }
541
+
542
+ if (type === currentLogType) {
543
+ renderLogs();
544
+ }
545
+ }
546
+
547
+ function renderLogs() {
548
+ const container = document.getElementById('logs');
549
+ container.innerHTML = logs[currentLogType].map(l =>
550
+ `<div class="log-line ${l.level}">${escapeHtml(l.text)}</div>`
551
+ ).join('');
552
+ container.scrollTop = container.scrollHeight;
553
+ }
554
+
555
+ function escapeHtml(text) {
556
+ const div = document.createElement('div');
557
+ div.textContent = text;
558
+ return div.innerHTML;
559
+ }
560
+
561
+ function showLogs(type) {
562
+ currentLogType = type;
563
+ document.querySelectorAll('.logs-header button').forEach(b => {
564
+ b.classList.toggle('active', b.dataset.type === type);
565
+ });
566
+ renderLogs();
567
+ }
568
+
569
+ function navigateTo(url) {
570
+ const proxyUrl = url.replace(/^http:\/\/localhost:\d+/, '/app');
571
+ document.getElementById('app-frame').src = proxyUrl;
572
+ }
573
+
574
+ function refreshPreview() {
575
+ const frame = document.getElementById('app-frame');
576
+ frame.src = frame.src;
577
+ }
578
+
579
+ function openInTab() {
580
+ window.open(`http://localhost:${appPort}`, '_blank');
581
+ }
582
+
583
+ async function runTests() {
584
+ addLog('test', '--- Running tests ---', 'info');
585
+ showLogs('test');
586
+ try {
587
+ await fetch('/api/test', { method: 'POST' });
588
+ } catch (e) {
589
+ addLog('test', 'Failed to start tests', 'error');
590
+ }
591
+ }
592
+
593
+ async function restartApp() {
594
+ try {
595
+ await fetch('/api/restart', { method: 'POST' });
596
+ } catch (e) {
597
+ addLog('app', 'Failed to restart app', 'error');
598
+ }
599
+ }
600
+
601
+ async function submitBug() {
602
+ const desc = document.getElementById('bug-desc').value.trim();
603
+ if (!desc) {
604
+ alert('Please describe the bug');
605
+ return;
606
+ }
607
+
608
+ try {
609
+ const res = await fetch('/api/bug', {
610
+ method: 'POST',
611
+ headers: { 'Content-Type': 'application/json' },
612
+ body: JSON.stringify({
613
+ description: desc,
614
+ url: document.getElementById('url-bar').value,
615
+ screenshot: window.lastScreenshot || null
616
+ })
617
+ });
618
+ const data = await res.json();
619
+
620
+ if (data.success) {
621
+ alert(`Bug ${data.bugId} created!`);
622
+ document.getElementById('bug-desc').value = '';
623
+ window.lastScreenshot = null;
624
+ } else {
625
+ alert('Failed to create bug: ' + (data.error || 'Unknown error'));
626
+ }
627
+ } catch (e) {
628
+ alert('Failed to submit bug');
629
+ }
630
+ }
631
+
632
+ async function takeScreenshot() {
633
+ // For now, just indicate screenshot capture
634
+ // Full implementation would use html2canvas or server-side puppeteer
635
+ alert('Screenshot feature: Click Submit to log bug with current URL');
636
+ window.lastScreenshot = 'url-only';
637
+ }
638
+
639
+ async function refreshTasks() {
640
+ try {
641
+ const res = await fetch('/api/tasks');
642
+ const tasks = await res.json();
643
+
644
+ const container = document.getElementById('tasks');
645
+ const phaseName = document.getElementById('phase-name');
646
+
647
+ if (tasks.phase) {
648
+ phaseName.textContent = `Phase ${tasks.phase}: ${tasks.phaseName || ''}`;
649
+ } else {
650
+ phaseName.textContent = 'No active phase';
651
+ }
652
+
653
+ const taskList = tasks.items || [];
654
+ if (taskList.length === 0) {
655
+ container.innerHTML = `
656
+ <div class="tasks-header">
657
+ <h2>Tasks</h2>
658
+ <span class="phase-name" id="phase-name">${phaseName.textContent}</span>
659
+ </div>
660
+ <div style="color: #8b949e; font-size: 13px; padding: 20px 0;">
661
+ No tasks found. Run /tlc:plan to create tasks.
662
+ </div>
663
+ `;
664
+ return;
665
+ }
666
+
667
+ container.innerHTML = `
668
+ <div class="tasks-header">
669
+ <h2>Tasks</h2>
670
+ <span class="phase-name" id="phase-name">${phaseName.textContent}</span>
671
+ </div>
672
+ ` + taskList.map(t => `
673
+ <div class="task ${t.status}">
674
+ <div class="title">
675
+ <span class="icon">${t.status === 'done' ? '✓' : t.status === 'working' ? '→' : '○'}</span>
676
+ ${escapeHtml(t.title)}
677
+ </div>
678
+ <div class="meta">${t.owner ? '@' + t.owner : 'Available'}</div>
679
+ </div>
680
+ `).join('');
681
+ } catch (e) {
682
+ console.error('Failed to refresh tasks:', e);
683
+ }
684
+ }
685
+
686
+ async function refreshStats() {
687
+ try {
688
+ const res = await fetch('/api/status');
689
+ const data = await res.json();
690
+
691
+ document.getElementById('tests-pass').textContent = data.testsPass || 0;
692
+ document.getElementById('tests-fail').textContent = data.testsFail || 0;
693
+ document.getElementById('bugs-open').textContent = data.bugsOpen || 0;
694
+ } catch (e) {
695
+ console.error('Failed to refresh stats:', e);
696
+ }
697
+ }
698
+
699
+ // Initialize
700
+ connect();
701
+ refreshTasks();
702
+ refreshStats();
703
+
704
+ // Refresh periodically
705
+ setInterval(refreshStats, 30000);
706
+ </script>
707
+ </body>
708
+ </html>