agentgui 1.0.568 → 1.0.570

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.568",
3
+ "version": "1.0.570",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
package/static/index.html CHANGED
@@ -948,6 +948,26 @@
948
948
  .inject-btn:disabled { opacity: 0.4; cursor: not-allowed; }
949
949
  .inject-btn.visible { display: flex; }
950
950
 
951
+ .queue-btn {
952
+ display: none;
953
+ align-items: center;
954
+ justify-content: center;
955
+ width: 40px;
956
+ height: 40px;
957
+ background-color: #8b5cf6;
958
+ color: white;
959
+ border: none;
960
+ border-radius: 0.5rem;
961
+ cursor: pointer;
962
+ flex-shrink: 0;
963
+ transition: background-color 0.15s;
964
+ margin-right: 0.25rem;
965
+ }
966
+
967
+ .queue-btn:hover:not(:disabled) { background-color: #7c3aed; }
968
+ .queue-btn:disabled { opacity: 0.4; cursor: not-allowed; }
969
+ .queue-btn.visible { display: flex; }
970
+
951
971
  .steer-btn {
952
972
  display: none;
953
973
  align-items: center;
@@ -3185,6 +3205,11 @@
3185
3205
  <path d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"/>
3186
3206
  </svg>
3187
3207
  </button>
3208
+ <button class="queue-btn" id="queueBtn" title="Queue message for running agent" aria-label="Queue message">
3209
+ <svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18">
3210
+ <path d="M15 13H1v-2h14v2zm0-4H1V7h14v2zM1 19h14v-2H1v2z"/>
3211
+ </svg>
3212
+ </button>
3188
3213
  <button class="steer-btn" id="steerBtn" title="Steer running agent with message" aria-label="Steer agent">
3189
3214
  <svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18">
3190
3215
  <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm3.5-9c.83 0 1.5-.67 1.5-1.5S16.33 8 15.5 8 14 8.67 14 9.5s.67 1.5 1.5 1.5zm-7 0c.83 0 1.5-.67 1.5-1.5S9.33 8 8.5 8 7 8.67 7 9.5 7.67 11 8.5 11zm3.5 6.5c2.33 0 4.31-1.46 5.11-3.5H6.89c.8 2.04 2.78 3.5 5.11 3.5z"/>
@@ -458,6 +458,7 @@ class AgentGUIClient {
458
458
 
459
459
  this.ui.stopButton = document.getElementById('stopBtn');
460
460
  this.ui.injectButton = document.getElementById('injectBtn');
461
+ this.ui.queueButton = document.getElementById('queueBtn');
461
462
  this.ui.steerButton = document.getElementById('steerBtn');
462
463
 
463
464
  if (this.ui.stopButton) {
@@ -486,6 +487,28 @@ class AgentGUIClient {
486
487
  });
487
488
  }
488
489
 
490
+ if (this.ui.queueButton) {
491
+ this.ui.queueButton.addEventListener('click', async () => {
492
+ if (!this.state.currentConversation) return;
493
+ const message = this.ui.messageInput?.value || '';
494
+ if (!message.trim()) {
495
+ this.showError('Please enter a message to queue');
496
+ return;
497
+ }
498
+ try {
499
+ const data = await window.wsClient.rpc('conv.queue', { id: this.state.currentConversation.id, content: message });
500
+ console.log('Queue response:', data);
501
+ if (this.ui.messageInput) {
502
+ this.ui.messageInput.value = '';
503
+ this.ui.messageInput.style.height = 'auto';
504
+ }
505
+ } catch (err) {
506
+ console.error('Failed to queue:', err);
507
+ this.showError('Failed to queue: ' + err.message);
508
+ }
509
+ });
510
+ }
511
+
489
512
  if (this.ui.steerButton) {
490
513
  this.ui.steerButton.addEventListener('click', async () => {
491
514
  if (!this.state.currentConversation) return;
@@ -726,19 +749,13 @@ class AgentGUIClient {
726
749
  if (this.state.currentConversation?.id !== data.conversationId) {
727
750
  console.log('Streaming started for non-active conversation:', data.conversationId);
728
751
  this.state.streamingConversations.set(data.conversationId, true);
752
+ this.updateBusyPromptArea(data.conversationId);
729
753
  this.emit('streaming:start', data);
730
754
  return;
731
755
  }
732
756
 
733
- // Show stop, steer, queue, and inject buttons when streaming starts for current conversation
734
- if (this.ui.stopButton) this.ui.stopButton.classList.add('visible');
735
- if (this.ui.injectButton) this.ui.injectButton.classList.add('visible');
736
- if (this.ui.steerButton) this.ui.steerButton.classList.add('visible');
737
- if (this.ui.queueButton) this.ui.queueButton.classList.add('visible');
738
- if (this.ui.sendButton) this.ui.sendButton.style.display = 'none';
739
- this.ensurePromptAreaAlwaysEnabled();
740
-
741
757
  this.state.streamingConversations.set(data.conversationId, true);
758
+ this.updateBusyPromptArea(data.conversationId);
742
759
  this.state.currentSession = {
743
760
  id: data.sessionId,
744
761
  conversationId: data.conversationId,
@@ -977,11 +994,13 @@ class AgentGUIClient {
977
994
  if (conversationId && this.state.currentConversation?.id !== conversationId) {
978
995
  console.log('Streaming error for non-active conversation:', conversationId);
979
996
  this.state.streamingConversations.delete(conversationId);
997
+ this.updateBusyPromptArea(conversationId);
980
998
  this.emit('streaming:error', data);
981
999
  return;
982
1000
  }
983
1001
 
984
1002
  this.state.streamingConversations.delete(conversationId);
1003
+ this.updateBusyPromptArea(conversationId);
985
1004
 
986
1005
  // Stop polling for chunks
987
1006
  this.stopChunkPolling();
@@ -1021,23 +1040,19 @@ class AgentGUIClient {
1021
1040
  console.log('Streaming completed:', data);
1022
1041
  this._clearThinkingCountdown();
1023
1042
 
1024
- // Hide stop, steer, and inject buttons when streaming completes
1025
- if (this.ui.stopButton) this.ui.stopButton.classList.remove('visible');
1026
- if (this.ui.injectButton) this.ui.injectButton.classList.remove('visible');
1027
- if (this.ui.steerButton) this.ui.steerButton.classList.remove('visible');
1028
- if (this.ui.sendButton) this.ui.sendButton.style.display = '';
1029
-
1030
1043
  const conversationId = data.conversationId || this.state.currentSession?.conversationId;
1031
1044
  if (conversationId) this.invalidateCache(conversationId);
1032
1045
 
1033
1046
  if (conversationId && this.state.currentConversation?.id !== conversationId) {
1034
1047
  console.log('Streaming completed for non-active conversation:', conversationId);
1035
1048
  this.state.streamingConversations.delete(conversationId);
1049
+ this.updateBusyPromptArea(conversationId);
1036
1050
  this.emit('streaming:complete', data);
1037
1051
  return;
1038
1052
  }
1039
1053
 
1040
1054
  this.state.streamingConversations.delete(conversationId);
1055
+ this.updateBusyPromptArea(conversationId);
1041
1056
 
1042
1057
  this.stopChunkPolling();
1043
1058
 
@@ -2473,6 +2488,7 @@ class AgentGUIClient {
2473
2488
  this._previousConvAbort = new AbortController();
2474
2489
  const convSignal = this._previousConvAbort.signal;
2475
2490
 
2491
+ const prevConversationId = this.state.currentConversation?.id;
2476
2492
  this.cacheCurrentConversation();
2477
2493
  this.stopChunkPolling();
2478
2494
  this.removeScrollUpDetection();
@@ -2489,6 +2505,7 @@ class AgentGUIClient {
2489
2505
 
2490
2506
  if (this.ui.stopButton) this.ui.stopButton.classList.remove('visible');
2491
2507
  if (this.ui.injectButton) this.ui.injectButton.classList.remove('visible');
2508
+ if (this.ui.queueButton) this.ui.queueButton.classList.remove('visible');
2492
2509
  if (this.ui.steerButton) this.ui.steerButton.classList.remove('visible');
2493
2510
  if (this.ui.sendButton) this.ui.sendButton.style.display = '';
2494
2511
 
@@ -2542,9 +2559,16 @@ class AgentGUIClient {
2542
2559
  console.warn('Conversation no longer exists:', conversationId);
2543
2560
  this.state.currentConversation = null;
2544
2561
  if (window.conversationManager) window.conversationManager.loadConversations();
2545
- const outputEl = document.getElementById('output');
2546
- if (outputEl) outputEl.innerHTML = '<p class="text-secondary" style="padding:2rem;text-align:center">Conversation not found. It may have been lost during a server restart.</p>';
2547
- this.enableControls();
2562
+ // Resume from last successful conversation if available
2563
+ if (prevConversationId && prevConversationId !== conversationId) {
2564
+ console.log('Resuming from previous conversation:', prevConversationId);
2565
+ this.showError('Conversation not found. Resuming previous conversation.');
2566
+ await this.loadConversationMessages(prevConversationId);
2567
+ } else {
2568
+ const outputEl = document.getElementById('output');
2569
+ if (outputEl) outputEl.innerHTML = '<p class="text-secondary" style="padding:2rem;text-align:center">Conversation not found. It may have been lost during a server restart.</p>';
2570
+ this.enableControls();
2571
+ }
2548
2572
  return;
2549
2573
  }
2550
2574
  throw e;
@@ -2668,27 +2692,27 @@ class AgentGUIClient {
2668
2692
  toolResultBlocks.set(chunk.id, chunk);
2669
2693
  return;
2670
2694
  }
2671
- const element = this.renderer.renderBlock(chunk.block, chunk);
2695
+ const element = this.renderer.renderBlock(chunk.block, chunk, blockFrag);
2672
2696
  if (!element) return;
2673
- // Ensure CSS classes are always applied for styling consistency
2674
2697
  element.classList.add('block-loaded');
2675
- element.classList.add(`block-type-${chunk.block.type}`);
2676
2698
  blockFrag.appendChild(element);
2677
2699
  });
2678
2700
 
2679
2701
  blocksEl.appendChild(blockFrag);
2680
2702
 
2681
- toolResultBlocks.forEach((chunk, chunkId) => {
2682
- const lastBlock = blocksEl.lastElementChild;
2683
- if (lastBlock?.classList?.contains('block-type-tool_use')) {
2684
- lastBlock.classList.remove('has-success', 'has-error');
2685
- lastBlock.classList.add(chunk.block.is_error ? 'has-error' : 'has-success');
2686
- const contextWithParent = { ...chunk, parentIsOpen: lastBlock.hasAttribute('open') };
2687
- const element = this.renderer.renderBlock(chunk.block, contextWithParent);
2688
- if (element && element !== blockFrag.lastElementChild) {
2689
- element.classList.add('block-loaded');
2690
- lastBlock.appendChild(element);
2691
- }
2703
+ toolResultBlocks.forEach((chunk) => {
2704
+ const toolUseId = chunk.block.tool_use_id;
2705
+ const toolUseEl = toolUseId
2706
+ ? blocksEl.querySelector(`.block-tool-use[data-tool-use-id="${toolUseId}"]`)
2707
+ : blocksEl.lastElementChild?.classList?.contains('block-type-tool_use') ? blocksEl.lastElementChild : null;
2708
+ if (!toolUseEl) return;
2709
+ toolUseEl.classList.remove('has-success', 'has-error');
2710
+ toolUseEl.classList.add(chunk.block.is_error ? 'has-error' : 'has-success');
2711
+ const contextWithParent = { ...chunk, parentIsOpen: toolUseEl.hasAttribute('open') };
2712
+ const element = this.renderer.renderBlock(chunk.block, contextWithParent, toolUseEl);
2713
+ if (element) {
2714
+ element.classList.add('block-loaded');
2715
+ toolUseEl.appendChild(element);
2692
2716
  }
2693
2717
  });
2694
2718
 
@@ -2763,7 +2787,19 @@ class AgentGUIClient {
2763
2787
  } catch (error) {
2764
2788
  if (error.name === 'AbortError') return;
2765
2789
  console.error('Failed to load conversation messages:', error);
2766
- this.showError('Failed to load conversation: ' + error.message);
2790
+ // Resume from last successful conversation if available
2791
+ if (prevConversationId && prevConversationId !== conversationId) {
2792
+ console.log('Resuming from previous conversation due to error:', prevConversationId);
2793
+ this.showError('Failed to load conversation. Resuming previous conversation.');
2794
+ try {
2795
+ await this.loadConversationMessages(prevConversationId);
2796
+ } catch (fallbackError) {
2797
+ console.error('Failed to resume previous conversation:', fallbackError);
2798
+ this.showError('Failed to load conversation: ' + error.message);
2799
+ }
2800
+ } else {
2801
+ this.showError('Failed to load conversation: ' + error.message);
2802
+ }
2767
2803
  }
2768
2804
  }
2769
2805
 
@@ -2779,20 +2815,22 @@ class AgentGUIClient {
2779
2815
  }
2780
2816
 
2781
2817
  updateBusyPromptArea(conversationId) {
2818
+ if (this.state.currentConversation?.id !== conversationId) return;
2782
2819
  const isStreaming = this.state.streamingConversations.has(conversationId);
2783
2820
  const isConnected = this.wsManager?.isConnected;
2784
2821
 
2785
2822
  const injectBtn = document.getElementById('injectBtn');
2786
2823
  const steerBtn = document.getElementById('steerBtn');
2824
+ const queueBtn = document.getElementById('queueBtn');
2787
2825
  const stopBtn = document.getElementById('stopBtn');
2788
2826
 
2789
- if (injectBtn) injectBtn.style.display = isStreaming ? 'flex' : 'none';
2790
- if (steerBtn) steerBtn.style.display = isStreaming ? 'flex' : 'none';
2791
- if (stopBtn) stopBtn.style.display = isStreaming ? 'flex' : 'none';
2827
+ [injectBtn, steerBtn, queueBtn, stopBtn].forEach(btn => {
2828
+ if (!btn) return;
2829
+ btn.classList.toggle('visible', isStreaming);
2830
+ btn.disabled = !isConnected;
2831
+ });
2792
2832
 
2793
- if (injectBtn) injectBtn.disabled = !isConnected;
2794
- if (steerBtn) steerBtn.disabled = !isConnected;
2795
- if (stopBtn) stopBtn.disabled = !isConnected;
2833
+ if (this.ui.sendButton) this.ui.sendButton.style.display = isStreaming ? 'none' : '';
2796
2834
  }
2797
2835
 
2798
2836
  removeScrollUpDetection() {
@@ -3070,6 +3108,9 @@ class AgentGUIClient {
3070
3108
  if (this.ui.steerButton && this.ui.steerButton.classList.contains('visible')) {
3071
3109
  this.ui.steerButton.disabled = !this.wsManager.isConnected;
3072
3110
  }
3111
+ if (this.ui.queueButton && this.ui.queueButton.classList.contains('visible')) {
3112
+ this.ui.queueButton.disabled = !this.wsManager.isConnected;
3113
+ }
3073
3114
  }
3074
3115
 
3075
3116
  /**
@@ -3103,6 +3144,10 @@ class AgentGUIClient {
3103
3144
  this.ui.steerButton.classList.add('visible');
3104
3145
  this.ui.steerButton.disabled = !this.wsManager.isConnected;
3105
3146
  }
3147
+ if (this.ui.queueButton) {
3148
+ this.ui.queueButton.classList.add('visible');
3149
+ this.ui.queueButton.disabled = !this.wsManager.isConnected;
3150
+ }
3106
3151
  }
3107
3152
 
3108
3153
  /**
@@ -751,7 +751,6 @@ class StreamingRenderer {
751
751
  if (block.id) details.dataset.toolUseId = block.id;
752
752
  details.classList.add(this._getBlockTypeClass('tool_use'));
753
753
  details.classList.add(this._getToolColorClass(toolName));
754
- details.open = true;
755
754
  const summary = document.createElement('summary');
756
755
  summary.className = 'folded-tool-bar';
757
756
  const displayName = this.getToolUseDisplayName(toolName);
@@ -1230,7 +1229,8 @@ class StreamingRenderer {
1230
1229
  const toolName = block.tool_name || block.name || '';
1231
1230
 
1232
1231
  // Special handling for TodoWrite: render directly without success wrapper
1233
- if (toolName.includes('TodoWrite') && typeof content === 'object' && content.todos && Array.isArray(content.todos)) {
1232
+ // Detect by tool name OR by content structure (todos array)
1233
+ if ((toolName.includes('TodoWrite') || (typeof content === 'object' && Array.isArray(content?.todos))) && typeof content === 'object' && content.todos && Array.isArray(content.todos)) {
1234
1234
  const statusIcons = { completed: '&#9989;', in_progress: '&#9881;', pending: '&#9744;' };
1235
1235
  const completedCount = content.todos.filter(t => t.status === 'completed').length;
1236
1236
  const totalCount = content.todos.length;
@@ -2200,8 +2200,8 @@ class StreamingRenderer {
2200
2200
  summary.textContent = summaryText;
2201
2201
 
2202
2202
  const details = document.createElement('details');
2203
- const className = `block-${block.type} block-type-${block.type} ${this._getBlockTypeClass(block.type)}`;
2204
- details.className = className;
2203
+ details.className = `block-${block.type}`;
2204
+ details.classList.add(this._getBlockTypeClass(block.type));
2205
2205
  details.setAttribute('data-block-type', block.type);
2206
2206
  details.setAttribute('data-lazy-load', 'pending');
2207
2207
  details.open = block.type === 'success' || (block.type === 'tool_result' && !block.is_error);