@neovate/code 0.12.1 → 0.12.3

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.
@@ -1,1465 +0,0 @@
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>Live Activity - Log Viewer</title>
7
- <style>
8
- * {
9
- margin: 0;
10
- padding: 0;
11
- box-sizing: border-box;
12
- }
13
-
14
- body {
15
- font-family:
16
- -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Microsoft YaHei',
17
- 'SimHei', sans-serif;
18
- background: #ffffff;
19
- min-height: 100vh;
20
- color: #333333;
21
- line-height: 1.4;
22
- }
23
-
24
- .container {
25
- max-width: 1400px;
26
- margin: 0 auto;
27
- padding: 1rem 2rem 2rem 2rem;
28
- }
29
-
30
- header {
31
- text-align: center;
32
- margin-bottom: 2rem;
33
- padding-bottom: 1rem;
34
- border-bottom: 1px solid #e0e0e0;
35
- }
36
-
37
- h1 {
38
- color: #000000;
39
- font-size: 1.8rem;
40
- margin-bottom: 0.5rem;
41
- font-weight: 600;
42
- }
43
-
44
- .subtitle {
45
- color: #666666;
46
- font-size: 1rem;
47
- margin-bottom: 1rem;
48
- }
49
-
50
- .nav-links {
51
- display: flex;
52
- justify-content: center;
53
- gap: 0;
54
- }
55
-
56
- .nav-link {
57
- background: #ffffff;
58
- color: #0066cc;
59
- text-decoration: none;
60
- padding: 0.5rem 1rem;
61
- border: 1px solid #e0e0e0;
62
- transition: all 0.2s ease;
63
- font-weight: normal;
64
- }
65
-
66
- .nav-link:hover {
67
- text-decoration: underline;
68
- border-color: #0066cc;
69
- }
70
-
71
- .nav-link.active {
72
- background: #0066cc;
73
- color: white;
74
- font-weight: 600;
75
- }
76
-
77
- .live-container {
78
- background: #ffffff;
79
- border: 1px solid #e0e0e0;
80
- overflow: hidden;
81
- height: calc(100vh - 200px);
82
- display: flex;
83
- flex-direction: column;
84
- }
85
-
86
- .live-header {
87
- background: #f5f5f5;
88
- color: #333333;
89
- padding: 1rem 1.5rem;
90
- display: flex;
91
- justify-content: space-between;
92
- align-items: center;
93
- border-bottom: 1px solid #e0e0e0;
94
- }
95
-
96
- .live-title {
97
- font-weight: 600;
98
- font-size: 1.1rem;
99
- display: flex;
100
- align-items: center;
101
- gap: 0.5rem;
102
- }
103
-
104
- .status-indicator {
105
- display: flex;
106
- align-items: center;
107
- gap: 0.5rem;
108
- font-size: 0.9rem;
109
- }
110
-
111
- .status-dot {
112
- width: 8px;
113
- height: 8px;
114
- border-radius: 50%;
115
- background: #cc0000;
116
- }
117
-
118
- .status-dot.connected {
119
- background: #0066cc;
120
- }
121
-
122
- .activity-stream {
123
- flex: 1;
124
- overflow-y: auto;
125
- padding: 0;
126
- background: #ffffff;
127
- }
128
-
129
- .activity-entry {
130
- border-bottom: 1px solid #e0e0e0;
131
- padding: 1rem 1.5rem;
132
- transition: background-color 0.2s ease;
133
- position: relative;
134
- }
135
-
136
- .activity-entry:hover {
137
- background: #f5f5f5;
138
- }
139
-
140
- .activity-entry.new {
141
- background: #f5f5f5;
142
- border-left: 3px solid #0066cc;
143
- }
144
-
145
- .activity-header {
146
- display: flex;
147
- justify-content: space-between;
148
- align-items: center;
149
- margin-bottom: 0.5rem;
150
- }
151
-
152
- .activity-source {
153
- font-weight: 600;
154
- color: #000000;
155
- font-size: 0.9rem;
156
- }
157
-
158
- .activity-timestamp {
159
- font-size: 0.8rem;
160
- color: #666666;
161
- font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;
162
- }
163
-
164
- .activity-content {
165
- background: #ffffff;
166
- padding: 1rem;
167
- border: 1px solid #e0e0e0;
168
- font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;
169
- font-size: 0.85rem;
170
- line-height: 1.4;
171
- max-height: 200px;
172
- overflow-y: auto;
173
- }
174
-
175
- .activity-type {
176
- display: inline-block;
177
- background: #333333;
178
- color: white;
179
- padding: 2px 6px;
180
- font-size: 0.75rem;
181
- font-weight: 600;
182
- margin-right: 0.5rem;
183
- }
184
-
185
- .activity-type.user {
186
- background: #0066cc;
187
- }
188
-
189
- .activity-type.assistant {
190
- background: #333333;
191
- }
192
-
193
- .activity-type.tool {
194
- background: #666666;
195
- color: white;
196
- }
197
-
198
- .activity-type.summary {
199
- background: #999999;
200
- }
201
-
202
- /* Tool rendering styles */
203
- .tool-call-container {
204
- background: #f5f5f5;
205
- border: 1px solid #e0e0e0;
206
- margin: 0.5rem 0;
207
- font-size: 0.85rem;
208
- }
209
-
210
- .tool-call-header {
211
- display: flex;
212
- align-items: center;
213
- gap: 8px;
214
- padding: 8px 12px;
215
- background: #e0e0e0;
216
- font-weight: 600;
217
- color: #333333;
218
- }
219
-
220
- .tool-call-content {
221
- background: #ffffff;
222
- padding: 8px;
223
- border-left: 2px solid #666666;
224
- }
225
-
226
- .tool-result-container {
227
- background: #f5f5f5;
228
- border: 1px solid #e0e0e0;
229
- margin: 0.5rem 0;
230
- font-size: 0.85rem;
231
- }
232
-
233
- .tool-result-header {
234
- display: flex;
235
- align-items: center;
236
- gap: 8px;
237
- padding: 8px 12px;
238
- background: #e0e0e0;
239
- font-weight: 600;
240
- color: #333333;
241
- }
242
-
243
- .tool-result-content {
244
- background: #ffffff;
245
- padding: 8px;
246
- border-left: 2px solid #666666;
247
- font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;
248
- font-size: 0.8rem;
249
- }
250
-
251
- .controls {
252
- padding: 1rem 1.5rem;
253
- background: #f5f5f5;
254
- border-top: 1px solid #e0e0e0;
255
- display: flex;
256
- justify-content: space-between;
257
- align-items: center;
258
- }
259
-
260
- .control-group {
261
- display: flex;
262
- gap: 0.5rem;
263
- align-items: center;
264
- }
265
-
266
- .btn {
267
- background: #0066cc;
268
- color: white;
269
- border: 1px solid #0066cc;
270
- padding: 0.5rem 1rem;
271
- cursor: pointer;
272
- font-size: 0.9rem;
273
- transition: background-color 0.2s ease;
274
- }
275
-
276
- .btn:hover {
277
- background: #0052a3;
278
- border-color: #0052a3;
279
- }
280
-
281
- .btn.btn-success {
282
- background: #0066cc;
283
- border-color: #0066cc;
284
- }
285
-
286
- .btn.btn-success:hover {
287
- background: #0052a3;
288
- border-color: #0052a3;
289
- }
290
-
291
- .btn.btn-danger {
292
- background: #cc0000;
293
- border-color: #cc0000;
294
- }
295
-
296
- .btn.btn-danger:hover {
297
- background: #a60000;
298
- border-color: #a60000;
299
- }
300
-
301
- .btn.btn-secondary {
302
- background: #666666;
303
- border-color: #666666;
304
- }
305
-
306
- .btn.btn-secondary:hover {
307
- background: #4d4d4d;
308
- border-color: #4d4d4d;
309
- }
310
-
311
- .activity-stats {
312
- font-size: 0.85rem;
313
- color: #666666;
314
- }
315
-
316
- .empty-state {
317
- display: flex;
318
- flex-direction: column;
319
- align-items: center;
320
- justify-content: center;
321
- height: 300px;
322
- color: #666666;
323
- text-align: center;
324
- }
325
-
326
- .empty-state-icon {
327
- font-size: 2rem;
328
- margin-bottom: 1rem;
329
- color: #999999;
330
- }
331
-
332
- @media (max-width: 768px) {
333
- .container {
334
- padding: 1rem;
335
- }
336
-
337
- h1 {
338
- font-size: 1.5rem;
339
- }
340
-
341
- .live-container {
342
- height: calc(100vh - 150px);
343
- }
344
-
345
- .controls {
346
- flex-direction: column;
347
- gap: 1rem;
348
- align-items: stretch;
349
- }
350
-
351
- .control-group {
352
- justify-content: center;
353
- }
354
- }
355
- </style>
356
- </head>
357
- <body>
358
- <div class="container">
359
- <header>
360
- <h1>Live Activity Stream</h1>
361
- <p class="subtitle">Real-time JSONL message feed from all projects</p>
362
-
363
- <div class="nav-links">
364
- <a href="/" class="nav-link">Projects</a>
365
- <a href="/live" class="nav-link active">Live Activity</a>
366
- </div>
367
- </header>
368
-
369
- <div class="live-container">
370
- <div class="live-header">
371
- <div class="live-title">Activity Stream</div>
372
- <div class="status-indicator">
373
- <div class="status-dot" id="status-dot"></div>
374
- <span id="status-text">Disconnected</span>
375
- </div>
376
- </div>
377
-
378
- <div class="activity-stream" id="activity-stream">
379
- <div class="empty-state" id="empty-state">
380
- <div class="empty-state-icon">📡</div>
381
- <h3>Waiting for Activity</h3>
382
- <p>
383
- Click "Start Watching" to begin monitoring activity in real-time.
384
- </p>
385
- </div>
386
- </div>
387
-
388
- <div class="controls">
389
- <div class="control-group">
390
- <button
391
- class="btn btn-success"
392
- id="start-btn"
393
- onclick="startWatching()"
394
- >
395
- Start Watching
396
- </button>
397
- <button
398
- class="btn btn-danger"
399
- id="stop-btn"
400
- onclick="stopWatching()"
401
- style="display: none"
402
- >
403
- Stop Watching
404
- </button>
405
- <button class="btn btn-secondary" onclick="clearActivity()">
406
- Clear
407
- </button>
408
- </div>
409
- <div class="activity-stats">
410
- <span id="message-count">0 messages</span> •
411
- <span id="uptime">Not connected</span>
412
- </div>
413
- </div>
414
- </div>
415
- </div>
416
-
417
- <script>
418
- // ABOUTME: Live activity stream WebSocket manager for real-time JSONL message monitoring
419
- // ABOUTME: Displays all incoming messages from all projects in a live scrolling feed
420
-
421
- // Base class for all tool handlers
422
- class ToolHandler {
423
- constructor(toolName) {
424
- this.toolName = toolName;
425
- }
426
-
427
- renderToolCall(toolCall) {
428
- const toolDiv = document.createElement('div');
429
- toolDiv.className = 'tool-call-container';
430
-
431
- const header = this.createHeader(toolCall);
432
- const content = this.renderInput(toolCall.input);
433
-
434
- toolDiv.appendChild(header);
435
- toolDiv.appendChild(content);
436
-
437
- return toolDiv;
438
- }
439
-
440
- renderToolResult(toolResult, toolCall) {
441
- const resultDiv = document.createElement('div');
442
- resultDiv.className = 'tool-result-container';
443
-
444
- const header = this.createResultHeader(toolCall);
445
- const content = this.renderOutput(toolResult, toolCall);
446
-
447
- resultDiv.appendChild(header);
448
- resultDiv.appendChild(content);
449
-
450
- return resultDiv;
451
- }
452
-
453
- createHeader(toolCall) {
454
- const header = document.createElement('div');
455
- header.className = 'tool-call-header';
456
- header.innerHTML = `<span>${this.getIcon()}</span><span>Tool: ${this.toolName}</span>`;
457
- return header;
458
- }
459
-
460
- createResultHeader(toolCall) {
461
- const header = document.createElement('div');
462
- header.className = 'tool-result-header';
463
- header.innerHTML = `<span>📋</span><span>${this.toolName} Result</span>`;
464
- return header;
465
- }
466
-
467
- renderInput(input) {
468
- const content = document.createElement('div');
469
- content.className = 'tool-call-content';
470
- content.textContent = JSON.stringify(input, null, 2);
471
- return content;
472
- }
473
-
474
- renderOutput(result, toolCall) {
475
- const content = document.createElement('div');
476
- content.className = 'tool-result-content';
477
- content.textContent =
478
- typeof result === 'string'
479
- ? result
480
- : JSON.stringify(result, null, 2);
481
- return content;
482
- }
483
-
484
- getIcon() {
485
- return '🔧';
486
- }
487
- }
488
-
489
- // Handler for Bash tool
490
- class BashHandler extends ToolHandler {
491
- constructor() {
492
- super('Bash');
493
- }
494
-
495
- renderInput(input) {
496
- const content = document.createElement('div');
497
- content.className = 'tool-call-content';
498
-
499
- const command = document.createElement('div');
500
- command.style.fontWeight = 'bold';
501
- command.style.marginBottom = '8px';
502
- command.textContent = `$ ${input.command}`;
503
-
504
- if (input.description) {
505
- const desc = document.createElement('div');
506
- desc.style.fontStyle = 'italic';
507
- desc.style.color = '#666';
508
- desc.style.marginBottom = '8px';
509
- desc.textContent = input.description;
510
- content.appendChild(desc);
511
- }
512
-
513
- content.appendChild(command);
514
- return content;
515
- }
516
-
517
- renderOutput(result, toolCall) {
518
- const content = document.createElement('div');
519
- content.className = 'tool-result-content';
520
- content.style.fontFamily = 'monospace';
521
- content.style.whiteSpace = 'pre-wrap';
522
- content.textContent = result;
523
- return content;
524
- }
525
-
526
- getIcon() {
527
- return '💻';
528
- }
529
- }
530
-
531
- // Handler for Read tool
532
- class ReadHandler extends ToolHandler {
533
- constructor() {
534
- super('Read');
535
- }
536
-
537
- renderInput(input) {
538
- const content = document.createElement('div');
539
- content.className = 'tool-call-content';
540
-
541
- const path = document.createElement('div');
542
- path.style.fontWeight = 'bold';
543
- path.textContent = `📄 ${input.file_path}`;
544
-
545
- content.appendChild(path);
546
-
547
- if (input.offset || input.limit) {
548
- const params = document.createElement('div');
549
- params.style.fontSize = '0.9em';
550
- params.style.color = '#666';
551
- params.style.marginTop = '4px';
552
- const offset = input.offset || 0;
553
- const limit = input.limit || 'end';
554
- params.textContent = `Lines: ${offset} to ${limit}`;
555
- content.appendChild(params);
556
- }
557
-
558
- return content;
559
- }
560
-
561
- renderOutput(result, toolCall) {
562
- const content = document.createElement('div');
563
- content.className = 'tool-result-content';
564
- content.style.fontFamily = 'monospace';
565
- content.style.fontSize = '0.85em';
566
- content.style.whiteSpace = 'pre';
567
- content.style.maxHeight = '400px';
568
- content.style.overflowY = 'auto';
569
- content.textContent = result;
570
- return content;
571
- }
572
-
573
- getIcon() {
574
- return '📖';
575
- }
576
- }
577
-
578
- // Handler for Edit/Write tools
579
- class EditHandler extends ToolHandler {
580
- constructor() {
581
- super('Edit');
582
- }
583
-
584
- renderInput(input) {
585
- const content = document.createElement('div');
586
- content.className = 'tool-call-content';
587
-
588
- const path = document.createElement('div');
589
- path.style.fontWeight = 'bold';
590
- path.style.marginBottom = '8px';
591
- path.textContent = `${input.file_path}`;
592
-
593
- if (input.old_string && input.new_string) {
594
- const changeDiv = document.createElement('div');
595
- changeDiv.style.marginTop = '8px';
596
- changeDiv.style.padding = '8px';
597
- changeDiv.style.background = '#f8f9fa';
598
- changeDiv.style.borderRadius = '4px';
599
- changeDiv.style.fontSize = '0.85em';
600
-
601
- const oldDiv = document.createElement('div');
602
- oldDiv.style.marginBottom = '4px';
603
- oldDiv.innerHTML = `<span style="color: #dc3545; font-weight: bold;">- </span>${this.escapeHtml(input.old_string.substring(0, 100))}${input.old_string.length > 100 ? '...' : ''}`;
604
-
605
- const newDiv = document.createElement('div');
606
- newDiv.innerHTML = `<span style="color: #28a745; font-weight: bold;">+ </span>${this.escapeHtml(input.new_string.substring(0, 100))}${input.new_string.length > 100 ? '...' : ''}`;
607
-
608
- changeDiv.appendChild(oldDiv);
609
- changeDiv.appendChild(newDiv);
610
- content.appendChild(changeDiv);
611
- }
612
-
613
- content.appendChild(path);
614
- return content;
615
- }
616
-
617
- escapeHtml(text) {
618
- const div = document.createElement('div');
619
- div.textContent = text;
620
- return div.innerHTML;
621
- }
622
-
623
- getIcon() {
624
- return '';
625
- }
626
- }
627
-
628
- // Handler for MultiEdit tool
629
- class MultiEditHandler extends ToolHandler {
630
- constructor() {
631
- super('MultiEdit');
632
- }
633
-
634
- renderInput(input) {
635
- const content = document.createElement('div');
636
- content.className = 'tool-call-content';
637
-
638
- const multiEditDiv = document.createElement('div');
639
- multiEditDiv.style.padding = '12px';
640
- multiEditDiv.style.background = '#f8f9fa';
641
- multiEditDiv.style.borderRadius = '8px';
642
- multiEditDiv.style.border = '1px solid #dee2e6';
643
-
644
- const header = document.createElement('div');
645
- header.style.fontWeight = 'bold';
646
- header.style.marginBottom = '12px';
647
- header.style.borderBottom = '1px solid #ddd';
648
- header.style.paddingBottom = '6px';
649
- header.textContent = `Multi-Edit: ${input.file_path}`;
650
-
651
- multiEditDiv.appendChild(header);
652
-
653
- if (input.edits && Array.isArray(input.edits)) {
654
- input.edits.forEach((edit, index) => {
655
- const editDiv = document.createElement('div');
656
- editDiv.style.marginBottom = '12px';
657
- editDiv.style.padding = '8px';
658
- editDiv.style.background = '#ffffff';
659
- editDiv.style.borderRadius = '4px';
660
- editDiv.style.border = '1px solid #e9ecef';
661
-
662
- const editHeader = document.createElement('div');
663
- editHeader.style.fontWeight = 'bold';
664
- editHeader.style.marginBottom = '6px';
665
- editHeader.style.fontSize = '0.9em';
666
- editHeader.textContent = `Edit ${index + 1}:`;
667
-
668
- const oldDiv = document.createElement('div');
669
- oldDiv.style.marginBottom = '4px';
670
- oldDiv.style.fontSize = '0.85em';
671
-
672
- // Create safe elements to prevent HTML injection
673
- const oldSymbol = document.createElement('span');
674
- oldSymbol.style.color = '#dc3545';
675
- oldSymbol.style.fontWeight = 'bold';
676
- oldSymbol.textContent = '- ';
677
-
678
- const oldText = document.createTextNode(
679
- edit.old_string.substring(0, 80) +
680
- (edit.old_string.length > 80 ? '...' : ''),
681
- );
682
-
683
- oldDiv.appendChild(oldSymbol);
684
- oldDiv.appendChild(oldText);
685
-
686
- const newDiv = document.createElement('div');
687
- newDiv.style.fontSize = '0.85em';
688
-
689
- // Create safe elements to prevent HTML injection
690
- const newSymbol = document.createElement('span');
691
- newSymbol.style.color = '#28a745';
692
- newSymbol.style.fontWeight = 'bold';
693
- newSymbol.textContent = '+ ';
694
-
695
- const newText = document.createTextNode(
696
- edit.new_string.substring(0, 80) +
697
- (edit.new_string.length > 80 ? '...' : ''),
698
- );
699
-
700
- newDiv.appendChild(newSymbol);
701
- newDiv.appendChild(newText);
702
-
703
- editDiv.appendChild(editHeader);
704
- editDiv.appendChild(oldDiv);
705
- editDiv.appendChild(newDiv);
706
- multiEditDiv.appendChild(editDiv);
707
- });
708
- }
709
-
710
- content.appendChild(multiEditDiv);
711
- return content;
712
- }
713
-
714
- getIcon() {
715
- return '';
716
- }
717
- }
718
-
719
- // Handler for Write tool
720
- class WriteHandler extends ToolHandler {
721
- constructor() {
722
- super('Write');
723
- }
724
-
725
- renderInput(input) {
726
- const content = document.createElement('div');
727
- content.className = 'tool-call-content';
728
-
729
- const path = document.createElement('div');
730
- path.style.fontWeight = 'bold';
731
- path.style.marginBottom = '8px';
732
- path.textContent = `${input.file_path}`;
733
-
734
- const contentInfo = document.createElement('div');
735
- contentInfo.style.fontSize = '0.9em';
736
- contentInfo.style.color = '#666';
737
- const contentLength = input.content ? input.content.length : 0;
738
- contentInfo.textContent = `Writing ${contentLength} characters`;
739
-
740
- content.appendChild(path);
741
- content.appendChild(contentInfo);
742
- return content;
743
- }
744
-
745
- getIcon() {
746
- return '';
747
- }
748
- }
749
-
750
- // Handler for LS tool
751
- class LSHandler extends ToolHandler {
752
- constructor() {
753
- super('LS');
754
- }
755
-
756
- renderInput(input) {
757
- const content = document.createElement('div');
758
- content.className = 'tool-call-content';
759
-
760
- const path = document.createElement('div');
761
- path.style.fontWeight = 'bold';
762
- path.textContent = `${input.path}`;
763
-
764
- content.appendChild(path);
765
- return content;
766
- }
767
-
768
- renderOutput(result, toolCall) {
769
- const content = document.createElement('div');
770
- content.className = 'tool-result-content';
771
- content.style.fontFamily = 'monospace';
772
- content.style.whiteSpace = 'pre';
773
- content.textContent = result;
774
- return content;
775
- }
776
-
777
- getIcon() {
778
- return '';
779
- }
780
- }
781
-
782
- // Handler for Grep tool
783
- class GrepHandler extends ToolHandler {
784
- constructor() {
785
- super('Grep');
786
- }
787
-
788
- renderInput(input) {
789
- const content = document.createElement('div');
790
- content.className = 'tool-call-content';
791
-
792
- const pattern = document.createElement('div');
793
- pattern.style.fontWeight = 'bold';
794
- pattern.style.marginBottom = '4px';
795
- pattern.textContent = `"${input.pattern}"`;
796
-
797
- if (input.path) {
798
- const path = document.createElement('div');
799
- path.style.fontSize = '0.9em';
800
- path.style.color = '#666';
801
- path.textContent = `in: ${input.path}`;
802
- content.appendChild(path);
803
- }
804
-
805
- content.appendChild(pattern);
806
- return content;
807
- }
808
-
809
- renderOutput(result, toolCall) {
810
- const content = document.createElement('div');
811
- content.className = 'tool-result-content';
812
- content.style.fontFamily = 'monospace';
813
- content.style.fontSize = '0.85em';
814
- content.style.whiteSpace = 'pre';
815
- content.textContent = result;
816
- return content;
817
- }
818
-
819
- getIcon() {
820
- return '';
821
- }
822
- }
823
-
824
- // Handler for Glob tool
825
- class GlobHandler extends ToolHandler {
826
- constructor() {
827
- super('Glob');
828
- }
829
-
830
- renderInput(input) {
831
- const content = document.createElement('div');
832
- content.className = 'tool-call-content';
833
-
834
- const globDiv = document.createElement('div');
835
- globDiv.style.padding = '12px';
836
- globDiv.style.background = '#fef3c7';
837
- globDiv.style.borderRadius = '8px';
838
- globDiv.style.border = '1px solid #fbbf24';
839
-
840
- const header = document.createElement('div');
841
- header.style.fontWeight = 'bold';
842
- header.style.marginBottom = '8px';
843
- header.style.color = '#92400e';
844
- header.textContent = 'File Pattern Search';
845
-
846
- const pattern = document.createElement('div');
847
- pattern.style.fontFamily = 'monospace';
848
- pattern.style.marginBottom = '4px';
849
- pattern.textContent = `Pattern: ${input.pattern}`;
850
-
851
- if (input.path) {
852
- const path = document.createElement('div');
853
- path.style.fontSize = '0.9em';
854
- path.style.color = '#666';
855
- path.textContent = `Path: ${input.path}`;
856
- globDiv.appendChild(path);
857
- }
858
-
859
- globDiv.appendChild(header);
860
- globDiv.appendChild(pattern);
861
- content.appendChild(globDiv);
862
- return content;
863
- }
864
-
865
- renderOutput(result, toolCall) {
866
- const content = document.createElement('div');
867
- content.className = 'tool-result-content';
868
-
869
- let resultText = result;
870
- if (Array.isArray(result) && result[0] && result[0].text) {
871
- resultText = result[0].text;
872
- }
873
-
874
- const resultDiv = document.createElement('div');
875
- resultDiv.style.fontFamily = 'monospace';
876
- resultDiv.style.fontSize = '0.9em';
877
-
878
- if (typeof resultText === 'string' && resultText.includes('\n')) {
879
- const lines = resultText
880
- .trim()
881
- .split('\n')
882
- .filter((line) => line.trim());
883
-
884
- if (lines.length > 0) {
885
- const headerDiv = document.createElement('div');
886
- headerDiv.style.fontWeight = 'bold';
887
- headerDiv.style.marginBottom = '12px';
888
- headerDiv.style.color = '#374151';
889
- headerDiv.textContent = `Found ${lines.length} matching file${lines.length === 1 ? '' : 's'}:`;
890
-
891
- const filesDiv = document.createElement('div');
892
-
893
- lines.forEach((filePath) => {
894
- const fileDiv = document.createElement('div');
895
- fileDiv.style.padding = '6px 8px';
896
- fileDiv.style.marginBottom = '4px';
897
- fileDiv.style.background = '#ffffff';
898
- fileDiv.style.borderRadius = '4px';
899
- fileDiv.style.border = '1px solid #e5e7eb';
900
- fileDiv.style.fontFamily = 'monospace';
901
- fileDiv.style.fontSize = '0.85em';
902
- fileDiv.style.wordBreak = 'break-all';
903
-
904
- // Add file icon based on extension
905
- const extension = filePath.split('.').pop()?.toLowerCase();
906
- let icon = '';
907
- if (['js', 'ts', 'jsx', 'tsx'].includes(extension)) icon = '';
908
- else if (['py'].includes(extension)) icon = '';
909
- else if (['rs'].includes(extension)) icon = '';
910
- else if (['html', 'htm'].includes(extension)) icon = '';
911
- else if (['css'].includes(extension)) icon = '';
912
- else if (['md'].includes(extension)) icon = '';
913
- else if (['json'].includes(extension)) icon = '';
914
-
915
- fileDiv.innerHTML = `${filePath}`;
916
- filesDiv.appendChild(fileDiv);
917
- });
918
-
919
- resultDiv.appendChild(headerDiv);
920
- resultDiv.appendChild(filesDiv);
921
- } else {
922
- resultDiv.style.background = '#fef2f2';
923
- resultDiv.style.border = '1px solid #fecaca';
924
- resultDiv.style.color = '#991b1b';
925
- resultDiv.innerHTML = 'No files found matching the pattern';
926
- }
927
- } else {
928
- resultDiv.style.whiteSpace = 'pre-wrap';
929
- resultDiv.style.fontFamily = 'monospace';
930
- resultDiv.style.fontSize = '0.9em';
931
- resultDiv.textContent =
932
- typeof resultText === 'string' ? resultText : 'No results';
933
- }
934
-
935
- content.appendChild(resultDiv);
936
- return content;
937
- }
938
-
939
- getIcon() {
940
- return '';
941
- }
942
- }
943
-
944
- // Handler for TodoWrite tool
945
- class TodoWriteHandler extends ToolHandler {
946
- constructor() {
947
- super('TodoWrite');
948
- }
949
-
950
- renderInput(input) {
951
- const content = document.createElement('div');
952
- content.className = 'tool-call-content';
953
-
954
- if (input.todos && Array.isArray(input.todos)) {
955
- const todoList = document.createElement('div');
956
- todoList.style.marginTop = '8px';
957
-
958
- const header = document.createElement('div');
959
- header.style.fontWeight = 'bold';
960
- header.style.marginBottom = '8px';
961
- header.style.borderBottom = '1px solid #ddd';
962
- header.style.paddingBottom = '4px';
963
- header.textContent = `Todo List (${input.todos.length} items)`;
964
-
965
- todoList.appendChild(header);
966
-
967
- input.todos.forEach((todo) => {
968
- const todoItem = document.createElement('div');
969
- todoItem.style.display = 'flex';
970
- todoItem.style.alignItems = 'flex-start';
971
- todoItem.style.marginBottom = '8px';
972
- todoItem.style.padding = '8px';
973
- todoItem.style.borderRadius = '4px';
974
- todoItem.style.fontSize = '0.9em';
975
-
976
- // Status-based styling
977
- if (todo.status === 'completed') {
978
- todoItem.style.background = '#e8f5e8';
979
- todoItem.style.borderLeft = '3px solid #28a745';
980
- } else if (todo.status === 'in_progress') {
981
- todoItem.style.background = '#fff3cd';
982
- todoItem.style.borderLeft = '3px solid #ffc107';
983
- } else {
984
- todoItem.style.background = '#f8f9fa';
985
- todoItem.style.borderLeft = '3px solid #6c757d';
986
- }
987
-
988
- // Status icon
989
- const statusIcon = document.createElement('span');
990
- statusIcon.style.marginRight = '8px';
991
- statusIcon.style.fontSize = '1.1em';
992
- if (todo.status === 'completed') {
993
- statusIcon.textContent = '✓';
994
- } else if (todo.status === 'in_progress') {
995
- statusIcon.textContent = '○';
996
- } else {
997
- statusIcon.textContent = '○';
998
- }
999
-
1000
- // Content container
1001
- const contentContainer = document.createElement('div');
1002
- contentContainer.style.flex = '1';
1003
-
1004
- // Todo content
1005
- const todoContent = document.createElement('div');
1006
- todoContent.style.marginBottom = '4px';
1007
- if (todo.status === 'completed') {
1008
- todoContent.style.textDecoration = 'line-through';
1009
- todoContent.style.color = '#666';
1010
- }
1011
- todoContent.textContent = todo.content;
1012
-
1013
- // Priority and ID
1014
- const metaInfo = document.createElement('div');
1015
- metaInfo.style.fontSize = '0.8em';
1016
- metaInfo.style.color = '#888';
1017
-
1018
- const priorityColor =
1019
- todo.priority === 'high'
1020
- ? '#dc3545'
1021
- : todo.priority === 'medium'
1022
- ? '#fd7e14'
1023
- : '#28a745';
1024
- metaInfo.innerHTML = `<span style="color: ${priorityColor}; font-weight: bold;">●</span> ${todo.priority} priority • ID: ${todo.id}`;
1025
-
1026
- contentContainer.appendChild(todoContent);
1027
- contentContainer.appendChild(metaInfo);
1028
-
1029
- todoItem.appendChild(statusIcon);
1030
- todoItem.appendChild(contentContainer);
1031
- todoList.appendChild(todoItem);
1032
- });
1033
-
1034
- content.appendChild(todoList);
1035
- } else {
1036
- content.textContent = JSON.stringify(input, null, 2);
1037
- }
1038
-
1039
- return content;
1040
- }
1041
-
1042
- getIcon() {
1043
- return '';
1044
- }
1045
- }
1046
-
1047
- // Tool handler registry
1048
- const toolHandlers = {
1049
- Bash: new BashHandler(),
1050
- Read: new ReadHandler(),
1051
- Edit: new EditHandler(),
1052
- Write: new WriteHandler(),
1053
- MultiEdit: new MultiEditHandler(),
1054
- LS: new LSHandler(),
1055
- Grep: new GrepHandler(),
1056
- Glob: new GlobHandler(),
1057
- TodoWrite: new TodoWriteHandler(),
1058
- Task: new ToolHandler('Task'),
1059
- WebFetch: new ToolHandler('WebFetch'),
1060
- };
1061
-
1062
- // Get appropriate handler for a tool
1063
- function getToolHandler(toolName) {
1064
- return toolHandlers[toolName] || new ToolHandler(toolName);
1065
- }
1066
-
1067
- class LiveActivityManager {
1068
- constructor() {
1069
- this.ws = null;
1070
- this.isWatching = false;
1071
- this.shouldReconnect = false;
1072
- this.messageCount = 0;
1073
- this.startTime = null;
1074
- this.reconnectAttempts = 0;
1075
- this.maxReconnectAttempts = 5;
1076
- this.reconnectDelay = 1000;
1077
- this.autoScroll = true;
1078
- }
1079
-
1080
- connect() {
1081
- if (this.ws && this.ws.readyState === WebSocket.OPEN) {
1082
- return;
1083
- }
1084
-
1085
- const protocol =
1086
- window.location.protocol === 'https:' ? 'wss:' : 'ws:';
1087
- const wsUrl = `${protocol}//${window.location.host}/ws/watch`;
1088
-
1089
- try {
1090
- this.ws = new WebSocket(wsUrl);
1091
-
1092
- this.ws.onopen = () => {
1093
- console.log('WebSocket connected');
1094
- this.reconnectAttempts = 0;
1095
- this.isWatching = true;
1096
- this.startTime = Date.now();
1097
- this.updateStatus('connected');
1098
- this.updateUptime();
1099
- };
1100
-
1101
- this.ws.onmessage = (event) => {
1102
- try {
1103
- const watchEvent = JSON.parse(event.data);
1104
- this.handleWatchEvent(watchEvent);
1105
- } catch (e) {
1106
- console.error('Failed to parse watch event:', e);
1107
- }
1108
- };
1109
-
1110
- this.ws.onclose = () => {
1111
- console.log('WebSocket disconnected');
1112
- this.isWatching = false;
1113
- this.updateStatus('disconnected');
1114
- this.scheduleReconnect();
1115
- };
1116
-
1117
- this.ws.onerror = (error) => {
1118
- console.error('WebSocket error:', error);
1119
- this.updateStatus('error');
1120
- };
1121
- } catch (e) {
1122
- console.error('Failed to create WebSocket connection:', e);
1123
- this.scheduleReconnect();
1124
- }
1125
- }
1126
-
1127
- disconnect() {
1128
- this.isWatching = false;
1129
- if (this.ws) {
1130
- this.ws.close();
1131
- this.ws = null;
1132
- }
1133
- this.updateStatus('disconnected');
1134
- }
1135
-
1136
- scheduleReconnect() {
1137
- if (
1138
- !this.shouldReconnect ||
1139
- this.reconnectAttempts >= this.maxReconnectAttempts
1140
- ) {
1141
- if (this.reconnectAttempts >= this.maxReconnectAttempts) {
1142
- this.updateStatus('failed');
1143
- }
1144
- return;
1145
- }
1146
-
1147
- this.reconnectAttempts++;
1148
- const delay =
1149
- this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
1150
-
1151
- setTimeout(() => {
1152
- if (this.shouldReconnect) {
1153
- console.log(
1154
- `Attempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts})`,
1155
- );
1156
- this.connect();
1157
- }
1158
- }, delay);
1159
- }
1160
-
1161
- handleWatchEvent(watchEvent) {
1162
- console.log('Received watch event:', watchEvent);
1163
-
1164
- if (watchEvent.type === 'log_entry' && watchEvent.entry) {
1165
- this.addActivityEntry(watchEvent);
1166
- this.messageCount++;
1167
- this.updateMessageCount();
1168
- }
1169
- }
1170
-
1171
- addActivityEntry(watchEvent) {
1172
- const stream = document.getElementById('activity-stream');
1173
- const emptyState = document.getElementById('empty-state');
1174
-
1175
- // Hide empty state if visible
1176
- if (emptyState && emptyState.style.display !== 'none') {
1177
- emptyState.style.display = 'none';
1178
- }
1179
-
1180
- const entry = watchEvent.entry;
1181
- const entryDiv = document.createElement('div');
1182
- entryDiv.className = 'activity-entry new';
1183
-
1184
- // Format project path to be more readable
1185
- let formattedProject = watchEvent.project;
1186
- if (formattedProject.startsWith('-Users-')) {
1187
- // Convert -Users-harper-Public-src-2389-cc-log-viewer to /Users/harper/Public/src/2389/cc-log-viewer
1188
- formattedProject = formattedProject
1189
- .replace(/^-/, '/')
1190
- .replace(/-/g, '/');
1191
- }
1192
-
1193
- // Add session ID in parentheses if available
1194
- const sessionInfo = watchEvent.session
1195
- ? ` (${watchEvent.session.substring(0, 8)}...)`
1196
- : '';
1197
- const source = `${formattedProject}${sessionInfo}`;
1198
-
1199
- // Determine entry type and content
1200
- let entryType = 'unknown';
1201
- let contentElements = [];
1202
-
1203
- if (entry.type) {
1204
- entryType = entry.type;
1205
- }
1206
-
1207
- if (entry.message && entry.message.content) {
1208
- if (typeof entry.message.content === 'string') {
1209
- contentElements.push({
1210
- type: 'text',
1211
- content: entry.message.content,
1212
- });
1213
- } else if (Array.isArray(entry.message.content)) {
1214
- // Handle mixed content including tool use with rich rendering
1215
- const textContent = entry.message.content
1216
- .filter((c) => c.type === 'text')
1217
- .map((c) => c.text)
1218
- .join('\n');
1219
-
1220
- const toolUse = entry.message.content.filter(
1221
- (c) => c.type === 'tool_use',
1222
- );
1223
-
1224
- const toolResults = entry.message.content.filter(
1225
- (c) => c.type === 'tool_result',
1226
- );
1227
-
1228
- // Add text content if present
1229
- if (textContent.trim()) {
1230
- contentElements.push({ type: 'text', content: textContent });
1231
- }
1232
-
1233
- // Add tool calls with rich rendering
1234
- if (toolUse.length > 0) {
1235
- entryType = 'tool';
1236
- toolUse.forEach((tool) => {
1237
- const handler = getToolHandler(tool.name);
1238
- contentElements.push({
1239
- type: 'tool_call',
1240
- element: handler.renderToolCall(tool),
1241
- });
1242
- });
1243
- }
1244
-
1245
- // Add tool results with rich rendering
1246
- if (toolResults.length > 0) {
1247
- entryType = 'tool';
1248
- toolResults.forEach((result) => {
1249
- const toolName = result.tool_use_id
1250
- ? result.tool_use_id.substring(0, 8)
1251
- : 'unknown';
1252
- const handler = getToolHandler(toolName);
1253
- const resultContent =
1254
- result.content ||
1255
- result.text ||
1256
- JSON.stringify(result, null, 2);
1257
- contentElements.push({
1258
- type: 'tool_result',
1259
- element: handler.renderToolResult(resultContent, {
1260
- name: toolName,
1261
- }),
1262
- });
1263
- });
1264
- }
1265
- }
1266
- } else if (entry.summary) {
1267
- contentElements.push({ type: 'text', content: entry.summary });
1268
- entryType = 'summary';
1269
- } else if (entry.tool_use_result) {
1270
- // Handle tool results as separate entries
1271
- entryType = 'tool';
1272
- const handler = getToolHandler('unknown');
1273
- const resultContent =
1274
- typeof entry.tool_use_result === 'string'
1275
- ? entry.tool_use_result
1276
- : JSON.stringify(entry.tool_use_result, null, 2);
1277
- contentElements.push({
1278
- type: 'tool_result',
1279
- element: handler.renderToolResult(resultContent, {
1280
- name: 'Tool',
1281
- }),
1282
- });
1283
- }
1284
-
1285
- const timestamp = new Date(watchEvent.timestamp).toLocaleTimeString();
1286
-
1287
- // Create header
1288
- const headerDiv = document.createElement('div');
1289
- headerDiv.className = 'activity-header';
1290
- headerDiv.innerHTML = `
1291
- <div class="activity-source">
1292
- <span class="activity-type ${entryType}">${entryType.toUpperCase()}</span>
1293
- ${source}
1294
- </div>
1295
- <div class="activity-timestamp">${timestamp}</div>
1296
- `;
1297
- entryDiv.appendChild(headerDiv);
1298
-
1299
- // Create content area
1300
- const contentDiv = document.createElement('div');
1301
- contentDiv.className = 'activity-content';
1302
-
1303
- if (contentElements.length === 0) {
1304
- contentDiv.innerHTML = '<em>No content</em>';
1305
- } else {
1306
- contentElements.forEach((elem) => {
1307
- if (elem.type === 'text') {
1308
- const textDiv = document.createElement('div');
1309
- textDiv.style.whiteSpace = 'pre-wrap';
1310
- textDiv.textContent =
1311
- elem.content.length > 500
1312
- ? elem.content.substring(0, 500) + '...'
1313
- : elem.content;
1314
- contentDiv.appendChild(textDiv);
1315
- } else if (
1316
- elem.type === 'tool_call' ||
1317
- elem.type === 'tool_result'
1318
- ) {
1319
- contentDiv.appendChild(elem.element);
1320
- }
1321
- });
1322
- }
1323
-
1324
- entryDiv.appendChild(contentDiv);
1325
-
1326
- // Add to stream
1327
- stream.appendChild(entryDiv);
1328
-
1329
- // Remove 'new' class after animation
1330
- setTimeout(() => {
1331
- entryDiv.classList.remove('new');
1332
- }, 300);
1333
-
1334
- // Auto-scroll to bottom if enabled
1335
- if (this.autoScroll) {
1336
- stream.scrollTop = stream.scrollHeight;
1337
- }
1338
-
1339
- // Limit number of entries to prevent memory issues
1340
- const maxEntries = 1000;
1341
- const entries = stream.querySelectorAll('.activity-entry');
1342
- if (entries.length > maxEntries) {
1343
- entries[0].remove();
1344
- }
1345
- }
1346
-
1347
- escapeHtml(text) {
1348
- const div = document.createElement('div');
1349
- div.textContent = text;
1350
- return div.innerHTML;
1351
- }
1352
-
1353
- updateStatus(status) {
1354
- const statusDot = document.getElementById('status-dot');
1355
- const statusText = document.getElementById('status-text');
1356
- const startBtn = document.getElementById('start-btn');
1357
- const stopBtn = document.getElementById('stop-btn');
1358
-
1359
- switch (status) {
1360
- case 'connected':
1361
- statusDot.className = 'status-dot connected';
1362
- statusText.textContent = 'Connected';
1363
- startBtn.style.display = 'none';
1364
- stopBtn.style.display = 'inline-block';
1365
- break;
1366
- case 'disconnected':
1367
- statusDot.className = 'status-dot';
1368
- statusText.textContent = 'Disconnected';
1369
- startBtn.style.display = 'inline-block';
1370
- stopBtn.style.display = 'none';
1371
- document.getElementById('uptime').textContent = 'Not connected';
1372
- break;
1373
- case 'error':
1374
- case 'failed':
1375
- statusDot.className = 'status-dot';
1376
- statusText.textContent = 'Connection Failed';
1377
- startBtn.style.display = 'inline-block';
1378
- stopBtn.style.display = 'none';
1379
- break;
1380
- }
1381
- }
1382
-
1383
- updateMessageCount() {
1384
- const countElement = document.getElementById('message-count');
1385
- countElement.textContent = `${this.messageCount} message${this.messageCount !== 1 ? 's' : ''}`;
1386
- }
1387
-
1388
- updateUptime() {
1389
- if (!this.startTime || !this.isWatching) {
1390
- return;
1391
- }
1392
-
1393
- const uptimeElement = document.getElementById('uptime');
1394
- const elapsed = Date.now() - this.startTime;
1395
- const seconds = Math.floor(elapsed / 1000);
1396
- const minutes = Math.floor(seconds / 60);
1397
- const hours = Math.floor(minutes / 60);
1398
-
1399
- let uptimeText = '';
1400
- if (hours > 0) {
1401
- uptimeText = `${hours}h ${minutes % 60}m`;
1402
- } else if (minutes > 0) {
1403
- uptimeText = `${minutes}m ${seconds % 60}s`;
1404
- } else {
1405
- uptimeText = `${seconds}s`;
1406
- }
1407
-
1408
- uptimeElement.textContent = `Connected for ${uptimeText}`;
1409
-
1410
- // Update every second
1411
- if (this.isWatching) {
1412
- setTimeout(() => this.updateUptime(), 1000);
1413
- }
1414
- }
1415
-
1416
- startWatching() {
1417
- this.shouldReconnect = true; // User wants to maintain connection
1418
- this.connect();
1419
- this.messageCount = 0;
1420
- this.updateMessageCount();
1421
- }
1422
-
1423
- stopWatching() {
1424
- this.shouldReconnect = false; // User wants to stop connection
1425
- this.disconnect();
1426
- }
1427
-
1428
- clearActivity() {
1429
- const stream = document.getElementById('activity-stream');
1430
- const entries = stream.querySelectorAll('.activity-entry');
1431
- entries.forEach((entry) => entry.remove());
1432
-
1433
- const emptyState = document.getElementById('empty-state');
1434
- if (emptyState) {
1435
- emptyState.style.display = 'flex';
1436
- }
1437
-
1438
- this.messageCount = 0;
1439
- this.updateMessageCount();
1440
- }
1441
- }
1442
-
1443
- // Global instance
1444
- const liveActivity = new LiveActivityManager();
1445
-
1446
- // Global functions for buttons
1447
- function startWatching() {
1448
- liveActivity.startWatching();
1449
- }
1450
-
1451
- function stopWatching() {
1452
- liveActivity.stopWatching();
1453
- }
1454
-
1455
- function clearActivity() {
1456
- liveActivity.clearActivity();
1457
- }
1458
-
1459
- // Initialize on page load
1460
- document.addEventListener('DOMContentLoaded', function () {
1461
- console.log('Live Activity Stream initialized');
1462
- });
1463
- </script>
1464
- </body>
1465
- </html>