agentgui 1.0.672 → 1.0.674
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/lib/tool-manager.js +71 -0
- package/lib/ws-handlers-conv.js +5 -3
- package/lib/ws-handlers-run.js +26 -0
- package/package.json +1 -1
- package/server.js +94 -10
- package/static/js/client.js +35 -23
- package/static/js/conversations.js +4 -1
- package/static/js/features.js +1 -0
- package/static/js/terminal.js +55 -83
package/lib/tool-manager.js
CHANGED
|
@@ -546,3 +546,74 @@ export async function autoProvision(broadcast) {
|
|
|
546
546
|
}
|
|
547
547
|
log('Provisioning complete');
|
|
548
548
|
}
|
|
549
|
+
|
|
550
|
+
// Periodic tool update checker - runs in background every 6 hours
|
|
551
|
+
let updateCheckInterval = null;
|
|
552
|
+
const UPDATE_CHECK_INTERVAL = 6 * 60 * 60 * 1000; // 6 hours
|
|
553
|
+
|
|
554
|
+
export function startPeriodicUpdateCheck(broadcast) {
|
|
555
|
+
const log = (msg) => console.log('[TOOLS-PERIODIC] ' + msg);
|
|
556
|
+
|
|
557
|
+
if (updateCheckInterval) {
|
|
558
|
+
log('Update check already running');
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
log('Starting periodic tool update checker (every 6 hours)');
|
|
563
|
+
|
|
564
|
+
// Run check immediately on startup (non-blocking)
|
|
565
|
+
setImmediate(() => {
|
|
566
|
+
checkAndUpdateTools(broadcast).catch(err => {
|
|
567
|
+
log(`Initial check failed: ${err.message}`);
|
|
568
|
+
});
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
// Then run periodically every 6 hours
|
|
572
|
+
updateCheckInterval = setInterval(() => {
|
|
573
|
+
checkAndUpdateTools(broadcast).catch(err => {
|
|
574
|
+
log(`Periodic check failed: ${err.message}`);
|
|
575
|
+
});
|
|
576
|
+
}, UPDATE_CHECK_INTERVAL);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
export function stopPeriodicUpdateCheck() {
|
|
580
|
+
if (updateCheckInterval) {
|
|
581
|
+
clearInterval(updateCheckInterval);
|
|
582
|
+
updateCheckInterval = null;
|
|
583
|
+
console.log('[TOOLS-PERIODIC] Update check stopped');
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
async function checkAndUpdateTools(broadcast) {
|
|
588
|
+
const log = (msg) => console.log('[TOOLS-PERIODIC] ' + msg);
|
|
589
|
+
log('Checking for tool updates...');
|
|
590
|
+
|
|
591
|
+
for (const tool of TOOLS) {
|
|
592
|
+
try {
|
|
593
|
+
const status = await checkToolViaBunx(tool.pkg, tool.pluginId, tool.category, tool.frameWork, false);
|
|
594
|
+
|
|
595
|
+
if (status.upgradeNeeded) {
|
|
596
|
+
log(`Update available for ${tool.id}: ${status.installedVersion} -> ${status.publishedVersion}`);
|
|
597
|
+
broadcast({ type: 'tool_update_available', toolId: tool.id, data: { installedVersion: status.installedVersion, publishedVersion: status.publishedVersion } });
|
|
598
|
+
|
|
599
|
+
// Auto-update in background (non-blocking)
|
|
600
|
+
log(`Auto-updating ${tool.id}...`);
|
|
601
|
+
const result = await update(tool.id, (msg) => {
|
|
602
|
+
broadcast({ type: 'tool_update_progress', toolId: tool.id, data: msg });
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
if (result.success) {
|
|
606
|
+
log(`${tool.id} auto-updated to v${result.version}`);
|
|
607
|
+
broadcast({ type: 'tool_update_complete', toolId: tool.id, data: { ...result, autoUpdated: true } });
|
|
608
|
+
} else {
|
|
609
|
+
log(`${tool.id} auto-update failed: ${result.error}`);
|
|
610
|
+
broadcast({ type: 'tool_update_failed', toolId: tool.id, data: { ...result, autoUpdated: true } });
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
} catch (err) {
|
|
614
|
+
log(`Error checking ${tool.id}: ${err.message}`);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
log('Update check complete');
|
|
619
|
+
}
|
package/lib/ws-handlers-conv.js
CHANGED
|
@@ -7,7 +7,7 @@ function expandTilde(p) { return p && p.startsWith('~') ? path.join(os.homedir()
|
|
|
7
7
|
|
|
8
8
|
export function register(router, deps) {
|
|
9
9
|
const { queries, activeExecutions, messageQueues, rateLimitState,
|
|
10
|
-
broadcastSync, processMessageWithStreaming, activeProcessesByConvId, steeringTimeouts } = deps;
|
|
10
|
+
broadcastSync, processMessageWithStreaming, activeProcessesByConvId, steeringTimeouts, cleanupExecution } = deps;
|
|
11
11
|
|
|
12
12
|
router.handle('conv.ls', () => {
|
|
13
13
|
const conversations = queries.getConversationsList();
|
|
@@ -95,8 +95,10 @@ export function register(router, deps) {
|
|
|
95
95
|
const { pid, sessionId } = entry;
|
|
96
96
|
if (pid) { try { process.kill(-pid, 'SIGKILL'); } catch { try { process.kill(pid, 'SIGKILL'); } catch {} } }
|
|
97
97
|
if (sessionId) queries.updateSession(sessionId, { status: 'interrupted', completed_at: Date.now() });
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
|
|
99
|
+
// Use atomic cleanup function to ensure state consistency
|
|
100
|
+
cleanupExecution(p.id, false);
|
|
101
|
+
|
|
100
102
|
broadcastSync({ type: 'streaming_complete', sessionId, conversationId: p.id, interrupted: true, timestamp: Date.now() });
|
|
101
103
|
return { ok: true, cancelled: true, conversationId: p.id, sessionId };
|
|
102
104
|
});
|
package/lib/ws-handlers-run.js
CHANGED
|
@@ -152,6 +152,32 @@ function register(router, deps) {
|
|
|
152
152
|
if (!run || run.thread_id !== threadId) err(404, 'Run not found on thread');
|
|
153
153
|
return run;
|
|
154
154
|
});
|
|
155
|
+
|
|
156
|
+
router.handle('thread.run.steer', async (p) => {
|
|
157
|
+
const threadId = need(p, 'id'), runId = need(p, 'runId'), instruction = need(p, 'instruction');
|
|
158
|
+
const thread = getThreadOrThrow(threadId);
|
|
159
|
+
const run = getRunOrThrow(runId);
|
|
160
|
+
if (run.thread_id !== threadId) err(400, 'Run does not belong to specified thread');
|
|
161
|
+
if (!['active', 'pending'].includes(run.status)) err(409, 'Run is not active or pending');
|
|
162
|
+
|
|
163
|
+
// Cancel current run
|
|
164
|
+
const cancelled = queries.cancelRun(runId);
|
|
165
|
+
const ex = killExecution(threadId, runId);
|
|
166
|
+
broadcastSync({ type: 'run_cancelled', runId, threadId, sessionId: ex?.sessionId, timestamp: Date.now() });
|
|
167
|
+
|
|
168
|
+
// Create new run with steering instruction injected
|
|
169
|
+
// The instruction will be processed as a follow-up in the same thread context
|
|
170
|
+
const newRun = queries.createRun(run.agent_id, threadId, { content: instruction }, run.config);
|
|
171
|
+
startExecution(newRun.run_id, threadId, run.agent_id, { content: instruction }, run.config);
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
steered: true,
|
|
175
|
+
cancelled_run: cancelled,
|
|
176
|
+
new_run: newRun,
|
|
177
|
+
instruction: instruction,
|
|
178
|
+
message: 'Run interrupted and restarted with steering instruction'
|
|
179
|
+
};
|
|
180
|
+
});
|
|
155
181
|
}
|
|
156
182
|
|
|
157
183
|
export { register };
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -311,6 +311,45 @@ const debugLog = (msg) => {
|
|
|
311
311
|
console.error(`[${timestamp}] ${msg}`);
|
|
312
312
|
};
|
|
313
313
|
|
|
314
|
+
// Atomic cleanup function - ensures consistent state across all data structures
|
|
315
|
+
function cleanupExecution(conversationId, broadcastCompletion = false) {
|
|
316
|
+
debugLog(`[cleanup] Starting cleanup for ${conversationId}`);
|
|
317
|
+
|
|
318
|
+
// Clean in-memory maps in atomic block
|
|
319
|
+
activeExecutions.delete(conversationId);
|
|
320
|
+
const proc = activeProcessesByConvId.get(conversationId);
|
|
321
|
+
activeProcessesByConvId.delete(conversationId);
|
|
322
|
+
|
|
323
|
+
// Cancel steering timeout if present
|
|
324
|
+
const steeringTimeout = steeringTimeouts.get(conversationId);
|
|
325
|
+
if (steeringTimeout) {
|
|
326
|
+
clearTimeout(steeringTimeout);
|
|
327
|
+
steeringTimeouts.delete(conversationId);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Try to kill process if still alive
|
|
331
|
+
if (proc) {
|
|
332
|
+
try {
|
|
333
|
+
if (proc.kill) proc.kill('SIGTERM');
|
|
334
|
+
} catch (e) {
|
|
335
|
+
debugLog(`[cleanup] Error killing process: ${e.message}`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Clean database state
|
|
340
|
+
queries.setIsStreaming(conversationId, false);
|
|
341
|
+
|
|
342
|
+
if (broadcastCompletion) {
|
|
343
|
+
broadcastSync({
|
|
344
|
+
type: 'execution_cleaned_up',
|
|
345
|
+
conversationId,
|
|
346
|
+
timestamp: Date.now()
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
debugLog(`[cleanup] Cleanup complete for ${conversationId}`);
|
|
351
|
+
}
|
|
352
|
+
|
|
314
353
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
315
354
|
const rootDir = process.env.PORTABLE_EXE_DIR || __dirname;
|
|
316
355
|
const PORT = process.env.PORT || 3000;
|
|
@@ -3237,7 +3276,7 @@ function serveFile(filePath, res, req) {
|
|
|
3237
3276
|
'Content-Type': contentType,
|
|
3238
3277
|
'Content-Length': stats.size,
|
|
3239
3278
|
'ETag': etag,
|
|
3240
|
-
'Cache-Control': ['.js', '.css'].includes(ext) ? '
|
|
3279
|
+
'Cache-Control': ['.js', '.css'].includes(ext) ? 'public, max-age=0, must-revalidate' : 'public, max-age=3600, must-revalidate'
|
|
3241
3280
|
};
|
|
3242
3281
|
if (acceptsEncoding(req, 'br') && stats.size > 860) {
|
|
3243
3282
|
const stream = fs.createReadStream(filePath);
|
|
@@ -3832,10 +3871,18 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
|
|
|
3832
3871
|
});
|
|
3833
3872
|
} finally {
|
|
3834
3873
|
batcher.drain();
|
|
3835
|
-
|
|
3874
|
+
// Use atomic cleanup but only if not in rate limit recovery
|
|
3836
3875
|
if (!rateLimitState.has(conversationId)) {
|
|
3837
|
-
|
|
3876
|
+
cleanupExecution(conversationId);
|
|
3838
3877
|
drainMessageQueue(conversationId);
|
|
3878
|
+
} else {
|
|
3879
|
+
// Rate limit in flight - keep execution entry for now, but clean process handle
|
|
3880
|
+
activeProcessesByConvId.delete(conversationId);
|
|
3881
|
+
const steeringTimeout = steeringTimeouts.get(conversationId);
|
|
3882
|
+
if (steeringTimeout) {
|
|
3883
|
+
clearTimeout(steeringTimeout);
|
|
3884
|
+
steeringTimeouts.delete(conversationId);
|
|
3885
|
+
}
|
|
3839
3886
|
}
|
|
3840
3887
|
}
|
|
3841
3888
|
}
|
|
@@ -3871,6 +3918,16 @@ function scheduleRetry(conversationId, messageId, content, agentId, model, subAg
|
|
|
3871
3918
|
.catch(err => {
|
|
3872
3919
|
debugLog(`[rate-limit] Retry failed: ${err.message}`);
|
|
3873
3920
|
console.error(`[rate-limit] Retry error for conv ${conversationId}:`, err);
|
|
3921
|
+
// Clean up state on retry failure
|
|
3922
|
+
cleanupExecution(conversationId);
|
|
3923
|
+
broadcastSync({
|
|
3924
|
+
type: 'streaming_error',
|
|
3925
|
+
sessionId: newSession.id,
|
|
3926
|
+
conversationId,
|
|
3927
|
+
error: `Rate limit retry failed: ${err.message}`,
|
|
3928
|
+
recoverable: false,
|
|
3929
|
+
timestamp: Date.now()
|
|
3930
|
+
});
|
|
3874
3931
|
});
|
|
3875
3932
|
}
|
|
3876
3933
|
|
|
@@ -3915,7 +3972,21 @@ function drainMessageQueue(conversationId) {
|
|
|
3915
3972
|
activeExecutions.set(conversationId, { pid: null, startTime, sessionId: session.id, lastActivity: startTime });
|
|
3916
3973
|
|
|
3917
3974
|
processMessageWithStreaming(conversationId, next.messageId, session.id, next.content, next.agentId, next.model, next.subAgent)
|
|
3918
|
-
.catch(err =>
|
|
3975
|
+
.catch(err => {
|
|
3976
|
+
debugLog(`[queue] Error processing queued message: ${err.message}`);
|
|
3977
|
+
// CRITICAL: Clean up state on error so next message can be retried
|
|
3978
|
+
cleanupExecution(conversationId);
|
|
3979
|
+
broadcastSync({
|
|
3980
|
+
type: 'streaming_error',
|
|
3981
|
+
sessionId: session.id,
|
|
3982
|
+
conversationId,
|
|
3983
|
+
error: `Queue processing failed: ${err.message}`,
|
|
3984
|
+
recoverable: true,
|
|
3985
|
+
timestamp: Date.now()
|
|
3986
|
+
});
|
|
3987
|
+
// Try to drain next message in queue
|
|
3988
|
+
setTimeout(() => drainMessageQueue(conversationId), 100);
|
|
3989
|
+
});
|
|
3919
3990
|
}
|
|
3920
3991
|
|
|
3921
3992
|
|
|
@@ -4026,7 +4097,8 @@ const wsRouter = new WsRouter();
|
|
|
4026
4097
|
|
|
4027
4098
|
registerConvHandlers(wsRouter, {
|
|
4028
4099
|
queries, activeExecutions, messageQueues, rateLimitState,
|
|
4029
|
-
broadcastSync, processMessageWithStreaming, activeProcessesByConvId, steeringTimeouts
|
|
4100
|
+
broadcastSync, processMessageWithStreaming, activeProcessesByConvId, steeringTimeouts,
|
|
4101
|
+
cleanupExecution
|
|
4030
4102
|
});
|
|
4031
4103
|
|
|
4032
4104
|
console.log('[INIT] About to call registerSessionHandlers, discoveredAgents.length:', discoveredAgents.length);
|
|
@@ -4151,7 +4223,8 @@ wsRouter.onLegacy((data, ws) => {
|
|
|
4151
4223
|
try { ws.terminalProc.kill(); } catch(e) {}
|
|
4152
4224
|
}
|
|
4153
4225
|
try {
|
|
4154
|
-
const
|
|
4226
|
+
const _req = createRequire(import.meta.url);
|
|
4227
|
+
const pty = _req('node-pty');
|
|
4155
4228
|
const shell = process.env.SHELL || '/bin/bash';
|
|
4156
4229
|
const cwd = data.cwd || process.env.STARTUP_CWD || process.env.HOME || '/';
|
|
4157
4230
|
const proc = pty.spawn(shell, [], {
|
|
@@ -4178,7 +4251,6 @@ wsRouter.onLegacy((data, ws) => {
|
|
|
4178
4251
|
sendWs(ws, ({ type: 'terminal_started', timestamp: Date.now() }));
|
|
4179
4252
|
} catch (e) {
|
|
4180
4253
|
console.error('[TERMINAL] Failed to spawn PTY, falling back to pipes:', e.message);
|
|
4181
|
-
const { spawn } = require('child_process');
|
|
4182
4254
|
const shell = process.env.SHELL || '/bin/bash';
|
|
4183
4255
|
const cwd = data.cwd || process.env.STARTUP_CWD || process.env.HOME || '/';
|
|
4184
4256
|
const proc = spawn(shell, ['-i'], { cwd, env: { ...process.env, TERM: 'xterm-256color', COLORTERM: 'truecolor' }, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
@@ -4540,10 +4612,12 @@ function onServerReady() {
|
|
|
4540
4612
|
}, 6000);
|
|
4541
4613
|
}).catch(err => console.error('[ACP] Startup error:', err.message));
|
|
4542
4614
|
|
|
4543
|
-
const toolIds = ['cli-claude', 'cli-opencode', 'cli-gemini', 'cli-kilo', 'cli-codex', 'gm-cc', 'gm-oc', 'gm-gc', 'gm-kilo'];
|
|
4615
|
+
const toolIds = ['cli-claude', 'cli-opencode', 'cli-gemini', 'cli-kilo', 'cli-codex', 'cli-agent-browser', 'gm-cc', 'gm-oc', 'gm-gc', 'gm-kilo'];
|
|
4544
4616
|
queries.initializeToolInstallations(toolIds.map(id => ({ id })));
|
|
4545
4617
|
console.log('[TOOLS] Starting background provisioning...');
|
|
4546
|
-
|
|
4618
|
+
|
|
4619
|
+
// Create broadcast handler for tool events
|
|
4620
|
+
const toolBroadcaster = (evt) => {
|
|
4547
4621
|
broadcastSync(evt);
|
|
4548
4622
|
if (evt.type === 'tool_install_complete' || evt.type === 'tool_update_complete') {
|
|
4549
4623
|
const d = evt.data || {};
|
|
@@ -4558,7 +4632,17 @@ function onServerReady() {
|
|
|
4558
4632
|
queries.updateToolStatus(evt.toolId, { status: 'installed', version: d.installedVersion || null, installed_at: Date.now() });
|
|
4559
4633
|
}
|
|
4560
4634
|
}
|
|
4561
|
-
}
|
|
4635
|
+
};
|
|
4636
|
+
|
|
4637
|
+
// Initial provisioning (blocks until complete)
|
|
4638
|
+
toolManager.autoProvision(toolBroadcaster)
|
|
4639
|
+
.catch(err => console.error('[TOOLS] Auto-provision error:', err.message))
|
|
4640
|
+
.then(() => {
|
|
4641
|
+
// Start periodic update checker AFTER initial provisioning completes
|
|
4642
|
+
// This runs in background and doesn't block GUI
|
|
4643
|
+
console.log('[TOOLS] Starting periodic update checker...');
|
|
4644
|
+
toolManager.startPeriodicUpdateCheck(toolBroadcaster);
|
|
4645
|
+
});
|
|
4562
4646
|
|
|
4563
4647
|
ensureModelsDownloaded().then(async ok => {
|
|
4564
4648
|
if (ok) console.log('[MODELS] Speech models ready');
|
package/static/js/client.js
CHANGED
|
@@ -157,6 +157,9 @@ class AgentGUIClient {
|
|
|
157
157
|
this._recoverMissedChunks();
|
|
158
158
|
this.updateSendButtonState();
|
|
159
159
|
this.enablePromptArea();
|
|
160
|
+
if (this.state.currentConversation?.id) {
|
|
161
|
+
this.updateBusyPromptArea(this.state.currentConversation.id);
|
|
162
|
+
}
|
|
160
163
|
this.emit('ws:connected');
|
|
161
164
|
// Check if server was updated while client was loaded - reload if version changed
|
|
162
165
|
if (window.__SERVER_VERSION) {
|
|
@@ -2045,8 +2048,6 @@ class AgentGUIClient {
|
|
|
2045
2048
|
</div>
|
|
2046
2049
|
</div>
|
|
2047
2050
|
`;
|
|
2048
|
-
// Keep loading spinner visible during hydration
|
|
2049
|
-
this.showLoadingSpinner();
|
|
2050
2051
|
}
|
|
2051
2052
|
|
|
2052
2053
|
async streamToConversation(conversationId, prompt, agentId, model, subAgent) {
|
|
@@ -2084,6 +2085,7 @@ class AgentGUIClient {
|
|
|
2084
2085
|
|
|
2085
2086
|
if (result.queued) {
|
|
2086
2087
|
console.log('Message queued, position:', result.queuePosition);
|
|
2088
|
+
this.enableControls();
|
|
2087
2089
|
return;
|
|
2088
2090
|
}
|
|
2089
2091
|
|
|
@@ -2491,22 +2493,20 @@ class AgentGUIClient {
|
|
|
2491
2493
|
}
|
|
2492
2494
|
|
|
2493
2495
|
/**
|
|
2494
|
-
* Disable UI controls during
|
|
2495
|
-
* NOTE: Prompt area is IMMUTABLE - always enabled while connected.
|
|
2496
|
-
* Streaming state is rendered via queue/steer buttons, not input disabling.
|
|
2496
|
+
* Disable UI controls during execution - prevents double-sends
|
|
2497
2497
|
*/
|
|
2498
2498
|
disableControls() {
|
|
2499
|
-
|
|
2500
|
-
// Never disable input during streaming - use queue/steer visibility instead
|
|
2499
|
+
if (this.ui.sendButton) this.ui.sendButton.disabled = true;
|
|
2501
2500
|
}
|
|
2502
2501
|
|
|
2503
2502
|
/**
|
|
2504
|
-
* Enable UI controls
|
|
2505
|
-
* NOTE: Prompt area is always enabled when connected.
|
|
2503
|
+
* Enable UI controls after execution completes or fails
|
|
2506
2504
|
*/
|
|
2507
2505
|
enableControls() {
|
|
2508
|
-
|
|
2509
|
-
|
|
2506
|
+
if (this.ui.sendButton) {
|
|
2507
|
+
this.ui.sendButton.disabled = !this.wsManager?.isConnected;
|
|
2508
|
+
}
|
|
2509
|
+
this.updateBusyPromptArea(this.state.currentConversation?.id);
|
|
2510
2510
|
}
|
|
2511
2511
|
|
|
2512
2512
|
/**
|
|
@@ -2686,10 +2686,6 @@ class AgentGUIClient {
|
|
|
2686
2686
|
this.conversationCache.delete(conversationId);
|
|
2687
2687
|
this.syncPromptState(conversationId);
|
|
2688
2688
|
this.restoreScrollPosition(conversationId);
|
|
2689
|
-
// Hydration complete - hide loading spinner
|
|
2690
|
-
this.hideLoadingSpinner();
|
|
2691
|
-
// Prompt state is immutable: computed from shouldResumeStreaming via syncPromptState
|
|
2692
|
-
// Do not call enableControls/disableControls here - prompt state is determined by streaming status
|
|
2693
2689
|
return;
|
|
2694
2690
|
}
|
|
2695
2691
|
}
|
|
@@ -2700,9 +2696,8 @@ class AgentGUIClient {
|
|
|
2700
2696
|
|
|
2701
2697
|
let fullData;
|
|
2702
2698
|
try {
|
|
2703
|
-
// Load only recent chunks initially (lazy load older ones)
|
|
2704
|
-
// Use chunkLimit of 50 to make page load faster
|
|
2705
2699
|
fullData = await window.wsClient.rpc('conv.full', { id: conversationId, chunkLimit: 50 });
|
|
2700
|
+
if (convSignal.aborted) return;
|
|
2706
2701
|
} catch (e) {
|
|
2707
2702
|
if (e.code === 404) {
|
|
2708
2703
|
console.warn('Conversation no longer exists:', conversationId);
|
|
@@ -2724,6 +2719,7 @@ class AgentGUIClient {
|
|
|
2724
2719
|
}
|
|
2725
2720
|
throw e;
|
|
2726
2721
|
}
|
|
2722
|
+
if (convSignal.aborted) return;
|
|
2727
2723
|
const { conversation, isActivelyStreaming, latestSession, chunks: rawChunks, totalChunks, messages: allMessages } = fullData;
|
|
2728
2724
|
|
|
2729
2725
|
window.ConversationState?.selectConversation(conversationId, 'server_load', 1);
|
|
@@ -2736,18 +2732,32 @@ class AgentGUIClient {
|
|
|
2736
2732
|
...chunk,
|
|
2737
2733
|
block: typeof chunk.data === 'string' ? JSON.parse(chunk.data) : chunk.data
|
|
2738
2734
|
}));
|
|
2739
|
-
|
|
2735
|
+
|
|
2736
|
+
// Fetch queue to exclude queued messages from being rendered as regular user messages
|
|
2737
|
+
let queuedMessageIds = new Set();
|
|
2738
|
+
try {
|
|
2739
|
+
const { queue } = await window.wsClient.rpc('q.ls', { id: conversationId });
|
|
2740
|
+
if (queue && queue.length > 0) {
|
|
2741
|
+
queuedMessageIds = new Set(queue.map(q => q.messageId));
|
|
2742
|
+
}
|
|
2743
|
+
} catch (e) {
|
|
2744
|
+
console.warn('Failed to fetch queue:', e.message);
|
|
2745
|
+
}
|
|
2746
|
+
|
|
2747
|
+
// Filter out queued messages from user messages - they'll be rendered in queue indicator instead
|
|
2748
|
+
const userMessages = (allMessages || []).filter(m => m.role === 'user' && !queuedMessageIds.has(m.id));
|
|
2740
2749
|
const hasMoreChunks = totalChunks && chunks.length < totalChunks;
|
|
2741
2750
|
|
|
2742
2751
|
const clientKnowsStreaming = this.state.streamingConversations.has(conversationId);
|
|
2743
2752
|
const shouldResumeStreaming = latestSession &&
|
|
2744
2753
|
(latestSession.status === 'active' || latestSession.status === 'pending');
|
|
2745
2754
|
|
|
2746
|
-
// IMMUTABLE: Update streaming state and disable prompt atomically
|
|
2747
2755
|
if (shouldResumeStreaming) {
|
|
2748
2756
|
this.state.streamingConversations.set(conversationId, true);
|
|
2757
|
+
window.dispatchEvent(new CustomEvent('ws-message', { detail: { type: 'streaming_start', conversationId, sessionId: latestSession?.id } }));
|
|
2749
2758
|
} else {
|
|
2750
2759
|
this.state.streamingConversations.delete(conversationId);
|
|
2760
|
+
window.dispatchEvent(new CustomEvent('ws-message', { detail: { type: 'streaming_complete', conversationId } }));
|
|
2751
2761
|
}
|
|
2752
2762
|
|
|
2753
2763
|
if (this.ui.messageInput) {
|
|
@@ -2948,12 +2958,13 @@ class AgentGUIClient {
|
|
|
2948
2958
|
|
|
2949
2959
|
this.restoreScrollPosition(conversationId);
|
|
2950
2960
|
this.setupScrollUpDetection(conversationId);
|
|
2951
|
-
|
|
2952
|
-
|
|
2961
|
+
|
|
2962
|
+
// Fetch and display queue items so queued messages show in yellow blocks, not as user messages
|
|
2963
|
+
this.fetchAndRenderQueue(conversationId);
|
|
2964
|
+
|
|
2953
2965
|
}
|
|
2954
2966
|
} catch (error) {
|
|
2955
2967
|
if (error.name === 'AbortError') return;
|
|
2956
|
-
this.hideLoadingSpinner();
|
|
2957
2968
|
console.error('Failed to load conversation messages:', error);
|
|
2958
2969
|
// Resume from last successful conversation if available, or fall back to any available conversation
|
|
2959
2970
|
const fallbackConv = prevConversationId ? prevConversationId : availableFallback?.id;
|
|
@@ -3198,7 +3209,7 @@ class AgentGUIClient {
|
|
|
3198
3209
|
}
|
|
3199
3210
|
|
|
3200
3211
|
saveAgentAndModelToConversation() {
|
|
3201
|
-
const convId = this.state.currentConversation;
|
|
3212
|
+
const convId = this.state.currentConversation?.id;
|
|
3202
3213
|
if (!convId || this._agentLocked) return;
|
|
3203
3214
|
const agentId = this.getEffectiveAgentId();
|
|
3204
3215
|
const subAgent = this.getEffectiveSubAgent();
|
|
@@ -3338,6 +3349,7 @@ let agentGUIClient = null;
|
|
|
3338
3349
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
3339
3350
|
try {
|
|
3340
3351
|
agentGUIClient = new AgentGUIClient();
|
|
3352
|
+
window.agentGuiClient = agentGUIClient;
|
|
3341
3353
|
await agentGUIClient.init();
|
|
3342
3354
|
console.log('AgentGUI ready');
|
|
3343
3355
|
} catch (error) {
|
|
@@ -433,8 +433,11 @@ class ConversationManager {
|
|
|
433
433
|
|
|
434
434
|
this._updateConversations(convList, 'poll');
|
|
435
435
|
|
|
436
|
+
const clientStreamingMap = window.agentGuiClient?.state?.streamingConversations;
|
|
436
437
|
for (const conv of this.conversations) {
|
|
437
|
-
|
|
438
|
+
const serverStreaming = conv.isStreaming === 1 || conv.isStreaming === true;
|
|
439
|
+
const clientStreaming = clientStreamingMap ? clientStreamingMap.has(conv.id) : false;
|
|
440
|
+
if (serverStreaming || clientStreaming) {
|
|
438
441
|
this.streamingConversations.add(conv.id);
|
|
439
442
|
} else {
|
|
440
443
|
this.streamingConversations.delete(conv.id);
|
package/static/js/features.js
CHANGED
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
}
|
|
40
40
|
function closeSidebar() {
|
|
41
41
|
sidebar.classList.remove('mobile-visible');
|
|
42
|
+
sidebar.classList.add('collapsed'); // Ensure sidebar is hidden on close
|
|
42
43
|
if (overlay) overlay.classList.remove('visible');
|
|
43
44
|
}
|
|
44
45
|
if (toggleBtn) toggleBtn.addEventListener('click', function(e) { e.stopPropagation(); toggleSidebar(); });
|
package/static/js/terminal.js
CHANGED
|
@@ -1,39 +1,31 @@
|
|
|
1
1
|
(function() {
|
|
2
|
-
var ws = null;
|
|
3
2
|
var term = null;
|
|
4
3
|
var fitAddon = null;
|
|
5
4
|
var termActive = false;
|
|
6
5
|
var BASE = window.__BASE_URL || '';
|
|
7
|
-
|
|
8
|
-
function getWsUrl() {
|
|
9
|
-
var proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
10
|
-
return proto + '//' + location.host + BASE + '/sync';
|
|
11
|
-
}
|
|
6
|
+
var _wsListener = null;
|
|
12
7
|
|
|
13
8
|
function getCwd() {
|
|
14
9
|
try {
|
|
15
|
-
// Try conversation manager first
|
|
16
10
|
if (window.conversationManager && window.conversationManager.activeId) {
|
|
17
11
|
var mgr = window.conversationManager;
|
|
18
|
-
var
|
|
19
|
-
if (
|
|
20
|
-
var conv = mgr.conversations.find(function(c) { return c.id === id; });
|
|
21
|
-
if (conv && conv.workingDirectory) return conv.workingDirectory;
|
|
22
|
-
}
|
|
12
|
+
var conv = mgr.conversations && mgr.conversations.find(function(c) { return c.id === mgr.activeId; });
|
|
13
|
+
if (conv && conv.workingDirectory) return conv.workingDirectory;
|
|
23
14
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
if (window.conversationManager && window.conversationManager.conversations) {
|
|
28
|
-
var convList = window.conversationManager.conversations;
|
|
29
|
-
var match = convList.find(function(c) { return c.id === convId; });
|
|
30
|
-
if (match && match.workingDirectory) return match.workingDirectory;
|
|
31
|
-
}
|
|
15
|
+
if (window.currentConversation && window.conversationManager && window.conversationManager.conversations) {
|
|
16
|
+
var match = window.conversationManager.conversations.find(function(c) { return c.id === window.currentConversation; });
|
|
17
|
+
if (match && match.workingDirectory) return match.workingDirectory;
|
|
32
18
|
}
|
|
33
19
|
} catch (_) {}
|
|
34
20
|
return undefined;
|
|
35
21
|
}
|
|
36
22
|
|
|
23
|
+
function wsSend(obj) {
|
|
24
|
+
if (window.wsManager && window.wsManager.send) {
|
|
25
|
+
window.wsManager.send(obj);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
37
29
|
function ensureTerm() {
|
|
38
30
|
var output = document.getElementById('terminalOutput');
|
|
39
31
|
if (!output) return false;
|
|
@@ -60,16 +52,12 @@
|
|
|
60
52
|
fitAddon.fit();
|
|
61
53
|
|
|
62
54
|
term.onData(function(data) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
ws.send(JSON.stringify({ type: 'terminal_input', data: encoded }));
|
|
66
|
-
}
|
|
55
|
+
var encoded = btoa(unescape(encodeURIComponent(data)));
|
|
56
|
+
wsSend({ type: 'terminal_input', data: encoded });
|
|
67
57
|
});
|
|
68
58
|
|
|
69
59
|
term.onResize(function(size) {
|
|
70
|
-
|
|
71
|
-
ws.send(JSON.stringify({ type: 'terminal_resize', cols: size.cols, rows: size.rows }));
|
|
72
|
-
}
|
|
60
|
+
wsSend({ type: 'terminal_resize', cols: size.cols, rows: size.rows });
|
|
73
61
|
});
|
|
74
62
|
|
|
75
63
|
var resizeTimer;
|
|
@@ -78,57 +66,42 @@
|
|
|
78
66
|
try { fitAddon.fit(); } catch(_) {}
|
|
79
67
|
clearTimeout(resizeTimer);
|
|
80
68
|
resizeTimer = setTimeout(function() {
|
|
81
|
-
if (term
|
|
82
|
-
ws.send(JSON.stringify({ type: 'terminal_resize', cols: term.cols, rows: term.rows }));
|
|
83
|
-
}
|
|
69
|
+
if (term) wsSend({ type: 'terminal_resize', cols: term.cols, rows: term.rows });
|
|
84
70
|
}, 200);
|
|
85
71
|
}
|
|
86
72
|
});
|
|
87
73
|
|
|
88
|
-
|
|
74
|
+
document.getElementById('terminalOutput').addEventListener('click', function() {
|
|
89
75
|
if (term && term.focus) term.focus();
|
|
90
76
|
});
|
|
91
77
|
|
|
92
78
|
return true;
|
|
93
79
|
}
|
|
94
80
|
|
|
95
|
-
function
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
ws.onopen = function() {
|
|
109
|
-
var dims = term ? { cols: term.cols, rows: term.rows } : { cols: 80, rows: 24 };
|
|
110
|
-
ws.send(JSON.stringify({ type: 'terminal_start', cwd: cwd, cols: dims.cols, rows: dims.rows }));
|
|
111
|
-
setTimeout(function() { if (term && term.focus) term.focus(); }, 100);
|
|
112
|
-
};
|
|
113
|
-
ws.onmessage = function(e) {
|
|
114
|
-
try {
|
|
115
|
-
var msg = JSON.parse(e.data);
|
|
116
|
-
if (msg.type === 'terminal_output' && term) {
|
|
117
|
-
var raw = msg.encoding === 'base64'
|
|
118
|
-
? decodeURIComponent(escape(atob(msg.data)))
|
|
119
|
-
: msg.data;
|
|
120
|
-
term.write(raw);
|
|
121
|
-
} else if (msg.type === 'terminal_exit' && term) {
|
|
122
|
-
term.write('\r\n[Process exited with code ' + msg.code + ']\r\n');
|
|
123
|
-
if (termActive) setTimeout(connectAndStart, 2000);
|
|
124
|
-
}
|
|
125
|
-
} catch(_) {}
|
|
126
|
-
};
|
|
127
|
-
ws.onclose = function() {
|
|
128
|
-
ws = null;
|
|
129
|
-
if (termActive) setTimeout(connectAndStart, 2000);
|
|
81
|
+
function installWsListener() {
|
|
82
|
+
if (_wsListener || !window.wsManager) return;
|
|
83
|
+
_wsListener = function(msg) {
|
|
84
|
+
if (!termActive) return;
|
|
85
|
+
if (msg.type === 'terminal_output' && term) {
|
|
86
|
+
var raw = msg.encoding === 'base64'
|
|
87
|
+
? decodeURIComponent(escape(atob(msg.data)))
|
|
88
|
+
: msg.data;
|
|
89
|
+
term.write(raw);
|
|
90
|
+
} else if (msg.type === 'terminal_exit' && term) {
|
|
91
|
+
term.write('\r\n[Process exited with code ' + msg.code + ']\r\n');
|
|
92
|
+
if (termActive) setTimeout(startSession, 2000);
|
|
93
|
+
}
|
|
130
94
|
};
|
|
131
|
-
|
|
95
|
+
window.wsManager.on('message', _wsListener);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function startSession() {
|
|
99
|
+
if (!window.wsManager) return;
|
|
100
|
+
installWsListener();
|
|
101
|
+
var cwd = getCwd();
|
|
102
|
+
var dims = term ? { cols: term.cols, rows: term.rows } : { cols: 80, rows: 24 };
|
|
103
|
+
wsSend({ type: 'terminal_start', cwd: cwd, cols: dims.cols, rows: dims.rows });
|
|
104
|
+
setTimeout(function() { if (term && term.focus) term.focus(); }, 100);
|
|
132
105
|
}
|
|
133
106
|
|
|
134
107
|
function startTerminal() {
|
|
@@ -137,19 +110,21 @@
|
|
|
137
110
|
return;
|
|
138
111
|
}
|
|
139
112
|
termActive = true;
|
|
140
|
-
|
|
113
|
+
if (window.wsManager && window.wsManager.isConnected) {
|
|
114
|
+
startSession();
|
|
115
|
+
} else if (window.wsManager) {
|
|
116
|
+
var onConnected = function() {
|
|
117
|
+
window.wsManager.off('connected', onConnected);
|
|
118
|
+
startSession();
|
|
119
|
+
};
|
|
120
|
+
window.wsManager.on('connected', onConnected);
|
|
121
|
+
}
|
|
141
122
|
setTimeout(function() { if (fitAddon) try { fitAddon.fit(); } catch(_) {} }, 100);
|
|
142
123
|
}
|
|
143
124
|
|
|
144
125
|
function stopTerminal() {
|
|
145
126
|
termActive = false;
|
|
146
|
-
|
|
147
|
-
ws.send(JSON.stringify({ type: 'terminal_stop' }));
|
|
148
|
-
}
|
|
149
|
-
if (ws) {
|
|
150
|
-
ws.close();
|
|
151
|
-
ws = null;
|
|
152
|
-
}
|
|
127
|
+
wsSend({ type: 'terminal_stop' });
|
|
153
128
|
}
|
|
154
129
|
|
|
155
130
|
function initTerminalEarly() {
|
|
@@ -172,7 +147,7 @@
|
|
|
172
147
|
return;
|
|
173
148
|
}
|
|
174
149
|
termActive = true;
|
|
175
|
-
|
|
150
|
+
startSession();
|
|
176
151
|
setTimeout(function() { if (fitAddon) try { fitAddon.fit(); } catch(_) {} }, 50);
|
|
177
152
|
setTimeout(function() { if (fitAddon) try { fitAddon.fit(); } catch(_) {} }, 300);
|
|
178
153
|
} else if (termActive) {
|
|
@@ -180,15 +155,12 @@
|
|
|
180
155
|
}
|
|
181
156
|
});
|
|
182
157
|
|
|
183
|
-
|
|
184
|
-
window.addEventListener('conversation-changed', function(e) {
|
|
158
|
+
window.addEventListener('conversation-changed', function() {
|
|
185
159
|
if (!termActive) return;
|
|
186
160
|
var cwd = getCwd();
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
if (term) term.write('\r\n\x1b[33m[Switched to: ' + (cwd || '/') + ']\x1b[0m\r\n');
|
|
191
|
-
}
|
|
161
|
+
var dims = term ? { cols: term.cols, rows: term.rows } : { cols: 80, rows: 24 };
|
|
162
|
+
wsSend({ type: 'terminal_start', cwd: cwd, cols: dims.cols, rows: dims.rows });
|
|
163
|
+
if (term) term.write('\r\n\x1b[33m[Switched to: ' + (cwd || '/') + ']\x1b[0m\r\n');
|
|
192
164
|
});
|
|
193
165
|
|
|
194
166
|
window.terminalModule = {
|