@neovate/code 0.12.1 → 0.12.2

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,2846 +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>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: 1200px;
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
- }
48
-
49
- .card {
50
- background: #ffffff;
51
- border: 1px solid #e0e0e0;
52
- margin-bottom: 2rem;
53
- }
54
-
55
- .card-header {
56
- background: #f5f5f5;
57
- color: #333333;
58
- padding: 1rem 1.5rem;
59
- font-weight: 600;
60
- font-size: 1.1rem;
61
- border-bottom: 1px solid #e0e0e0;
62
- }
63
-
64
- .card-body {
65
- padding: 1.5rem;
66
- }
67
-
68
- .projects-grid {
69
- display: grid;
70
- grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
71
- gap: 1rem;
72
- }
73
-
74
- .project-card {
75
- background: #ffffff;
76
- border: 1px solid #e0e0e0;
77
- padding: 1.5rem;
78
- cursor: pointer;
79
- transition: border-color 0.2s ease;
80
- }
81
-
82
- .project-card:hover {
83
- border-color: #0066cc;
84
- }
85
-
86
- .project-name {
87
- font-weight: 600;
88
- font-size: 1.1rem;
89
- margin-bottom: 0.5rem;
90
- color: #000000;
91
- }
92
-
93
- .project-stats {
94
- display: flex;
95
- justify-content: space-between;
96
- font-size: 0.9rem;
97
- color: #666666;
98
- margin-bottom: 0.5rem;
99
- }
100
-
101
- .project-activity {
102
- font-size: 0.8rem;
103
- color: #999999;
104
- }
105
-
106
- .sessions-list {
107
- display: none;
108
- }
109
-
110
- .sessions-list.active {
111
- display: block;
112
- }
113
-
114
- .session-item {
115
- border-bottom: 1px solid #e0e0e0;
116
- padding: 1rem;
117
- cursor: pointer;
118
- transition: background-color 0.2s ease;
119
- }
120
-
121
- .session-item:hover {
122
- background-color: #f5f5f5;
123
- }
124
-
125
- .session-item:last-child {
126
- border-bottom: none;
127
- }
128
-
129
- .session-title {
130
- font-weight: 600;
131
- margin-bottom: 0.5rem;
132
- color: #000000;
133
- }
134
-
135
- .session-meta {
136
- display: flex;
137
- justify-content: space-between;
138
- font-size: 0.85rem;
139
- color: #666666;
140
- }
141
-
142
- .log-viewer {
143
- display: none;
144
- }
145
-
146
- .log-viewer.active {
147
- display: block;
148
- }
149
-
150
- .conversation-container {
151
- max-width: 800px;
152
- margin: 0 auto;
153
- padding: 0;
154
- }
155
-
156
- .message {
157
- display: flex;
158
- margin-bottom: 1.5rem;
159
- gap: 12px;
160
- }
161
-
162
- .message.user {
163
- flex-direction: row-reverse;
164
- }
165
-
166
- .message.assistant {
167
- flex-direction: row;
168
- }
169
-
170
- .message.tool {
171
- justify-content: center;
172
- margin: 1rem 0;
173
- }
174
-
175
- .message.summary {
176
- justify-content: center;
177
- margin: 2rem 0;
178
- }
179
-
180
- .avatar {
181
- width: 32px;
182
- height: 32px;
183
- border: 1px solid #e0e0e0;
184
- flex-shrink: 0;
185
- display: flex;
186
- align-items: center;
187
- justify-content: center;
188
- font-size: 14px;
189
- font-weight: 600;
190
- background: #f5f5f5;
191
- color: #333333;
192
- }
193
-
194
- .avatar.user {
195
- background: #ffffff;
196
- border-color: #0066cc;
197
- color: #0066cc;
198
- }
199
-
200
- .avatar.assistant {
201
- background: #ffffff;
202
- border-color: #333333;
203
- color: #333333;
204
- }
205
-
206
- .message-content {
207
- flex: 1;
208
- max-width: calc(100% - 44px);
209
- }
210
-
211
- .message.user .message-content {
212
- background: #ffffff;
213
- color: #333333;
214
- padding: 12px 16px;
215
- border: 1px solid #e0e0e0;
216
- border-left: 3px solid #0066cc;
217
- }
218
-
219
- .message.assistant .message-content {
220
- background: #f5f5f5;
221
- color: #333333;
222
- padding: 12px 16px;
223
- border: 1px solid #e0e0e0;
224
- }
225
-
226
- .message-text {
227
- line-height: 1.5;
228
- white-space: pre-wrap;
229
- word-wrap: break-word;
230
- margin: 0;
231
- }
232
-
233
- .message-meta {
234
- font-size: 0.75rem;
235
- color: #999999;
236
- margin-top: 4px;
237
- text-align: right;
238
- }
239
-
240
- .message.assistant .message-meta {
241
- text-align: left;
242
- }
243
-
244
- .tool-call-container {
245
- background: #f5f5f5;
246
- border: 1px solid #e0e0e0;
247
- padding: 12px;
248
- margin: 1rem auto;
249
- max-width: 600px;
250
- font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;
251
- font-size: 0.85rem;
252
- }
253
-
254
- .tool-call-header {
255
- display: flex;
256
- align-items: center;
257
- gap: 8px;
258
- margin-bottom: 8px;
259
- font-weight: 600;
260
- color: #333333;
261
- }
262
-
263
- .tool-call-content {
264
- background: #ffffff;
265
- padding: 8px;
266
- border-left: 2px solid #666666;
267
- }
268
-
269
- .tool-result-container {
270
- background: #f5f5f5;
271
- border: 1px solid #e0e0e0;
272
- padding: 12px;
273
- margin: 1rem auto;
274
- max-width: 600px;
275
- font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;
276
- font-size: 0.85rem;
277
- }
278
-
279
- .tool-result-header {
280
- display: flex;
281
- align-items: center;
282
- gap: 8px;
283
- margin-bottom: 8px;
284
- font-weight: 600;
285
- color: #333333;
286
- }
287
-
288
- .tool-result-content {
289
- background: #ffffff;
290
- padding: 8px;
291
- border-left: 2px solid #666666;
292
- max-height: 300px;
293
- overflow-y: auto;
294
- }
295
-
296
- .session-summary {
297
- background: #f5f5f5;
298
- color: #333333;
299
- padding: 20px;
300
- border: 1px solid #e0e0e0;
301
- text-align: center;
302
- margin: 2rem auto;
303
- max-width: 600px;
304
- }
305
-
306
- .session-summary h3 {
307
- margin: 0 0 8px 0;
308
- font-size: 1.2rem;
309
- color: #000000;
310
- }
311
-
312
- .session-summary p {
313
- margin: 0;
314
- color: #666666;
315
- }
316
-
317
- .back-btn {
318
- background: #ffffff;
319
- color: #333333;
320
- border: 1px solid #e0e0e0;
321
- padding: 0.5rem 1rem;
322
- cursor: pointer;
323
- font-size: 0.9rem;
324
- margin-bottom: 1rem;
325
- transition: border-color 0.2s ease;
326
- }
327
-
328
- .back-btn:hover {
329
- border-color: #0066cc;
330
- color: #0066cc;
331
- }
332
-
333
- .loading {
334
- text-align: center;
335
- padding: 2rem;
336
- color: #666666;
337
- font-style: italic;
338
- }
339
-
340
- .error {
341
- background: #ffffff;
342
- border: 1px solid #cc0000;
343
- color: #cc0000;
344
- padding: 1rem;
345
- margin: 1rem 0;
346
- }
347
-
348
- .timestamp {
349
- font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;
350
- font-size: 0.8rem;
351
- }
352
-
353
- .tool-call {
354
- background: #f5f5f5;
355
- border: 1px solid #e0e0e0;
356
- padding: 0.75rem;
357
- margin: 0.5rem 0;
358
- font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;
359
- font-size: 0.85rem;
360
- }
361
-
362
- .tool-result {
363
- background: #ffffff;
364
- border: 1px solid #e0e0e0;
365
- padding: 0.75rem;
366
- margin: 0.5rem 0;
367
- font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;
368
- font-size: 0.85rem;
369
- }
370
-
371
- .watch-btn {
372
- background: #0066cc;
373
- color: white;
374
- border: 1px solid #0066cc;
375
- padding: 0.75rem 1.5rem;
376
- cursor: pointer;
377
- font-size: 0.9rem;
378
- font-weight: 600;
379
- transition: background-color 0.2s ease;
380
- display: inline-flex;
381
- align-items: center;
382
- gap: 0.5rem;
383
- margin-left: 1rem;
384
- }
385
-
386
- .watch-btn:hover {
387
- background: #0052a3;
388
- border-color: #0052a3;
389
- }
390
-
391
- .activity-badge {
392
- position: absolute;
393
- top: 10px;
394
- right: 10px;
395
- width: 8px;
396
- height: 8px;
397
- background: #0066cc;
398
- border-radius: 50%;
399
- z-index: 10;
400
- }
401
-
402
- .nav-links {
403
- display: flex;
404
- justify-content: center;
405
- gap: 0;
406
- margin-top: 1rem;
407
- }
408
-
409
- .nav-link {
410
- background: #ffffff;
411
- color: #0066cc;
412
- text-decoration: none;
413
- padding: 0.5rem 1rem;
414
- border: 1px solid #e0e0e0;
415
- transition: all 0.2s ease;
416
- font-weight: normal;
417
- }
418
-
419
- .nav-link:hover {
420
- text-decoration: underline;
421
- border-color: #0066cc;
422
- }
423
-
424
- .nav-link.active {
425
- background: #0066cc;
426
- color: white;
427
- font-weight: 600;
428
- }
429
-
430
- @media (max-width: 768px) {
431
- .container {
432
- padding: 1rem;
433
- }
434
-
435
- h1 {
436
- font-size: 1.5rem;
437
- }
438
-
439
- .projects-grid {
440
- grid-template-columns: 1fr;
441
- }
442
-
443
- .project-stats {
444
- flex-direction: column;
445
- gap: 0.25rem;
446
- }
447
-
448
- .watch-btn {
449
- margin-left: 0;
450
- margin-top: 0.5rem;
451
- }
452
- }
453
- </style>
454
- </head>
455
- <body>
456
- <div class="container">
457
- <header>
458
- <h1>Log Viewer</h1>
459
- <p class="subtitle">Explore and audit your conversation logs</p>
460
-
461
- <div class="nav-links">
462
- <a href="/" class="nav-link active">Projects</a>
463
- <a href="/live" class="nav-link">Live Activity</a>
464
- </div>
465
- </header>
466
-
467
- <div id="projects-view" class="card">
468
- <div class="card-header">Projects</div>
469
- <div class="card-body">
470
- <div id="projects-loading" class="loading">Loading projects...</div>
471
- <div id="projects-list" class="projects-grid"></div>
472
- </div>
473
- </div>
474
-
475
- <div id="sessions-view" class="card sessions-list">
476
- <div class="card-header">
477
- <button class="back-btn" onclick="showProjects()">
478
- ← Back to Projects
479
- </button>
480
- <span id="project-title">Sessions</span>
481
- </div>
482
- <div class="card-body">
483
- <div id="sessions-loading" class="loading">Loading sessions...</div>
484
- <div id="sessions-list"></div>
485
- </div>
486
- </div>
487
-
488
- <div id="log-view" class="card log-viewer">
489
- <div class="card-header">
490
- <button class="back-btn" onclick="showSessions()">
491
- ← Back to Sessions
492
- </button>
493
- <span id="session-title">Conversation Log</span>
494
- </div>
495
- <div class="card-body">
496
- <div id="log-loading" class="loading">Loading conversation...</div>
497
- <div id="log-entries" class="conversation-container"></div>
498
- </div>
499
- </div>
500
- </div>
501
-
502
- <script>
503
- // ABOUTME: Tool handler classes for rendering different tool types with specific formatting
504
- // ABOUTME: Each tool type has its own renderer for inputs and outputs based on the tool's schema
505
-
506
- // Base class for all tool handlers
507
- class ToolHandler {
508
- constructor(toolName) {
509
- this.toolName = toolName;
510
- }
511
-
512
- renderToolCall(toolCall) {
513
- const toolDiv = document.createElement('div');
514
- toolDiv.className = 'tool-call-container';
515
-
516
- const header = this.createHeader(toolCall);
517
- const content = this.renderInput(toolCall.input);
518
-
519
- toolDiv.appendChild(header);
520
- toolDiv.appendChild(content);
521
- return toolDiv;
522
- }
523
-
524
- renderToolResult(toolResult, toolCall) {
525
- const resultDiv = document.createElement('div');
526
- resultDiv.className = 'tool-result-container';
527
-
528
- const header = this.createResultHeader(toolCall);
529
- const content = this.renderOutput(toolResult, toolCall);
530
-
531
- resultDiv.appendChild(header);
532
- resultDiv.appendChild(content);
533
- return resultDiv;
534
- }
535
-
536
- createHeader(toolCall) {
537
- const header = document.createElement('div');
538
- header.className = 'tool-call-header';
539
- header.innerHTML = `<span>${this.getIcon()}</span><span>Tool: ${this.toolName}</span>`;
540
- return header;
541
- }
542
-
543
- createResultHeader(toolCall) {
544
- const header = document.createElement('div');
545
- header.className = 'tool-result-header';
546
- header.innerHTML = `<span>📋</span><span>${this.toolName} Result</span>`;
547
- return header;
548
- }
549
-
550
- renderInput(input) {
551
- const content = document.createElement('div');
552
- content.className = 'tool-call-content';
553
- content.textContent = JSON.stringify(input, null, 2);
554
- return content;
555
- }
556
-
557
- renderOutput(result, toolCall) {
558
- const content = document.createElement('div');
559
- content.className = 'tool-result-content';
560
- content.textContent =
561
- typeof result === 'string'
562
- ? result
563
- : JSON.stringify(result, null, 2);
564
- return content;
565
- }
566
-
567
- getIcon() {
568
- return '🔧';
569
- }
570
- }
571
-
572
- // Handler for Bash tool
573
- class BashHandler extends ToolHandler {
574
- constructor() {
575
- super('Bash');
576
- }
577
-
578
- renderInput(input) {
579
- const content = document.createElement('div');
580
- content.className = 'tool-call-content';
581
-
582
- const command = document.createElement('div');
583
- command.style.fontWeight = 'bold';
584
- command.style.marginBottom = '8px';
585
- command.textContent = `$ ${input.command}`;
586
-
587
- if (input.description) {
588
- const desc = document.createElement('div');
589
- desc.style.fontStyle = 'italic';
590
- desc.style.color = '#666';
591
- desc.textContent = input.description;
592
- content.appendChild(desc);
593
- }
594
-
595
- content.appendChild(command);
596
- return content;
597
- }
598
-
599
- renderOutput(result, toolCall) {
600
- const content = document.createElement('div');
601
- content.className = 'tool-result-content';
602
- content.style.fontFamily = 'monospace';
603
- content.style.whiteSpace = 'pre-wrap';
604
- content.textContent = result;
605
- return content;
606
- }
607
-
608
- getIcon() {
609
- return '💻';
610
- }
611
- }
612
-
613
- // Handler for Read tool
614
- class ReadHandler extends ToolHandler {
615
- constructor() {
616
- super('Read');
617
- }
618
-
619
- renderInput(input) {
620
- const content = document.createElement('div');
621
- content.className = 'tool-call-content';
622
-
623
- const path = document.createElement('div');
624
- path.style.fontWeight = 'bold';
625
- path.textContent = `📄 ${input.file_path}`;
626
-
627
- content.appendChild(path);
628
-
629
- if (input.offset || input.limit) {
630
- const range = document.createElement('div');
631
- range.style.fontSize = '0.9em';
632
- range.style.color = '#666';
633
- range.textContent = `Lines: ${input.offset || 1} - ${(input.offset || 1) + (input.limit || 'end')}`;
634
- content.appendChild(range);
635
- }
636
-
637
- return content;
638
- }
639
-
640
- renderOutput(result, toolCall) {
641
- const content = document.createElement('div');
642
- content.className = 'tool-result-content';
643
- content.style.fontFamily = 'monospace';
644
- content.style.fontSize = '0.85em';
645
- content.style.whiteSpace = 'pre';
646
- content.style.maxHeight = '400px';
647
- content.style.overflowY = 'auto';
648
- content.textContent = result;
649
- return content;
650
- }
651
-
652
- getIcon() {
653
- return '📖';
654
- }
655
- }
656
-
657
- // Handler for Edit/Write tools
658
- class EditHandler extends ToolHandler {
659
- constructor() {
660
- super('Edit');
661
- }
662
-
663
- renderInput(input) {
664
- const content = document.createElement('div');
665
- content.className = 'tool-call-content';
666
-
667
- const path = document.createElement('div');
668
- path.style.fontWeight = 'bold';
669
- path.style.marginBottom = '8px';
670
- path.textContent = `✏️ ${input.file_path}`;
671
-
672
- if (input.old_string && input.new_string) {
673
- const changeDiv = document.createElement('div');
674
- changeDiv.style.fontSize = '0.9em';
675
-
676
- const oldDiv = document.createElement('div');
677
- oldDiv.style.background = '#ffe6e6';
678
- oldDiv.style.padding = '4px';
679
- oldDiv.style.marginBottom = '4px';
680
- oldDiv.style.whiteSpace = 'pre-wrap';
681
- oldDiv.textContent = `- ${input.old_string}`;
682
-
683
- const newDiv = document.createElement('div');
684
- newDiv.style.background = '#e6ffe6';
685
- newDiv.style.padding = '4px';
686
- newDiv.style.whiteSpace = 'pre-wrap';
687
- newDiv.textContent = `+ ${input.new_string}`;
688
-
689
- changeDiv.appendChild(oldDiv);
690
- changeDiv.appendChild(newDiv);
691
- content.appendChild(path);
692
- content.appendChild(changeDiv);
693
- } else {
694
- content.appendChild(path);
695
- }
696
-
697
- return content;
698
- }
699
-
700
- getIcon() {
701
- return '✏️';
702
- }
703
- }
704
-
705
- // Handler for MultiEdit tool
706
- class MultiEditHandler extends ToolHandler {
707
- constructor() {
708
- super('MultiEdit');
709
- }
710
-
711
- renderInput(input) {
712
- const content = document.createElement('div');
713
- content.className = 'tool-call-content';
714
-
715
- const multiEditDiv = document.createElement('div');
716
- multiEditDiv.style.padding = '12px';
717
- multiEditDiv.style.background = '#f8f9fa';
718
- multiEditDiv.style.borderRadius = '8px';
719
- multiEditDiv.style.border = '1px solid #dee2e6';
720
-
721
- const header = document.createElement('div');
722
- header.style.fontWeight = 'bold';
723
- header.style.marginBottom = '12px';
724
- header.style.color = '#2c3e50';
725
- header.style.borderBottom = '1px solid #ddd';
726
- header.style.paddingBottom = '6px';
727
-
728
- const editCount = input.edits ? input.edits.length : 0;
729
- header.textContent = `🔄 Multiple Edits to ${input.file_path} (${editCount} changes)`;
730
-
731
- multiEditDiv.appendChild(header);
732
-
733
- if (input.edits && Array.isArray(input.edits)) {
734
- input.edits.forEach((edit, index) => {
735
- const editDiv = document.createElement('div');
736
- editDiv.style.marginBottom = '16px';
737
- editDiv.style.padding = '12px';
738
- editDiv.style.background = '#ffffff';
739
- editDiv.style.borderRadius = '6px';
740
- editDiv.style.border = '1px solid #e5e7eb';
741
-
742
- const editHeader = document.createElement('div');
743
- editHeader.style.fontWeight = 'bold';
744
- editHeader.style.marginBottom = '8px';
745
- editHeader.style.color = '#4b5563';
746
- editHeader.style.fontSize = '0.9em';
747
- editHeader.innerHTML = `Edit ${index + 1}${edit.replace_all ? ' <span style="color: #dc3545;">(replace all)</span>' : ''}`;
748
-
749
- const changeDiv = document.createElement('div');
750
- changeDiv.style.fontSize = '0.85em';
751
-
752
- if (edit.old_string && edit.new_string) {
753
- const oldDiv = document.createElement('div');
754
- oldDiv.style.background = '#ffe6e6';
755
- oldDiv.style.padding = '8px';
756
- oldDiv.style.marginBottom = '4px';
757
- oldDiv.style.whiteSpace = 'pre-wrap';
758
- oldDiv.style.borderRadius = '4px';
759
- oldDiv.style.fontFamily = 'monospace';
760
- oldDiv.style.fontSize = '0.8em';
761
- oldDiv.style.maxHeight = '150px';
762
- oldDiv.style.overflowY = 'auto';
763
- oldDiv.textContent = `- ${edit.old_string}`;
764
-
765
- const newDiv = document.createElement('div');
766
- newDiv.style.background = '#e6ffe6';
767
- newDiv.style.padding = '8px';
768
- newDiv.style.whiteSpace = 'pre-wrap';
769
- newDiv.style.borderRadius = '4px';
770
- newDiv.style.fontFamily = 'monospace';
771
- newDiv.style.fontSize = '0.8em';
772
- newDiv.style.maxHeight = '150px';
773
- newDiv.style.overflowY = 'auto';
774
- newDiv.textContent = `+ ${edit.new_string}`;
775
-
776
- changeDiv.appendChild(oldDiv);
777
- changeDiv.appendChild(newDiv);
778
- } else {
779
- changeDiv.textContent = JSON.stringify(edit, null, 2);
780
- }
781
-
782
- editDiv.appendChild(editHeader);
783
- editDiv.appendChild(changeDiv);
784
- multiEditDiv.appendChild(editDiv);
785
- });
786
- } else {
787
- const noEditsDiv = document.createElement('div');
788
- noEditsDiv.style.color = '#6b7280';
789
- noEditsDiv.style.fontStyle = 'italic';
790
- noEditsDiv.textContent = 'No edits provided';
791
- multiEditDiv.appendChild(noEditsDiv);
792
- }
793
-
794
- content.appendChild(multiEditDiv);
795
- return content;
796
- }
797
-
798
- renderOutput(result, toolCall) {
799
- const content = document.createElement('div');
800
- content.className = 'tool-result-content';
801
-
802
- // Try to extract text from array format first
803
- let resultText = result;
804
- if (Array.isArray(result) && result[0] && result[0].text) {
805
- resultText = result[0].text;
806
- }
807
-
808
- const resultDiv = document.createElement('div');
809
- resultDiv.style.padding = '12px';
810
- resultDiv.style.borderRadius = '6px';
811
-
812
- // Check if it's a success message
813
- if (
814
- typeof resultText === 'string' &&
815
- (resultText.includes('successfully') ||
816
- resultText.includes('updated') ||
817
- resultText.includes('file has been updated'))
818
- ) {
819
- resultDiv.style.background = '#d4edda';
820
- resultDiv.style.border = '1px solid #c3e6cb';
821
- resultDiv.style.color = '#155724';
822
-
823
- const successIcon = document.createElement('span');
824
- successIcon.textContent = '✅ ';
825
- successIcon.style.fontWeight = 'bold';
826
-
827
- const message = document.createElement('span');
828
- message.textContent = resultText;
829
-
830
- resultDiv.appendChild(successIcon);
831
- resultDiv.appendChild(message);
832
- } else {
833
- // Default rendering
834
- resultDiv.style.whiteSpace = 'pre-wrap';
835
- resultDiv.style.fontFamily = 'monospace';
836
- resultDiv.style.fontSize = '0.9em';
837
- resultDiv.textContent = resultText;
838
- }
839
-
840
- content.appendChild(resultDiv);
841
- return content;
842
- }
843
-
844
- getIcon() {
845
- return '🔄';
846
- }
847
- }
848
-
849
- // Handler for Write tool
850
- class WriteHandler extends ToolHandler {
851
- constructor() {
852
- super('Write');
853
- }
854
-
855
- renderInput(input) {
856
- const content = document.createElement('div');
857
- content.className = 'tool-call-content';
858
-
859
- const path = document.createElement('div');
860
- path.style.fontWeight = 'bold';
861
- path.style.marginBottom = '8px';
862
- path.textContent = `📝 ${input.file_path}`;
863
-
864
- const contentInfo = document.createElement('div');
865
- contentInfo.style.fontSize = '0.9em';
866
- contentInfo.style.color = '#666';
867
- contentInfo.textContent = `${input.content ? input.content.length : 0} characters`;
868
-
869
- content.appendChild(path);
870
- content.appendChild(contentInfo);
871
- return content;
872
- }
873
-
874
- getIcon() {
875
- return '📝';
876
- }
877
- }
878
-
879
- // Handler for LS tool
880
- class LSHandler extends ToolHandler {
881
- constructor() {
882
- super('LS');
883
- }
884
-
885
- renderInput(input) {
886
- const content = document.createElement('div');
887
- content.className = 'tool-call-content';
888
-
889
- const path = document.createElement('div');
890
- path.style.fontWeight = 'bold';
891
- path.textContent = `📁 ${input.path}`;
892
-
893
- content.appendChild(path);
894
- return content;
895
- }
896
-
897
- renderOutput(result, toolCall) {
898
- const content = document.createElement('div');
899
- content.className = 'tool-result-content';
900
- content.style.fontFamily = 'monospace';
901
- content.style.whiteSpace = 'pre';
902
- content.textContent = result;
903
- return content;
904
- }
905
-
906
- getIcon() {
907
- return '📁';
908
- }
909
- }
910
-
911
- // Handler for Grep tool
912
- class GrepHandler extends ToolHandler {
913
- constructor() {
914
- super('Grep');
915
- }
916
-
917
- renderInput(input) {
918
- const content = document.createElement('div');
919
- content.className = 'tool-call-content';
920
-
921
- const pattern = document.createElement('div');
922
- pattern.style.fontWeight = 'bold';
923
- pattern.style.marginBottom = '4px';
924
- pattern.textContent = `🔍 "${input.pattern}"`;
925
-
926
- if (input.path) {
927
- const path = document.createElement('div');
928
- path.style.fontSize = '0.9em';
929
- path.style.color = '#666';
930
- path.textContent = `in ${input.path}`;
931
- content.appendChild(pattern);
932
- content.appendChild(path);
933
- } else {
934
- content.appendChild(pattern);
935
- }
936
-
937
- return content;
938
- }
939
-
940
- renderOutput(result, toolCall) {
941
- const content = document.createElement('div');
942
- content.className = 'tool-result-content';
943
- content.style.fontFamily = 'monospace';
944
- content.style.fontSize = '0.85em';
945
- content.style.whiteSpace = 'pre';
946
- content.textContent = result;
947
- return content;
948
- }
949
-
950
- getIcon() {
951
- return '🔍';
952
- }
953
- }
954
-
955
- // Handler for Glob tool
956
- class GlobHandler extends ToolHandler {
957
- constructor() {
958
- super('Glob');
959
- }
960
-
961
- renderInput(input) {
962
- const content = document.createElement('div');
963
- content.className = 'tool-call-content';
964
-
965
- const globDiv = document.createElement('div');
966
- globDiv.style.padding = '12px';
967
- globDiv.style.background = '#fef3c7';
968
- globDiv.style.borderRadius = '8px';
969
- globDiv.style.border = '1px solid #fbbf24';
970
-
971
- const header = document.createElement('div');
972
- header.style.fontWeight = 'bold';
973
- header.style.marginBottom = '12px';
974
- header.style.color = '#92400e';
975
- header.textContent = '🗂️ File Pattern Search';
976
-
977
- const patternDiv = document.createElement('div');
978
- patternDiv.style.marginBottom = '8px';
979
-
980
- const patternLabel = document.createElement('div');
981
- patternLabel.style.fontWeight = 'bold';
982
- patternLabel.style.marginBottom = '4px';
983
- patternLabel.style.color = '#2c3e50';
984
- patternLabel.textContent = 'Pattern:';
985
-
986
- const patternContent = document.createElement('div');
987
- patternContent.style.padding = '8px';
988
- patternContent.style.background = '#ffffff';
989
- patternContent.style.borderRadius = '4px';
990
- patternContent.style.border = '1px solid #e5e7eb';
991
- patternContent.style.fontFamily = 'monospace';
992
- patternContent.style.fontSize = '0.9em';
993
- patternContent.textContent = input.pattern || '';
994
-
995
- patternDiv.appendChild(patternLabel);
996
- patternDiv.appendChild(patternContent);
997
- globDiv.appendChild(header);
998
- globDiv.appendChild(patternDiv);
999
-
1000
- if (input.path) {
1001
- const pathDiv = document.createElement('div');
1002
- pathDiv.style.marginBottom = '8px';
1003
-
1004
- const pathLabel = document.createElement('div');
1005
- pathLabel.style.fontWeight = 'bold';
1006
- pathLabel.style.marginBottom = '4px';
1007
- pathLabel.style.color = '#2c3e50';
1008
- pathLabel.textContent = 'Search Path:';
1009
-
1010
- const pathContent = document.createElement('div');
1011
- pathContent.style.padding = '8px';
1012
- pathContent.style.background = '#ffffff';
1013
- pathContent.style.borderRadius = '4px';
1014
- pathContent.style.border = '1px solid #e5e7eb';
1015
- pathContent.style.fontFamily = 'monospace';
1016
- pathContent.style.fontSize = '0.9em';
1017
- pathContent.textContent = input.path;
1018
-
1019
- pathDiv.appendChild(pathLabel);
1020
- pathDiv.appendChild(pathContent);
1021
- globDiv.appendChild(pathDiv);
1022
- }
1023
-
1024
- content.appendChild(globDiv);
1025
- return content;
1026
- }
1027
-
1028
- renderOutput(result, toolCall) {
1029
- const content = document.createElement('div');
1030
- content.className = 'tool-result-content';
1031
-
1032
- // Try to extract text from array format first
1033
- let resultText = result;
1034
- if (Array.isArray(result) && result[0] && result[0].text) {
1035
- resultText = result[0].text;
1036
- }
1037
-
1038
- const resultDiv = document.createElement('div');
1039
- resultDiv.style.padding = '12px';
1040
- resultDiv.style.borderRadius = '6px';
1041
- resultDiv.style.background = '#f9fafb';
1042
- resultDiv.style.border = '1px solid #d1d5db';
1043
-
1044
- // Check if we have file paths (likely line-separated)
1045
- if (typeof resultText === 'string' && resultText.trim()) {
1046
- const lines = resultText
1047
- .trim()
1048
- .split('\n')
1049
- .filter((line) => line.trim());
1050
-
1051
- if (lines.length > 0) {
1052
- const headerDiv = document.createElement('div');
1053
- headerDiv.style.fontWeight = 'bold';
1054
- headerDiv.style.marginBottom = '12px';
1055
- headerDiv.style.color = '#374151';
1056
- headerDiv.textContent = `Found ${lines.length} matching file${lines.length === 1 ? '' : 's'}:`;
1057
-
1058
- const filesDiv = document.createElement('div');
1059
-
1060
- lines.forEach((filePath) => {
1061
- const fileDiv = document.createElement('div');
1062
- fileDiv.style.padding = '6px 8px';
1063
- fileDiv.style.marginBottom = '4px';
1064
- fileDiv.style.background = '#ffffff';
1065
- fileDiv.style.borderRadius = '4px';
1066
- fileDiv.style.border = '1px solid #e5e7eb';
1067
- fileDiv.style.fontFamily = 'monospace';
1068
- fileDiv.style.fontSize = '0.85em';
1069
- fileDiv.style.wordBreak = 'break-all';
1070
-
1071
- // Add file icon based on extension
1072
- const extension = filePath.split('.').pop()?.toLowerCase();
1073
- let icon = '📄';
1074
- if (['js', 'ts', 'jsx', 'tsx'].includes(extension)) icon = '📜';
1075
- else if (['py'].includes(extension)) icon = '🐍';
1076
- else if (['rs'].includes(extension)) icon = '🦀';
1077
- else if (['html', 'htm'].includes(extension)) icon = '🌐';
1078
- else if (['css'].includes(extension)) icon = '🎨';
1079
- else if (['md'].includes(extension)) icon = '📝';
1080
- else if (['json'].includes(extension)) icon = '📋';
1081
-
1082
- fileDiv.innerHTML = `${icon} ${filePath}`;
1083
- filesDiv.appendChild(fileDiv);
1084
- });
1085
-
1086
- resultDiv.appendChild(headerDiv);
1087
- resultDiv.appendChild(filesDiv);
1088
- } else {
1089
- resultDiv.style.background = '#fef2f2';
1090
- resultDiv.style.border = '1px solid #fecaca';
1091
- resultDiv.style.color = '#991b1b';
1092
- resultDiv.innerHTML = '❌ No files found matching the pattern';
1093
- }
1094
- } else {
1095
- resultDiv.style.whiteSpace = 'pre-wrap';
1096
- resultDiv.style.fontFamily = 'monospace';
1097
- resultDiv.style.fontSize = '0.9em';
1098
- resultDiv.textContent =
1099
- typeof resultText === 'string' ? resultText : 'No results';
1100
- }
1101
-
1102
- content.appendChild(resultDiv);
1103
- return content;
1104
- }
1105
-
1106
- getIcon() {
1107
- return '🗂️';
1108
- }
1109
- }
1110
-
1111
- // Handler for TodoWrite tool
1112
- class TodoWriteHandler extends ToolHandler {
1113
- constructor() {
1114
- super('TodoWrite');
1115
- }
1116
-
1117
- renderInput(input) {
1118
- const content = document.createElement('div');
1119
- content.className = 'tool-call-content';
1120
-
1121
- if (input.todos && Array.isArray(input.todos)) {
1122
- const todoList = document.createElement('div');
1123
- todoList.style.marginTop = '8px';
1124
-
1125
- const header = document.createElement('div');
1126
- header.style.fontWeight = 'bold';
1127
- header.style.marginBottom = '8px';
1128
- header.style.borderBottom = '1px solid #ddd';
1129
- header.style.paddingBottom = '4px';
1130
- header.textContent = `📝 Todo List (${input.todos.length} items)`;
1131
-
1132
- todoList.appendChild(header);
1133
-
1134
- input.todos.forEach((todo) => {
1135
- const todoItem = document.createElement('div');
1136
- todoItem.style.display = 'flex';
1137
- todoItem.style.alignItems = 'flex-start';
1138
- todoItem.style.marginBottom = '8px';
1139
- todoItem.style.padding = '8px';
1140
- todoItem.style.borderRadius = '4px';
1141
- todoItem.style.fontSize = '0.9em';
1142
-
1143
- // Status-based styling
1144
- if (todo.status === 'completed') {
1145
- todoItem.style.background = '#e8f5e8';
1146
- todoItem.style.borderLeft = '3px solid #28a745';
1147
- } else if (todo.status === 'in_progress') {
1148
- todoItem.style.background = '#fff3cd';
1149
- todoItem.style.borderLeft = '3px solid #ffc107';
1150
- } else {
1151
- todoItem.style.background = '#f8f9fa';
1152
- todoItem.style.borderLeft = '3px solid #6c757d';
1153
- }
1154
-
1155
- // Status icon
1156
- const statusIcon = document.createElement('span');
1157
- statusIcon.style.marginRight = '8px';
1158
- statusIcon.style.fontSize = '1.1em';
1159
- if (todo.status === 'completed') {
1160
- statusIcon.textContent = '✅';
1161
- } else if (todo.status === 'in_progress') {
1162
- statusIcon.textContent = '🔄';
1163
- } else {
1164
- statusIcon.textContent = '⭕';
1165
- }
1166
-
1167
- // Content container
1168
- const contentContainer = document.createElement('div');
1169
- contentContainer.style.flex = '1';
1170
-
1171
- // Todo content
1172
- const todoContent = document.createElement('div');
1173
- todoContent.style.marginBottom = '4px';
1174
- if (todo.status === 'completed') {
1175
- todoContent.style.textDecoration = 'line-through';
1176
- todoContent.style.color = '#666';
1177
- }
1178
- todoContent.textContent = todo.content;
1179
-
1180
- // Priority and ID
1181
- const metaInfo = document.createElement('div');
1182
- metaInfo.style.fontSize = '0.8em';
1183
- metaInfo.style.color = '#888';
1184
-
1185
- const priorityColor =
1186
- todo.priority === 'high'
1187
- ? '#dc3545'
1188
- : todo.priority === 'medium'
1189
- ? '#fd7e14'
1190
- : '#28a745';
1191
- metaInfo.innerHTML = `<span style="color: ${priorityColor}; font-weight: bold;">●</span> ${todo.priority} priority • ID: ${todo.id}`;
1192
-
1193
- contentContainer.appendChild(todoContent);
1194
- contentContainer.appendChild(metaInfo);
1195
-
1196
- todoItem.appendChild(statusIcon);
1197
- todoItem.appendChild(contentContainer);
1198
- todoList.appendChild(todoItem);
1199
- });
1200
-
1201
- content.appendChild(todoList);
1202
- } else {
1203
- content.textContent = JSON.stringify(input, null, 2);
1204
- }
1205
-
1206
- return content;
1207
- }
1208
-
1209
- getIcon() {
1210
- return '📝';
1211
- }
1212
- }
1213
-
1214
- // Handler for mcp__private-journal__process_thoughts tool
1215
- class PrivateJournalHandler extends ToolHandler {
1216
- constructor() {
1217
- super('mcp__private-journal__process_thoughts');
1218
- }
1219
-
1220
- renderInput(input) {
1221
- const content = document.createElement('div');
1222
- content.className = 'tool-call-content';
1223
-
1224
- const journalEntry = document.createElement('div');
1225
- journalEntry.style.marginTop = '8px';
1226
-
1227
- const header = document.createElement('div');
1228
- header.style.fontWeight = 'bold';
1229
- header.style.marginBottom = '12px';
1230
- header.style.borderBottom = '2px solid #ddd';
1231
- header.style.paddingBottom = '6px';
1232
- header.style.color = '#2c3e50';
1233
- header.textContent = '🧠 Private Journal Entry';
1234
-
1235
- journalEntry.appendChild(header);
1236
-
1237
- // Define journal sections with their icons and colors
1238
- const sections = [
1239
- {
1240
- key: 'feelings',
1241
- label: 'Feelings',
1242
- icon: '💭',
1243
- color: '#e74c3c',
1244
- },
1245
- {
1246
- key: 'user_context',
1247
- label: 'User Context',
1248
- icon: '👤',
1249
- color: '#3498db',
1250
- },
1251
- {
1252
- key: 'technical_insights',
1253
- label: 'Technical Insights',
1254
- icon: '🔧',
1255
- color: '#2ecc71',
1256
- },
1257
- {
1258
- key: 'project_notes',
1259
- label: 'Project Notes',
1260
- icon: '📋',
1261
- color: '#f39c12',
1262
- },
1263
- {
1264
- key: 'world_knowledge',
1265
- label: 'World Knowledge',
1266
- icon: '🌍',
1267
- color: '#9b59b6',
1268
- },
1269
- ];
1270
-
1271
- sections.forEach((section) => {
1272
- if (input[section.key]) {
1273
- const sectionDiv = document.createElement('div');
1274
- sectionDiv.style.marginBottom = '16px';
1275
- sectionDiv.style.padding = '12px';
1276
- sectionDiv.style.borderRadius = '8px';
1277
- sectionDiv.style.border = `1px solid ${section.color}30`;
1278
- sectionDiv.style.background = `${section.color}08`;
1279
-
1280
- const sectionHeader = document.createElement('div');
1281
- sectionHeader.style.fontWeight = 'bold';
1282
- sectionHeader.style.marginBottom = '8px';
1283
- sectionHeader.style.color = section.color;
1284
- sectionHeader.style.fontSize = '0.95em';
1285
- sectionHeader.innerHTML = `${section.icon} ${section.label}`;
1286
-
1287
- const sectionContent = document.createElement('div');
1288
- sectionContent.style.lineHeight = '1.5';
1289
- sectionContent.style.color = '#333';
1290
- sectionContent.style.fontSize = '0.9em';
1291
- sectionContent.style.whiteSpace = 'pre-wrap';
1292
- sectionContent.textContent = input[section.key];
1293
-
1294
- sectionDiv.appendChild(sectionHeader);
1295
- sectionDiv.appendChild(sectionContent);
1296
- journalEntry.appendChild(sectionDiv);
1297
- }
1298
- });
1299
-
1300
- // If no recognized sections found, fall back to JSON
1301
- if (!sections.some((section) => input[section.key])) {
1302
- content.textContent = JSON.stringify(input, null, 2);
1303
- return content;
1304
- }
1305
-
1306
- content.appendChild(journalEntry);
1307
- return content;
1308
- }
1309
-
1310
- renderOutput(result, toolCall) {
1311
- const content = document.createElement('div');
1312
- content.className = 'tool-result-content';
1313
-
1314
- // Try to extract text from array format first
1315
- let resultText = result;
1316
- if (Array.isArray(result) && result[0] && result[0].text) {
1317
- resultText = result[0].text;
1318
- }
1319
-
1320
- const resultDiv = document.createElement('div');
1321
- resultDiv.style.padding = '12px';
1322
- resultDiv.style.borderRadius = '6px';
1323
- resultDiv.style.background = '#d4edda';
1324
- resultDiv.style.border = '1px solid #c3e6cb';
1325
- resultDiv.style.color = '#155724';
1326
-
1327
- const successIcon = document.createElement('span');
1328
- successIcon.textContent = '✅ ';
1329
- successIcon.style.fontWeight = 'bold';
1330
-
1331
- const message = document.createElement('span');
1332
- message.textContent =
1333
- typeof resultText === 'string'
1334
- ? resultText
1335
- : 'Thoughts recorded successfully.';
1336
-
1337
- resultDiv.appendChild(successIcon);
1338
- resultDiv.appendChild(message);
1339
- content.appendChild(resultDiv);
1340
- return content;
1341
- }
1342
-
1343
- getIcon() {
1344
- return '🧠';
1345
- }
1346
- }
1347
-
1348
- // Handler for MCP Social Media Login tool
1349
- class SocialMediaLoginHandler extends ToolHandler {
1350
- constructor() {
1351
- super('mcp__socialmedia__login');
1352
- }
1353
-
1354
- renderInput(input) {
1355
- const content = document.createElement('div');
1356
- content.className = 'tool-call-content';
1357
-
1358
- const loginDiv = document.createElement('div');
1359
- loginDiv.style.padding = '12px';
1360
- loginDiv.style.background = '#e8f4fd';
1361
- loginDiv.style.borderRadius = '8px';
1362
- loginDiv.style.border = '1px solid #b8daff';
1363
-
1364
- const header = document.createElement('div');
1365
- header.style.fontWeight = 'bold';
1366
- header.style.marginBottom = '8px';
1367
- header.style.color = '#2c3e50';
1368
- header.textContent = '🔐 Social Media Login';
1369
-
1370
- const agentName = document.createElement('div');
1371
- agentName.style.fontSize = '0.9em';
1372
- agentName.innerHTML = `<strong>Agent Name:</strong> ${input.agent_name || 'Unknown'}`;
1373
-
1374
- loginDiv.appendChild(header);
1375
- loginDiv.appendChild(agentName);
1376
- content.appendChild(loginDiv);
1377
- return content;
1378
- }
1379
-
1380
- renderOutput(result, toolCall) {
1381
- const content = document.createElement('div');
1382
- content.className = 'tool-result-content';
1383
-
1384
- try {
1385
- // Try to parse JSON from the text result
1386
- let jsonData;
1387
- if (Array.isArray(result) && result[0] && result[0].text) {
1388
- jsonData = JSON.parse(result[0].text);
1389
- } else if (typeof result === 'string') {
1390
- jsonData = JSON.parse(result);
1391
- } else {
1392
- jsonData = result;
1393
- }
1394
-
1395
- const resultDiv = document.createElement('div');
1396
- resultDiv.style.padding = '12px';
1397
- resultDiv.style.borderRadius = '6px';
1398
-
1399
- if (jsonData.success) {
1400
- resultDiv.style.background = '#d4edda';
1401
- resultDiv.style.border = '1px solid #c3e6cb';
1402
- resultDiv.style.color = '#155724';
1403
-
1404
- const successIcon = document.createElement('span');
1405
- successIcon.textContent = '✅ ';
1406
- successIcon.style.fontWeight = 'bold';
1407
-
1408
- const message = document.createElement('div');
1409
- message.appendChild(successIcon);
1410
- message.appendChild(document.createTextNode('Login Successful!'));
1411
-
1412
- if (jsonData.message) {
1413
- const welcomeMsg = document.createElement('div');
1414
- welcomeMsg.style.marginTop = '8px';
1415
- welcomeMsg.style.fontStyle = 'italic';
1416
- welcomeMsg.textContent = jsonData.message;
1417
- message.appendChild(welcomeMsg);
1418
- }
1419
-
1420
- resultDiv.appendChild(message);
1421
- } else {
1422
- resultDiv.style.background = '#f8d7da';
1423
- resultDiv.style.border = '1px solid #f5c6cb';
1424
- resultDiv.style.color = '#721c24';
1425
- resultDiv.textContent = '❌ Login Failed';
1426
- }
1427
-
1428
- content.appendChild(resultDiv);
1429
- } catch (e) {
1430
- // Fallback to default text rendering
1431
- content.style.whiteSpace = 'pre-wrap';
1432
- content.textContent =
1433
- typeof result === 'string'
1434
- ? result
1435
- : JSON.stringify(result, null, 2);
1436
- }
1437
-
1438
- return content;
1439
- }
1440
-
1441
- getIcon() {
1442
- return '🔐';
1443
- }
1444
- }
1445
-
1446
- // Handler for MCP Social Media Create Post tool
1447
- class SocialMediaCreatePostHandler extends ToolHandler {
1448
- constructor() {
1449
- super('mcp__socialmedia__create_post');
1450
- }
1451
-
1452
- renderInput(input) {
1453
- const content = document.createElement('div');
1454
- content.className = 'tool-call-content';
1455
-
1456
- const postDiv = document.createElement('div');
1457
- postDiv.style.padding = '12px';
1458
- postDiv.style.background = '#f0f9ff';
1459
- postDiv.style.borderRadius = '8px';
1460
- postDiv.style.border = '1px solid #bae6fd';
1461
-
1462
- const header = document.createElement('div');
1463
- header.style.fontWeight = 'bold';
1464
- header.style.marginBottom = '12px';
1465
- header.style.color = '#2c3e50';
1466
- header.textContent = '📱 Creating Social Media Post';
1467
-
1468
- const postContent = document.createElement('div');
1469
- postContent.style.padding = '12px';
1470
- postContent.style.background = '#ffffff';
1471
- postContent.style.borderRadius = '6px';
1472
- postContent.style.border = '1px solid #e5e7eb';
1473
- postContent.style.marginBottom = '8px';
1474
- postContent.style.lineHeight = '1.5';
1475
- postContent.textContent = input.content || '';
1476
-
1477
- postDiv.appendChild(header);
1478
- postDiv.appendChild(postContent);
1479
-
1480
- if (
1481
- input.tags &&
1482
- Array.isArray(input.tags) &&
1483
- input.tags.length > 0
1484
- ) {
1485
- const tagsDiv = document.createElement('div');
1486
- tagsDiv.style.marginTop = '8px';
1487
-
1488
- const tagsLabel = document.createElement('span');
1489
- tagsLabel.style.fontSize = '0.85em';
1490
- tagsLabel.style.color = '#666';
1491
- tagsLabel.textContent = 'Tags: ';
1492
- tagsDiv.appendChild(tagsLabel);
1493
-
1494
- input.tags.forEach((tag) => {
1495
- const tagSpan = document.createElement('span');
1496
- tagSpan.style.display = 'inline-block';
1497
- tagSpan.style.background = '#3b82f6';
1498
- tagSpan.style.color = 'white';
1499
- tagSpan.style.padding = '2px 6px';
1500
- tagSpan.style.borderRadius = '12px';
1501
- tagSpan.style.fontSize = '0.8em';
1502
- tagSpan.style.marginRight = '4px';
1503
- tagSpan.textContent = `#${tag}`;
1504
- tagsDiv.appendChild(tagSpan);
1505
- });
1506
-
1507
- postDiv.appendChild(tagsDiv);
1508
- }
1509
-
1510
- content.appendChild(postDiv);
1511
- return content;
1512
- }
1513
-
1514
- renderOutput(result, toolCall) {
1515
- const content = document.createElement('div');
1516
- content.className = 'tool-result-content';
1517
-
1518
- try {
1519
- // Try to parse JSON from the text result
1520
- let jsonData;
1521
- if (Array.isArray(result) && result[0] && result[0].text) {
1522
- jsonData = JSON.parse(result[0].text);
1523
- } else if (typeof result === 'string') {
1524
- jsonData = JSON.parse(result);
1525
- } else {
1526
- jsonData = result;
1527
- }
1528
-
1529
- const resultDiv = document.createElement('div');
1530
- resultDiv.style.padding = '12px';
1531
- resultDiv.style.borderRadius = '6px';
1532
-
1533
- if (jsonData.success && jsonData.post) {
1534
- resultDiv.style.background = '#d4edda';
1535
- resultDiv.style.border = '1px solid #c3e6cb';
1536
- resultDiv.style.color = '#155724';
1537
-
1538
- const successMsg = document.createElement('div');
1539
- successMsg.innerHTML =
1540
- '✅ <strong>Post Created Successfully!</strong>';
1541
- successMsg.style.marginBottom = '8px';
1542
-
1543
- const postInfo = document.createElement('div');
1544
- postInfo.style.fontSize = '0.9em';
1545
- postInfo.innerHTML = `
1546
- <div><strong>Post ID:</strong> ${jsonData.post.id}</div>
1547
- <div><strong>Author:</strong> ${jsonData.post.author_name}</div>
1548
- <div><strong>Timestamp:</strong> ${new Date(jsonData.post.timestamp).toLocaleString()}</div>
1549
- `;
1550
-
1551
- resultDiv.appendChild(successMsg);
1552
- resultDiv.appendChild(postInfo);
1553
- } else {
1554
- resultDiv.style.background = '#f8d7da';
1555
- resultDiv.style.border = '1px solid #f5c6cb';
1556
- resultDiv.style.color = '#721c24';
1557
- resultDiv.textContent = '❌ Failed to create post';
1558
- }
1559
-
1560
- content.appendChild(resultDiv);
1561
- } catch (e) {
1562
- // Fallback to default text rendering
1563
- content.style.whiteSpace = 'pre-wrap';
1564
- content.textContent =
1565
- typeof result === 'string'
1566
- ? result
1567
- : JSON.stringify(result, null, 2);
1568
- }
1569
-
1570
- return content;
1571
- }
1572
-
1573
- getIcon() {
1574
- return '📱';
1575
- }
1576
- }
1577
-
1578
- // Handler for Task tool
1579
- class TaskHandler extends ToolHandler {
1580
- constructor() {
1581
- super('Task');
1582
- }
1583
-
1584
- renderInput(input) {
1585
- const content = document.createElement('div');
1586
- content.className = 'tool-call-content';
1587
-
1588
- const taskDiv = document.createElement('div');
1589
- taskDiv.style.padding = '12px';
1590
- taskDiv.style.background = '#fff7ed';
1591
- taskDiv.style.borderRadius = '8px';
1592
- taskDiv.style.border = '1px solid #fed7aa';
1593
-
1594
- const header = document.createElement('div');
1595
- header.style.fontWeight = 'bold';
1596
- header.style.marginBottom = '12px';
1597
- header.style.color = '#ea580c';
1598
- header.textContent = `🎯 Task: ${input.description || 'Unnamed Task'}`;
1599
-
1600
- if (input.prompt) {
1601
- const promptDiv = document.createElement('div');
1602
- promptDiv.style.background = '#ffffff';
1603
- promptDiv.style.padding = '12px';
1604
- promptDiv.style.borderRadius = '6px';
1605
- promptDiv.style.border = '1px solid #e5e7eb';
1606
- promptDiv.style.lineHeight = '1.5';
1607
- promptDiv.style.whiteSpace = 'pre-wrap';
1608
- promptDiv.textContent = input.prompt;
1609
-
1610
- const promptLabel = document.createElement('div');
1611
- promptLabel.style.fontWeight = 'bold';
1612
- promptLabel.style.marginBottom = '8px';
1613
- promptLabel.style.color = '#2c3e50';
1614
- promptLabel.textContent = '📋 Task Instructions';
1615
-
1616
- taskDiv.appendChild(header);
1617
- taskDiv.appendChild(promptLabel);
1618
- taskDiv.appendChild(promptDiv);
1619
- } else {
1620
- taskDiv.appendChild(header);
1621
- }
1622
-
1623
- content.appendChild(taskDiv);
1624
- return content;
1625
- }
1626
-
1627
- renderOutput(result, toolCall) {
1628
- const content = document.createElement('div');
1629
- content.className = 'tool-result-content';
1630
- content.style.maxHeight = '400px';
1631
- content.style.overflowY = 'auto';
1632
- content.style.lineHeight = '1.5';
1633
- content.style.whiteSpace = 'pre-wrap';
1634
-
1635
- // Try to extract text from array format
1636
- if (Array.isArray(result) && result[0] && result[0].text) {
1637
- content.textContent = result[0].text;
1638
- } else {
1639
- content.textContent =
1640
- typeof result === 'string'
1641
- ? result
1642
- : JSON.stringify(result, null, 2);
1643
- }
1644
-
1645
- return content;
1646
- }
1647
-
1648
- getIcon() {
1649
- return '🎯';
1650
- }
1651
- }
1652
-
1653
- // Handler for WebFetch tool
1654
- class WebFetchHandler extends ToolHandler {
1655
- constructor() {
1656
- super('WebFetch');
1657
- }
1658
-
1659
- renderInput(input) {
1660
- const content = document.createElement('div');
1661
- content.className = 'tool-call-content';
1662
-
1663
- const webFetch = document.createElement('div');
1664
- webFetch.style.marginTop = '8px';
1665
-
1666
- // URL section
1667
- const urlDiv = document.createElement('div');
1668
- urlDiv.style.marginBottom = '12px';
1669
-
1670
- const urlLabel = document.createElement('div');
1671
- urlLabel.style.fontWeight = 'bold';
1672
- urlLabel.style.marginBottom = '4px';
1673
- urlLabel.style.color = '#2c3e50';
1674
- urlLabel.textContent = '🌐 URL';
1675
-
1676
- const urlContent = document.createElement('div');
1677
- urlContent.style.padding = '8px';
1678
- urlContent.style.background = '#f8f9fa';
1679
- urlContent.style.borderRadius = '4px';
1680
- urlContent.style.border = '1px solid #dee2e6';
1681
- urlContent.style.fontFamily = 'monospace';
1682
- urlContent.style.fontSize = '0.9em';
1683
- urlContent.style.wordBreak = 'break-all';
1684
-
1685
- // Make URL clickable
1686
- const urlLink = document.createElement('a');
1687
- urlLink.href = input.url;
1688
- urlLink.target = '_blank';
1689
- urlLink.rel = 'noopener noreferrer';
1690
- urlLink.style.color = '#007bff';
1691
- urlLink.style.textDecoration = 'none';
1692
- urlLink.textContent = input.url;
1693
- urlLink.onmouseover = () =>
1694
- (urlLink.style.textDecoration = 'underline');
1695
- urlLink.onmouseout = () => (urlLink.style.textDecoration = 'none');
1696
-
1697
- urlContent.appendChild(urlLink);
1698
- urlDiv.appendChild(urlLabel);
1699
- urlDiv.appendChild(urlContent);
1700
-
1701
- // Prompt section
1702
- if (input.prompt) {
1703
- const promptDiv = document.createElement('div');
1704
- promptDiv.style.marginBottom = '12px';
1705
-
1706
- const promptLabel = document.createElement('div');
1707
- promptLabel.style.fontWeight = 'bold';
1708
- promptLabel.style.marginBottom = '4px';
1709
- promptLabel.style.color = '#2c3e50';
1710
- promptLabel.textContent = '📝 Analysis Prompt';
1711
-
1712
- const promptContent = document.createElement('div');
1713
- promptContent.style.padding = '12px';
1714
- promptContent.style.background = '#e8f4fd';
1715
- promptContent.style.borderRadius = '4px';
1716
- promptContent.style.border = '1px solid #b8daff';
1717
- promptContent.style.lineHeight = '1.5';
1718
- promptContent.style.fontSize = '0.9em';
1719
- promptContent.style.whiteSpace = 'pre-wrap';
1720
- promptContent.textContent = input.prompt;
1721
-
1722
- promptDiv.appendChild(promptLabel);
1723
- promptDiv.appendChild(promptContent);
1724
- webFetch.appendChild(promptDiv);
1725
- }
1726
-
1727
- webFetch.appendChild(urlDiv);
1728
- content.appendChild(webFetch);
1729
- return content;
1730
- }
1731
-
1732
- renderOutput(result, toolCall) {
1733
- const content = document.createElement('div');
1734
- content.className = 'tool-result-content';
1735
- content.style.maxHeight = '400px';
1736
- content.style.overflowY = 'auto';
1737
- content.style.lineHeight = '1.5';
1738
- content.style.whiteSpace = 'pre-wrap';
1739
- content.textContent = result;
1740
- return content;
1741
- }
1742
-
1743
- getIcon() {
1744
- return '🌐';
1745
- }
1746
- }
1747
-
1748
- // Tool handler registry
1749
- const toolHandlers = {
1750
- Bash: new BashHandler(),
1751
- Read: new ReadHandler(),
1752
- Edit: new EditHandler(),
1753
- MultiEdit: new MultiEditHandler(),
1754
- Write: new WriteHandler(),
1755
- LS: new LSHandler(),
1756
- Grep: new GrepHandler(),
1757
- Glob: new GlobHandler(),
1758
- TodoWrite: new TodoWriteHandler(),
1759
- 'mcp__private-journal__process_thoughts': new PrivateJournalHandler(),
1760
- WebFetch: new WebFetchHandler(),
1761
- mcp__socialmedia__login: new SocialMediaLoginHandler(),
1762
- mcp__socialmedia__create_post: new SocialMediaCreatePostHandler(),
1763
- Task: new TaskHandler(),
1764
- };
1765
-
1766
- // Get appropriate handler for a tool
1767
- function getToolHandler(toolName) {
1768
- return toolHandlers[toolName] || new ToolHandler(toolName);
1769
- }
1770
-
1771
- // WebSocket Watch Manager for real-time updates
1772
- class WatchManager {
1773
- constructor() {
1774
- this.ws = null;
1775
- this.subscribedProjects = new Set();
1776
- this.subscribedSessions = new Set();
1777
- this.activityIndicators = new Map();
1778
- this.reconnectAttempts = 0;
1779
- this.maxReconnectAttempts = 5;
1780
- this.reconnectDelay = 1000;
1781
- this.isWatching = false;
1782
- this.shouldReconnect = false;
1783
- this.cleanupTimer = null;
1784
- }
1785
-
1786
- connect() {
1787
- if (this.ws && this.ws.readyState === WebSocket.OPEN) {
1788
- return;
1789
- }
1790
-
1791
- const protocol =
1792
- window.location.protocol === 'https:' ? 'wss:' : 'ws:';
1793
- const wsUrl = `${protocol}//${window.location.host}/ws/watch`;
1794
-
1795
- try {
1796
- this.ws = new WebSocket(wsUrl);
1797
-
1798
- this.ws.onopen = () => {
1799
- console.log('WebSocket connected');
1800
- this.reconnectAttempts = 0;
1801
- this.isWatching = true;
1802
- this.updateWatchStatus('connected');
1803
- };
1804
-
1805
- this.ws.onmessage = (event) => {
1806
- try {
1807
- const watchEvent = JSON.parse(event.data);
1808
- this.handleWatchEvent(watchEvent);
1809
- } catch (e) {
1810
- console.error('Failed to parse watch event:', e);
1811
- }
1812
- };
1813
-
1814
- this.ws.onclose = () => {
1815
- console.log('WebSocket disconnected');
1816
- this.isWatching = false;
1817
- this.updateWatchStatus('disconnected');
1818
- if (this.shouldReconnect) {
1819
- this.scheduleReconnect();
1820
- }
1821
- };
1822
-
1823
- this.ws.onerror = (error) => {
1824
- console.error('WebSocket error:', error);
1825
- this.updateWatchStatus('error');
1826
- };
1827
- } catch (e) {
1828
- console.error('Failed to create WebSocket connection:', e);
1829
- this.scheduleReconnect();
1830
- }
1831
- }
1832
-
1833
- disconnect() {
1834
- this.isWatching = false;
1835
- this.shouldReconnect = false;
1836
- this.reconnectAttempts = 0;
1837
- this.stopCleanupTimer(); // Stop periodic cleanup
1838
- if (this.ws) {
1839
- this.ws.close();
1840
- this.ws = null;
1841
- }
1842
- this.updateWatchStatus('disconnected');
1843
- }
1844
-
1845
- scheduleReconnect() {
1846
- if (
1847
- !this.shouldReconnect ||
1848
- this.reconnectAttempts >= this.maxReconnectAttempts
1849
- ) {
1850
- if (this.reconnectAttempts >= this.maxReconnectAttempts) {
1851
- console.error('Max reconnection attempts reached');
1852
- this.updateWatchStatus('failed');
1853
- }
1854
- return;
1855
- }
1856
-
1857
- this.reconnectAttempts++;
1858
- const delay =
1859
- this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
1860
-
1861
- setTimeout(() => {
1862
- if (this.shouldReconnect && !this.isWatching) {
1863
- console.log(
1864
- `Attempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts})`,
1865
- );
1866
- this.connect();
1867
- }
1868
- }, delay);
1869
- }
1870
-
1871
- handleWatchEvent(watchEvent) {
1872
- console.log('Received watch event:', watchEvent);
1873
-
1874
- switch (watchEvent.type) {
1875
- case 'log_entry':
1876
- this.handleNewLogEntry(watchEvent);
1877
- break;
1878
- case 'session_created':
1879
- this.handleSessionCreated(watchEvent);
1880
- break;
1881
- case 'project_activity':
1882
- this.handleProjectActivity(watchEvent);
1883
- break;
1884
- default:
1885
- console.log('Unknown watch event type:', watchEvent.type);
1886
- }
1887
- }
1888
-
1889
- handleNewLogEntry(watchEvent) {
1890
- // Only handle if we're currently viewing this session
1891
- if (
1892
- currentProject === watchEvent.project &&
1893
- currentSession === watchEvent.session
1894
- ) {
1895
- this.appendLogEntry(watchEvent.entry);
1896
- }
1897
-
1898
- // Show activity indicator
1899
- this.showActivityIndicator(watchEvent.project, watchEvent.session);
1900
- }
1901
-
1902
- handleSessionCreated(watchEvent) {
1903
- // If we're viewing the project's sessions, refresh the list
1904
- if (currentProject === watchEvent.project && !currentSession) {
1905
- // Add a small delay to ensure the file is fully written
1906
- setTimeout(() => {
1907
- loadSessions(currentProject, false);
1908
- }, 500);
1909
- }
1910
- }
1911
-
1912
- handleProjectActivity(watchEvent) {
1913
- this.showActivityIndicator(watchEvent.project, null);
1914
- }
1915
-
1916
- appendLogEntry(entry) {
1917
- if (!entry) return;
1918
-
1919
- const logEntries = document.getElementById('log-entries');
1920
- if (!logEntries) return;
1921
-
1922
- // Defensive checks for entry structure
1923
- try {
1924
- // Validate entry has required type property
1925
- if (typeof entry.type !== 'string' || !entry.type) {
1926
- console.warn(
1927
- 'appendLogEntry: Invalid or missing entry.type',
1928
- entry,
1929
- );
1930
- return;
1931
- }
1932
-
1933
- // Validate entry has message object
1934
- if (!entry.message || typeof entry.message !== 'object') {
1935
- console.warn(
1936
- 'appendLogEntry: Invalid or missing entry.message',
1937
- entry,
1938
- );
1939
- return;
1940
- }
1941
-
1942
- // Validate message has content property
1943
- if (!entry.message.hasOwnProperty('content')) {
1944
- console.warn(
1945
- 'appendLogEntry: Missing entry.message.content',
1946
- entry,
1947
- );
1948
- return;
1949
- }
1950
-
1951
- // Create a new message element
1952
- const messageDiv = document.createElement('div');
1953
- messageDiv.className = 'message assistant';
1954
- messageDiv.style.opacity = '0';
1955
- messageDiv.style.transition = 'opacity 0.3s ease';
1956
-
1957
- // Determine the content based on entry type
1958
- let content = '';
1959
-
1960
- if (entry.type === 'assistant') {
1961
- messageDiv.className = 'message assistant';
1962
- content = this.extractTextContent(entry.message.content);
1963
- } else if (entry.type === 'user') {
1964
- messageDiv.className = 'message user';
1965
- content = this.extractTextContent(entry.message.content);
1966
- } else {
1967
- // Unknown entry type, skip processing
1968
- console.warn('appendLogEntry: Unknown entry type:', entry.type);
1969
- return;
1970
- }
1971
- } catch (error) {
1972
- console.error(
1973
- 'appendLogEntry: Error processing entry:',
1974
- error,
1975
- entry,
1976
- );
1977
- return;
1978
- }
1979
-
1980
- if (content && content.trim()) {
1981
- const avatar = document.createElement('div');
1982
- avatar.className =
1983
- entry.type === 'user' ? 'avatar user' : 'avatar assistant';
1984
- avatar.textContent = entry.type === 'user' ? 'U' : 'AI';
1985
-
1986
- const messageContent = document.createElement('div');
1987
- messageContent.className = 'message-content';
1988
-
1989
- const messageText = document.createElement('div');
1990
- messageText.className = 'message-text';
1991
- messageText.textContent = content;
1992
-
1993
- const messageMeta = document.createElement('div');
1994
- messageMeta.className = 'message-meta';
1995
- try {
1996
- const timestamp = entry.timestamp
1997
- ? new Date(entry.timestamp).toLocaleString()
1998
- : '';
1999
- messageMeta.textContent = timestamp;
2000
- } catch (error) {
2001
- console.warn(
2002
- 'appendLogEntry: Invalid timestamp:',
2003
- entry.timestamp,
2004
- );
2005
- messageMeta.textContent = '';
2006
- }
2007
-
2008
- messageContent.appendChild(messageText);
2009
- messageContent.appendChild(messageMeta);
2010
- messageDiv.appendChild(avatar);
2011
- messageDiv.appendChild(messageContent);
2012
-
2013
- // Add highlight border for new entries
2014
- messageDiv.style.borderLeft = '4px solid #28a745';
2015
- messageDiv.style.paddingLeft = '1rem';
2016
-
2017
- logEntries.appendChild(messageDiv);
2018
-
2019
- // Animate the entry in
2020
- setTimeout(() => {
2021
- messageDiv.style.opacity = '1';
2022
- }, 10);
2023
-
2024
- // Remove highlight after a few seconds
2025
- setTimeout(() => {
2026
- messageDiv.style.borderLeft = 'none';
2027
- messageDiv.style.paddingLeft = '0';
2028
- }, 3000);
2029
-
2030
- // Auto-scroll to the new entry unless user has scrolled up
2031
- this.autoScrollToBottom();
2032
- }
2033
- }
2034
-
2035
- // Helper method to safely extract text content from various content formats
2036
- extractTextContent(content) {
2037
- try {
2038
- if (!content) return '';
2039
-
2040
- if (typeof content === 'string') {
2041
- return content;
2042
- }
2043
-
2044
- if (Array.isArray(content)) {
2045
- return content
2046
- .filter(
2047
- (c) =>
2048
- c &&
2049
- typeof c === 'object' &&
2050
- c.type === 'text' &&
2051
- typeof c.text === 'string',
2052
- )
2053
- .map((c) => c.text)
2054
- .join('\n');
2055
- }
2056
-
2057
- // If content is an object but not an array, try to extract meaningful text
2058
- if (typeof content === 'object') {
2059
- if (content.text && typeof content.text === 'string') {
2060
- return content.text;
2061
- }
2062
- if (content.content && typeof content.content === 'string') {
2063
- return content.content;
2064
- }
2065
- }
2066
-
2067
- return '';
2068
- } catch (error) {
2069
- console.warn(
2070
- 'extractTextContent: Error extracting text:',
2071
- error,
2072
- content,
2073
- );
2074
- return '';
2075
- }
2076
- }
2077
-
2078
- showActivityIndicator(project, session) {
2079
- // Create activity indicator for the project or session
2080
- const key = session ? `${project}:${session}` : project;
2081
-
2082
- // Store activity timestamp
2083
- this.activityIndicators.set(key, Date.now());
2084
-
2085
- // Add visual indicator if element exists
2086
- this.addActivityBadge(project, session);
2087
-
2088
- // Remove indicator after a delay
2089
- setTimeout(() => {
2090
- this.removeActivityBadge(project, session);
2091
- }, 5000);
2092
- }
2093
-
2094
- addActivityBadge(project, session) {
2095
- // Find the relevant DOM element and add activity indicator
2096
- let targetElement = null;
2097
-
2098
- if (session && currentProject === project) {
2099
- // Find session item
2100
- const sessionItems = document.querySelectorAll('.session-item');
2101
- sessionItems.forEach((item) => {
2102
- if (item.textContent.includes(session)) {
2103
- targetElement = item;
2104
- }
2105
- });
2106
- } else {
2107
- // Find project card
2108
- const projectCards = document.querySelectorAll('.project-card');
2109
- projectCards.forEach((card) => {
2110
- if (card.textContent.includes(project)) {
2111
- targetElement = card;
2112
- }
2113
- });
2114
- }
2115
-
2116
- if (
2117
- targetElement &&
2118
- !targetElement.querySelector('.activity-badge')
2119
- ) {
2120
- const badge = document.createElement('div');
2121
- badge.className = 'activity-badge';
2122
- badge.style.cssText = `
2123
- position: absolute;
2124
- top: 10px;
2125
- right: 10px;
2126
- width: 12px;
2127
- height: 12px;
2128
- background: #28a745;
2129
- border-radius: 50%;
2130
- animation: pulse 1.5s infinite;
2131
- `;
2132
-
2133
- targetElement.style.position = 'relative';
2134
- targetElement.appendChild(badge);
2135
- }
2136
- }
2137
-
2138
- removeActivityBadge(project, session) {
2139
- // Remove activity badge
2140
- let targetElement = null;
2141
-
2142
- if (session && currentProject === project) {
2143
- const sessionItems = document.querySelectorAll('.session-item');
2144
- sessionItems.forEach((item) => {
2145
- if (item.textContent.includes(session)) {
2146
- targetElement = item;
2147
- }
2148
- });
2149
- } else {
2150
- const projectCards = document.querySelectorAll('.project-card');
2151
- projectCards.forEach((card) => {
2152
- if (card.textContent.includes(project)) {
2153
- targetElement = card;
2154
- }
2155
- });
2156
- }
2157
-
2158
- if (targetElement) {
2159
- const badge = targetElement.querySelector('.activity-badge');
2160
- if (badge) {
2161
- badge.remove();
2162
- }
2163
- }
2164
- }
2165
-
2166
- cleanupStaleActivityBadges() {
2167
- const STALE_THRESHOLD = 30000; // 30 seconds
2168
- const now = Date.now();
2169
- const staleBadges = [];
2170
-
2171
- // Find stale entries in the activityIndicators map
2172
- for (const [key, timestamp] of this.activityIndicators.entries()) {
2173
- if (now - timestamp > STALE_THRESHOLD) {
2174
- staleBadges.push(key);
2175
- }
2176
- }
2177
-
2178
- // Clean up stale entries and their DOM badges
2179
- staleBadges.forEach((key) => {
2180
- const [project, session] = key.includes(':')
2181
- ? key.split(':')
2182
- : [key, null];
2183
-
2184
- // Remove from map
2185
- this.activityIndicators.delete(key);
2186
-
2187
- // Remove DOM badge if it still exists
2188
- this.removeActivityBadge(project, session);
2189
- });
2190
-
2191
- // Also clean up any orphaned DOM badges that might exist
2192
- const allBadges = document.querySelectorAll('.activity-badge');
2193
- allBadges.forEach((badge) => {
2194
- const parent = badge.parentElement;
2195
- if (!parent || !parent.isConnected) {
2196
- // Badge's parent is no longer in the DOM, remove badge
2197
- badge.remove();
2198
- }
2199
- });
2200
-
2201
- if (staleBadges.length > 0) {
2202
- console.debug(
2203
- `Cleaned up ${staleBadges.length} stale activity badges`,
2204
- );
2205
- }
2206
- }
2207
-
2208
- autoScrollToBottom() {
2209
- const logEntries = document.getElementById('log-entries');
2210
- if (!logEntries) return;
2211
-
2212
- const container = logEntries.closest('.card-body');
2213
- if (!container) return;
2214
-
2215
- // Check if user is near the bottom (within 100px)
2216
- const isNearBottom =
2217
- container.scrollTop + container.clientHeight >=
2218
- container.scrollHeight - 100;
2219
-
2220
- if (isNearBottom) {
2221
- container.scrollTop = container.scrollHeight;
2222
- }
2223
- }
2224
-
2225
- updateWatchStatus(status) {
2226
- // Update UI to show connection status
2227
- let statusElement = document.getElementById('watch-status');
2228
- if (!statusElement) {
2229
- statusElement = document.createElement('div');
2230
- statusElement.id = 'watch-status';
2231
- statusElement.style.cssText = `
2232
- position: fixed;
2233
- top: 20px;
2234
- right: 20px;
2235
- padding: 8px 12px;
2236
- border-radius: 6px;
2237
- font-size: 0.85em;
2238
- font-weight: 600;
2239
- z-index: 1000;
2240
- transition: all 0.3s ease;
2241
- `;
2242
- document.body.appendChild(statusElement);
2243
- }
2244
-
2245
- switch (status) {
2246
- case 'connected':
2247
- statusElement.textContent = '🟢 LIVE';
2248
- statusElement.style.background = '#d4edda';
2249
- statusElement.style.color = '#155724';
2250
- statusElement.style.border = '1px solid #c3e6cb';
2251
- break;
2252
- case 'disconnected':
2253
- statusElement.textContent = '🟡 Reconnecting...';
2254
- statusElement.style.background = '#fff3cd';
2255
- statusElement.style.color = '#856404';
2256
- statusElement.style.border = '1px solid #ffeaa7';
2257
- break;
2258
- case 'error':
2259
- case 'failed':
2260
- statusElement.textContent = '🔴 Offline';
2261
- statusElement.style.background = '#f8d7da';
2262
- statusElement.style.color = '#721c24';
2263
- statusElement.style.border = '1px solid #f5c6cb';
2264
- break;
2265
- }
2266
- }
2267
-
2268
- startCleanupTimer() {
2269
- // Start periodic cleanup every 60 seconds
2270
- if (!this.cleanupTimer) {
2271
- this.cleanupTimer = setInterval(() => {
2272
- this.cleanupStaleActivityBadges();
2273
- }, 60000); // 60 seconds
2274
- }
2275
- }
2276
-
2277
- stopCleanupTimer() {
2278
- if (this.cleanupTimer) {
2279
- clearInterval(this.cleanupTimer);
2280
- this.cleanupTimer = null;
2281
- }
2282
- }
2283
-
2284
- startWatching() {
2285
- this.shouldReconnect = true; // User wants to maintain connection
2286
- if (!this.isWatching) {
2287
- this.connect();
2288
- }
2289
- this.startCleanupTimer(); // Start periodic cleanup
2290
- this.showWatchControls();
2291
- }
2292
-
2293
- stopWatching() {
2294
- this.stopCleanupTimer(); // Stop periodic cleanup
2295
- this.disconnect();
2296
- this.hideWatchControls();
2297
- }
2298
-
2299
- showWatchControls() {
2300
- // Add watch control buttons to the UI
2301
- let controlsContainer = document.getElementById('watch-controls');
2302
- if (!controlsContainer) {
2303
- controlsContainer = document.createElement('div');
2304
- controlsContainer.id = 'watch-controls';
2305
- controlsContainer.style.cssText = `
2306
- position: fixed;
2307
- bottom: 20px;
2308
- right: 20px;
2309
- display: flex;
2310
- gap: 10px;
2311
- z-index: 1000;
2312
- `;
2313
-
2314
- const stopBtn = document.createElement('button');
2315
- stopBtn.textContent = '⏹️ Stop Watching';
2316
- stopBtn.style.cssText = `
2317
- background: #dc3545;
2318
- color: white;
2319
- border: none;
2320
- padding: 10px 15px;
2321
- border-radius: 6px;
2322
- cursor: pointer;
2323
- font-size: 0.9em;
2324
- transition: background-color 0.2s ease;
2325
- `;
2326
- stopBtn.onmouseover = () => (stopBtn.style.background = '#c82333');
2327
- stopBtn.onmouseout = () => (stopBtn.style.background = '#dc3545');
2328
- stopBtn.onclick = () => this.stopWatching();
2329
-
2330
- controlsContainer.appendChild(stopBtn);
2331
- document.body.appendChild(controlsContainer);
2332
- }
2333
- }
2334
-
2335
- hideWatchControls() {
2336
- const controlsContainer = document.getElementById('watch-controls');
2337
- if (controlsContainer) {
2338
- controlsContainer.remove();
2339
- }
2340
- }
2341
- }
2342
-
2343
- // Global watch manager instance
2344
- let watchManager = null;
2345
-
2346
- // Function to start watching
2347
- function startWatching() {
2348
- if (!watchManager) {
2349
- watchManager = new WatchManager();
2350
- }
2351
- watchManager.startWatching();
2352
- }
2353
-
2354
- // Function to stop watching
2355
- function stopWatching() {
2356
- if (watchManager) {
2357
- watchManager.stopWatching();
2358
- }
2359
- }
2360
-
2361
- // Function to clean up watch manager and prevent connection leaks
2362
- function cleanupWatchManager() {
2363
- if (watchManager) {
2364
- // Clean up stale activity badges before disconnecting
2365
- watchManager.cleanupStaleActivityBadges();
2366
- watchManager.disconnect();
2367
- watchManager = null;
2368
- }
2369
- }
2370
-
2371
- let currentProject = null;
2372
- let currentSession = null;
2373
-
2374
- // URL routing functions
2375
- function updateURL(view, project = null, session = null) {
2376
- let url = '/';
2377
- if (view === 'sessions' && project) {
2378
- url = `/project/${encodeURIComponent(project)}`;
2379
- } else if (view === 'conversation' && project && session) {
2380
- url = `/project/${encodeURIComponent(project)}/session/${encodeURIComponent(session)}`;
2381
- }
2382
- history.pushState({ view, project, session }, '', url);
2383
- }
2384
-
2385
- function parseURL() {
2386
- const path = window.location.pathname;
2387
- const matches = path.match(
2388
- /^\/project\/([^\/]+)(?:\/session\/([^\/]+))?$/,
2389
- );
2390
-
2391
- if (matches) {
2392
- const project = decodeURIComponent(matches[1]);
2393
- const session = matches[2] ? decodeURIComponent(matches[2]) : null;
2394
-
2395
- if (session) {
2396
- loadSession(project, session, false);
2397
- } else {
2398
- loadSessions(project, false);
2399
- }
2400
- } else {
2401
- showProjects(false);
2402
- }
2403
- }
2404
-
2405
- // Clean up WebSocket connections on page unload to prevent leaks
2406
- window.addEventListener('beforeunload', function (event) {
2407
- cleanupWatchManager();
2408
- });
2409
-
2410
- // Handle browser back/forward buttons
2411
- window.addEventListener('popstate', function (event) {
2412
- if (event.state) {
2413
- const { view, project, session } = event.state;
2414
- if (view === 'conversation' && project && session) {
2415
- loadSession(project, session, false);
2416
- } else if (view === 'sessions' && project) {
2417
- loadSessions(project, false);
2418
- } else {
2419
- showProjects(false);
2420
- }
2421
- } else {
2422
- showProjects(false);
2423
- }
2424
- });
2425
-
2426
- async function loadProjects() {
2427
- document.getElementById('projects-loading').style.display = 'block';
2428
- document.getElementById('projects-list').innerHTML = '';
2429
-
2430
- try {
2431
- const response = await fetch('/api/projects');
2432
- const projects = await response.json();
2433
-
2434
- const projectsList = document.getElementById('projects-list');
2435
-
2436
- if (projects.length === 0) {
2437
- projectsList.innerHTML = '<p class="loading">No projects found</p>';
2438
- return;
2439
- }
2440
-
2441
- projects.forEach((project) => {
2442
- const projectCard = document.createElement('div');
2443
- projectCard.className = 'project-card';
2444
- projectCard.onclick = () => loadSessions(project.name, true);
2445
-
2446
- const lastActivity = project.latestActivity
2447
- ? new Date(project.latestActivity).toLocaleDateString()
2448
- : 'No activity';
2449
-
2450
- projectCard.innerHTML = `
2451
- <div class="project-name">${project.name}</div>
2452
- <div class="project-stats">
2453
- <span>${project.sessionCount} sessions</span>
2454
- </div>
2455
- <div class="project-activity">Last activity: ${lastActivity}</div>
2456
- `;
2457
-
2458
- projectsList.appendChild(projectCard);
2459
- });
2460
- } catch (error) {
2461
- document.getElementById('projects-list').innerHTML =
2462
- `<div class="error">Failed to load projects: ${error.message}</div>`;
2463
- } finally {
2464
- document.getElementById('projects-loading').style.display = 'none';
2465
- }
2466
- }
2467
-
2468
- async function loadSessions(projectName, updateUrl = true) {
2469
- cleanupWatchManager(); // Clean up any active WebSocket connections
2470
- currentProject = projectName;
2471
- document.getElementById('project-title').textContent =
2472
- `Sessions - ${projectName}`;
2473
- document.getElementById('projects-view').style.display = 'none';
2474
- document.getElementById('sessions-view').style.display = 'block';
2475
- document.getElementById('sessions-loading').style.display = 'block';
2476
- document.getElementById('sessions-list').innerHTML = '';
2477
-
2478
- if (updateUrl) {
2479
- updateURL('sessions', projectName);
2480
- }
2481
-
2482
- try {
2483
- const response = await fetch(
2484
- `/api/projects/${encodeURIComponent(projectName)}/sessions`,
2485
- );
2486
- const sessions = await response.json();
2487
-
2488
- const sessionsList = document.getElementById('sessions-list');
2489
-
2490
- if (sessions.length === 0) {
2491
- sessionsList.innerHTML = '<p class="loading">No sessions found</p>';
2492
- return;
2493
- }
2494
-
2495
- sessions.forEach((session) => {
2496
- const sessionItem = document.createElement('div');
2497
- sessionItem.className = 'session-item';
2498
- sessionItem.onclick = () =>
2499
- loadSession(projectName, session.id, true);
2500
-
2501
- sessionItem.innerHTML = `
2502
- <div class="session-title">${session.summary}</div>
2503
- <div class="session-meta">
2504
- <span class="timestamp">${new Date(session.timestamp).toLocaleString()}</span>
2505
- <span>${session.messageCount} entries</span>
2506
- </div>
2507
- `;
2508
-
2509
- sessionsList.appendChild(sessionItem);
2510
- });
2511
- } catch (error) {
2512
- document.getElementById('sessions-list').innerHTML =
2513
- `<div class="error">Failed to load sessions: ${error.message}</div>`;
2514
- } finally {
2515
- document.getElementById('sessions-loading').style.display = 'none';
2516
- }
2517
- }
2518
-
2519
- async function loadSession(projectName, sessionId, updateUrl = true) {
2520
- cleanupWatchManager(); // Clean up any active WebSocket connections
2521
- currentSession = sessionId;
2522
- document.getElementById('session-title').textContent =
2523
- `Conversation - ${sessionId}`;
2524
- document.getElementById('sessions-view').style.display = 'none';
2525
- document.getElementById('log-view').style.display = 'block';
2526
- document.getElementById('log-loading').style.display = 'block';
2527
- document.getElementById('log-entries').innerHTML = '';
2528
-
2529
- if (updateUrl) {
2530
- updateURL('conversation', projectName, sessionId);
2531
- }
2532
-
2533
- try {
2534
- const response = await fetch(
2535
- `/api/projects/${encodeURIComponent(projectName)}/sessions/${encodeURIComponent(sessionId)}`,
2536
- );
2537
- const entries = await response.json();
2538
-
2539
- const logEntries = document.getElementById('log-entries');
2540
-
2541
- if (entries.length === 0) {
2542
- logEntries.innerHTML =
2543
- '<p class="loading">No log entries found</p>';
2544
- return;
2545
- }
2546
-
2547
- // First pass: collect tool calls and their results for proper grouping
2548
- const toolCallMap = new Map(); // tool_use_id -> {call, result}
2549
- const processedEntries = [];
2550
-
2551
- entries.forEach((entry) => {
2552
- if (
2553
- entry.type === 'user' &&
2554
- entry.message &&
2555
- entry.message.content
2556
- ) {
2557
- if (Array.isArray(entry.message.content)) {
2558
- // Check if this user message contains only tool results
2559
- const toolResults = entry.message.content.filter(
2560
- (c) => c.type === 'tool_result',
2561
- );
2562
- const otherContent = entry.message.content.filter(
2563
- (c) => c.type !== 'tool_result',
2564
- );
2565
-
2566
- // Store tool results in the map
2567
- toolResults.forEach((result) => {
2568
- if (result.tool_use_id) {
2569
- if (!toolCallMap.has(result.tool_use_id)) {
2570
- toolCallMap.set(result.tool_use_id, {
2571
- call: null,
2572
- result: null,
2573
- });
2574
- }
2575
- toolCallMap.get(result.tool_use_id).result = result;
2576
- }
2577
- });
2578
-
2579
- // Only include user message if it has non-tool-result content
2580
- if (otherContent.length > 0) {
2581
- const modifiedEntry = { ...entry };
2582
- modifiedEntry.message = { ...entry.message };
2583
- modifiedEntry.message.content = otherContent;
2584
- processedEntries.push(modifiedEntry);
2585
- }
2586
- } else {
2587
- // Regular user message
2588
- processedEntries.push(entry);
2589
- }
2590
- } else if (
2591
- entry.type === 'assistant' &&
2592
- entry.message &&
2593
- entry.message.content
2594
- ) {
2595
- // Store tool calls in the map
2596
- if (Array.isArray(entry.message.content)) {
2597
- entry.message.content.forEach((c) => {
2598
- if (c.type === 'tool_use') {
2599
- if (!toolCallMap.has(c.id)) {
2600
- toolCallMap.set(c.id, { call: null, result: null });
2601
- }
2602
- toolCallMap.get(c.id).call = c;
2603
- }
2604
- });
2605
- }
2606
- processedEntries.push(entry);
2607
- } else {
2608
- processedEntries.push(entry);
2609
- }
2610
- });
2611
-
2612
- // Second pass: render entries with proper tool call/result grouping
2613
- processedEntries.forEach((entry) => {
2614
- if (entry.type === 'summary') {
2615
- const summaryDiv = document.createElement('div');
2616
- summaryDiv.className = 'session-summary';
2617
- summaryDiv.innerHTML = `
2618
- <h3>📋 Session Summary</h3>
2619
- <p>${entry.summary || 'No summary available'}</p>
2620
- `;
2621
- logEntries.appendChild(summaryDiv);
2622
- } else if (entry.type === 'user') {
2623
- const messageDiv = document.createElement('div');
2624
- messageDiv.className = 'message user';
2625
-
2626
- let content = '';
2627
- if (entry.message && entry.message.content) {
2628
- if (typeof entry.message.content === 'string') {
2629
- content = entry.message.content;
2630
- } else if (Array.isArray(entry.message.content)) {
2631
- // Handle mixed content including images
2632
- const contentElements = [];
2633
- entry.message.content.forEach((c) => {
2634
- if (typeof c === 'string') {
2635
- contentElements.push({ type: 'text', content: c });
2636
- } else if (c.type === 'text') {
2637
- contentElements.push({ type: 'text', content: c.text });
2638
- } else if (c.type === 'image' && c.source) {
2639
- contentElements.push({ type: 'image', content: c });
2640
- } else if (c.type === 'tool_result') {
2641
- contentElements.push({
2642
- type: 'text',
2643
- content: `Tool Result:\n${JSON.stringify(c.content, null, 2)}`,
2644
- });
2645
- } else {
2646
- contentElements.push({
2647
- type: 'text',
2648
- content: JSON.stringify(c, null, 2),
2649
- });
2650
- }
2651
- });
2652
-
2653
- // Store elements for later rendering
2654
- entry._parsedContent = contentElements;
2655
- content = contentElements
2656
- .filter((e) => e.type === 'text')
2657
- .map((e) => e.content)
2658
- .join('\n');
2659
- }
2660
- }
2661
-
2662
- const avatar = document.createElement('div');
2663
- avatar.className = 'avatar user';
2664
- avatar.textContent = 'U';
2665
-
2666
- const messageContent = document.createElement('div');
2667
- messageContent.className = 'message-content';
2668
-
2669
- const messageText = document.createElement('div');
2670
- messageText.className = 'message-text';
2671
-
2672
- // Render mixed content including images
2673
- if (entry._parsedContent) {
2674
- entry._parsedContent.forEach((element) => {
2675
- if (element.type === 'text' && element.content.trim()) {
2676
- const textDiv = document.createElement('div');
2677
- textDiv.style.marginBottom = '8px';
2678
- textDiv.textContent = element.content;
2679
- messageText.appendChild(textDiv);
2680
- } else if (element.type === 'image') {
2681
- const imageContainer = document.createElement('div');
2682
- imageContainer.style.marginBottom = '12px';
2683
- imageContainer.style.padding = '8px';
2684
- imageContainer.style.border = '1px solid #ddd';
2685
- imageContainer.style.borderRadius = '6px';
2686
- imageContainer.style.background = '#f9f9f9';
2687
-
2688
- const imageLabel = document.createElement('div');
2689
- imageLabel.style.fontSize = '0.85em';
2690
- imageLabel.style.color = '#666';
2691
- imageLabel.style.marginBottom = '8px';
2692
- imageLabel.textContent = '🖼️ Image';
2693
-
2694
- const img = document.createElement('img');
2695
- img.style.maxWidth = '100%';
2696
- img.style.height = 'auto';
2697
- img.style.borderRadius = '4px';
2698
- img.style.cursor = 'pointer';
2699
-
2700
- if (element.content.source && element.content.source.data) {
2701
- const mediaType =
2702
- element.content.source.media_type || 'image/png';
2703
- img.src = `data:${mediaType};base64,${element.content.source.data}`;
2704
-
2705
- // Click to open in new tab
2706
- img.onclick = () => {
2707
- const newWindow = window.open();
2708
- newWindow.document.write(
2709
- `<img src="${img.src}" style="max-width: 100%; height: auto;" />`,
2710
- );
2711
- };
2712
- }
2713
-
2714
- imageContainer.appendChild(imageLabel);
2715
- imageContainer.appendChild(img);
2716
- messageText.appendChild(imageContainer);
2717
- }
2718
- });
2719
- } else {
2720
- messageText.textContent = content;
2721
- }
2722
-
2723
- const messageMeta = document.createElement('div');
2724
- messageMeta.className = 'message-meta';
2725
- messageMeta.textContent = entry.timestamp
2726
- ? new Date(entry.timestamp).toLocaleString()
2727
- : '';
2728
-
2729
- messageContent.appendChild(messageText);
2730
- messageContent.appendChild(messageMeta);
2731
- messageDiv.appendChild(avatar);
2732
- messageDiv.appendChild(messageContent);
2733
-
2734
- logEntries.appendChild(messageDiv);
2735
- } else if (entry.type === 'assistant') {
2736
- const messageDiv = document.createElement('div');
2737
- messageDiv.className = 'message assistant';
2738
-
2739
- let content = '';
2740
- let toolCalls = [];
2741
-
2742
- if (entry.message && entry.message.content) {
2743
- if (Array.isArray(entry.message.content)) {
2744
- entry.message.content.forEach((c) => {
2745
- if (c.type === 'text') {
2746
- content += c.text;
2747
- } else if (c.type === 'tool_use') {
2748
- toolCalls.push(c);
2749
- }
2750
- });
2751
- } else if (typeof entry.message.content === 'string') {
2752
- content = entry.message.content;
2753
- }
2754
- }
2755
-
2756
- // Add the assistant message
2757
- if (content.trim()) {
2758
- const avatar = document.createElement('div');
2759
- avatar.className = 'avatar assistant';
2760
- avatar.textContent = 'AI';
2761
-
2762
- const messageContent = document.createElement('div');
2763
- messageContent.className = 'message-content';
2764
-
2765
- const messageText = document.createElement('div');
2766
- messageText.className = 'message-text';
2767
- messageText.textContent = content;
2768
-
2769
- const messageMeta = document.createElement('div');
2770
- messageMeta.className = 'message-meta';
2771
- messageMeta.textContent = entry.timestamp
2772
- ? new Date(entry.timestamp).toLocaleString()
2773
- : '';
2774
-
2775
- messageContent.appendChild(messageText);
2776
- messageContent.appendChild(messageMeta);
2777
- messageDiv.appendChild(avatar);
2778
- messageDiv.appendChild(messageContent);
2779
-
2780
- logEntries.appendChild(messageDiv);
2781
- }
2782
-
2783
- // Add tool calls as separate items using tool handlers, with their results
2784
- toolCalls.forEach((toolCall) => {
2785
- const handler = getToolHandler(toolCall.name);
2786
- const toolDiv = handler.renderToolCall(toolCall);
2787
- logEntries.appendChild(toolDiv);
2788
-
2789
- // Add corresponding tool result if it exists
2790
- if (
2791
- toolCallMap.has(toolCall.id) &&
2792
- toolCallMap.get(toolCall.id).result
2793
- ) {
2794
- const toolResult = toolCallMap.get(toolCall.id).result;
2795
- const resultContent =
2796
- toolResult.content ||
2797
- toolResult.text ||
2798
- JSON.stringify(toolResult, null, 2);
2799
- const toolResultDiv = handler.renderToolResult(
2800
- resultContent,
2801
- toolCall,
2802
- );
2803
- logEntries.appendChild(toolResultDiv);
2804
- }
2805
- });
2806
- }
2807
- });
2808
- } catch (error) {
2809
- document.getElementById('log-entries').innerHTML =
2810
- `<div class="error">Failed to load session logs: ${error.message}</div>`;
2811
- } finally {
2812
- document.getElementById('log-loading').style.display = 'none';
2813
- }
2814
- }
2815
-
2816
- function showProjects(updateUrl = true) {
2817
- cleanupWatchManager(); // Clean up any active WebSocket connections
2818
- document.getElementById('sessions-view').style.display = 'none';
2819
- document.getElementById('log-view').style.display = 'none';
2820
- document.getElementById('projects-view').style.display = 'block';
2821
- currentProject = null;
2822
- currentSession = null;
2823
-
2824
- if (updateUrl) {
2825
- updateURL('projects');
2826
- }
2827
- }
2828
-
2829
- function showSessions(updateUrl = true) {
2830
- document.getElementById('log-view').style.display = 'none';
2831
- document.getElementById('sessions-view').style.display = 'block';
2832
- currentSession = null;
2833
-
2834
- if (updateUrl && currentProject) {
2835
- updateURL('sessions', currentProject);
2836
- }
2837
- }
2838
-
2839
- // Initialize on page load
2840
- loadProjects().then(() => {
2841
- // Parse URL and show appropriate view
2842
- parseURL();
2843
- });
2844
- </script>
2845
- </body>
2846
- </html>