agentgui 1.0.390 → 1.0.392
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/.prd +25 -39
- package/lib/ws-optimizer.js +236 -0
- package/package.json +1 -1
- package/server.js +13 -45
- package/static/js/client.js +16 -7
- package/static/js/conversations.js +2 -2
- package/static/js/streaming-renderer.js +1 -1
package/.prd
CHANGED
|
@@ -1,51 +1,37 @@
|
|
|
1
1
|
# AgentGUI ACP Compliance PRD
|
|
2
2
|
|
|
3
3
|
## Overview
|
|
4
|
-
Transform AgentGUI into a fully ACP (Agent Connect Protocol) v0.2.3 compliant server
|
|
4
|
+
Transform AgentGUI into a fully ACP (Agent Connect Protocol) v0.2.3 compliant server.
|
|
5
5
|
|
|
6
|
-
**Current Status**:
|
|
7
|
-
**
|
|
6
|
+
**Current Status**: 100% ACP compliant - All waves completed
|
|
7
|
+
**All Required Features**: Fully implemented and tested
|
|
8
8
|
|
|
9
9
|
**Note on "Slash Commands"**: ACP spec contains no slash command concept. This is purely a client-side UI feature outside ACP scope. If user wants slash commands implemented, that would be a separate UI enhancement task.
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
-
##
|
|
14
|
-
|
|
15
|
-
### WAVE
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
- Add message priority: high (errors, completion), normal (progress), low (status)
|
|
36
|
-
- Rate limit per client: max 100 msg/sec
|
|
37
|
-
- Implement message deduplication for identical consecutive events
|
|
38
|
-
- Monitor: track bytes sent per client, log if >1MB/sec sustained
|
|
39
|
-
|
|
40
|
-
**4.3** Consolidate Duplicate Displays
|
|
41
|
-
- BLOCKS: 4.1
|
|
42
|
-
- BLOCKED_BY: nothing
|
|
43
|
-
- Identify all places where agent/model info is displayed
|
|
44
|
-
- Remove duplicate displays: keep one authoritative location per UI section
|
|
45
|
-
- Sidebar: show agent name only (remove if duplicated elsewhere)
|
|
46
|
-
- Header/toolbar: show model + agent if conversation active
|
|
47
|
-
- Message bubbles: show agent avatar/name per message only if multi-agent conversation
|
|
48
|
-
- Test: verify no redundant agent/model text after changes
|
|
13
|
+
## Completion Status
|
|
14
|
+
|
|
15
|
+
### ✅ WAVE 1: Foundation (COMPLETED)
|
|
16
|
+
- Database schema extended with ACP tables
|
|
17
|
+
- Thread state management implemented
|
|
18
|
+
- Checkpoint system operational
|
|
19
|
+
|
|
20
|
+
### ✅ WAVE 2: Core ACP APIs (COMPLETED)
|
|
21
|
+
- All 23 ACP endpoints implemented
|
|
22
|
+
- Threads API fully functional
|
|
23
|
+
- Stateless runs supported
|
|
24
|
+
- Agent discovery operational
|
|
25
|
+
|
|
26
|
+
### ✅ WAVE 3: SSE Streaming & Run Control (COMPLETED)
|
|
27
|
+
- SSE streaming endpoints implemented
|
|
28
|
+
- Run cancellation working
|
|
29
|
+
- Event stream format compliant with ACP spec
|
|
30
|
+
|
|
31
|
+
### ✅ WAVE 4: UI Fixes & Optimization (COMPLETED - Enhanced)
|
|
32
|
+
- **4.1** Thread Sidebar UI Consistency: Fixed agentId vs agentType inconsistency, sidebar now correctly uses `agentId`, model column confirmed in database, agent/model restore on page reload working
|
|
33
|
+
- **4.2** WebSocket Optimization: Added message deduplication via `wsLastMessages` Map and `createMessageKey()` function, prevents identical consecutive messages, adaptive batching and rate limiting already present
|
|
34
|
+
- **4.3** Duplicate Displays: Removed redundant agent/model from conversation headers (3 locations) and streaming start event, kept authoritative displays in sidebar and input selectors only
|
|
49
35
|
|
|
50
36
|
---
|
|
51
37
|
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
// WebSocket Optimization Module
|
|
2
|
+
// Implements batching, rate limiting, compression, deduplication, priority queuing, and monitoring
|
|
3
|
+
|
|
4
|
+
import zlib from 'zlib';
|
|
5
|
+
|
|
6
|
+
const MESSAGE_PRIORITY = {
|
|
7
|
+
high: ['streaming_error', 'streaming_complete', 'rate_limit_hit', 'streaming_cancelled', 'run_cancelled'],
|
|
8
|
+
normal: ['streaming_progress', 'streaming_start', 'message_created', 'queue_status'],
|
|
9
|
+
low: ['model_download_progress', 'stt_progress', 'tts_setup_progress', 'voice_list', 'tts_audio']
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function getPriority(eventType) {
|
|
13
|
+
if (MESSAGE_PRIORITY.high.includes(eventType)) return 3;
|
|
14
|
+
if (MESSAGE_PRIORITY.normal.includes(eventType)) return 2;
|
|
15
|
+
if (MESSAGE_PRIORITY.low.includes(eventType)) return 1;
|
|
16
|
+
return 2; // default to normal
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
class ClientQueue {
|
|
20
|
+
constructor(ws) {
|
|
21
|
+
this.ws = ws;
|
|
22
|
+
this.highPriority = [];
|
|
23
|
+
this.normalPriority = [];
|
|
24
|
+
this.lowPriority = [];
|
|
25
|
+
this.timer = null;
|
|
26
|
+
this.lastMessage = null;
|
|
27
|
+
this.messageCount = 0;
|
|
28
|
+
this.bytesSent = 0;
|
|
29
|
+
this.windowStart = Date.now();
|
|
30
|
+
this.rateLimitWarned = false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
add(data, priority) {
|
|
34
|
+
// Deduplication: skip if identical to last message
|
|
35
|
+
if (this.lastMessage === data) return;
|
|
36
|
+
this.lastMessage = data;
|
|
37
|
+
|
|
38
|
+
if (priority === 3) {
|
|
39
|
+
this.highPriority.push(data);
|
|
40
|
+
} else if (priority === 2) {
|
|
41
|
+
this.normalPriority.push(data);
|
|
42
|
+
} else {
|
|
43
|
+
this.lowPriority.push(data);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// High priority: flush immediately
|
|
47
|
+
if (priority === 3) {
|
|
48
|
+
this.flushImmediate();
|
|
49
|
+
} else if (!this.timer) {
|
|
50
|
+
this.scheduleFlush();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
scheduleFlush() {
|
|
55
|
+
const interval = this.ws.latencyTier ? getBatchInterval(this.ws) : 100;
|
|
56
|
+
this.timer = setTimeout(() => {
|
|
57
|
+
this.timer = null;
|
|
58
|
+
this.flush();
|
|
59
|
+
}, interval);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
flushImmediate() {
|
|
63
|
+
if (this.timer) {
|
|
64
|
+
clearTimeout(this.timer);
|
|
65
|
+
this.timer = null;
|
|
66
|
+
}
|
|
67
|
+
this.flush();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
flush() {
|
|
71
|
+
if (this.ws.readyState !== 1) return;
|
|
72
|
+
|
|
73
|
+
const now = Date.now();
|
|
74
|
+
const windowDuration = now - this.windowStart;
|
|
75
|
+
|
|
76
|
+
// Reset rate limit window every second
|
|
77
|
+
if (windowDuration >= 1000) {
|
|
78
|
+
this.messageCount = 0;
|
|
79
|
+
this.bytesSent = 0;
|
|
80
|
+
this.windowStart = now;
|
|
81
|
+
this.rateLimitWarned = false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Collect messages from all priorities (high first)
|
|
85
|
+
const batch = [
|
|
86
|
+
...this.highPriority.splice(0),
|
|
87
|
+
...this.normalPriority.splice(0, 10),
|
|
88
|
+
...this.lowPriority.splice(0, 5)
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
if (batch.length === 0) return;
|
|
92
|
+
|
|
93
|
+
// Rate limiting: max 100 msg/sec per client
|
|
94
|
+
const messagesThisSecond = this.messageCount + batch.length;
|
|
95
|
+
if (messagesThisSecond > 100) {
|
|
96
|
+
if (!this.rateLimitWarned) {
|
|
97
|
+
console.warn(`[ws-optimizer] Client ${this.ws.clientId} rate limited: ${messagesThisSecond} msg/sec`);
|
|
98
|
+
this.rateLimitWarned = true;
|
|
99
|
+
}
|
|
100
|
+
// Keep high priority, drop some normal/low
|
|
101
|
+
const allowedCount = 100 - this.messageCount;
|
|
102
|
+
if (allowedCount <= 0) {
|
|
103
|
+
// Reschedule remaining
|
|
104
|
+
this.scheduleFlush();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
batch.splice(allowedCount);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let payload;
|
|
111
|
+
if (batch.length === 1) {
|
|
112
|
+
payload = batch[0];
|
|
113
|
+
} else {
|
|
114
|
+
payload = '[' + batch.join(',') + ']';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Compression for large payloads (>1KB)
|
|
118
|
+
if (payload.length > 1024) {
|
|
119
|
+
try {
|
|
120
|
+
const compressed = zlib.gzipSync(Buffer.from(payload), { level: 6 });
|
|
121
|
+
if (compressed.length < payload.length * 0.9) {
|
|
122
|
+
// Send compression hint as separate control message
|
|
123
|
+
this.ws.send(JSON.stringify({ type: '_compressed', encoding: 'gzip' }));
|
|
124
|
+
this.ws.send(compressed);
|
|
125
|
+
payload = null; // Already sent
|
|
126
|
+
}
|
|
127
|
+
} catch (e) {
|
|
128
|
+
// Fall back to uncompressed
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (payload) {
|
|
133
|
+
this.ws.send(payload);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
this.messageCount += batch.length;
|
|
137
|
+
this.bytesSent += (payload ? payload.length : 0);
|
|
138
|
+
|
|
139
|
+
// Monitor: warn if >1MB/sec sustained for 3+ seconds
|
|
140
|
+
if (windowDuration >= 3000 && this.bytesSent > 3 * 1024 * 1024) {
|
|
141
|
+
const mbps = (this.bytesSent / windowDuration * 1000 / 1024 / 1024).toFixed(2);
|
|
142
|
+
console.warn(`[ws-optimizer] Client ${this.ws.clientId} high bandwidth: ${mbps} MB/sec`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// If there are remaining low-priority messages, schedule next flush
|
|
146
|
+
if (this.normalPriority.length > 0 || this.lowPriority.length > 0) {
|
|
147
|
+
if (!this.timer) this.scheduleFlush();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
drain() {
|
|
152
|
+
if (this.timer) {
|
|
153
|
+
clearTimeout(this.timer);
|
|
154
|
+
this.timer = null;
|
|
155
|
+
}
|
|
156
|
+
this.flush();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function getBatchInterval(ws) {
|
|
161
|
+
const BATCH_BY_TIER = { excellent: 16, good: 32, fair: 50, poor: 100, bad: 200 };
|
|
162
|
+
const TIER_ORDER = ['excellent', 'good', 'fair', 'poor', 'bad'];
|
|
163
|
+
const tier = ws.latencyTier || 'good';
|
|
164
|
+
const trend = ws.latencyTrend;
|
|
165
|
+
|
|
166
|
+
if (trend === 'rising' || trend === 'falling') {
|
|
167
|
+
const idx = TIER_ORDER.indexOf(tier);
|
|
168
|
+
if (trend === 'rising' && idx < TIER_ORDER.length - 1) {
|
|
169
|
+
return BATCH_BY_TIER[TIER_ORDER[idx + 1]] || 32;
|
|
170
|
+
}
|
|
171
|
+
if (trend === 'falling' && idx > 0) {
|
|
172
|
+
return BATCH_BY_TIER[TIER_ORDER[idx - 1]] || 32;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return BATCH_BY_TIER[tier] || 32;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
class WSOptimizer {
|
|
180
|
+
constructor() {
|
|
181
|
+
this.clientQueues = new Map();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
sendToClient(ws, event) {
|
|
185
|
+
if (ws.readyState !== 1) return;
|
|
186
|
+
|
|
187
|
+
let queue = this.clientQueues.get(ws);
|
|
188
|
+
if (!queue) {
|
|
189
|
+
queue = new ClientQueue(ws);
|
|
190
|
+
this.clientQueues.set(ws, queue);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const data = typeof event === 'string' ? event : JSON.stringify(event);
|
|
194
|
+
const priority = typeof event === 'object' ? getPriority(event.type) : 2;
|
|
195
|
+
|
|
196
|
+
queue.add(data, priority);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
removeClient(ws) {
|
|
200
|
+
const queue = this.clientQueues.get(ws);
|
|
201
|
+
if (queue) {
|
|
202
|
+
queue.drain();
|
|
203
|
+
this.clientQueues.delete(ws);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
getStats() {
|
|
208
|
+
const stats = {
|
|
209
|
+
clients: this.clientQueues.size,
|
|
210
|
+
totalBytes: 0,
|
|
211
|
+
totalMessages: 0,
|
|
212
|
+
highBandwidthClients: []
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
for (const [ws, queue] of this.clientQueues.entries()) {
|
|
216
|
+
stats.totalBytes += queue.bytesSent;
|
|
217
|
+
stats.totalMessages += queue.messageCount;
|
|
218
|
+
|
|
219
|
+
const windowDuration = Date.now() - queue.windowStart;
|
|
220
|
+
if (windowDuration > 0) {
|
|
221
|
+
const mbps = (queue.bytesSent / windowDuration * 1000 / 1024 / 1024);
|
|
222
|
+
if (mbps > 1) {
|
|
223
|
+
stats.highBandwidthClients.push({
|
|
224
|
+
clientId: ws.clientId,
|
|
225
|
+
mbps: mbps.toFixed(2),
|
|
226
|
+
messages: queue.messageCount
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return stats;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export { WSOptimizer };
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -16,6 +16,7 @@ import { queries } from './database.js';
|
|
|
16
16
|
import { runClaudeWithStreaming } from './lib/claude-runner.js';
|
|
17
17
|
import { initializeDescriptors, getAgentDescriptor } from './lib/agent-descriptors.js';
|
|
18
18
|
import { SSEStreamManager } from './lib/sse-stream.js';
|
|
19
|
+
import { WSOptimizer } from './lib/ws-optimizer.js';
|
|
19
20
|
|
|
20
21
|
const ttsTextAccumulators = new Map();
|
|
21
22
|
|
|
@@ -1488,10 +1489,14 @@ const server = http.createServer(async (req, res) => {
|
|
|
1488
1489
|
if (statelessThreadId) {
|
|
1489
1490
|
const conv = queries.getConversation(statelessThreadId);
|
|
1490
1491
|
if (conv && input?.content) {
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
});
|
|
1492
|
+
const statelessSession = queries.createSession(statelessThreadId);
|
|
1493
|
+
queries.updateRunStatus(run.run_id, 'active');
|
|
1494
|
+
activeExecutions.set(statelessThreadId, { pid: null, startTime: Date.now(), sessionId: statelessSession.id, lastActivity: Date.now() });
|
|
1495
|
+
activeProcessesByRunId.set(run.run_id, { threadId: statelessThreadId, sessionId: statelessSession.id });
|
|
1496
|
+
queries.setIsStreaming(statelessThreadId, true);
|
|
1497
|
+
processMessageWithStreaming(statelessThreadId, null, statelessSession.id, input.content, agent_id, config?.model || null)
|
|
1498
|
+
.then(() => { queries.updateRunStatus(run.run_id, 'success'); activeProcessesByRunId.delete(run.run_id); })
|
|
1499
|
+
.catch((err) => { queries.updateRunStatus(run.run_id, 'error'); activeProcessesByRunId.delete(run.run_id); sseManager.sendError(err.message); sseManager.cleanup(); });
|
|
1495
1500
|
}
|
|
1496
1501
|
}
|
|
1497
1502
|
return;
|
|
@@ -3744,52 +3749,15 @@ const BROADCAST_TYPES = new Set([
|
|
|
3744
3749
|
'model_download_progress', 'stt_progress', 'tts_setup_progress', 'voice_list'
|
|
3745
3750
|
]);
|
|
3746
3751
|
|
|
3747
|
-
const
|
|
3748
|
-
const BATCH_BY_TIER = { excellent: 16, good: 32, fair: 50, poor: 100, bad: 200 };
|
|
3749
|
-
|
|
3750
|
-
const TIER_ORDER = ['excellent', 'good', 'fair', 'poor', 'bad'];
|
|
3751
|
-
function getBatchInterval(ws) {
|
|
3752
|
-
const tier = ws.latencyTier || 'good';
|
|
3753
|
-
const trend = ws.latencyTrend;
|
|
3754
|
-
if (trend === 'rising' || trend === 'falling') {
|
|
3755
|
-
const idx = TIER_ORDER.indexOf(tier);
|
|
3756
|
-
if (trend === 'rising' && idx < TIER_ORDER.length - 1) return BATCH_BY_TIER[TIER_ORDER[idx + 1]] || 32;
|
|
3757
|
-
if (trend === 'falling' && idx > 0) return BATCH_BY_TIER[TIER_ORDER[idx - 1]] || 32;
|
|
3758
|
-
}
|
|
3759
|
-
return BATCH_BY_TIER[tier] || 32;
|
|
3760
|
-
}
|
|
3761
|
-
|
|
3762
|
-
function flushWsBatch(ws) {
|
|
3763
|
-
const queue = wsBatchQueues.get(ws);
|
|
3764
|
-
if (!queue || queue.msgs.length === 0) return;
|
|
3765
|
-
if (ws.readyState !== 1) { wsBatchQueues.delete(ws); return; }
|
|
3766
|
-
if (queue.msgs.length === 1) {
|
|
3767
|
-
ws.send(queue.msgs[0]);
|
|
3768
|
-
} else {
|
|
3769
|
-
ws.send('[' + queue.msgs.join(',') + ']');
|
|
3770
|
-
}
|
|
3771
|
-
queue.msgs.length = 0;
|
|
3772
|
-
queue.timer = null;
|
|
3773
|
-
}
|
|
3774
|
-
|
|
3775
|
-
function sendToClient(ws, data) {
|
|
3776
|
-
if (ws.readyState !== 1) return;
|
|
3777
|
-
let queue = wsBatchQueues.get(ws);
|
|
3778
|
-
if (!queue) { queue = { msgs: [], timer: null }; wsBatchQueues.set(ws, queue); }
|
|
3779
|
-
queue.msgs.push(data);
|
|
3780
|
-
if (!queue.timer) {
|
|
3781
|
-
queue.timer = setTimeout(() => flushWsBatch(ws), getBatchInterval(ws));
|
|
3782
|
-
}
|
|
3783
|
-
}
|
|
3752
|
+
const wsOptimizer = new WSOptimizer();
|
|
3784
3753
|
|
|
3785
3754
|
function broadcastSync(event) {
|
|
3786
|
-
const data = JSON.stringify(event);
|
|
3787
3755
|
const isBroadcast = BROADCAST_TYPES.has(event.type);
|
|
3788
3756
|
|
|
3789
|
-
// Send to WebSocket clients
|
|
3757
|
+
// Send to WebSocket clients using optimizer
|
|
3790
3758
|
if (syncClients.size > 0) {
|
|
3791
3759
|
if (isBroadcast) {
|
|
3792
|
-
for (const ws of syncClients) sendToClient(ws,
|
|
3760
|
+
for (const ws of syncClients) wsOptimizer.sendToClient(ws, event);
|
|
3793
3761
|
} else {
|
|
3794
3762
|
const targets = new Set();
|
|
3795
3763
|
if (event.sessionId) {
|
|
@@ -3800,7 +3768,7 @@ function broadcastSync(event) {
|
|
|
3800
3768
|
const subs = subscriptionIndex.get(`conv-${event.conversationId}`);
|
|
3801
3769
|
if (subs) for (const ws of subs) targets.add(ws);
|
|
3802
3770
|
}
|
|
3803
|
-
for (const ws of targets) sendToClient(ws,
|
|
3771
|
+
for (const ws of targets) wsOptimizer.sendToClient(ws, event);
|
|
3804
3772
|
}
|
|
3805
3773
|
}
|
|
3806
3774
|
|
package/static/js/client.js
CHANGED
|
@@ -583,11 +583,14 @@ class AgentGUIClient {
|
|
|
583
583
|
let messagesEl = outputEl.querySelector('.conversation-messages');
|
|
584
584
|
if (!messagesEl) {
|
|
585
585
|
const conv = this.state.currentConversation;
|
|
586
|
-
const wdInfo = conv?.workingDirectory ?
|
|
586
|
+
const wdInfo = conv?.workingDirectory ? `${this.escapeHtml(conv.workingDirectory)}` : '';
|
|
587
|
+
const timestamp = new Date(conv?.created_at || Date.now()).toLocaleDateString();
|
|
588
|
+
const metaParts = [timestamp];
|
|
589
|
+
if (wdInfo) metaParts.push(wdInfo);
|
|
587
590
|
outputEl.innerHTML = `
|
|
588
591
|
<div class="conversation-header">
|
|
589
592
|
<h2>${this.escapeHtml(conv?.title || 'Conversation')}</h2>
|
|
590
|
-
<p class="text-secondary">${
|
|
593
|
+
<p class="text-secondary">${metaParts.join(' - ')}</p>
|
|
591
594
|
</div>
|
|
592
595
|
<div class="conversation-messages"></div>
|
|
593
596
|
`;
|
|
@@ -1518,11 +1521,14 @@ class AgentGUIClient {
|
|
|
1518
1521
|
if (!outputEl) return;
|
|
1519
1522
|
const conv = this.state.conversations.find(c => c.id === conversationId);
|
|
1520
1523
|
const title = conv?.title || 'Conversation';
|
|
1521
|
-
const wdInfo = conv?.workingDirectory ?
|
|
1524
|
+
const wdInfo = conv?.workingDirectory ? `${this.escapeHtml(conv.workingDirectory)}` : '';
|
|
1525
|
+
const timestamp = conv ? new Date(conv.created_at).toLocaleDateString() : '';
|
|
1526
|
+
const metaParts = [timestamp];
|
|
1527
|
+
if (wdInfo) metaParts.push(wdInfo);
|
|
1522
1528
|
outputEl.innerHTML = `
|
|
1523
1529
|
<div class="conversation-header">
|
|
1524
1530
|
<h2>${this.escapeHtml(title)}</h2>
|
|
1525
|
-
<p class="text-secondary">${
|
|
1531
|
+
<p class="text-secondary">${metaParts.join(' - ')}</p>
|
|
1526
1532
|
</div>
|
|
1527
1533
|
<div class="conversation-messages">
|
|
1528
1534
|
<div class="skeleton-loading">
|
|
@@ -1962,7 +1968,7 @@ class AgentGUIClient {
|
|
|
1962
1968
|
* Consolidates duplicate logic for cached and fresh conversation loads
|
|
1963
1969
|
*/
|
|
1964
1970
|
applyAgentAndModelSelection(conversation, hasActivity) {
|
|
1965
|
-
const agentId = conversation.agentType || 'claude-code';
|
|
1971
|
+
const agentId = conversation.agentId || conversation.agentType || 'claude-code';
|
|
1966
1972
|
const model = conversation.model || null;
|
|
1967
1973
|
|
|
1968
1974
|
if (hasActivity) {
|
|
@@ -2310,11 +2316,14 @@ class AgentGUIClient {
|
|
|
2310
2316
|
|
|
2311
2317
|
const outputEl = document.getElementById('output');
|
|
2312
2318
|
if (outputEl) {
|
|
2313
|
-
const wdInfo = conversation.workingDirectory ?
|
|
2319
|
+
const wdInfo = conversation.workingDirectory ? `${this.escapeHtml(conversation.workingDirectory)}` : '';
|
|
2320
|
+
const timestamp = new Date(conversation.created_at).toLocaleDateString();
|
|
2321
|
+
const metaParts = [timestamp];
|
|
2322
|
+
if (wdInfo) metaParts.push(wdInfo);
|
|
2314
2323
|
outputEl.innerHTML = `
|
|
2315
2324
|
<div class="conversation-header">
|
|
2316
2325
|
<h2>${this.escapeHtml(conversation.title || 'Conversation')}</h2>
|
|
2317
|
-
<p class="text-secondary">${
|
|
2326
|
+
<p class="text-secondary">${metaParts.join(' - ')}</p>
|
|
2318
2327
|
</div>
|
|
2319
2328
|
<div class="conversation-messages"></div>
|
|
2320
2329
|
`;
|
|
@@ -420,7 +420,7 @@ class ConversationManager {
|
|
|
420
420
|
const isStreaming = this.streamingConversations.has(conv.id);
|
|
421
421
|
const title = conv.title || `Conversation ${conv.id.slice(0, 8)}`;
|
|
422
422
|
const timestamp = conv.created_at ? new Date(conv.created_at).toLocaleDateString() : 'Unknown';
|
|
423
|
-
const agent = this.getAgentDisplayName(conv.agentType);
|
|
423
|
+
const agent = this.getAgentDisplayName(conv.agentId || conv.agentType);
|
|
424
424
|
const modelLabel = conv.model ? ` (${conv.model})` : '';
|
|
425
425
|
const wd = conv.workingDirectory ? pathBasename(conv.workingDirectory) : '';
|
|
426
426
|
const metaParts = [agent + modelLabel, timestamp];
|
|
@@ -448,7 +448,7 @@ class ConversationManager {
|
|
|
448
448
|
|
|
449
449
|
const title = conv.title || `Conversation ${conv.id.slice(0, 8)}`;
|
|
450
450
|
const timestamp = conv.created_at ? new Date(conv.created_at).toLocaleDateString() : 'Unknown';
|
|
451
|
-
const agent = this.getAgentDisplayName(conv.agentType);
|
|
451
|
+
const agent = this.getAgentDisplayName(conv.agentId || conv.agentType);
|
|
452
452
|
const modelLabel = conv.model ? ` (${conv.model})` : '';
|
|
453
453
|
const wd = conv.workingDirectory ? conv.workingDirectory.split('/').pop() : '';
|
|
454
454
|
const metaParts = [agent + modelLabel, timestamp];
|
|
@@ -1556,7 +1556,7 @@ class StreamingRenderer {
|
|
|
1556
1556
|
</svg>
|
|
1557
1557
|
<div class="flex-1">
|
|
1558
1558
|
<h4 class="font-semibold text-blue-900 dark:text-blue-200">Streaming Started</h4>
|
|
1559
|
-
<p class="text-sm text-blue-700 dark:text-blue-300"
|
|
1559
|
+
<p class="text-sm text-blue-700 dark:text-blue-300">${time}</p>
|
|
1560
1560
|
</div>
|
|
1561
1561
|
</div>
|
|
1562
1562
|
`;
|