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.
- package/README.md +7 -7
- package/bin/create-claude-config.js +17 -8
- package/package.json +2 -3
- package/src/analytics/core/AgentAnalyzer.js +17 -3
- package/src/analytics/core/ProcessDetector.js +23 -7
- package/src/analytics/core/StateCalculator.js +102 -33
- package/src/analytics/data/DataCache.js +7 -7
- package/src/analytics-web/chats_mobile.html +2590 -0
- package/src/analytics-web/components/App.js +10 -10
- package/src/analytics-web/components/SessionTimer.js +1 -1
- package/src/analytics-web/components/Sidebar.js +5 -14
- package/src/analytics-web/index.html +932 -78
- package/src/analytics.js +263 -5
- package/src/chats-mobile.js +682 -0
- package/src/claude-api-proxy.js +460 -0
- package/src/file-operations.js +239 -36
- package/src/health-check.js +310 -0
- package/src/index.js +1256 -36
- package/src/tracking-service.js +31 -34
- package/components/agents/api-security-audit.md +0 -92
- package/components/agents/database-optimization.md +0 -94
- package/components/agents/react-performance-optimization.md +0 -64
- package/components/commands/check-file.md +0 -53
- package/components/commands/generate-tests.md +0 -68
- package/components/mcps/deepgraph-nextjs.json +0 -12
- package/components/mcps/deepgraph-react.json +0 -12
- package/components/mcps/deepgraph-typescript.json +0 -12
- package/components/mcps/deepgraph-vue.json +0 -12
- package/components/mcps/filesystem-access.json +0 -12
- package/components/mcps/github-integration.json +0 -11
- package/components/mcps/memory-integration.json +0 -8
- package/components/mcps/mysql-integration.json +0 -11
- package/components/mcps/postgresql-integration.json +0 -11
- package/components/mcps/web-fetch.json +0 -8
- package/src/analytics-web/components/AgentsPage.js +0 -4761
- package/templates/common/.claude/commands/git-workflow.md +0 -239
- package/templates/common/.claude/commands/project-setup.md +0 -316
- package/templates/common/.mcp.json +0 -41
- package/templates/common/CLAUDE.md +0 -109
- package/templates/common/README.md +0 -96
- package/templates/go/.mcp.json +0 -78
- package/templates/go/README.md +0 -25
- package/templates/javascript-typescript/.claude/commands/api-endpoint.md +0 -51
- package/templates/javascript-typescript/.claude/commands/debug.md +0 -52
- package/templates/javascript-typescript/.claude/commands/lint.md +0 -48
- package/templates/javascript-typescript/.claude/commands/npm-scripts.md +0 -48
- package/templates/javascript-typescript/.claude/commands/refactor.md +0 -55
- package/templates/javascript-typescript/.claude/commands/test.md +0 -61
- package/templates/javascript-typescript/.claude/commands/typescript-migrate.md +0 -51
- package/templates/javascript-typescript/.claude/settings.json +0 -142
- package/templates/javascript-typescript/.mcp.json +0 -80
- package/templates/javascript-typescript/CLAUDE.md +0 -185
- package/templates/javascript-typescript/README.md +0 -259
- package/templates/javascript-typescript/examples/angular-app/.claude/commands/components.md +0 -63
- package/templates/javascript-typescript/examples/angular-app/.claude/commands/services.md +0 -62
- package/templates/javascript-typescript/examples/node-api/.claude/commands/api-endpoint.md +0 -46
- package/templates/javascript-typescript/examples/node-api/.claude/commands/database.md +0 -56
- package/templates/javascript-typescript/examples/node-api/.claude/commands/middleware.md +0 -61
- package/templates/javascript-typescript/examples/node-api/.claude/commands/route.md +0 -57
- package/templates/javascript-typescript/examples/node-api/CLAUDE.md +0 -102
- package/templates/javascript-typescript/examples/react-app/.claude/commands/component.md +0 -29
- package/templates/javascript-typescript/examples/react-app/.claude/commands/hooks.md +0 -44
- package/templates/javascript-typescript/examples/react-app/.claude/commands/state-management.md +0 -45
- package/templates/javascript-typescript/examples/react-app/CLAUDE.md +0 -81
- package/templates/javascript-typescript/examples/react-app/agents/react-performance-optimization.md +0 -530
- package/templates/javascript-typescript/examples/react-app/agents/react-state-management.md +0 -295
- package/templates/javascript-typescript/examples/vue-app/.claude/commands/components.md +0 -46
- package/templates/javascript-typescript/examples/vue-app/.claude/commands/composables.md +0 -51
- package/templates/python/.claude/commands/lint.md +0 -111
- package/templates/python/.claude/commands/test.md +0 -73
- package/templates/python/.claude/settings.json +0 -153
- package/templates/python/.mcp.json +0 -78
- package/templates/python/CLAUDE.md +0 -276
- package/templates/python/examples/django-app/.claude/commands/admin.md +0 -264
- package/templates/python/examples/django-app/.claude/commands/django-model.md +0 -124
- package/templates/python/examples/django-app/.claude/commands/views.md +0 -222
- package/templates/python/examples/django-app/CLAUDE.md +0 -313
- package/templates/python/examples/django-app/agents/django-api-security.md +0 -642
- package/templates/python/examples/django-app/agents/django-database-optimization.md +0 -752
- package/templates/python/examples/fastapi-app/.claude/commands/api-endpoints.md +0 -513
- package/templates/python/examples/fastapi-app/.claude/commands/auth.md +0 -775
- package/templates/python/examples/fastapi-app/.claude/commands/database.md +0 -657
- package/templates/python/examples/fastapi-app/.claude/commands/deployment.md +0 -160
- package/templates/python/examples/fastapi-app/.claude/commands/testing.md +0 -927
- package/templates/python/examples/fastapi-app/CLAUDE.md +0 -229
- package/templates/python/examples/flask-app/.claude/commands/app-factory.md +0 -384
- package/templates/python/examples/flask-app/.claude/commands/blueprint.md +0 -243
- package/templates/python/examples/flask-app/.claude/commands/database.md +0 -410
- package/templates/python/examples/flask-app/.claude/commands/deployment.md +0 -620
- package/templates/python/examples/flask-app/.claude/commands/flask-route.md +0 -217
- package/templates/python/examples/flask-app/.claude/commands/testing.md +0 -559
- package/templates/python/examples/flask-app/CLAUDE.md +0 -391
- package/templates/ruby/.claude/commands/model.md +0 -360
- package/templates/ruby/.claude/commands/test.md +0 -480
- package/templates/ruby/.claude/settings.json +0 -146
- package/templates/ruby/.mcp.json +0 -83
- package/templates/ruby/CLAUDE.md +0 -284
- package/templates/ruby/examples/rails-app/.claude/commands/authentication.md +0 -490
- package/templates/ruby/examples/rails-app/CLAUDE.md +0 -376
- package/templates/rust/.mcp.json +0 -78
- 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>
|