claude-code-templates 1.16.1 → 1.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/README.md +7 -7
  2. package/bin/create-claude-config.js +17 -8
  3. package/package.json +2 -3
  4. package/src/analytics/core/AgentAnalyzer.js +17 -3
  5. package/src/analytics/core/ProcessDetector.js +23 -7
  6. package/src/analytics/core/StateCalculator.js +102 -33
  7. package/src/analytics/data/DataCache.js +7 -7
  8. package/src/analytics-web/chats_mobile.html +2590 -0
  9. package/src/analytics-web/components/App.js +10 -10
  10. package/src/analytics-web/components/SessionTimer.js +1 -1
  11. package/src/analytics-web/components/Sidebar.js +5 -14
  12. package/src/analytics-web/index.html +932 -78
  13. package/src/analytics.js +263 -5
  14. package/src/chats-mobile.js +682 -0
  15. package/src/claude-api-proxy.js +460 -0
  16. package/src/file-operations.js +239 -36
  17. package/src/health-check.js +310 -0
  18. package/src/index.js +1256 -36
  19. package/src/tracking-service.js +31 -34
  20. package/components/agents/api-security-audit.md +0 -92
  21. package/components/agents/database-optimization.md +0 -94
  22. package/components/agents/react-performance-optimization.md +0 -64
  23. package/components/commands/check-file.md +0 -53
  24. package/components/commands/generate-tests.md +0 -68
  25. package/components/mcps/deepgraph-nextjs.json +0 -12
  26. package/components/mcps/deepgraph-react.json +0 -12
  27. package/components/mcps/deepgraph-typescript.json +0 -12
  28. package/components/mcps/deepgraph-vue.json +0 -12
  29. package/components/mcps/filesystem-access.json +0 -12
  30. package/components/mcps/github-integration.json +0 -11
  31. package/components/mcps/memory-integration.json +0 -8
  32. package/components/mcps/mysql-integration.json +0 -11
  33. package/components/mcps/postgresql-integration.json +0 -11
  34. package/components/mcps/web-fetch.json +0 -8
  35. package/src/analytics-web/components/AgentsPage.js +0 -4761
  36. package/templates/common/.claude/commands/git-workflow.md +0 -239
  37. package/templates/common/.claude/commands/project-setup.md +0 -316
  38. package/templates/common/.mcp.json +0 -41
  39. package/templates/common/CLAUDE.md +0 -109
  40. package/templates/common/README.md +0 -96
  41. package/templates/go/.mcp.json +0 -78
  42. package/templates/go/README.md +0 -25
  43. package/templates/javascript-typescript/.claude/commands/api-endpoint.md +0 -51
  44. package/templates/javascript-typescript/.claude/commands/debug.md +0 -52
  45. package/templates/javascript-typescript/.claude/commands/lint.md +0 -48
  46. package/templates/javascript-typescript/.claude/commands/npm-scripts.md +0 -48
  47. package/templates/javascript-typescript/.claude/commands/refactor.md +0 -55
  48. package/templates/javascript-typescript/.claude/commands/test.md +0 -61
  49. package/templates/javascript-typescript/.claude/commands/typescript-migrate.md +0 -51
  50. package/templates/javascript-typescript/.claude/settings.json +0 -142
  51. package/templates/javascript-typescript/.mcp.json +0 -80
  52. package/templates/javascript-typescript/CLAUDE.md +0 -185
  53. package/templates/javascript-typescript/README.md +0 -259
  54. package/templates/javascript-typescript/examples/angular-app/.claude/commands/components.md +0 -63
  55. package/templates/javascript-typescript/examples/angular-app/.claude/commands/services.md +0 -62
  56. package/templates/javascript-typescript/examples/node-api/.claude/commands/api-endpoint.md +0 -46
  57. package/templates/javascript-typescript/examples/node-api/.claude/commands/database.md +0 -56
  58. package/templates/javascript-typescript/examples/node-api/.claude/commands/middleware.md +0 -61
  59. package/templates/javascript-typescript/examples/node-api/.claude/commands/route.md +0 -57
  60. package/templates/javascript-typescript/examples/node-api/CLAUDE.md +0 -102
  61. package/templates/javascript-typescript/examples/react-app/.claude/commands/component.md +0 -29
  62. package/templates/javascript-typescript/examples/react-app/.claude/commands/hooks.md +0 -44
  63. package/templates/javascript-typescript/examples/react-app/.claude/commands/state-management.md +0 -45
  64. package/templates/javascript-typescript/examples/react-app/CLAUDE.md +0 -81
  65. package/templates/javascript-typescript/examples/react-app/agents/react-performance-optimization.md +0 -530
  66. package/templates/javascript-typescript/examples/react-app/agents/react-state-management.md +0 -295
  67. package/templates/javascript-typescript/examples/vue-app/.claude/commands/components.md +0 -46
  68. package/templates/javascript-typescript/examples/vue-app/.claude/commands/composables.md +0 -51
  69. package/templates/python/.claude/commands/lint.md +0 -111
  70. package/templates/python/.claude/commands/test.md +0 -73
  71. package/templates/python/.claude/settings.json +0 -153
  72. package/templates/python/.mcp.json +0 -78
  73. package/templates/python/CLAUDE.md +0 -276
  74. package/templates/python/examples/django-app/.claude/commands/admin.md +0 -264
  75. package/templates/python/examples/django-app/.claude/commands/django-model.md +0 -124
  76. package/templates/python/examples/django-app/.claude/commands/views.md +0 -222
  77. package/templates/python/examples/django-app/CLAUDE.md +0 -313
  78. package/templates/python/examples/django-app/agents/django-api-security.md +0 -642
  79. package/templates/python/examples/django-app/agents/django-database-optimization.md +0 -752
  80. package/templates/python/examples/fastapi-app/.claude/commands/api-endpoints.md +0 -513
  81. package/templates/python/examples/fastapi-app/.claude/commands/auth.md +0 -775
  82. package/templates/python/examples/fastapi-app/.claude/commands/database.md +0 -657
  83. package/templates/python/examples/fastapi-app/.claude/commands/deployment.md +0 -160
  84. package/templates/python/examples/fastapi-app/.claude/commands/testing.md +0 -927
  85. package/templates/python/examples/fastapi-app/CLAUDE.md +0 -229
  86. package/templates/python/examples/flask-app/.claude/commands/app-factory.md +0 -384
  87. package/templates/python/examples/flask-app/.claude/commands/blueprint.md +0 -243
  88. package/templates/python/examples/flask-app/.claude/commands/database.md +0 -410
  89. package/templates/python/examples/flask-app/.claude/commands/deployment.md +0 -620
  90. package/templates/python/examples/flask-app/.claude/commands/flask-route.md +0 -217
  91. package/templates/python/examples/flask-app/.claude/commands/testing.md +0 -559
  92. package/templates/python/examples/flask-app/CLAUDE.md +0 -391
  93. package/templates/ruby/.claude/commands/model.md +0 -360
  94. package/templates/ruby/.claude/commands/test.md +0 -480
  95. package/templates/ruby/.claude/settings.json +0 -146
  96. package/templates/ruby/.mcp.json +0 -83
  97. package/templates/ruby/CLAUDE.md +0 -284
  98. package/templates/ruby/examples/rails-app/.claude/commands/authentication.md +0 -490
  99. package/templates/ruby/examples/rails-app/CLAUDE.md +0 -376
  100. package/templates/rust/.mcp.json +0 -78
  101. package/templates/rust/README.md +0 -26
@@ -0,0 +1,2590 @@
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>Claude Code Analytics - Chats</title>
7
+ <style>
8
+ * {
9
+ box-sizing: border-box;
10
+ margin: 0;
11
+ padding: 0;
12
+ }
13
+
14
+ :root {
15
+ /* Terminal theme - black and orange */
16
+ --bg-primary: #0d1117;
17
+ --bg-secondary: #161b22;
18
+ --bg-tertiary: #21262d;
19
+ --text-primary: #ffffff;
20
+ --text-secondary: #8b949e;
21
+ --text-accent: #ff6b35;
22
+ --text-success: #3fb950;
23
+ --text-warning: #d29922;
24
+ --text-error: #da3633;
25
+ --border-primary: #30363d;
26
+ --border-secondary: #40464e;
27
+ --shadow: rgba(0, 0, 0, 0.5);
28
+
29
+ /* Terminal specific colors */
30
+ --terminal-orange: #ff6b35;
31
+ --terminal-orange-hover: #ff8659;
32
+ --terminal-dark: #0d1117;
33
+ --terminal-gray: #21262d;
34
+
35
+ /* Message bubble colors */
36
+ --message-received: #cc5500; /* Warm orange for received messages */
37
+ --message-sent: #1e7e34; /* Darker green for sent messages */
38
+ }
39
+
40
+ body {
41
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
42
+ background: var(--bg-primary);
43
+ color: var(--text-primary);
44
+ height: 100vh;
45
+ overflow: hidden;
46
+ }
47
+
48
+ /* Mobile-first chat app layout */
49
+ .chat-app {
50
+ display: flex;
51
+ height: 100vh;
52
+ position: relative;
53
+ }
54
+
55
+ /* Mobile sidebar (conversations list) */
56
+ .chat-sidebar {
57
+ width: 100%;
58
+ height: 100%;
59
+ background: var(--bg-primary);
60
+ display: flex;
61
+ flex-direction: column;
62
+ position: relative;
63
+ z-index: 100;
64
+ }
65
+
66
+ /* Chat sidebar header */
67
+ .chat-header {
68
+ background: var(--bg-secondary);
69
+ padding: 16px 20px;
70
+ border-bottom: 1px solid var(--border-primary);
71
+ display: flex;
72
+ align-items: center;
73
+ justify-content: between;
74
+ min-height: 64px;
75
+ }
76
+
77
+ .chat-header-content {
78
+ display: flex;
79
+ align-items: center;
80
+ justify-content: space-between;
81
+ width: 100%;
82
+ }
83
+
84
+ .chat-title {
85
+ font-size: 1.2rem;
86
+ font-weight: 700;
87
+ color: var(--text-primary);
88
+ }
89
+
90
+ .header-actions {
91
+ display: flex;
92
+ gap: 12px;
93
+ align-items: center;
94
+ }
95
+
96
+ .header-btn {
97
+ background: rgba(255, 107, 53, 0.1);
98
+ border: 1px solid rgba(255, 107, 53, 0.2);
99
+ color: var(--terminal-orange);
100
+ font-size: 0.8rem;
101
+ font-weight: 600;
102
+ cursor: pointer;
103
+ padding: 6px 12px;
104
+ border-radius: 8px;
105
+ transition: all 0.2s ease;
106
+ text-transform: uppercase;
107
+ letter-spacing: 0.5px;
108
+ }
109
+
110
+ .header-btn:hover {
111
+ background: rgba(255, 107, 53, 0.15);
112
+ border-color: rgba(255, 107, 53, 0.3);
113
+ color: var(--terminal-orange);
114
+ transform: translateY(-1px);
115
+ box-shadow: 0 2px 4px rgba(255, 107, 53, 0.2);
116
+ }
117
+
118
+ .header-btn:active {
119
+ transform: translateY(0);
120
+ box-shadow: 0 1px 2px rgba(255, 107, 53, 0.2);
121
+ }
122
+
123
+ .back-btn {
124
+ background: rgba(255, 107, 53, 0.15) !important;
125
+ border: 1px solid rgba(255, 107, 53, 0.3) !important;
126
+ color: var(--terminal-orange) !important;
127
+ font-size: 1.2rem !important;
128
+ font-weight: 700 !important;
129
+ padding: 8px 12px !important;
130
+ min-width: 40px !important;
131
+ text-align: center !important;
132
+ }
133
+
134
+ .back-btn:hover {
135
+ background: rgba(255, 107, 53, 0.2) !important;
136
+ border-color: rgba(255, 107, 53, 0.4) !important;
137
+ }
138
+
139
+ /* Search bar */
140
+ .chat-search {
141
+ padding: 16px 20px;
142
+ background: var(--bg-primary);
143
+ border-bottom: 1px solid var(--border-primary);
144
+ }
145
+
146
+ .search-input {
147
+ width: 100%;
148
+ padding: 12px 16px;
149
+ background: var(--bg-tertiary);
150
+ border: 1px solid var(--border-primary);
151
+ border-radius: 24px;
152
+ color: var(--text-primary);
153
+ font-size: 1rem;
154
+ outline: none;
155
+ transition: border-color 0.2s ease;
156
+ }
157
+
158
+ .search-input:focus {
159
+ border-color: var(--terminal-orange);
160
+ }
161
+
162
+ .search-input::placeholder {
163
+ color: var(--text-secondary);
164
+ }
165
+
166
+ /* Conversations list */
167
+ .conversations-list {
168
+ flex: 1;
169
+ overflow-y: auto;
170
+ background: var(--bg-primary);
171
+ }
172
+
173
+ .conversation-item {
174
+ display: flex;
175
+ align-items: center;
176
+ padding: 16px 20px;
177
+ border-bottom: 1px solid var(--border-primary);
178
+ cursor: pointer;
179
+ transition: background-color 0.2s ease;
180
+ position: relative;
181
+ }
182
+
183
+ .conversation-item:hover {
184
+ background: var(--bg-secondary);
185
+ }
186
+
187
+ .conversation-item.active {
188
+ background: var(--bg-secondary);
189
+ border-left: 4px solid var(--terminal-orange);
190
+ }
191
+
192
+ .conversation-avatar {
193
+ width: 48px;
194
+ height: 48px;
195
+ border-radius: 50%;
196
+ background: var(--terminal-orange);
197
+ display: flex;
198
+ align-items: center;
199
+ justify-content: center;
200
+ font-size: 1.2rem;
201
+ margin-right: 16px;
202
+ flex-shrink: 0;
203
+ color: var(--bg-primary);
204
+ font-weight: bold;
205
+ }
206
+
207
+ .conversation-content {
208
+ flex: 1;
209
+ min-width: 0;
210
+ }
211
+
212
+ .conversation-header {
213
+ display: flex;
214
+ justify-content: space-between;
215
+ align-items: flex-start;
216
+ margin-bottom: 4px;
217
+ }
218
+
219
+ .conversation-name {
220
+ font-weight: 600;
221
+ color: var(--text-primary);
222
+ font-size: 1rem;
223
+ text-overflow: ellipsis;
224
+ overflow: hidden;
225
+ white-space: nowrap;
226
+ }
227
+
228
+ .conversation-time {
229
+ color: var(--text-secondary);
230
+ font-size: 0.8rem;
231
+ flex-shrink: 0;
232
+ margin-left: 8px;
233
+ }
234
+
235
+ .conversation-preview {
236
+ color: var(--text-secondary);
237
+ font-size: 0.9rem;
238
+ text-overflow: ellipsis;
239
+ overflow: hidden;
240
+ white-space: nowrap;
241
+ line-height: 1.3;
242
+ }
243
+
244
+ .conversation-meta {
245
+ display: flex;
246
+ justify-content: space-between;
247
+ align-items: center;
248
+ margin-top: 4px;
249
+ }
250
+
251
+ .conversation-state {
252
+ font-size: 0.7rem;
253
+ padding: 3px 8px;
254
+ border-radius: 12px;
255
+ text-transform: uppercase;
256
+ font-weight: 600;
257
+ letter-spacing: 0.5px;
258
+ display: inline-flex;
259
+ align-items: center;
260
+ gap: 4px;
261
+ transition: all 0.2s ease;
262
+ }
263
+
264
+ /* Working states - Green (Claude is actively working) */
265
+ .state-working {
266
+ background: rgba(40, 167, 69, 0.15);
267
+ color: #28a745;
268
+ border: 1px solid rgba(40, 167, 69, 0.3);
269
+ animation: pulse-working 2s infinite;
270
+ }
271
+
272
+ @keyframes pulse-working {
273
+ 0%, 100% { opacity: 1; }
274
+ 50% { opacity: 0.7; }
275
+ }
276
+
277
+ /* Active states - Blue/Cyan (Recent activity, ready) */
278
+ .state-active {
279
+ background: rgba(23, 162, 184, 0.15);
280
+ color: #17a2b8;
281
+ border: 1px solid rgba(23, 162, 184, 0.3);
282
+ }
283
+
284
+ /* Waiting/Responding states - Orange (Waiting for input/response) */
285
+ .state-waiting {
286
+ background: rgba(255, 193, 7, 0.15);
287
+ color: #ffc107;
288
+ border: 1px solid rgba(255, 193, 7, 0.3);
289
+ }
290
+
291
+ .state-responding {
292
+ background: rgba(255, 152, 0, 0.15);
293
+ color: #ff9800;
294
+ border: 1px solid rgba(255, 152, 0, 0.3);
295
+ }
296
+
297
+ /* Typing states - Purple (User interaction) */
298
+ .state-typing {
299
+ background: rgba(156, 39, 176, 0.15);
300
+ color: #9c27b0;
301
+ border: 1px solid rgba(156, 39, 176, 0.3);
302
+ }
303
+
304
+ /* Idle states - Gray (Inactive but not old) */
305
+ .state-idle {
306
+ background: rgba(108, 117, 125, 0.15);
307
+ color: #6c757d;
308
+ border: 1px solid rgba(108, 117, 125, 0.3);
309
+ }
310
+
311
+ /* Inactive states - Red/Gray (Old conversations) */
312
+ .state-inactive {
313
+ background: rgba(134, 142, 150, 0.1);
314
+ color: #868e96;
315
+ border: 1px solid rgba(134, 142, 150, 0.2);
316
+ }
317
+
318
+ /* Special states */
319
+ .state-recent {
320
+ background: rgba(0, 123, 255, 0.15);
321
+ color: #007bff;
322
+ border: 1px solid rgba(0, 123, 255, 0.3);
323
+ }
324
+
325
+ .state-finishing {
326
+ background: rgba(40, 167, 69, 0.2);
327
+ color: #28a745;
328
+ border: 1px solid rgba(40, 167, 69, 0.4);
329
+ }
330
+
331
+ .state-empty {
332
+ background: rgba(184, 188, 200, 0.1);
333
+ color: var(--text-secondary);
334
+ border: 1px solid rgba(184, 188, 200, 0.2);
335
+ }
336
+
337
+ /* Hover effects */
338
+ .conversation-state:hover {
339
+ transform: translateY(-1px);
340
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
341
+ }
342
+
343
+ .message-count {
344
+ background: rgba(255, 107, 53, 0.2);
345
+ color: var(--terminal-orange);
346
+ border: 1px solid rgba(255, 107, 53, 0.3);
347
+ border-radius: 10px;
348
+ font-size: 0.7rem;
349
+ font-weight: 600;
350
+ padding: 2px 6px;
351
+ min-width: 18px;
352
+ text-align: center;
353
+ }
354
+
355
+ /* Message styles - matching AgentsPage.js format */
356
+ .messages-list {
357
+ padding: 16px;
358
+ display: flex;
359
+ flex-direction: column;
360
+ gap: 16px;
361
+ }
362
+
363
+ .message {
364
+ display: flex;
365
+ flex-direction: column;
366
+ max-width: 85%;
367
+ }
368
+
369
+ .message-user {
370
+ align-self: flex-end;
371
+ }
372
+
373
+ .message-assistant {
374
+ align-self: flex-start;
375
+ }
376
+
377
+ .message-bubble {
378
+ position: relative;
379
+ }
380
+
381
+ .message-user .message-bubble .message-content {
382
+ background: var(--message-sent);
383
+ color: white;
384
+ padding: 12px 16px;
385
+ border-radius: 18px 18px 4px 18px;
386
+ font-size: 1rem;
387
+ line-height: 1.4;
388
+ word-wrap: break-word;
389
+ }
390
+
391
+ .message-assistant .message-bubble .message-content {
392
+ background: var(--terminal-dark);
393
+ color: var(--text-primary);
394
+ padding: 12px 16px;
395
+ border: 2px solid var(--message-received);
396
+ border-radius: 8px;
397
+ font-size: 1rem;
398
+ line-height: 1.4;
399
+ word-wrap: break-word;
400
+ }
401
+
402
+ .message-meta {
403
+ display: flex;
404
+ align-items: center;
405
+ gap: 8px;
406
+ margin-top: 4px;
407
+ font-size: 0.7rem;
408
+ color: var(--text-secondary);
409
+ }
410
+
411
+ .message-user .message-meta {
412
+ justify-content: flex-end;
413
+ }
414
+
415
+ .message-assistant .message-meta {
416
+ justify-content: flex-start;
417
+ }
418
+
419
+ .message-time {
420
+ font-size: 0.7rem;
421
+ color: var(--text-secondary);
422
+ }
423
+
424
+ .tool-indicator {
425
+ color: var(--terminal-orange);
426
+ padding: 2px 6px;
427
+ border-radius: 10px;
428
+ font-size: 0.6rem;
429
+ font-weight: bold;
430
+ }
431
+
432
+ .token-indicator {
433
+ background: var(--bg-tertiary);
434
+ color: var(--text-secondary);
435
+ padding: 2px 6px;
436
+ border-radius: 8px;
437
+ font-size: 0.6rem;
438
+ }
439
+
440
+ /* Code formatting in messages */
441
+ .message-content pre {
442
+ background: var(--terminal-dark);
443
+ color: #ffab70;
444
+ padding: 12px;
445
+ border-radius: 8px;
446
+ overflow-x: auto;
447
+ margin: 8px 0;
448
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
449
+ font-size: 0.9rem;
450
+ border: 1px solid var(--border-primary);
451
+ }
452
+
453
+ .message-content code {
454
+ background: var(--terminal-gray);
455
+ color: #ffab70;
456
+ padding: 2px 6px;
457
+ border-radius: 4px;
458
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
459
+ font-size: 0.9rem;
460
+ }
461
+
462
+ .message-content pre code {
463
+ background: none;
464
+ padding: 0;
465
+ }
466
+
467
+ .message-content strong {
468
+ font-weight: 600;
469
+ color: #ffab70;
470
+ }
471
+
472
+ .message-content em {
473
+ font-style: italic;
474
+ color: var(--text-secondary);
475
+ }
476
+
477
+ /* Expandable message styles */
478
+ .expandable-message {
479
+ width: 100%;
480
+ }
481
+
482
+ .message-expand-link {
483
+ color: var(--terminal-orange);
484
+ cursor: pointer;
485
+ font-size: 0.9rem;
486
+ margin-top: 8px;
487
+ padding: 4px 0;
488
+ border-top: 1px solid rgba(255, 107, 53, 0.3);
489
+ text-align: center;
490
+ transition: all 0.2s ease;
491
+ }
492
+
493
+ .message-expand-link:hover {
494
+ color: var(--terminal-orange-hover);
495
+ background: rgba(255, 107, 53, 0.1);
496
+ }
497
+
498
+ .message-collapse-link {
499
+ color: var(--text-secondary);
500
+ cursor: pointer;
501
+ font-size: 0.8rem;
502
+ margin-top: 8px;
503
+ padding: 4px 0;
504
+ border-top: 1px solid var(--border-primary);
505
+ text-align: center;
506
+ transition: all 0.2s ease;
507
+ }
508
+
509
+ .message-collapse-link:hover {
510
+ color: var(--text-primary);
511
+ background: rgba(255, 255, 255, 0.05);
512
+ }
513
+
514
+ /* Tool call and result styles */
515
+ .tool-call {
516
+ margin: 8px 0;
517
+ padding: 8px 0;
518
+ background: none;
519
+ }
520
+
521
+ .tool-summary {
522
+ display: flex;
523
+ align-items: center;
524
+ gap: 6px;
525
+ font-size: 0.9rem;
526
+ color: var(--text-primary);
527
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
528
+ cursor: pointer;
529
+ transition: all 0.2s ease;
530
+ word-wrap: break-word;
531
+ word-break: break-all;
532
+ overflow-wrap: break-word;
533
+ max-width: 100%;
534
+ flex-wrap: wrap;
535
+ }
536
+
537
+ .tool-summary:hover {
538
+ background: rgba(255, 107, 53, 0.1);
539
+ padding: 2px 4px;
540
+ border-radius: 4px;
541
+ }
542
+
543
+ .tool-bullet {
544
+ color: var(--terminal-orange);
545
+ font-weight: bold;
546
+ }
547
+
548
+ .tool-name {
549
+ font-weight: bold;
550
+ color: var(--terminal-orange);
551
+ word-wrap: break-word;
552
+ word-break: break-all;
553
+ overflow-wrap: break-word;
554
+ }
555
+
556
+ .tool-summary-text {
557
+ word-wrap: break-word;
558
+ word-break: break-all;
559
+ overflow-wrap: break-word;
560
+ flex-shrink: 1;
561
+ }
562
+
563
+ .tool-expand-note {
564
+ display: flex;
565
+ align-items: center;
566
+ gap: 6px;
567
+ margin-top: 4px;
568
+ font-size: 0.8rem;
569
+ color: var(--text-secondary);
570
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
571
+ cursor: pointer;
572
+ transition: all 0.2s ease;
573
+ }
574
+
575
+ .tool-expand-note:hover {
576
+ color: var(--terminal-orange);
577
+ }
578
+
579
+ .tool-branch {
580
+ color: var(--text-secondary);
581
+ }
582
+
583
+ .tool-note {
584
+ font-style: italic;
585
+ color: var(--text-secondary);
586
+ }
587
+
588
+ .tool-parameters {
589
+ margin-top: 8px;
590
+ border-top: 1px solid var(--border-primary);
591
+ padding-top: 8px;
592
+ }
593
+
594
+ .tool-parameters-header {
595
+ display: flex;
596
+ align-items: center;
597
+ gap: 6px;
598
+ font-size: 0.8rem;
599
+ color: var(--text-secondary);
600
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
601
+ margin-bottom: 8px;
602
+ }
603
+
604
+ .tool-param-title {
605
+ font-weight: bold;
606
+ }
607
+
608
+ .tool-parameters-content {
609
+ background: var(--terminal-dark);
610
+ color: var(--terminal-orange);
611
+ padding: 8px;
612
+ border-radius: 4px;
613
+ font-size: 0.8rem;
614
+ margin: 0;
615
+ border: 1px solid var(--border-primary);
616
+ word-wrap: break-word;
617
+ word-break: break-all;
618
+ overflow-wrap: break-word;
619
+ max-width: 100%;
620
+ overflow-x: auto;
621
+ white-space: pre-wrap;
622
+ }
623
+
624
+ .tool-result {
625
+ background: var(--bg-tertiary);
626
+ border: 1px solid var(--border-primary);
627
+ border-radius: 8px;
628
+ margin: 8px 0;
629
+ overflow: hidden;
630
+ }
631
+
632
+ .tool-result-header {
633
+ background: transparent;
634
+ color: var(--text-primary);
635
+ padding: 8px 12px;
636
+ font-weight: bold;
637
+ font-size: 0.9rem;
638
+ display: flex;
639
+ align-items: center;
640
+ gap: 8px;
641
+ }
642
+
643
+ .tool-result-content {
644
+ padding: 12px;
645
+ }
646
+
647
+ .tool-result-content pre {
648
+ background: var(--terminal-dark);
649
+ color: var(--text-primary);
650
+ padding: 8px;
651
+ border-radius: 4px;
652
+ font-size: 0.8rem;
653
+ margin: 0;
654
+ white-space: pre-wrap;
655
+ }
656
+
657
+ /* Expandable tool result styles */
658
+ .expandable-tool-result {
659
+ width: 100%;
660
+ }
661
+
662
+ .tool-result-expand-link {
663
+ color: var(--terminal-orange);
664
+ cursor: pointer;
665
+ font-size: 0.8rem;
666
+ margin-top: 8px;
667
+ padding: 6px 12px;
668
+ border-top: 1px solid rgba(255, 107, 53, 0.3);
669
+ text-align: center;
670
+ transition: all 0.2s ease;
671
+ background: rgba(255, 107, 53, 0.05);
672
+ border-radius: 0 0 4px 4px;
673
+ }
674
+
675
+ .tool-result-expand-link:hover {
676
+ color: var(--terminal-orange-hover);
677
+ background: rgba(255, 107, 53, 0.1);
678
+ }
679
+
680
+ .tool-result-collapse-link {
681
+ color: var(--text-secondary);
682
+ cursor: pointer;
683
+ font-size: 0.7rem;
684
+ margin-top: 8px;
685
+ padding: 4px 8px;
686
+ border-top: 1px solid var(--border-primary);
687
+ text-align: center;
688
+ transition: all 0.2s ease;
689
+ background: rgba(255, 255, 255, 0.02);
690
+ }
691
+
692
+ .tool-result-collapse-link:hover {
693
+ color: var(--text-primary);
694
+ background: rgba(255, 255, 255, 0.05);
695
+ }
696
+
697
+ /* Loading and error states */
698
+ .messages-loading {
699
+ display: flex;
700
+ flex-direction: column;
701
+ align-items: center;
702
+ padding: 40px 20px;
703
+ color: var(--text-secondary);
704
+ }
705
+
706
+ .messages-loading .loading-spinner {
707
+ margin-bottom: 12px;
708
+ }
709
+
710
+ .no-messages-found {
711
+ text-align: center;
712
+ padding: 40px 20px;
713
+ color: var(--text-secondary);
714
+ }
715
+
716
+ .no-messages-icon {
717
+ font-size: 3rem;
718
+ margin-bottom: 16px;
719
+ }
720
+
721
+ .error-loading-messages {
722
+ text-align: center;
723
+ padding: 40px 20px;
724
+ color: var(--text-error);
725
+ }
726
+
727
+ .error-icon {
728
+ font-size: 3rem;
729
+ margin-bottom: 16px;
730
+ }
731
+
732
+ .retry-messages {
733
+ background: var(--terminal-orange);
734
+ color: white;
735
+ border: none;
736
+ padding: 10px 20px;
737
+ border-radius: 6px;
738
+ cursor: pointer;
739
+ font-weight: bold;
740
+ margin-top: 12px;
741
+ transition: background-color 0.2s ease;
742
+ }
743
+
744
+ .retry-messages:hover {
745
+ background: var(--terminal-orange-hover);
746
+ }
747
+
748
+ /* Chat view (initially hidden) */
749
+ .chat-view {
750
+ width: 100%;
751
+ height: 100%;
752
+ background: var(--bg-primary);
753
+ position: absolute;
754
+ top: 0;
755
+ left: 100%;
756
+ z-index: 200;
757
+ display: flex;
758
+ flex-direction: column;
759
+ transition: left 0.3s ease;
760
+ }
761
+
762
+ .chat-view.active {
763
+ left: 0;
764
+ }
765
+
766
+ /* Chat view header */
767
+ .chat-view-header {
768
+ background: var(--bg-secondary);
769
+ padding: 16px 20px;
770
+ border-bottom: 1px solid var(--border-primary);
771
+ display: flex;
772
+ align-items: center;
773
+ min-height: 64px;
774
+ }
775
+
776
+ .chat-view-back {
777
+ margin-right: 16px;
778
+ }
779
+
780
+ .chat-view-info {
781
+ flex: 1;
782
+ }
783
+
784
+ .chat-view-title {
785
+ font-size: 1rem;
786
+ font-weight: 600;
787
+ color: var(--text-primary);
788
+ margin: 0;
789
+ }
790
+
791
+ .chat-view-subtitle {
792
+ font-size: 0.8rem;
793
+ color: var(--text-secondary);
794
+ margin: 0;
795
+ }
796
+
797
+ /* Tools toggle switch */
798
+ .tools-toggle {
799
+ margin-left: auto;
800
+ display: flex;
801
+ align-items: center;
802
+ gap: 8px;
803
+ opacity: 0;
804
+ transition: opacity 0.3s ease;
805
+ }
806
+
807
+ .chat-view.conversation-selected .tools-toggle {
808
+ opacity: 1;
809
+ }
810
+
811
+ .tools-toggle-label {
812
+ font-size: 13px;
813
+ color: var(--text-secondary);
814
+ user-select: none;
815
+ cursor: pointer;
816
+ }
817
+
818
+ .toggle-switch {
819
+ position: relative;
820
+ display: inline-block;
821
+ width: 44px;
822
+ height: 24px;
823
+ }
824
+
825
+ .toggle-switch input {
826
+ opacity: 0;
827
+ width: 0;
828
+ height: 0;
829
+ }
830
+
831
+ .toggle-slider {
832
+ position: absolute;
833
+ cursor: pointer;
834
+ top: 0;
835
+ left: 0;
836
+ right: 0;
837
+ bottom: 0;
838
+ background-color: #555;
839
+ transition: 0.3s;
840
+ border-radius: 12px;
841
+ }
842
+
843
+ .toggle-slider:before {
844
+ position: absolute;
845
+ content: "";
846
+ height: 18px;
847
+ width: 18px;
848
+ left: 3px;
849
+ bottom: 3px;
850
+ background-color: white;
851
+ transition: 0.3s;
852
+ border-radius: 50%;
853
+ }
854
+
855
+ .toggle-switch input:checked + .toggle-slider {
856
+ background-color: #cc5500;
857
+ }
858
+
859
+ .toggle-switch input:checked + .toggle-slider:before {
860
+ transform: translateX(20px);
861
+ }
862
+
863
+ .toggle-switch input:hover + .toggle-slider {
864
+ background-color: #666;
865
+ }
866
+
867
+ .toggle-switch input:checked:hover + .toggle-slider {
868
+ background-color: #e66600;
869
+ }
870
+
871
+ /* Hide complete assistant messages with tools when toggle is off */
872
+ .chat-view:not(.show-tools) .message-assistant.has-tools {
873
+ display: none !important;
874
+ }
875
+
876
+ /* Messages area */
877
+ .chat-messages {
878
+ flex: 1;
879
+ overflow-y: auto;
880
+ padding: 20px;
881
+ padding-bottom: 80px; /* Space for status footer */
882
+ background: var(--bg-primary);
883
+ }
884
+
885
+ /* Loading states */
886
+ .loading-spinner {
887
+ border: 2px solid var(--border-primary);
888
+ border-top: 2px solid var(--terminal-orange);
889
+ border-radius: 50%;
890
+ width: 24px;
891
+ height: 24px;
892
+ animation: spin 1s linear infinite;
893
+ margin: 0 auto;
894
+ }
895
+
896
+ @keyframes spin {
897
+ 0% { transform: rotate(0deg); }
898
+ 100% { transform: rotate(360deg); }
899
+ }
900
+
901
+ .no-conversations {
902
+ text-align: center;
903
+ padding: 40px 20px;
904
+ color: var(--text-secondary);
905
+ }
906
+
907
+ /* Status Footer */
908
+ .status-footer {
909
+ background: var(--bg-primary);
910
+ border-top: 1px solid var(--border-primary);
911
+ padding: 12px 20px;
912
+ display: flex;
913
+ align-items: center;
914
+ justify-content: space-between;
915
+ min-height: 50px;
916
+ position: sticky;
917
+ bottom: 0;
918
+ backdrop-filter: blur(10px);
919
+ z-index: 10;
920
+ }
921
+
922
+ .status-indicator {
923
+ display: flex;
924
+ align-items: center;
925
+ gap: 8px;
926
+ }
927
+
928
+ .status-dot {
929
+ width: 10px;
930
+ height: 10px;
931
+ border-radius: 50%;
932
+ background: var(--text-secondary);
933
+ transition: all 0.3s ease;
934
+ }
935
+
936
+ .status-dot.ready {
937
+ background: #28a745;
938
+ box-shadow: 0 0 6px rgba(40, 167, 69, 0.4);
939
+ }
940
+
941
+ .status-dot.working {
942
+ background: var(--terminal-orange);
943
+ box-shadow: 0 0 6px rgba(255, 107, 53, 0.4);
944
+ animation: pulse 2s infinite;
945
+ }
946
+
947
+ .status-dot.waiting {
948
+ background: #ffc107;
949
+ box-shadow: 0 0 6px rgba(255, 193, 7, 0.4);
950
+ }
951
+
952
+ .status-dot.error {
953
+ background: #dc3545;
954
+ box-shadow: 0 0 6px rgba(220, 53, 69, 0.4);
955
+ }
956
+
957
+ .status-dot.idle {
958
+ background: var(--text-secondary);
959
+ }
960
+
961
+ @keyframes pulse {
962
+ 0%, 100% {
963
+ opacity: 1;
964
+ transform: scale(1);
965
+ }
966
+ 50% {
967
+ opacity: 0.7;
968
+ transform: scale(1.1);
969
+ }
970
+ }
971
+
972
+ .status-text {
973
+ color: var(--text-primary);
974
+ font-size: 0.9rem;
975
+ font-weight: 500;
976
+ }
977
+
978
+ .status-details {
979
+ color: var(--text-secondary);
980
+ font-size: 0.8rem;
981
+ text-align: right;
982
+ }
983
+
984
+ /* Hide status footer when no conversation is selected */
985
+ .chat-view:not(.conversation-selected) .status-footer {
986
+ display: none;
987
+ }
988
+
989
+ .no-conversations-icon {
990
+ font-size: 3rem;
991
+ margin-bottom: 16px;
992
+ }
993
+
994
+ /* Responsive adjustments */
995
+ @media (min-width: 769px) {
996
+ .chat-app {
997
+ /* On larger screens, show side-by-side if needed */
998
+ }
999
+ }
1000
+ </style>
1001
+ </head>
1002
+ <body>
1003
+ <div class="chat-app" id="chatApp">
1004
+ <!-- Conversations List (Mobile Sidebar) -->
1005
+ <div class="chat-sidebar" id="chatSidebar">
1006
+ <!-- Header -->
1007
+ <div class="chat-header">
1008
+ <div class="chat-header-content">
1009
+ <h1 class="chat-title">Claude Code Chats</h1>
1010
+ <div class="header-actions">
1011
+ <button class="header-btn" id="refreshBtn" title="Refresh">
1012
+ Refresh
1013
+ </button>
1014
+ </div>
1015
+ </div>
1016
+ </div>
1017
+
1018
+ <!-- Search -->
1019
+ <div class="chat-search">
1020
+ <input
1021
+ type="text"
1022
+ class="search-input"
1023
+ placeholder="Search conversations..."
1024
+ id="searchInput"
1025
+ />
1026
+ </div>
1027
+
1028
+ <!-- Conversations List -->
1029
+ <div class="conversations-list" id="conversationsList">
1030
+ <div class="loading-spinner" style="margin: 40px auto;"></div>
1031
+ </div>
1032
+ </div>
1033
+
1034
+ <!-- Chat View -->
1035
+ <div class="chat-view" id="chatView">
1036
+ <div class="chat-view-header">
1037
+ <button class="header-btn back-btn chat-view-back" id="backToList">
1038
+
1039
+ </button>
1040
+ <div class="chat-view-info">
1041
+ <h2 class="chat-view-title" id="chatViewTitle">Select a conversation</h2>
1042
+ <p class="chat-view-subtitle" id="chatViewSubtitle"></p>
1043
+ </div>
1044
+ <div class="tools-toggle" id="toolsToggle">
1045
+ <span class="tools-toggle-label" onclick="document.getElementById('showToolsSwitch').click()">Show Tools</span>
1046
+ <label class="toggle-switch">
1047
+ <input type="checkbox" id="showToolsSwitch" checked>
1048
+ <span class="toggle-slider"></span>
1049
+ </label>
1050
+ </div>
1051
+ </div>
1052
+ <div class="chat-messages" id="chatMessages">
1053
+ <div class="no-conversations">
1054
+ <div class="no-conversations-icon">💬</div>
1055
+ <h3>No conversation selected</h3>
1056
+ <p>Choose a conversation from the list to view messages</p>
1057
+ </div>
1058
+ </div>
1059
+
1060
+ <!-- Status Footer -->
1061
+ <div class="status-footer" id="statusFooter">
1062
+ <div class="status-indicator">
1063
+ <div class="status-dot" id="statusDot"></div>
1064
+ <span class="status-text" id="statusText">Ready</span>
1065
+ </div>
1066
+ <div class="status-details" id="statusDetails"></div>
1067
+ </div>
1068
+ </div>
1069
+ </div>
1070
+
1071
+ <!-- Import WebSocket and Data Services -->
1072
+ <script src="services/WebSocketService.js"></script>
1073
+ <script src="services/DataService.js"></script>
1074
+ <script src="services/StateService.js"></script>
1075
+
1076
+ <script>
1077
+ class ChatsMobileApp {
1078
+ constructor() {
1079
+ this.conversations = [];
1080
+ this.selectedConversationId = null;
1081
+ this.loadedMessages = new Map(); // Cache messages by conversation ID (stores paginated data)
1082
+
1083
+ // Pagination state for messages
1084
+ this.messagesPagination = {
1085
+ currentPage: 0,
1086
+ limit: 15, // Load 15 messages per page
1087
+ hasMore: true,
1088
+ isLoading: false,
1089
+ conversationId: null
1090
+ };
1091
+
1092
+ // Message scroll listener reference
1093
+ this.messagesScrollListener = null;
1094
+
1095
+ // Tools visibility state
1096
+ this.showTools = true;
1097
+
1098
+ // Auto-scroll state tracking
1099
+ this.isUserScrolling = false;
1100
+ this.autoScrollEnabled = true;
1101
+ this.scrollThreshold = 100; // pixels from bottom to consider "at bottom"
1102
+ this.userScrollTimeout = null;
1103
+
1104
+ // Initialize services for real-time updates
1105
+ this.webSocketService = new WebSocketService();
1106
+ this.stateService = new StateService();
1107
+ this.dataService = new DataService(this.webSocketService);
1108
+
1109
+ this.init();
1110
+ }
1111
+
1112
+ async init() {
1113
+ this.bindEvents();
1114
+ this.setupRealTimeUpdates();
1115
+ this.loadToolsPreference();
1116
+ await this.loadConversations();
1117
+ }
1118
+
1119
+ bindEvents() {
1120
+ // Refresh conversations
1121
+ document.getElementById('refreshBtn').addEventListener('click', () => {
1122
+ this.loadConversations();
1123
+ });
1124
+
1125
+ // Back to conversations list
1126
+ document.getElementById('backToList').addEventListener('click', () => {
1127
+ this.showConversationsList();
1128
+ });
1129
+
1130
+ // Search functionality
1131
+ const searchInput = document.getElementById('searchInput');
1132
+ searchInput.addEventListener('input', (e) => {
1133
+ this.filterConversations(e.target.value);
1134
+ });
1135
+
1136
+ // Show Tools toggle functionality
1137
+ const showToolsSwitch = document.getElementById('showToolsSwitch');
1138
+ showToolsSwitch.addEventListener('change', (e) => {
1139
+ this.toggleTools(e.target.checked);
1140
+ });
1141
+ }
1142
+
1143
+ setupRealTimeUpdates() {
1144
+ console.log('🔧 Setting up real-time updates...');
1145
+
1146
+ // Subscribe to DataService events for real-time updates
1147
+ this.dataService.addEventListener((type, data) => {
1148
+ console.log('📡 DataService event:', type, data);
1149
+ if (type === 'new_message') {
1150
+ console.log('🔄 WebSocket: New message received', { conversationId: data.conversationId });
1151
+ this.handleNewMessage(data.conversationId, data.message, data.metadata);
1152
+ }
1153
+ });
1154
+
1155
+ // Try to connect WebSocket
1156
+ try {
1157
+ console.log('🌐 Attempting WebSocket connection...');
1158
+ this.webSocketService.connect();
1159
+
1160
+ // Log WebSocket connection status
1161
+ setTimeout(() => {
1162
+ const status = this.webSocketService.getStatus();
1163
+ console.log('🔗 WebSocket status:', status);
1164
+ }, 2000);
1165
+ } catch (error) {
1166
+ console.warn('WebSocket connection failed, using fallback polling:', error);
1167
+ }
1168
+ }
1169
+
1170
+ async loadConversations() {
1171
+ const conversationsList = document.getElementById('conversationsList');
1172
+
1173
+ try {
1174
+ conversationsList.innerHTML = '<div class="loading-spinner" style="margin: 40px auto;"></div>';
1175
+
1176
+ // Fetch conversations and states simultaneously (like AgentsPage.js)
1177
+ const [conversationsResponse, statesResponse] = await Promise.all([
1178
+ fetch('/api/conversations'),
1179
+ fetch('/api/conversation-state') // Use singular like AgentsPage.js
1180
+ ]);
1181
+
1182
+ if (!conversationsResponse.ok) {
1183
+ throw new Error(`HTTP error! status: ${conversationsResponse.status}`);
1184
+ }
1185
+
1186
+ const conversationsData = await conversationsResponse.json();
1187
+ this.conversations = conversationsData.conversations || [];
1188
+
1189
+ // Get conversation states (like AgentsPage.js)
1190
+ let states = {};
1191
+ if (statesResponse.ok) {
1192
+ const statesData = await statesResponse.json();
1193
+ states = statesData.activeStates || {};
1194
+ console.log('📊 Loaded conversation states:', Object.keys(states).length, 'conversations');
1195
+ } else {
1196
+ console.warn('Could not load conversation states:', statesResponse.status);
1197
+ }
1198
+
1199
+ this.renderConversations(this.conversations, states);
1200
+
1201
+ } catch (error) {
1202
+ console.error('Error loading conversations:', error);
1203
+ conversationsList.innerHTML = `
1204
+ <div class="no-conversations">
1205
+ <div class="no-conversations-icon">⚠️</div>
1206
+ <h3>Error loading conversations</h3>
1207
+ <p>${error.message}</p>
1208
+ <button onclick="location.reload()" style="margin-top: 12px; padding: 8px 16px; background: var(--text-accent); color: white; border: none; border-radius: 4px; cursor: pointer;">Retry</button>
1209
+ </div>
1210
+ `;
1211
+ }
1212
+ }
1213
+
1214
+ renderConversations(conversations, states = {}) {
1215
+ const conversationsList = document.getElementById('conversationsList');
1216
+
1217
+ if (conversations.length === 0) {
1218
+ conversationsList.innerHTML = `
1219
+ <div class="no-conversations">
1220
+ <div class="no-conversations-icon">💬</div>
1221
+ <h3>No conversations found</h3>
1222
+ <p>Start a conversation with Claude Code to see it here</p>
1223
+ </div>
1224
+ `;
1225
+ return;
1226
+ }
1227
+
1228
+ conversationsList.innerHTML = conversations.map(conv => {
1229
+ const state = states[conv.id] || 'inactive';
1230
+ const stateClass = this.getStateClass(state);
1231
+ const stateLabel = this.getStateLabel(state);
1232
+
1233
+ // Debug logging for first few conversations
1234
+ console.log(`🔍 Conversation ${conv.id.slice(-8)}: State="${state}" -> Label="${stateLabel}" Class="${stateClass}"`);
1235
+
1236
+ const lastActivity = this.formatRelativeTime(new Date(conv.lastModified));
1237
+ const messageCount = conv.messageCount || 0;
1238
+ const projectName = conv.project || 'Unknown Project';
1239
+ const conversationId = conv.id.slice(-8);
1240
+
1241
+ // Get first letter of project name for avatar
1242
+ const firstLetter = projectName.charAt(0).toUpperCase();
1243
+
1244
+ return `
1245
+ <div class="conversation-item" data-conversation-id="${conv.id}">
1246
+ <div class="conversation-avatar">
1247
+ ${firstLetter}
1248
+ </div>
1249
+ <div class="conversation-content">
1250
+ <div class="conversation-header">
1251
+ <div class="conversation-name">${projectName}</div>
1252
+ <div class="conversation-time">${lastActivity}</div>
1253
+ </div>
1254
+ <div class="conversation-preview">
1255
+ Conversation ${conversationId}
1256
+ </div>
1257
+ <div class="conversation-meta">
1258
+ <span class="conversation-state ${stateClass}">${stateLabel}</span>
1259
+ ${messageCount > 0 ? `<span class="message-count">${messageCount}</span>` : ''}
1260
+ </div>
1261
+ </div>
1262
+ </div>
1263
+ `;
1264
+ }).join('');
1265
+
1266
+ // Bind conversation click events
1267
+ conversationsList.querySelectorAll('.conversation-item').forEach(item => {
1268
+ item.addEventListener('click', () => {
1269
+ const conversationId = item.dataset.conversationId;
1270
+ this.selectConversation(conversationId);
1271
+ });
1272
+ });
1273
+ }
1274
+
1275
+ selectConversation(conversationId) {
1276
+ this.selectedConversationId = conversationId;
1277
+
1278
+ // Update active state
1279
+ document.querySelectorAll('.conversation-item').forEach(item => {
1280
+ item.classList.toggle('active', item.dataset.conversationId === conversationId);
1281
+ });
1282
+
1283
+ // Show chat view
1284
+ this.showChatView(conversationId);
1285
+ }
1286
+
1287
+ showChatView(conversationId) {
1288
+ const conversation = this.conversations.find(conv => conv.id === conversationId);
1289
+ if (!conversation) return;
1290
+
1291
+ const chatView = document.getElementById('chatView');
1292
+ const chatViewTitle = document.getElementById('chatViewTitle');
1293
+ const chatViewSubtitle = document.getElementById('chatViewSubtitle');
1294
+
1295
+ // Update chat view header
1296
+ const projectName = conversation.project || 'Unknown Project';
1297
+ const convId = conversation.id.slice(-8);
1298
+ chatViewTitle.textContent = projectName;
1299
+ chatViewSubtitle.textContent = `Conversation ${convId}`;
1300
+
1301
+ // Show chat view with animation
1302
+ chatView.classList.add('active');
1303
+
1304
+ // Apply tools visibility state
1305
+ if (this.showTools) {
1306
+ chatView.classList.add('show-tools');
1307
+ } else {
1308
+ chatView.classList.remove('show-tools');
1309
+ }
1310
+
1311
+ // Load messages (placeholder for now)
1312
+ this.loadChatMessages(conversationId);
1313
+ }
1314
+
1315
+ showConversationsList() {
1316
+ const chatView = document.getElementById('chatView');
1317
+ chatView.classList.remove('active');
1318
+ chatView.classList.remove('conversation-selected'); // Hide status footer
1319
+ this.selectedConversationId = null;
1320
+
1321
+ // Clean up scroll tracking when leaving conversation
1322
+ this.removeScrollTracking();
1323
+
1324
+ // Remove active state from conversations
1325
+ document.querySelectorAll('.conversation-item').forEach(item => {
1326
+ item.classList.remove('active');
1327
+ });
1328
+
1329
+ // Reset status
1330
+ this.updateStatus('idle', 'Ready', '');
1331
+ }
1332
+
1333
+ async loadChatMessages(conversationId) {
1334
+ const chatView = document.getElementById('chatView');
1335
+
1336
+ // Mark conversation as selected and show status footer
1337
+ chatView.classList.add('conversation-selected');
1338
+
1339
+ // Reset pagination for new conversation
1340
+ this.messagesPagination = {
1341
+ currentPage: 0,
1342
+ limit: 15,
1343
+ hasMore: true,
1344
+ isLoading: false,
1345
+ conversationId: conversationId
1346
+ };
1347
+
1348
+ // Clear cached messages for this conversation
1349
+ this.loadedMessages.delete(conversationId);
1350
+
1351
+ // Load first page of messages
1352
+ await this.loadMoreMessages(conversationId, true);
1353
+ }
1354
+
1355
+ /**
1356
+ * Load more messages (for infinite scroll)
1357
+ * @param {string} conversationId - Conversation ID
1358
+ * @param {boolean} isInitialLoad - Whether this is the initial load
1359
+ */
1360
+ async loadMoreMessages(conversationId, isInitialLoad = false) {
1361
+ const chatMessages = document.getElementById('chatMessages');
1362
+ if (!chatMessages) return;
1363
+
1364
+ // Prevent concurrent loading
1365
+ if (this.messagesPagination.isLoading || !this.messagesPagination.hasMore) {
1366
+ return;
1367
+ }
1368
+
1369
+ // Ensure we're loading for the correct conversation
1370
+ if (this.messagesPagination.conversationId !== conversationId) {
1371
+ return;
1372
+ }
1373
+
1374
+ try {
1375
+ this.messagesPagination.isLoading = true;
1376
+
1377
+ if (isInitialLoad) {
1378
+ // Show loading state for initial load
1379
+ chatMessages.innerHTML = `
1380
+ <div class="messages-loading">
1381
+ <div class="loading-spinner"></div>
1382
+ <span>Loading messages...</span>
1383
+ </div>
1384
+ `;
1385
+ // Update status while loading
1386
+ this.updateStatus('working', 'Loading conversation...', 'Fetching messages from server');
1387
+ } else {
1388
+ // Show loading indicator at top for infinite scroll
1389
+ this.showMessagesLoadingIndicator(true);
1390
+ }
1391
+
1392
+ // Fetch paginated messages from the server
1393
+ const response = await fetch(`/api/conversations/${conversationId}/messages?page=${this.messagesPagination.currentPage}&limit=${this.messagesPagination.limit}`);
1394
+
1395
+ if (!response.ok) {
1396
+ throw new Error(`HTTP error! status: ${response.status}`);
1397
+ }
1398
+
1399
+ const messagesData = await response.json();
1400
+
1401
+ if (messagesData && messagesData.messages) {
1402
+ // Update pagination state - handle both paginated and non-paginated responses
1403
+ if (messagesData.pagination) {
1404
+ // Paginated response
1405
+ this.messagesPagination.hasMore = messagesData.pagination.hasMore;
1406
+ this.messagesPagination.currentPage = messagesData.pagination.page + 1;
1407
+ } else {
1408
+ // Non-paginated response (fallback) - treat as complete data
1409
+ this.messagesPagination.hasMore = false;
1410
+ this.messagesPagination.currentPage = 1;
1411
+ }
1412
+
1413
+ // Get existing messages or initialize
1414
+ let existingMessages = this.loadedMessages.get(conversationId) || [];
1415
+
1416
+ if (isInitialLoad) {
1417
+ // For initial load, replace all messages (newest messages first)
1418
+ existingMessages = messagesData.messages;
1419
+ } else {
1420
+ // For infinite scroll, prepend older messages to the beginning
1421
+ existingMessages = [...messagesData.messages, ...existingMessages];
1422
+ }
1423
+
1424
+ // Cache the combined messages
1425
+ this.loadedMessages.set(conversationId, existingMessages);
1426
+
1427
+ // Render messages
1428
+ this.renderCachedMessages(existingMessages, !isInitialLoad);
1429
+
1430
+ // Setup scroll listener for infinite scroll (only on initial load)
1431
+ if (isInitialLoad) {
1432
+ this.setupMessagesScrollListener(conversationId);
1433
+ // Setup intelligent scroll tracking for chat behavior
1434
+ this.setupScrollTracking();
1435
+ // Enable auto-scroll for initial load
1436
+ this.autoScrollEnabled = true;
1437
+ // Scroll to bottom for initial load
1438
+ this.scrollToBottom();
1439
+ // Update status based on conversation state
1440
+ this.analyzeConversationStatus(existingMessages);
1441
+ }
1442
+
1443
+ } else if (isInitialLoad) {
1444
+ chatMessages.innerHTML = `
1445
+ <div class="no-messages-found">
1446
+ <div class="no-messages-icon">💭</div>
1447
+ <h4>No messages found</h4>
1448
+ <p>This conversation has no messages or they could not be loaded.</p>
1449
+ </div>
1450
+ `;
1451
+ }
1452
+
1453
+ } catch (error) {
1454
+ console.error('Error loading messages:', error);
1455
+
1456
+ if (isInitialLoad) {
1457
+ this.updateStatus('error', 'Error loading conversation', error.message);
1458
+ chatMessages.innerHTML = `
1459
+ <div class="error-loading-messages">
1460
+ <div class="error-icon">⚠️</div>
1461
+ <h4>Error loading messages</h4>
1462
+ <p>${error.message}</p>
1463
+ <button class="retry-messages">Retry</button>
1464
+ </div>
1465
+ `;
1466
+
1467
+ // Add retry functionality
1468
+ const retryBtn = chatMessages.querySelector('.retry-messages');
1469
+ if (retryBtn) {
1470
+ retryBtn.addEventListener('click', () => {
1471
+ this.loadChatMessages(conversationId);
1472
+ });
1473
+ }
1474
+ }
1475
+ } finally {
1476
+ this.messagesPagination.isLoading = false;
1477
+ if (!isInitialLoad) {
1478
+ this.showMessagesLoadingIndicator(false);
1479
+ }
1480
+ }
1481
+ }
1482
+
1483
+ renderCachedMessages(messages, prepend = false) {
1484
+ const chatMessages = document.getElementById('chatMessages');
1485
+ if (!chatMessages) return;
1486
+
1487
+ if (prepend) {
1488
+ // For infinite scroll, prepend messages to existing content
1489
+ let existingMessagesDiv = chatMessages.querySelector('.messages-list');
1490
+
1491
+ if (!existingMessagesDiv) {
1492
+ // Create messages div if it doesn't exist
1493
+ existingMessagesDiv = document.createElement('div');
1494
+ existingMessagesDiv.className = 'messages-list';
1495
+ chatMessages.appendChild(existingMessagesDiv);
1496
+ }
1497
+
1498
+ // Remember scroll position to maintain scroll position after prepending
1499
+ const scrollTop = chatMessages.scrollTop;
1500
+ const scrollHeight = chatMessages.scrollHeight;
1501
+
1502
+ // Prepend new messages
1503
+ const newMessagesHTML = messages.map(msg => this.renderMessage(msg)).join('');
1504
+ existingMessagesDiv.innerHTML = newMessagesHTML + existingMessagesDiv.innerHTML;
1505
+
1506
+ // Restore scroll position (account for new content)
1507
+ const newScrollHeight = chatMessages.scrollHeight;
1508
+ const scrollDiff = newScrollHeight - scrollHeight;
1509
+ chatMessages.scrollTop = scrollTop + scrollDiff;
1510
+
1511
+ } else {
1512
+ // Normal render (initial load or replace all)
1513
+ const messageHTML = `
1514
+ <div class="messages-list">
1515
+ ${messages.map(msg => this.renderMessage(msg)).join('')}
1516
+ </div>
1517
+ `;
1518
+
1519
+ chatMessages.innerHTML = messageHTML;
1520
+ }
1521
+ }
1522
+
1523
+ /**
1524
+ * Show/hide messages loading indicator
1525
+ * @param {boolean} show - Whether to show the indicator
1526
+ */
1527
+ showMessagesLoadingIndicator(show) {
1528
+ const chatMessages = document.getElementById('chatMessages');
1529
+ if (!chatMessages) return;
1530
+
1531
+ let indicator = chatMessages.querySelector('.messages-loading-indicator');
1532
+ if (!indicator) {
1533
+ // Create the loading indicator if it doesn't exist
1534
+ indicator = document.createElement('div');
1535
+ indicator.className = 'messages-loading-indicator';
1536
+ indicator.innerHTML = `
1537
+ <div class="loading-spinner"></div>
1538
+ <span>Loading older messages...</span>
1539
+ `;
1540
+ indicator.style.cssText = `
1541
+ display: none;
1542
+ justify-content: center;
1543
+ align-items: center;
1544
+ padding: 15px;
1545
+ background: rgba(255, 255, 255, 0.05);
1546
+ border-radius: 8px;
1547
+ margin-bottom: 10px;
1548
+ gap: 10px;
1549
+ font-size: 14px;
1550
+ color: var(--text-secondary);
1551
+ `;
1552
+ chatMessages.insertBefore(indicator, chatMessages.firstChild);
1553
+ }
1554
+
1555
+ indicator.style.display = show ? 'flex' : 'none';
1556
+ }
1557
+
1558
+ /**
1559
+ * Setup scroll listener for infinite scroll in messages
1560
+ * @param {string} conversationId - Current conversation ID
1561
+ */
1562
+ setupMessagesScrollListener(conversationId) {
1563
+ const chatMessages = document.getElementById('chatMessages');
1564
+ if (!chatMessages) return;
1565
+
1566
+ // Remove existing listener if any
1567
+ if (this.messagesScrollListener) {
1568
+ chatMessages.removeEventListener('scroll', this.messagesScrollListener);
1569
+ }
1570
+
1571
+ // Create new listener
1572
+ this.messagesScrollListener = () => {
1573
+ // Check if we've scrolled near the top (for loading older messages)
1574
+ const scrollTop = chatMessages.scrollTop;
1575
+ const threshold = 100; // pixels from top
1576
+
1577
+ if (scrollTop <= threshold && this.messagesPagination.hasMore && !this.messagesPagination.isLoading) {
1578
+ this.loadMoreMessages(conversationId, false);
1579
+ }
1580
+ };
1581
+
1582
+ // Add listener
1583
+ chatMessages.addEventListener('scroll', this.messagesScrollListener);
1584
+ }
1585
+
1586
+ renderMessage(message) {
1587
+ const timestamp = this.formatRelativeTime(new Date(message.timestamp));
1588
+ const fullTimestamp = new Date(message.timestamp).toLocaleString();
1589
+ const isUser = message.role === 'user' && !message.isCompactSummary;
1590
+
1591
+ // Detect if message contains tools (either in content or as correlated toolResults)
1592
+ const hasToolsInContent = Array.isArray(message.content) &&
1593
+ message.content.some(block => block.type === 'tool_use');
1594
+ const hasCorrelatedTools = message.toolResults && message.toolResults.length > 0;
1595
+ const hasTools = hasToolsInContent || hasCorrelatedTools;
1596
+
1597
+ // Debug logging for tool detection
1598
+ if (hasTools) {
1599
+ console.log('🔧 Rendering message with tools', {
1600
+ messageId: message.id,
1601
+ role: message.role,
1602
+ hasToolsInContent,
1603
+ hasCorrelatedTools,
1604
+ toolResultsCount: message.toolResults?.length || 0,
1605
+ contentType: Array.isArray(message.content) ? 'array' : typeof message.content,
1606
+ willHaveHasToolsClass: !isUser && hasTools
1607
+ });
1608
+ }
1609
+
1610
+ const toolCount = hasToolsInContent ?
1611
+ message.content.filter(block => block.type === 'tool_use').length :
1612
+ (hasCorrelatedTools ? message.toolResults.length : 0);
1613
+
1614
+ // Add has-tools class to assistant messages that contain tools
1615
+ const hasToolsClass = (!isUser && hasTools) ? ' has-tools' : '';
1616
+
1617
+ return `
1618
+ <div class="message message-${isUser ? 'user' : 'assistant'}${hasToolsClass}" data-message-id="${message.id || ''}">
1619
+ <div class="message-bubble">
1620
+ <div class="message-content">
1621
+ ${this.formatMessageContent(message.content, message)}
1622
+ </div>
1623
+ <div class="message-meta">
1624
+ <span class="message-time" title="${fullTimestamp}">${timestamp}</span>
1625
+ ${hasTools ? `<span class="tool-indicator">🔧 ${toolCount}</span>` : ''}
1626
+ ${message.usage && (message.usage.input_tokens > 0 || message.usage.output_tokens > 0) ? `
1627
+ <span class="token-indicator">
1628
+ In: ${message.usage.input_tokens} • Out: ${message.usage.output_tokens}
1629
+ </span>
1630
+ ` : ''}
1631
+ </div>
1632
+ </div>
1633
+ </div>
1634
+ `;
1635
+ }
1636
+
1637
+ formatMessageContent(content, message = null) {
1638
+ let result = '';
1639
+
1640
+ // Handle different content formats
1641
+ if (Array.isArray(content)) {
1642
+ // Assistant messages with content blocks
1643
+ content.forEach((block, index) => {
1644
+ if (block.type === 'text') {
1645
+ result += this.formatTextContent(block.text);
1646
+ } else if (block.type === 'tool_use') {
1647
+ result += this.formatToolCall(block);
1648
+ } else if (block.type === 'tool_result') {
1649
+ result += this.formatToolResult(block);
1650
+ }
1651
+ });
1652
+ } else if (typeof content === 'string') {
1653
+ // Simple text content
1654
+ result = this.formatTextContent(content);
1655
+ } else if (typeof content === 'object' && content.text) {
1656
+ // Object with text property
1657
+ result = this.formatTextContent(content.text);
1658
+ }
1659
+
1660
+ // Handle correlated tool results from ConversationAnalyzer
1661
+ if (message && message.toolResults && message.toolResults.length > 0) {
1662
+ console.log('🔧 Formatting tool results for message', {
1663
+ messageId: message.id,
1664
+ toolResultsCount: message.toolResults.length,
1665
+ toolResults: message.toolResults.map(tr => ({ tool_use_id: tr.tool_use_id, contentLength: tr.content?.length }))
1666
+ });
1667
+ message.toolResults.forEach(toolResult => {
1668
+ result += this.formatToolResult(toolResult);
1669
+ });
1670
+ }
1671
+
1672
+ return result || '<em>Empty message</em>';
1673
+ }
1674
+
1675
+ formatTextContent(text) {
1676
+ if (!text) return '';
1677
+
1678
+ // Check if text is very long (more than 10 lines or 800 characters)
1679
+ const lines = text.split('\n');
1680
+ const isLongText = lines.length > 10 || text.length > 800;
1681
+
1682
+ let processedText = text;
1683
+ let expandableContent = '';
1684
+
1685
+ if (isLongText) {
1686
+ // Take first 5 lines or 400 characters, whichever comes first
1687
+ const previewLines = lines.slice(0, 5);
1688
+ const previewText = previewLines.join('\n');
1689
+ const remainingLines = lines.length - 5;
1690
+
1691
+ if (previewText.length > 400) {
1692
+ processedText = text.substring(0, 400) + '...';
1693
+ expandableContent = text.substring(400);
1694
+ } else {
1695
+ processedText = previewText;
1696
+ expandableContent = lines.slice(5).join('\n');
1697
+ }
1698
+
1699
+ // Create unique ID for this message
1700
+ const messageId = 'msg_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
1701
+
1702
+ // Format the preview text
1703
+ const formattedPreview = processedText
1704
+ .replace(/```(\w+)?\n([\s\S]+?)\n```/g, '<pre><code class="$1">$2</code></pre>')
1705
+ .replace(/`([^`]+)`/g, '<code>$1</code>')
1706
+ .replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
1707
+ .replace(/\*([^*]+)\*/g, '<em>$1</em>')
1708
+ .replace(/\n/g, '<br>');
1709
+
1710
+ // Format the hidden content
1711
+ const formattedHidden = expandableContent
1712
+ .replace(/```(\w+)?\n([\s\S]+?)\n```/g, '<pre><code class="$1">$2</code></pre>')
1713
+ .replace(/`([^`]+)`/g, '<code>$1</code>')
1714
+ .replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
1715
+ .replace(/\*([^*]+)\*/g, '<em>$1</em>')
1716
+ .replace(/\n/g, '<br>');
1717
+
1718
+ return `
1719
+ <div class="expandable-message">
1720
+ <div class="message-preview">${formattedPreview}</div>
1721
+ <div class="message-expand-link" onclick="this.parentNode.querySelector('.message-preview').style.display='none'; this.parentNode.querySelector('.message-full').style.display='block'; this.style.display='none';">
1722
+ + see ${remainingLines > 0 ? remainingLines + ' more lines' : 'more'}
1723
+ </div>
1724
+ <div class="message-full" style="display: none;">
1725
+ ${formattedPreview}<br>${formattedHidden}
1726
+ <div class="message-collapse-link" onclick="this.parentNode.style.display='none'; this.parentNode.parentNode.querySelector('.message-preview').style.display='block'; this.parentNode.parentNode.querySelector('.message-expand-link').style.display='block';">
1727
+ - show less
1728
+ </div>
1729
+ </div>
1730
+ </div>
1731
+ `;
1732
+ }
1733
+
1734
+ // Basic markdown-like formatting for normal length messages
1735
+ return text
1736
+ .replace(/```(\w+)?\n([\s\S]+?)\n```/g, '<pre><code class="$1">$2</code></pre>')
1737
+ .replace(/`([^`]+)`/g, '<code>$1</code>')
1738
+ .replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
1739
+ .replace(/\*([^*]+)\*/g, '<em>$1</em>')
1740
+ .replace(/\n/g, '<br>');
1741
+ }
1742
+
1743
+ formatToolCall(toolCall) {
1744
+ const toolName = toolCall.name || 'Tool';
1745
+ const input = toolCall.input || {};
1746
+
1747
+ // Create a readable summary of the tool call
1748
+ let toolSummary = '';
1749
+ if (toolName === 'Read') {
1750
+ const filePath = input.file_path || input.path || '';
1751
+ const fileName = filePath.split('/').pop() || filePath;
1752
+ const limit = input.limit ? ` ${input.limit} lines` : '';
1753
+ toolSummary = `${fileName}${limit ? ` (${limit})` : ''}`;
1754
+ } else if (toolName === 'Edit' || toolName === 'MultiEdit') {
1755
+ const filePath = input.file_path || input.path || '';
1756
+ const fileName = filePath.split('/').pop() || filePath;
1757
+ toolSummary = fileName;
1758
+ } else if (toolName === 'Bash') {
1759
+ const command = input.command || '';
1760
+ toolSummary = command.length > 50 ? command.substring(0, 50) + '...' : command;
1761
+ } else if (toolName === 'Grep') {
1762
+ const pattern = input.pattern || '';
1763
+ const path = input.path ? ` in ${input.path.split('/').pop()}` : '';
1764
+ toolSummary = `"${pattern}"${path}`;
1765
+ } else if (toolName === 'Write') {
1766
+ const filePath = input.file_path || input.path || '';
1767
+ const fileName = filePath.split('/').pop() || filePath;
1768
+ toolSummary = fileName;
1769
+ } else {
1770
+ // Generic fallback
1771
+ const firstKey = Object.keys(input)[0];
1772
+ if (firstKey) {
1773
+ const value = input[firstKey];
1774
+ if (typeof value === 'string') {
1775
+ toolSummary = value.length > 30 ? value.substring(0, 30) + '...' : value;
1776
+ }
1777
+ }
1778
+ }
1779
+
1780
+ // Create escaped JSON for parameters
1781
+ const inputContent = JSON.stringify(input, null, 2);
1782
+ const escapedInput = this.escapeHtml(inputContent);
1783
+
1784
+ return `
1785
+ <div class="tool-call">
1786
+ <div class="tool-summary" onclick="
1787
+ const toolCall = this.parentNode;
1788
+ const expandNote = toolCall.querySelector('.tool-expand-note');
1789
+ const parameters = toolCall.querySelector('.tool-parameters');
1790
+
1791
+ if (parameters.style.display === 'none') {
1792
+ expandNote.style.display = 'none';
1793
+ parameters.style.display = 'block';
1794
+ } else {
1795
+ expandNote.style.display = 'flex';
1796
+ parameters.style.display = 'none';
1797
+ }
1798
+ ">
1799
+ <span class="tool-bullet">⏺</span>
1800
+ <span class="tool-name">${toolName}</span>${toolSummary ? `<span class="tool-summary-text">(${toolSummary})</span>` : ''}
1801
+ </div>
1802
+ <div class="tool-expand-note" onclick="this.parentNode.querySelector('.tool-summary').click();">
1803
+ <span class="tool-branch">⎿</span>
1804
+ <span class="tool-note">Click to expand parameters</span>
1805
+ </div>
1806
+ <div class="tool-parameters" style="display: none;">
1807
+ <div class="tool-parameters-header">
1808
+ <span class="tool-branch">⎿</span>
1809
+ <span class="tool-param-title">Parameters:</span>
1810
+ </div>
1811
+ <pre class="tool-parameters-content"><code>${escapedInput}</code></pre>
1812
+ </div>
1813
+ </div>
1814
+ `;
1815
+ }
1816
+
1817
+ formatToolResult(toolResult) {
1818
+ // Properly escape HTML content
1819
+ const content = typeof toolResult.content === 'string'
1820
+ ? toolResult.content
1821
+ : JSON.stringify(toolResult.content, null, 2);
1822
+
1823
+ const escapedContent = this.escapeHtml(content);
1824
+
1825
+ // Check if content is long (more than 5 lines or 300 characters)
1826
+ const lines = content.split('\n');
1827
+ const isLongContent = lines.length > 5 || content.length > 300;
1828
+
1829
+ if (isLongContent) {
1830
+ // Show collapsed version for long content
1831
+ const previewLines = lines.slice(0, 3);
1832
+ const previewContent = previewLines.join('\n');
1833
+ const remainingLines = lines.length - 3;
1834
+ const escapedPreview = this.escapeHtml(previewContent);
1835
+
1836
+ const messageId = 'tool_result_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
1837
+
1838
+ return `
1839
+ <div class="tool-result">
1840
+ <div class="tool-result-header">
1841
+ <span>Tool Result</span>
1842
+ </div>
1843
+ <div class="tool-result-content">
1844
+ <div class="expandable-tool-result">
1845
+ <div class="tool-result-preview">
1846
+ <pre><code>${escapedPreview}</code></pre>
1847
+ </div>
1848
+ <div class="tool-result-expand-link" onclick="this.parentNode.querySelector('.tool-result-preview').style.display='none'; this.parentNode.querySelector('.tool-result-full').style.display='block'; this.style.display='none';">
1849
+ + see ${remainingLines} more lines
1850
+ </div>
1851
+ <div class="tool-result-full" style="display: none;">
1852
+ <pre><code>${escapedContent}</code></pre>
1853
+ <div class="tool-result-collapse-link" onclick="this.parentNode.style.display='none'; this.parentNode.parentNode.querySelector('.tool-result-preview').style.display='block'; this.parentNode.parentNode.querySelector('.tool-result-expand-link').style.display='block';">
1854
+ - show less
1855
+ </div>
1856
+ </div>
1857
+ </div>
1858
+ </div>
1859
+ </div>
1860
+ `;
1861
+ }
1862
+
1863
+ // Regular format for short content
1864
+ return `
1865
+ <div class="tool-result">
1866
+ <div class="tool-result-header">
1867
+ <span>Tool Result</span>
1868
+ </div>
1869
+ <div class="tool-result-content">
1870
+ <pre><code>${escapedContent}</code></pre>
1871
+ </div>
1872
+ </div>
1873
+ `;
1874
+ }
1875
+
1876
+ /**
1877
+ * Check if user is near the bottom of the chat
1878
+ * @returns {boolean} True if user is near the bottom
1879
+ */
1880
+ isNearBottom() {
1881
+ const chatMessages = document.getElementById('chatMessages');
1882
+ if (!chatMessages) return false;
1883
+
1884
+ const scrollTop = chatMessages.scrollTop;
1885
+ const scrollHeight = chatMessages.scrollHeight;
1886
+ const clientHeight = chatMessages.clientHeight;
1887
+
1888
+ // Consider "near bottom" if within scrollThreshold pixels
1889
+ return scrollHeight - scrollTop - clientHeight <= this.scrollThreshold;
1890
+ }
1891
+
1892
+ /**
1893
+ * Smart scroll to bottom with chat logic
1894
+ * Only scrolls if user is near bottom or auto-scroll is enabled
1895
+ */
1896
+ scrollToBottom() {
1897
+ const chatMessages = document.getElementById('chatMessages');
1898
+ if (!chatMessages) return;
1899
+
1900
+ // Always scroll on initial load or if user is near bottom
1901
+ if (this.autoScrollEnabled || this.isNearBottom()) {
1902
+ console.log('📱 Auto-scrolling to bottom', {
1903
+ autoScrollEnabled: this.autoScrollEnabled,
1904
+ isNearBottom: this.isNearBottom(),
1905
+ userScrolling: this.isUserScrolling
1906
+ });
1907
+
1908
+ chatMessages.scrollTop = chatMessages.scrollHeight;
1909
+ } else {
1910
+ console.log('📱 Skipping auto-scroll (user viewing older messages)', {
1911
+ scrollTop: chatMessages.scrollTop,
1912
+ scrollHeight: chatMessages.scrollHeight,
1913
+ isNearBottom: this.isNearBottom()
1914
+ });
1915
+ }
1916
+ }
1917
+
1918
+ /**
1919
+ * Setup scroll tracking for intelligent auto-scroll
1920
+ */
1921
+ setupScrollTracking() {
1922
+ const chatMessages = document.getElementById('chatMessages');
1923
+ if (!chatMessages || this.scrollListener) return;
1924
+
1925
+ this.scrollListener = () => {
1926
+ // Clear previous timeout
1927
+ if (this.userScrollTimeout) {
1928
+ clearTimeout(this.userScrollTimeout);
1929
+ }
1930
+
1931
+ // Mark as user scrolling
1932
+ this.isUserScrolling = true;
1933
+
1934
+ // Check if user scrolled back to bottom
1935
+ if (this.isNearBottom()) {
1936
+ this.autoScrollEnabled = true;
1937
+ console.log('📱 User scrolled to bottom, re-enabling auto-scroll');
1938
+ } else {
1939
+ this.autoScrollEnabled = false;
1940
+ }
1941
+
1942
+ // Reset user scrolling flag after a delay
1943
+ this.userScrollTimeout = setTimeout(() => {
1944
+ this.isUserScrolling = false;
1945
+ }, 1000);
1946
+ };
1947
+
1948
+ chatMessages.addEventListener('scroll', this.scrollListener, { passive: true });
1949
+ console.log('📱 Scroll tracking enabled for intelligent auto-scroll');
1950
+ }
1951
+
1952
+ /**
1953
+ * Remove scroll tracking
1954
+ */
1955
+ removeScrollTracking() {
1956
+ const chatMessages = document.getElementById('chatMessages');
1957
+ if (chatMessages && this.scrollListener) {
1958
+ chatMessages.removeEventListener('scroll', this.scrollListener);
1959
+ this.scrollListener = null;
1960
+ }
1961
+
1962
+ if (this.userScrollTimeout) {
1963
+ clearTimeout(this.userScrollTimeout);
1964
+ this.userScrollTimeout = null;
1965
+ }
1966
+ }
1967
+
1968
+ /**
1969
+ * Escape HTML characters to prevent double-encoding issues
1970
+ * @param {string} text - Text to escape
1971
+ * @returns {string} Escaped text
1972
+ */
1973
+ escapeHtml(text) {
1974
+ if (typeof text !== 'string') return text;
1975
+
1976
+ const div = document.createElement('div');
1977
+ div.textContent = text;
1978
+ return div.innerHTML;
1979
+ }
1980
+
1981
+
1982
+
1983
+ filterConversations(searchTerm) {
1984
+ const items = document.querySelectorAll('.conversation-item');
1985
+ const term = searchTerm.toLowerCase();
1986
+
1987
+ items.forEach(item => {
1988
+ const name = item.querySelector('.conversation-name').textContent.toLowerCase();
1989
+ const preview = item.querySelector('.conversation-preview').textContent.toLowerCase();
1990
+ const matches = name.includes(term) || preview.includes(term);
1991
+ item.style.display = matches ? 'flex' : 'none';
1992
+ });
1993
+ }
1994
+
1995
+ getStateLabel(state) {
1996
+ // Handle all possible states from StateCalculator with icons
1997
+ const stateLabels = {
1998
+ // Basic states
1999
+ 'active': '● Active',
2000
+ 'idle': '◐ Idle',
2001
+ 'inactive': '○ Inactive',
2002
+
2003
+ // Detailed states from StateCalculator
2004
+ 'Claude Code working...': '⚡ Working',
2005
+ 'Claude Code finishing...': '✓ Finishing',
2006
+ 'Active session': '● Active',
2007
+ 'Active conversation': '● Active',
2008
+ 'Recently active': '◉ Recent',
2009
+ 'Awaiting user input...': '⏳ Waiting',
2010
+ 'Awaiting response...': '💭 Responding',
2011
+ 'User typing...': '⌨️ Typing',
2012
+ 'Waiting for input...': '⏳ Waiting',
2013
+ 'No messages': '○ Empty',
2014
+
2015
+ // Fallback for exact matches
2016
+ 'Claude Code working': '⚡ Working',
2017
+ 'Claude Code': '⚡ Working',
2018
+ 'working': '⚡ Working',
2019
+ 'working...': '⚡ Working',
2020
+ 'recent': '◉ Recent',
2021
+ 'waiting': '⏳ Waiting'
2022
+ };
2023
+
2024
+ // Try exact match first
2025
+ if (stateLabels[state]) {
2026
+ return stateLabels[state];
2027
+ }
2028
+
2029
+ // Try partial matches for complex states with icons
2030
+ const stateLower = state.toLowerCase();
2031
+ if (stateLower.includes('working') || stateLower.includes('claude')) {
2032
+ return '⚡ Working';
2033
+ }
2034
+ if (stateLower.includes('active')) {
2035
+ return '● Active';
2036
+ }
2037
+ if (stateLower.includes('recent')) {
2038
+ return '◉ Recent';
2039
+ }
2040
+ if (stateLower.includes('waiting') || stateLower.includes('awaiting')) {
2041
+ return '⏳ Waiting';
2042
+ }
2043
+ if (stateLower.includes('responding')) {
2044
+ return '💭 Responding';
2045
+ }
2046
+ if (stateLower.includes('typing')) {
2047
+ return '⌨️ Typing';
2048
+ }
2049
+ if (stateLower.includes('idle')) {
2050
+ return '◐ Idle';
2051
+ }
2052
+ if (stateLower.includes('inactive')) {
2053
+ return '○ Inactive';
2054
+ }
2055
+
2056
+ // Return the state as-is if we can't categorize it
2057
+ return state || 'Unknown';
2058
+ }
2059
+
2060
+ getStateClass(state) {
2061
+ // Generate appropriate CSS class based on state
2062
+ const stateLower = (state || '').toLowerCase();
2063
+
2064
+ if (stateLower.includes('working') || stateLower.includes('claude')) {
2065
+ return 'state-working';
2066
+ }
2067
+ if (stateLower.includes('active') || stateLower.includes('recent')) {
2068
+ return 'state-active';
2069
+ }
2070
+ if (stateLower.includes('waiting') || stateLower.includes('awaiting') || stateLower.includes('responding')) {
2071
+ return 'state-waiting';
2072
+ }
2073
+ if (stateLower.includes('typing')) {
2074
+ return 'state-typing';
2075
+ }
2076
+ if (stateLower.includes('idle')) {
2077
+ return 'state-idle';
2078
+ }
2079
+ if (stateLower.includes('inactive')) {
2080
+ return 'state-inactive';
2081
+ }
2082
+
2083
+ // Fallback to basic classification
2084
+ return `state-${state.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z-]/g, '')}`;
2085
+ }
2086
+
2087
+ formatRelativeTime(date) {
2088
+ if (!date) return '';
2089
+
2090
+ const now = new Date();
2091
+ const diff = now.getTime() - date.getTime();
2092
+ const minutes = Math.floor(diff / 60000);
2093
+ const hours = Math.floor(minutes / 60);
2094
+ const days = Math.floor(hours / 24);
2095
+ const weeks = Math.floor(days / 7);
2096
+ const months = Math.floor(days / 30);
2097
+
2098
+ if (minutes < 1) return 'Just now';
2099
+ if (minutes < 60) return `${minutes}m ago`;
2100
+ if (hours < 24) return `${hours}h ago`;
2101
+ if (days < 7) return `${days}d ago`;
2102
+ if (weeks < 4) return `${weeks}w ago`;
2103
+ if (months < 12) return `${months}mo ago`;
2104
+
2105
+ return date.toLocaleDateString();
2106
+ }
2107
+
2108
+ /**
2109
+ * Handle new message received via WebSocket
2110
+ * @param {string} conversationId - Conversation ID that received new message
2111
+ * @param {Object} message - New message object
2112
+ * @param {Object} metadata - Additional metadata
2113
+ */
2114
+ handleNewMessage(conversationId, message, metadata) {
2115
+ console.log('🔄 Processing new message', {
2116
+ conversationId,
2117
+ role: message?.role,
2118
+ selectedConversationId: this.selectedConversationId,
2119
+ hasToolResults: !!(message?.toolResults && message.toolResults.length > 0),
2120
+ toolResultsCount: message?.toolResults?.length || 0,
2121
+ hasToolsInContent: Array.isArray(message?.content) && message.content.some(block => block.type === 'tool_use'),
2122
+ contentType: Array.isArray(message?.content) ? 'array' : typeof message?.content,
2123
+ metadata: metadata
2124
+ });
2125
+
2126
+ // Update message cache for this conversation
2127
+ const existingMessages = this.loadedMessages.get(conversationId) || [];
2128
+
2129
+ // Check if we already have this message (ONLY by ID - no timestamp fallback)
2130
+ // This ensures we only merge/replace the exact same message, never different ones
2131
+ // CRITICAL: Only match if both messages have valid, non-null IDs
2132
+ const existingIndex = existingMessages.findIndex(msg =>
2133
+ msg.id && message.id && msg.id === message.id
2134
+ );
2135
+
2136
+ console.log('🔍 WebSocket message processing:', {
2137
+ messageId: message.id,
2138
+ messageIdValid: !!message.id,
2139
+ existingIndex,
2140
+ totalExisting: existingMessages.length,
2141
+ hasToolResults: !!(message.toolResults && message.toolResults.length > 0),
2142
+ messageRole: message.role,
2143
+ contentType: Array.isArray(message.content) ? `array(${message.content.length})` : typeof message.content,
2144
+ existingIds: existingMessages.map(m => m.id).slice(-3) // Last 3 IDs for debugging
2145
+ });
2146
+
2147
+ let updatedMessages;
2148
+ if (existingIndex >= 0) {
2149
+ const existingMessage = existingMessages[existingIndex];
2150
+
2151
+ // CRITICAL PROTECTION: If existing message is text and was rendered in DOM, NEVER replace it
2152
+ const existingIsText = !existingMessage.toolResults || existingMessage.toolResults.length === 0;
2153
+ const newIsText = !message.toolResults || message.toolResults.length === 0;
2154
+
2155
+ // Check if this message is currently rendered in the DOM
2156
+ const isRenderedInDOM = this.isMessageRenderedInDOM(existingMessage);
2157
+
2158
+ if (existingIsText && isRenderedInDOM) {
2159
+ // This is a text message already in DOM - NEVER replace it, always add new message
2160
+ console.log('🛡️ PROTECTING text message already rendered in DOM', {
2161
+ messageId: message.id,
2162
+ existingType: 'TEXT',
2163
+ newType: newIsText ? 'TEXT' : 'TOOL',
2164
+ reason: 'TEXT_MESSAGE_IN_DOM_PROTECTED',
2165
+ action: 'ADD_AS_NEW'
2166
+ });
2167
+
2168
+ // Add as new message instead of replacing
2169
+ updatedMessages = [...existingMessages, message];
2170
+ } else if (existingIsText && !newIsText) {
2171
+ // Existing is text, new has tools - DON'T REPLACE, they're different messages
2172
+ console.log('⚠️ PREVENTING text message replacement with tool message', {
2173
+ messageId: message.id,
2174
+ existingType: 'TEXT',
2175
+ newType: 'TOOL',
2176
+ reason: 'DIFFERENT_MESSAGE_TYPES_DONT_REPLACE'
2177
+ });
2178
+
2179
+ // Add as new message instead
2180
+ updatedMessages = [...existingMessages, message];
2181
+ } else if (!existingIsText && newIsText) {
2182
+ // Existing has tools, new is text - DON'T REPLACE, they're different messages
2183
+ console.log('⚠️ PREVENTING tool message replacement with text message', {
2184
+ messageId: message.id,
2185
+ existingType: 'TOOL',
2186
+ newType: 'TEXT',
2187
+ reason: 'DIFFERENT_MESSAGE_TYPES_DONT_REPLACE'
2188
+ });
2189
+
2190
+ // Add as new message instead
2191
+ updatedMessages = [...existingMessages, message];
2192
+ } else {
2193
+ // Same type of message - safe to replace ONLY if not a protected text message
2194
+ console.log('🔄 REPLACING existing message with server-correlated version', {
2195
+ messageId: message.id,
2196
+ existingToolResults: existingMessage.toolResults?.length || 0,
2197
+ newToolResults: message.toolResults?.length || 0,
2198
+ messageType: newIsText ? 'TEXT' : 'TOOL',
2199
+ reason: 'SERVER_CORRELATION_AUTHORITATIVE'
2200
+ });
2201
+
2202
+ updatedMessages = [...existingMessages];
2203
+ updatedMessages[existingIndex] = message;
2204
+ }
2205
+ } else {
2206
+ // Add new message to the end
2207
+ console.log('➕ Adding new message from WebSocket', {
2208
+ messageId: message.id,
2209
+ toolResults: message.toolResults?.length || 0,
2210
+ hasContent: !!(message.content && (typeof message.content === 'string' || Array.isArray(message.content))),
2211
+ messageType: (!message.toolResults || message.toolResults.length === 0) ? 'TEXT' : 'TOOL'
2212
+ });
2213
+ updatedMessages = [...existingMessages, message];
2214
+ }
2215
+
2216
+ this.loadedMessages.set(conversationId, updatedMessages);
2217
+
2218
+ // If this conversation is currently selected, update the messages view immediately
2219
+ if (this.selectedConversationId === conversationId) {
2220
+ console.log('💬 Updating messages view in real-time');
2221
+ this.renderCachedMessages(updatedMessages);
2222
+
2223
+ // Smart scroll behavior: scroll to bottom for new messages
2224
+ // or when user is already near the bottom
2225
+ if (existingIndex === -1) {
2226
+ // This is a completely new message
2227
+ console.log('📱 New message received, checking auto-scroll...');
2228
+ this.scrollToBottom();
2229
+ } else if (this.isNearBottom()) {
2230
+ // This is an update to existing message and user is near bottom
2231
+ console.log('📱 Message updated, user near bottom, scrolling...');
2232
+ this.scrollToBottom();
2233
+ } else {
2234
+ console.log('📱 Message updated, user viewing older messages, not scrolling');
2235
+ }
2236
+
2237
+ // Update status based on the message
2238
+ this.analyzeConversationStatus(updatedMessages);
2239
+ }
2240
+ }
2241
+
2242
+ /**
2243
+ * Update status footer
2244
+ * @param {string} status - Status type: 'ready', 'working', 'waiting', 'error', 'idle'
2245
+ * @param {string} text - Status text
2246
+ * @param {string} details - Optional details text
2247
+ */
2248
+ updateStatus(status, text, details = '') {
2249
+ const statusDot = document.getElementById('statusDot');
2250
+ const statusText = document.getElementById('statusText');
2251
+ const statusDetails = document.getElementById('statusDetails');
2252
+
2253
+ if (!statusDot || !statusText || !statusDetails) return;
2254
+
2255
+ // Remove all status classes
2256
+ statusDot.className = 'status-dot';
2257
+
2258
+ // Add new status class
2259
+ statusDot.classList.add(status);
2260
+
2261
+ // Update text
2262
+ statusText.textContent = text;
2263
+ statusDetails.textContent = details;
2264
+ }
2265
+
2266
+ /**
2267
+ * Analyze conversation messages to determine current status
2268
+ * @param {Array} messages - Array of messages
2269
+ */
2270
+ analyzeConversationStatus(messages) {
2271
+ if (!messages || messages.length === 0) {
2272
+ this.updateStatus('idle', 'No messages', '');
2273
+ return;
2274
+ }
2275
+
2276
+ const lastMessage = messages[messages.length - 1];
2277
+ const secondLastMessage = messages.length > 1 ? messages[messages.length - 2] : null;
2278
+ const now = new Date();
2279
+ const messageTime = new Date(lastMessage.timestamp);
2280
+ const timeDiff = (now - messageTime) / 1000 / 60; // minutes ago
2281
+
2282
+ // Analyze message patterns to determine status
2283
+ if (lastMessage.role === 'user') {
2284
+ // Last message is from user - Claude should be working
2285
+ if (timeDiff < 2) {
2286
+ this.updateStatus('working', 'Claude Code is working...', 'Processing your request');
2287
+ } else if (timeDiff < 10) {
2288
+ this.updateStatus('working', 'Claude Code is thinking...', `Started ${Math.round(timeDiff)}m ago`);
2289
+ } else {
2290
+ this.updateStatus('waiting', 'Waiting for response', `User input ${Math.round(timeDiff)}m ago`);
2291
+ }
2292
+ } else if (lastMessage.role === 'assistant') {
2293
+ // Check if assistant is using tools
2294
+ const hasTools = (Array.isArray(lastMessage.content) &&
2295
+ lastMessage.content.some(block => block.type === 'tool_use')) ||
2296
+ (lastMessage.toolResults && lastMessage.toolResults.length > 0);
2297
+
2298
+ if (hasTools) {
2299
+ // Assistant used tools - might still be working
2300
+ if (timeDiff < 1) {
2301
+ this.updateStatus('working', 'Claude Code executing tools...', 'Running commands');
2302
+ } else if (timeDiff < 5) {
2303
+ this.updateStatus('working', 'Processing tool results...', `Tools executed ${Math.round(timeDiff)}m ago`);
2304
+ } else {
2305
+ this.updateStatus('ready', 'Tools completed', `Waiting for user input`);
2306
+ }
2307
+ } else {
2308
+ // Regular assistant message
2309
+ if (timeDiff < 5) {
2310
+ this.updateStatus('ready', 'Claude Code ready', 'Waiting for user input');
2311
+ } else if (timeDiff < 60) {
2312
+ this.updateStatus('ready', 'Ready', `Response sent ${Math.round(timeDiff)}m ago`);
2313
+ } else if (timeDiff < 1440) { // 24 hours
2314
+ const hours = Math.round(timeDiff / 60);
2315
+ this.updateStatus('idle', 'Conversation idle', `Last activity ${hours}h ago`);
2316
+ } else {
2317
+ const days = Math.round(timeDiff / 1440);
2318
+ this.updateStatus('idle', 'Conversation inactive', `Last activity ${days}d ago`);
2319
+ }
2320
+ }
2321
+ }
2322
+
2323
+ // Special handling for sequences of messages
2324
+ if (messages.length >= 2) {
2325
+ const recentMessages = messages.slice(-3); // Last 3 messages
2326
+ const hasRecentTools = recentMessages.some(msg =>
2327
+ (Array.isArray(msg.content) && msg.content.some(block => block.type === 'tool_use')) ||
2328
+ (msg.toolResults && msg.toolResults.length > 0)
2329
+ );
2330
+
2331
+ // If there's been recent tool activity, show more specific status
2332
+ if (hasRecentTools && lastMessage.role === 'assistant' && timeDiff < 2) {
2333
+ const toolCount = recentMessages.reduce((count, msg) => {
2334
+ const contentTools = Array.isArray(msg.content) ?
2335
+ msg.content.filter(block => block.type === 'tool_use').length : 0;
2336
+ const resultTools = msg.toolResults ? msg.toolResults.length : 0;
2337
+ return count + contentTools + resultTools;
2338
+ }, 0);
2339
+
2340
+ this.updateStatus('working', 'Processing multiple tools...', `${toolCount} tools in progress`);
2341
+ }
2342
+ }
2343
+ }
2344
+
2345
+ /**
2346
+ * Determine if two messages should be merged or replaced
2347
+ * @param {Object} existingMessage - The existing message in cache
2348
+ * @param {Object} newMessage - The new message from WebSocket
2349
+ * @returns {boolean} True if should merge, false if should replace
2350
+ */
2351
+ shouldMergeMessages(existingMessage, newMessage) {
2352
+ // SIMPLE RULE: Only merge if it's the EXACT same message getting tool results added
2353
+ // This mirrors server behavior: text messages stay separate, tools stay separate
2354
+
2355
+ // If messages have different IDs, they are DIFFERENT messages - never merge
2356
+ if (existingMessage.id !== newMessage.id) {
2357
+ console.log('🤔 shouldMergeMessages decision:', {
2358
+ existingId: existingMessage.id,
2359
+ newId: newMessage.id,
2360
+ decision: 'NEVER_MERGE_DIFFERENT_IDS',
2361
+ reason: 'DIFFERENT_MESSAGE_IDS'
2362
+ });
2363
+ return false; // Different messages - never merge
2364
+ }
2365
+
2366
+ // If IDs are the same, check if this is just adding tool results to the same message
2367
+ const existingToolResults = existingMessage.toolResults?.length || 0;
2368
+ const newToolResults = newMessage.toolResults?.length || 0;
2369
+
2370
+ // If new message has more tool results for the SAME message ID, we can replace
2371
+ if (newToolResults > existingToolResults) {
2372
+ console.log('🤔 shouldMergeMessages decision:', {
2373
+ messageId: newMessage.id,
2374
+ existingToolResults,
2375
+ newToolResults,
2376
+ decision: 'REPLACE_SAME_ID_MORE_TOOLS',
2377
+ reason: 'MORE_TOOLS_SAME_MESSAGE'
2378
+ });
2379
+ return false; // Replace with more complete version of same message
2380
+ }
2381
+
2382
+ // If they're the same message but new one has less or equal tools, keep existing
2383
+ console.log('🤔 shouldMergeMessages decision:', {
2384
+ messageId: newMessage.id,
2385
+ existingToolResults,
2386
+ newToolResults,
2387
+ decision: 'KEEP_EXISTING',
2388
+ reason: 'SAME_MESSAGE_NO_NEW_TOOLS'
2389
+ });
2390
+ return true; // Keep existing (merge, but existing wins)
2391
+ }
2392
+
2393
+ /**
2394
+ * Merge two message objects, preserving content and adding tool results
2395
+ * @param {Object} existingMessage - The existing message in cache
2396
+ * @param {Object} newMessage - The new message from WebSocket
2397
+ * @returns {Object} Merged message object
2398
+ */
2399
+ mergeMessages(existingMessage, newMessage) {
2400
+ // Start with the existing message as base
2401
+ const mergedMessage = { ...existingMessage };
2402
+
2403
+ // Always use the latest timestamp, role, and metadata from new message
2404
+ mergedMessage.timestamp = newMessage.timestamp || existingMessage.timestamp;
2405
+ mergedMessage.role = newMessage.role || existingMessage.role;
2406
+ mergedMessage.id = newMessage.id || existingMessage.id;
2407
+ mergedMessage.model = newMessage.model || existingMessage.model;
2408
+ mergedMessage.usage = newMessage.usage || existingMessage.usage;
2409
+
2410
+ // Merge content intelligently
2411
+ if (existingMessage.content && newMessage.content) {
2412
+ // Both have content - need to merge intelligently
2413
+ const existingIsArray = Array.isArray(existingMessage.content);
2414
+ const newIsArray = Array.isArray(newMessage.content);
2415
+
2416
+ if (existingIsArray && newIsArray) {
2417
+ // Both are arrays - combine unique content blocks
2418
+ const combinedContent = [...existingMessage.content];
2419
+ for (const newBlock of newMessage.content) {
2420
+ const existsAlready = combinedContent.some(block =>
2421
+ block.type === newBlock.type &&
2422
+ (block.type === 'text' ? block.text === newBlock.text :
2423
+ block.type === 'tool_use' ? block.id === newBlock.id :
2424
+ JSON.stringify(block) === JSON.stringify(newBlock))
2425
+ );
2426
+ if (!existsAlready) {
2427
+ combinedContent.push(newBlock);
2428
+ }
2429
+ }
2430
+ mergedMessage.content = combinedContent;
2431
+ } else if (existingIsArray) {
2432
+ // Existing is array, new is string - keep array (more complete)
2433
+ mergedMessage.content = existingMessage.content;
2434
+ } else if (newIsArray) {
2435
+ // New is array, existing is string - use array (more complete)
2436
+ mergedMessage.content = newMessage.content;
2437
+ } else {
2438
+ // Both strings - use the longer one
2439
+ mergedMessage.content = existingMessage.content.length >= newMessage.content.length ?
2440
+ existingMessage.content : newMessage.content;
2441
+ }
2442
+ } else if (newMessage.content) {
2443
+ mergedMessage.content = newMessage.content;
2444
+ } else if (existingMessage.content) {
2445
+ mergedMessage.content = existingMessage.content;
2446
+ }
2447
+
2448
+ // Merge tool results - use the most complete set
2449
+ const existingToolResults = existingMessage.toolResults || [];
2450
+ const newToolResults = newMessage.toolResults || [];
2451
+
2452
+ if (newToolResults.length > existingToolResults.length) {
2453
+ // New message has more tool results
2454
+ mergedMessage.toolResults = newToolResults;
2455
+ } else if (existingToolResults.length > 0) {
2456
+ // Keep existing tool results if they're more complete
2457
+ mergedMessage.toolResults = existingToolResults;
2458
+ } else {
2459
+ // Use whatever we have
2460
+ mergedMessage.toolResults = newToolResults.length > 0 ? newToolResults : existingToolResults;
2461
+ }
2462
+
2463
+ // Preserve any other fields that might be important
2464
+ mergedMessage.isCompactSummary = newMessage.isCompactSummary || existingMessage.isCompactSummary;
2465
+ mergedMessage.uuid = newMessage.uuid || existingMessage.uuid;
2466
+ mergedMessage.type = newMessage.type || existingMessage.type;
2467
+
2468
+ console.log('🔀 Message merge result', {
2469
+ messageId: mergedMessage.id,
2470
+ existingContentType: Array.isArray(existingMessage.content) ? `array(${existingMessage.content.length})` : typeof existingMessage.content,
2471
+ newContentType: Array.isArray(newMessage.content) ? `array(${newMessage.content.length})` : typeof newMessage.content,
2472
+ finalContentType: Array.isArray(mergedMessage.content) ? `array(${mergedMessage.content.length})` : typeof mergedMessage.content,
2473
+ existingToolResults: existingMessage.toolResults?.length || 0,
2474
+ newToolResults: newMessage.toolResults?.length || 0,
2475
+ finalToolResults: mergedMessage.toolResults?.length || 0,
2476
+ mergeStrategy: existingMessage.content && newMessage.content ? 'MERGED_CONTENT' :
2477
+ existingMessage.content ? 'EXISTING_CONTENT' : 'NEW_CONTENT'
2478
+ });
2479
+
2480
+ return mergedMessage;
2481
+ }
2482
+
2483
+ /**
2484
+ * Load tools preference from localStorage
2485
+ */
2486
+ loadToolsPreference() {
2487
+ const saved = localStorage.getItem('showTools');
2488
+ const showTools = saved !== null ? saved === 'true' : true; // Default to true
2489
+
2490
+ this.showTools = showTools;
2491
+ const chatView = document.getElementById('chatView');
2492
+ const showToolsSwitch = document.getElementById('showToolsSwitch');
2493
+
2494
+ // Update switch state
2495
+ showToolsSwitch.checked = showTools;
2496
+
2497
+ // Update CSS class
2498
+ if (showTools) {
2499
+ chatView.classList.add('show-tools');
2500
+ } else {
2501
+ chatView.classList.remove('show-tools');
2502
+ }
2503
+
2504
+ console.log('🔧 Tools preference loaded:', showTools ? 'ON' : 'OFF');
2505
+ }
2506
+
2507
+ /**
2508
+ * Check if a message is currently rendered in the DOM
2509
+ * @param {Object} message - The message to check
2510
+ * @returns {boolean} True if message is rendered in DOM
2511
+ */
2512
+ isMessageRenderedInDOM(message) {
2513
+ if (!message || !message.id) return false;
2514
+
2515
+ const chatMessages = document.querySelector('.chat-messages');
2516
+ if (!chatMessages) return false;
2517
+
2518
+ // Look for message element with matching data-message-id
2519
+ const messageElement = chatMessages.querySelector(`[data-message-id="${message.id}"]`);
2520
+ if (messageElement) {
2521
+ console.log('🔍 DOM check: Found message in DOM', {
2522
+ messageId: message.id,
2523
+ hasTools: messageElement.classList.contains('has-tools'),
2524
+ isText: !messageElement.classList.contains('has-tools')
2525
+ });
2526
+ return true;
2527
+ }
2528
+
2529
+ // Additional check: look for text content in DOM if message has no tools
2530
+ const isTextMessage = !message.toolResults || message.toolResults.length === 0;
2531
+ if (isTextMessage && message.content) {
2532
+ const contentText = typeof message.content === 'string' ?
2533
+ message.content :
2534
+ (Array.isArray(message.content) ?
2535
+ message.content.filter(block => block.type === 'text').map(block => block.text).join(' ') :
2536
+ ''
2537
+ );
2538
+
2539
+ if (contentText && contentText.length > 10) {
2540
+ // Check if this text content appears in any message bubble
2541
+ const messageElements = chatMessages.querySelectorAll('.message .message-content');
2542
+ for (const element of messageElements) {
2543
+ if (element.textContent.includes(contentText.substring(0, 50))) {
2544
+ console.log('🔍 DOM check: Found text message by content', {
2545
+ messageId: message.id,
2546
+ contentPreview: contentText.substring(0, 50)
2547
+ });
2548
+ return true;
2549
+ }
2550
+ }
2551
+ }
2552
+ }
2553
+
2554
+ console.log('🔍 DOM check: Message NOT found in DOM', {
2555
+ messageId: message.id,
2556
+ isText: isTextMessage,
2557
+ hasContent: !!message.content
2558
+ });
2559
+ return false;
2560
+ }
2561
+
2562
+ /**
2563
+ * Toggle tools visibility
2564
+ * @param {boolean} show - Whether to show tools
2565
+ */
2566
+ toggleTools(show) {
2567
+ this.showTools = show;
2568
+ const chatView = document.getElementById('chatView');
2569
+
2570
+ if (show) {
2571
+ chatView.classList.add('show-tools');
2572
+ } else {
2573
+ chatView.classList.remove('show-tools');
2574
+ }
2575
+
2576
+ console.log('🔧 Tools visibility toggled:', show ? 'ON' : 'OFF');
2577
+ console.log('🔧 Messages with .has-tools class will be:', show ? 'VISIBLE' : 'HIDDEN');
2578
+
2579
+ // Store preference in localStorage
2580
+ localStorage.setItem('showTools', show);
2581
+ }
2582
+ }
2583
+
2584
+ // Initialize the app
2585
+ document.addEventListener('DOMContentLoaded', () => {
2586
+ new ChatsMobileApp();
2587
+ });
2588
+ </script>
2589
+ </body>
2590
+ </html>