agentgui 1.0.130 → 1.0.138
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/CLAUDE.md +46 -0
- package/bin/gmgui.cjs +1 -1
- package/database.js +11 -2
- package/package.json +2 -1
- package/server.js +77 -10
- package/setup-npm-token.sh +68 -0
- package/static/app.js +5 -3
- package/static/index.html +245 -3
- package/static/js/client.js +66 -8
- package/static/js/conversations.js +34 -6
- package/static/js/features.js +17 -17
- package/static/js/streaming-renderer.js +48 -24
- package/static/js/syntax-highlighter.js +1 -1
- package/static/js/voice.js +430 -0
- package/static/styles.css +86 -0
package/static/index.html
CHANGED
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
|
6
6
|
<meta name="description" content="AgentGUI - Real-time Claude Code Execution Visualization">
|
|
7
7
|
<title>AgentGUI</title>
|
|
8
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.ico">
|
|
8
9
|
|
|
9
|
-
<link href="https://
|
|
10
|
-
<link href="https://
|
|
10
|
+
<link href="https://cdn.jsdelivr.net/npm/rippleui@1.12.1/dist/css/styles.css" rel="stylesheet">
|
|
11
|
+
<link href="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism-dark.css" rel="stylesheet">
|
|
11
12
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/github-dark.min.css" rel="stylesheet">
|
|
12
13
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js"></script>
|
|
13
14
|
|
|
@@ -161,6 +162,26 @@
|
|
|
161
162
|
|
|
162
163
|
.conversation-item.active .conversation-item-meta { color: rgba(255,255,255,0.7); }
|
|
163
164
|
|
|
165
|
+
.conversation-streaming-badge {
|
|
166
|
+
display: inline-flex;
|
|
167
|
+
align-items: center;
|
|
168
|
+
margin-right: 0.375rem;
|
|
169
|
+
vertical-align: middle;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.streaming-dot {
|
|
173
|
+
display: inline-block;
|
|
174
|
+
width: 0.5rem;
|
|
175
|
+
height: 0.5rem;
|
|
176
|
+
border-radius: 50%;
|
|
177
|
+
background-color: var(--color-success);
|
|
178
|
+
animation: pulse 1.5s ease-in-out infinite;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.conversation-item.active .streaming-dot {
|
|
182
|
+
background-color: #fff;
|
|
183
|
+
}
|
|
184
|
+
|
|
164
185
|
.conversation-item {
|
|
165
186
|
display: flex;
|
|
166
187
|
align-items: center;
|
|
@@ -801,6 +822,7 @@
|
|
|
801
822
|
/* ===== File browser ===== */
|
|
802
823
|
.file-browser-container { flex:1; min-height:0; overflow:hidden; }
|
|
803
824
|
.file-browser-iframe { width:100%; height:100%; border:none; }
|
|
825
|
+
.voice-iframe { width:100%; height:100%; border:none; }
|
|
804
826
|
|
|
805
827
|
/* ===== RESPONSIVE: MOBILE ===== */
|
|
806
828
|
@media (max-width: 768px) {
|
|
@@ -894,6 +916,191 @@
|
|
|
894
916
|
scrollbar-color: #475569 transparent;
|
|
895
917
|
}
|
|
896
918
|
|
|
919
|
+
/* ===== VOICE VIEW ===== */
|
|
920
|
+
.voice-container {
|
|
921
|
+
flex: 1;
|
|
922
|
+
min-height: 0;
|
|
923
|
+
overflow: hidden;
|
|
924
|
+
display: flex;
|
|
925
|
+
flex-direction: column;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
.voice-scroll {
|
|
929
|
+
flex: 1;
|
|
930
|
+
overflow-y: auto;
|
|
931
|
+
overflow-x: hidden;
|
|
932
|
+
padding: 1.5rem 2rem;
|
|
933
|
+
-webkit-overflow-scrolling: touch;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
.voice-messages {
|
|
937
|
+
display: flex;
|
|
938
|
+
flex-direction: column;
|
|
939
|
+
gap: 1rem;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
.voice-block {
|
|
943
|
+
padding: 1rem 1.25rem;
|
|
944
|
+
border-radius: 0.75rem;
|
|
945
|
+
background: var(--color-bg-secondary);
|
|
946
|
+
line-height: 1.7;
|
|
947
|
+
font-size: 1rem;
|
|
948
|
+
max-width: 85%;
|
|
949
|
+
margin-right: auto;
|
|
950
|
+
position: relative;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
.voice-block.speaking {
|
|
954
|
+
box-shadow: 0 0 0 2px var(--color-primary);
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
.voice-block-user {
|
|
958
|
+
background: var(--color-primary);
|
|
959
|
+
color: white;
|
|
960
|
+
margin-left: auto;
|
|
961
|
+
margin-right: 0;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
.voice-input-section {
|
|
965
|
+
flex-shrink: 0;
|
|
966
|
+
background: var(--color-bg-primary);
|
|
967
|
+
padding: 0.75rem 1rem;
|
|
968
|
+
border-top: 1px solid var(--color-border);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
.voice-input-wrapper {
|
|
972
|
+
display: flex;
|
|
973
|
+
gap: 0.5rem;
|
|
974
|
+
align-items: center;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
.voice-transcript {
|
|
978
|
+
flex: 1;
|
|
979
|
+
min-height: 40px;
|
|
980
|
+
max-height: 100px;
|
|
981
|
+
padding: 0.5rem 0.875rem;
|
|
982
|
+
border-radius: 0.75rem;
|
|
983
|
+
background: var(--color-bg-secondary);
|
|
984
|
+
color: var(--color-text-primary);
|
|
985
|
+
font-size: 0.9375rem;
|
|
986
|
+
line-height: 1.5;
|
|
987
|
+
overflow-y: auto;
|
|
988
|
+
white-space: pre-wrap;
|
|
989
|
+
word-break: break-word;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
.voice-transcript:empty::before {
|
|
993
|
+
content: attr(data-placeholder);
|
|
994
|
+
color: var(--color-text-secondary);
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
.voice-mic-btn {
|
|
998
|
+
display: flex;
|
|
999
|
+
align-items: center;
|
|
1000
|
+
justify-content: center;
|
|
1001
|
+
width: 44px;
|
|
1002
|
+
height: 44px;
|
|
1003
|
+
background: var(--color-bg-secondary);
|
|
1004
|
+
color: var(--color-text-secondary);
|
|
1005
|
+
border: 2px solid var(--color-border);
|
|
1006
|
+
border-radius: 50%;
|
|
1007
|
+
cursor: pointer;
|
|
1008
|
+
flex-shrink: 0;
|
|
1009
|
+
transition: all 0.2s;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
.voice-mic-btn:hover {
|
|
1013
|
+
border-color: var(--color-primary);
|
|
1014
|
+
color: var(--color-primary);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
.voice-mic-btn.loading {
|
|
1018
|
+
opacity: 0.5;
|
|
1019
|
+
cursor: wait;
|
|
1020
|
+
animation: mic-loading-spin 2s linear infinite;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
@keyframes mic-loading-spin {
|
|
1024
|
+
0% { border-color: var(--color-border); }
|
|
1025
|
+
50% { border-color: var(--color-primary); }
|
|
1026
|
+
100% { border-color: var(--color-border); }
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
.voice-mic-btn.recording {
|
|
1030
|
+
background: var(--color-error);
|
|
1031
|
+
border-color: var(--color-error);
|
|
1032
|
+
color: white;
|
|
1033
|
+
animation: pulse 1s ease-in-out infinite;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
.voice-mic-btn svg {
|
|
1037
|
+
width: 20px;
|
|
1038
|
+
height: 20px;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
.voice-send-btn {
|
|
1042
|
+
flex-shrink: 0;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
.voice-tts-controls {
|
|
1046
|
+
display: flex;
|
|
1047
|
+
align-items: center;
|
|
1048
|
+
justify-content: space-between;
|
|
1049
|
+
margin-top: 0.5rem;
|
|
1050
|
+
font-size: 0.8rem;
|
|
1051
|
+
color: var(--color-text-secondary);
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
.voice-toggle-label {
|
|
1055
|
+
display: flex;
|
|
1056
|
+
align-items: center;
|
|
1057
|
+
gap: 0.375rem;
|
|
1058
|
+
cursor: pointer;
|
|
1059
|
+
user-select: none;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
.voice-toggle-label input[type="checkbox"] {
|
|
1063
|
+
accent-color: var(--color-primary);
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
.voice-stop-btn {
|
|
1067
|
+
padding: 0.25rem 0.75rem;
|
|
1068
|
+
background: var(--color-bg-secondary);
|
|
1069
|
+
border: 1px solid var(--color-border);
|
|
1070
|
+
border-radius: 0.375rem;
|
|
1071
|
+
cursor: pointer;
|
|
1072
|
+
font-size: 0.75rem;
|
|
1073
|
+
color: var(--color-text-secondary);
|
|
1074
|
+
transition: all 0.15s;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
.voice-stop-btn:hover {
|
|
1078
|
+
background: var(--color-error);
|
|
1079
|
+
color: white;
|
|
1080
|
+
border-color: var(--color-error);
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
.voice-empty {
|
|
1084
|
+
text-align: center;
|
|
1085
|
+
color: var(--color-text-secondary);
|
|
1086
|
+
padding: 4rem 2rem;
|
|
1087
|
+
font-size: 1rem;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
.voice-empty-icon {
|
|
1091
|
+
font-size: 3rem;
|
|
1092
|
+
margin-bottom: 1rem;
|
|
1093
|
+
opacity: 0.3;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
.voice-block .voice-result-stats {
|
|
1097
|
+
font-size: 0.8rem;
|
|
1098
|
+
color: var(--color-text-secondary);
|
|
1099
|
+
margin-top: 0.5rem;
|
|
1100
|
+
padding-top: 0.5rem;
|
|
1101
|
+
border-top: 1px solid var(--color-border);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
897
1104
|
/* ===== RESPONSIVE: TABLET ===== */
|
|
898
1105
|
@media (min-width: 769px) and (max-width: 1024px) {
|
|
899
1106
|
:root { --sidebar-width: 260px; }
|
|
@@ -1629,6 +1836,7 @@
|
|
|
1629
1836
|
<div class="view-toggle-bar" id="viewToggleBar" style="display:none;">
|
|
1630
1837
|
<button class="view-toggle-btn active" data-view="chat">Chat</button>
|
|
1631
1838
|
<button class="view-toggle-btn" data-view="files">Files</button>
|
|
1839
|
+
<button class="view-toggle-btn" data-view="voice">Voice</button>
|
|
1632
1840
|
</div>
|
|
1633
1841
|
|
|
1634
1842
|
<!-- Messages scroll area -->
|
|
@@ -1646,7 +1854,40 @@
|
|
|
1646
1854
|
|
|
1647
1855
|
<!-- File browser (hidden by default) -->
|
|
1648
1856
|
<div id="fileBrowserContainer" class="file-browser-container" style="display:none;">
|
|
1649
|
-
<iframe id="fileBrowserIframe" class="file-browser-iframe"
|
|
1857
|
+
<iframe id="fileBrowserIframe" class="file-browser-iframe"></iframe>
|
|
1858
|
+
</div>
|
|
1859
|
+
|
|
1860
|
+
<!-- Voice STT/TTS view -->
|
|
1861
|
+
<div id="voiceContainer" class="voice-container" style="display:none;">
|
|
1862
|
+
<div id="voiceScroll" class="voice-scroll">
|
|
1863
|
+
<div class="voice-messages" id="voiceMessages"></div>
|
|
1864
|
+
</div>
|
|
1865
|
+
<div class="voice-input-section">
|
|
1866
|
+
<div class="voice-input-wrapper">
|
|
1867
|
+
<select class="agent-selector voice-agent-selector" data-voice-agent-selector title="Select agent"></select>
|
|
1868
|
+
<div class="voice-transcript" id="voiceTranscript" data-placeholder="Tap mic and speak..."></div>
|
|
1869
|
+
<button class="voice-mic-btn" id="voiceMicBtn" title="Toggle recording" aria-label="Voice input">
|
|
1870
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
1871
|
+
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/>
|
|
1872
|
+
<path d="M19 10v2a7 7 0 0 1-14 0v-2"/>
|
|
1873
|
+
<line x1="12" y1="19" x2="12" y2="23"/>
|
|
1874
|
+
<line x1="8" y1="23" x2="16" y2="23"/>
|
|
1875
|
+
</svg>
|
|
1876
|
+
</button>
|
|
1877
|
+
<button class="send-btn voice-send-btn" id="voiceSendBtn" title="Send message" aria-label="Send message">
|
|
1878
|
+
<svg viewBox="0 0 24 24" fill="currentColor">
|
|
1879
|
+
<path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
|
|
1880
|
+
</svg>
|
|
1881
|
+
</button>
|
|
1882
|
+
</div>
|
|
1883
|
+
<div class="voice-tts-controls">
|
|
1884
|
+
<label class="voice-toggle-label">
|
|
1885
|
+
<input type="checkbox" id="voiceTTSToggle" checked>
|
|
1886
|
+
<span>Auto-speak responses</span>
|
|
1887
|
+
</label>
|
|
1888
|
+
<button class="voice-stop-btn" id="voiceStopSpeaking" title="Stop speaking">Stop</button>
|
|
1889
|
+
</div>
|
|
1890
|
+
</div>
|
|
1650
1891
|
</div>
|
|
1651
1892
|
|
|
1652
1893
|
<!-- Input area: fixed at bottom -->
|
|
@@ -1710,6 +1951,7 @@
|
|
|
1710
1951
|
<script src="/gm/js/ui-components.js"></script>
|
|
1711
1952
|
<script src="/gm/js/conversations.js"></script>
|
|
1712
1953
|
<script src="/gm/js/client.js"></script>
|
|
1954
|
+
<script type="module" src="/gm/js/voice.js"></script>
|
|
1713
1955
|
<script src="/gm/js/features.js"></script>
|
|
1714
1956
|
|
|
1715
1957
|
<script>
|
package/static/js/client.js
CHANGED
|
@@ -192,6 +192,7 @@ class AgentGUIClient {
|
|
|
192
192
|
*/
|
|
193
193
|
updateUrlForConversation(conversationId, sessionId) {
|
|
194
194
|
if (!this.isValidId(conversationId)) return;
|
|
195
|
+
if (!this.routerState) return;
|
|
195
196
|
|
|
196
197
|
this.routerState.currentConversationId = conversationId;
|
|
197
198
|
if (sessionId && this.isValidId(sessionId)) {
|
|
@@ -344,6 +345,9 @@ class AgentGUIClient {
|
|
|
344
345
|
|
|
345
346
|
handleWebSocketMessage(data) {
|
|
346
347
|
try {
|
|
348
|
+
// Dispatch to window so other modules (conversations.js) can listen
|
|
349
|
+
window.dispatchEvent(new CustomEvent('ws-message', { detail: data }));
|
|
350
|
+
|
|
347
351
|
switch (data.type) {
|
|
348
352
|
case 'streaming_start':
|
|
349
353
|
this.handleStreamingStart(data);
|
|
@@ -388,6 +392,15 @@ class AgentGUIClient {
|
|
|
388
392
|
|
|
389
393
|
handleStreamingStart(data) {
|
|
390
394
|
console.log('Streaming started:', data);
|
|
395
|
+
|
|
396
|
+
// If this streaming event is for a different conversation than what we are viewing,
|
|
397
|
+
// just track the state but do not modify the DOM or start polling
|
|
398
|
+
if (this.state.currentConversation?.id !== data.conversationId) {
|
|
399
|
+
console.log('Streaming started for non-active conversation:', data.conversationId);
|
|
400
|
+
this.emit('streaming:start', data);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
|
|
391
404
|
this.state.isStreaming = true;
|
|
392
405
|
this.state.currentSession = {
|
|
393
406
|
id: data.sessionId,
|
|
@@ -490,8 +503,21 @@ class AgentGUIClient {
|
|
|
490
503
|
|
|
491
504
|
handleStreamingError(data) {
|
|
492
505
|
console.error('Streaming error:', data);
|
|
506
|
+
|
|
507
|
+
const conversationId = data.conversationId || this.state.currentSession?.conversationId;
|
|
508
|
+
|
|
509
|
+
// If this event is for a conversation we are NOT currently viewing, just track state
|
|
510
|
+
if (conversationId && this.state.currentConversation?.id !== conversationId) {
|
|
511
|
+
console.log('Streaming error for non-active conversation:', conversationId);
|
|
512
|
+
this.emit('streaming:error', data);
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
|
|
493
516
|
this.state.isStreaming = false;
|
|
494
517
|
|
|
518
|
+
// Stop polling for chunks
|
|
519
|
+
this.stopChunkPolling();
|
|
520
|
+
|
|
495
521
|
const sessionId = data.sessionId || this.state.currentSession?.id;
|
|
496
522
|
const streamingEl = document.getElementById(`streaming-${sessionId}`);
|
|
497
523
|
if (streamingEl) {
|
|
@@ -507,6 +533,16 @@ class AgentGUIClient {
|
|
|
507
533
|
|
|
508
534
|
handleStreamingComplete(data) {
|
|
509
535
|
console.log('Streaming completed:', data);
|
|
536
|
+
|
|
537
|
+
const conversationId = data.conversationId || this.state.currentSession?.conversationId;
|
|
538
|
+
|
|
539
|
+
// If this event is for a conversation we are NOT currently viewing, just track state
|
|
540
|
+
if (conversationId && this.state.currentConversation?.id !== conversationId) {
|
|
541
|
+
console.log('Streaming completed for non-active conversation:', conversationId);
|
|
542
|
+
this.emit('streaming:complete', data);
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
|
|
510
546
|
this.state.isStreaming = false;
|
|
511
547
|
|
|
512
548
|
// Stop polling for chunks
|
|
@@ -528,7 +564,6 @@ class AgentGUIClient {
|
|
|
528
564
|
}
|
|
529
565
|
|
|
530
566
|
// Save scroll position after streaming completes
|
|
531
|
-
const conversationId = data.conversationId || this.state.currentSession?.conversationId;
|
|
532
567
|
if (conversationId) {
|
|
533
568
|
this.saveScrollPosition(conversationId);
|
|
534
569
|
}
|
|
@@ -655,7 +690,8 @@ class AgentGUIClient {
|
|
|
655
690
|
</div>
|
|
656
691
|
`;
|
|
657
692
|
} else {
|
|
658
|
-
|
|
693
|
+
const lineCount = code.split('\n').length;
|
|
694
|
+
return `<div class="message-code"><details class="collapsible-code"><summary class="collapsible-code-summary">${this.escapeHtml(language)} - ${lineCount} line${lineCount !== 1 ? 's' : ''}</summary><pre style="margin:0;border-radius:0 0 0.375rem 0.375rem">${this.escapeHtml(code)}</pre></details></div>`;
|
|
659
695
|
}
|
|
660
696
|
}
|
|
661
697
|
|
|
@@ -697,7 +733,8 @@ class AgentGUIClient {
|
|
|
697
733
|
</div>
|
|
698
734
|
`;
|
|
699
735
|
} else {
|
|
700
|
-
|
|
736
|
+
const blkLineCount = block.code.split('\n').length;
|
|
737
|
+
html += `<div class="message-code"><details class="collapsible-code"><summary class="collapsible-code-summary">${this.escapeHtml(block.language || 'code')} - ${blkLineCount} line${blkLineCount !== 1 ? 's' : ''}</summary><pre style="margin:0;border-radius:0 0 0.375rem 0.375rem">${this.escapeHtml(block.code)}</pre></details></div>`;
|
|
701
738
|
}
|
|
702
739
|
} else if (block.type === 'tool_use') {
|
|
703
740
|
let inputHtml = '';
|
|
@@ -1099,9 +1136,21 @@ class AgentGUIClient {
|
|
|
1099
1136
|
|
|
1100
1137
|
async loadConversationMessages(conversationId) {
|
|
1101
1138
|
try {
|
|
1139
|
+
// Save scroll position of current conversation before switching
|
|
1140
|
+
if (this.state.currentConversation?.id) {
|
|
1141
|
+
this.saveScrollPosition(this.state.currentConversation.id);
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1102
1144
|
// Stop any existing polling when switching conversations
|
|
1103
1145
|
this.stopChunkPolling();
|
|
1104
1146
|
|
|
1147
|
+
// Clear streaming state from previous conversation view
|
|
1148
|
+
// (the actual streaming continues on the server, we just stop tracking it on the UI side)
|
|
1149
|
+
if (this.state.isStreaming && this.state.currentConversation?.id !== conversationId) {
|
|
1150
|
+
this.state.isStreaming = false;
|
|
1151
|
+
this.state.currentSession = null;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1105
1154
|
const convResponse = await fetch(window.__BASE_URL + `/api/conversations/${conversationId}`);
|
|
1106
1155
|
const { conversation, isActivelyStreaming, latestSession } = await convResponse.json();
|
|
1107
1156
|
this.state.currentConversation = conversation;
|
|
@@ -1114,7 +1163,10 @@ class AgentGUIClient {
|
|
|
1114
1163
|
}
|
|
1115
1164
|
|
|
1116
1165
|
// Check if there's an active streaming session that needs to be resumed
|
|
1117
|
-
|
|
1166
|
+
// isActivelyStreaming comes from the server checking both in-memory activeExecutions map
|
|
1167
|
+
// AND database session status. Use it as primary signal, with session status as confirmation.
|
|
1168
|
+
const shouldResumeStreaming = isActivelyStreaming && latestSession &&
|
|
1169
|
+
(latestSession.status === 'active' || latestSession.status === 'pending');
|
|
1118
1170
|
|
|
1119
1171
|
// Try to fetch chunks first (Wave 3 architecture)
|
|
1120
1172
|
try {
|
|
@@ -1202,17 +1254,22 @@ class AgentGUIClient {
|
|
|
1202
1254
|
startTime: latestSession.created_at
|
|
1203
1255
|
};
|
|
1204
1256
|
|
|
1205
|
-
// Subscribe to WebSocket updates
|
|
1257
|
+
// Subscribe to WebSocket updates for BOTH conversation and session
|
|
1206
1258
|
if (this.wsManager.isConnected) {
|
|
1207
1259
|
this.wsManager.subscribeToSession(latestSession.id);
|
|
1260
|
+
this.wsManager.sendMessage({ type: 'subscribe', conversationId });
|
|
1208
1261
|
}
|
|
1209
1262
|
|
|
1263
|
+
// Update URL with session ID
|
|
1264
|
+
this.updateUrlForConversation(conversationId, latestSession.id);
|
|
1265
|
+
|
|
1210
1266
|
// Get the timestamp of the last chunk to start polling from
|
|
1267
|
+
// Use the last chunk's created_at to avoid re-fetching already-rendered chunks
|
|
1211
1268
|
const lastChunkTime = chunks.length > 0
|
|
1212
1269
|
? chunks[chunks.length - 1].created_at
|
|
1213
|
-
:
|
|
1270
|
+
: 0;
|
|
1214
1271
|
|
|
1215
|
-
// Start polling for new chunks
|
|
1272
|
+
// Start polling for new chunks from where we left off
|
|
1216
1273
|
this.chunkPollState.lastFetchTimestamp = lastChunkTime;
|
|
1217
1274
|
this.startChunkPolling(conversationId);
|
|
1218
1275
|
|
|
@@ -1300,7 +1357,8 @@ class AgentGUIClient {
|
|
|
1300
1357
|
</div>
|
|
1301
1358
|
`;
|
|
1302
1359
|
} else {
|
|
1303
|
-
|
|
1360
|
+
const cBlkLineCount = block.code.split('\n').length;
|
|
1361
|
+
contentHtml += `<div class="message-code"><details class="collapsible-code"><summary class="collapsible-code-summary">${this.escapeHtml(block.language || 'code')} - ${cBlkLineCount} line${cBlkLineCount !== 1 ? 's' : ''}</summary><pre style="margin:0;border-radius:0 0 0.375rem 0.375rem">${this.escapeHtml(block.code)}</pre></details></div>`;
|
|
1304
1362
|
}
|
|
1305
1363
|
} else if (block.type === 'tool_use') {
|
|
1306
1364
|
let inputHtml = '';
|
|
@@ -12,13 +12,16 @@ class ConversationManager {
|
|
|
12
12
|
this.emptyEl = document.querySelector('[data-conversation-empty]');
|
|
13
13
|
this.newBtn = document.querySelector('[data-new-conversation]');
|
|
14
14
|
this.sidebarEl = document.querySelector('[data-sidebar]');
|
|
15
|
+
this.streamingConversations = new Set();
|
|
15
16
|
|
|
16
17
|
this.folderBrowser = {
|
|
17
18
|
modal: null,
|
|
18
19
|
listEl: null,
|
|
19
20
|
breadcrumbEl: null,
|
|
20
21
|
currentPath: '~',
|
|
21
|
-
homePath: '~'
|
|
22
|
+
homePath: '~',
|
|
23
|
+
cwdPath: null,
|
|
24
|
+
homePathReady: null
|
|
22
25
|
};
|
|
23
26
|
|
|
24
27
|
if (!this.listEl) return;
|
|
@@ -54,7 +57,7 @@ class ConversationManager {
|
|
|
54
57
|
if (e.target === this.folderBrowser.modal) this.closeFolderBrowser();
|
|
55
58
|
});
|
|
56
59
|
|
|
57
|
-
this.fetchHomePath();
|
|
60
|
+
this.folderBrowser.homePathReady = this.fetchHomePath();
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
async fetchHomePath() {
|
|
@@ -63,20 +66,25 @@ class ConversationManager {
|
|
|
63
66
|
if (res.ok) {
|
|
64
67
|
const data = await res.json();
|
|
65
68
|
this.folderBrowser.homePath = data.home || '~';
|
|
69
|
+
this.folderBrowser.cwdPath = data.cwd || null;
|
|
66
70
|
}
|
|
67
71
|
} catch (e) {
|
|
68
72
|
console.error('Failed to fetch home path:', e);
|
|
69
73
|
}
|
|
70
74
|
}
|
|
71
75
|
|
|
72
|
-
openFolderBrowser() {
|
|
76
|
+
async openFolderBrowser() {
|
|
73
77
|
if (!this.folderBrowser.modal) {
|
|
74
78
|
this.createNew();
|
|
75
79
|
return;
|
|
76
80
|
}
|
|
77
|
-
this.folderBrowser.
|
|
81
|
+
if (this.folderBrowser.homePathReady) {
|
|
82
|
+
await this.folderBrowser.homePathReady;
|
|
83
|
+
}
|
|
84
|
+
const startPath = this.folderBrowser.cwdPath || '~';
|
|
85
|
+
this.folderBrowser.currentPath = startPath;
|
|
78
86
|
this.folderBrowser.modal.classList.add('visible');
|
|
79
|
-
this.loadFolders(
|
|
87
|
+
this.loadFolders(startPath);
|
|
80
88
|
}
|
|
81
89
|
|
|
82
90
|
closeFolderBrowser() {
|
|
@@ -191,6 +199,14 @@ class ConversationManager {
|
|
|
191
199
|
|
|
192
200
|
const data = await res.json();
|
|
193
201
|
this.conversations = data.conversations || [];
|
|
202
|
+
|
|
203
|
+
// Seed streaming state from database isStreaming flag
|
|
204
|
+
for (const conv of this.conversations) {
|
|
205
|
+
if (conv.isStreaming === 1 || conv.isStreaming === true) {
|
|
206
|
+
this.streamingConversations.add(conv.id);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
194
210
|
this.render();
|
|
195
211
|
} catch (err) {
|
|
196
212
|
console.error('Failed to load conversations:', err);
|
|
@@ -225,6 +241,8 @@ class ConversationManager {
|
|
|
225
241
|
li.dataset.convId = conv.id;
|
|
226
242
|
if (conv.id === this.activeId) li.classList.add('active');
|
|
227
243
|
|
|
244
|
+
const isStreaming = conv.isStreaming === 1 || conv.isStreaming === true || this.streamingConversations?.has(conv.id);
|
|
245
|
+
|
|
228
246
|
const title = conv.title || `Conversation ${conv.id.slice(0, 8)}`;
|
|
229
247
|
const timestamp = conv.created_at ? new Date(conv.created_at).toLocaleDateString() : 'Unknown';
|
|
230
248
|
const agent = conv.agentType || 'unknown';
|
|
@@ -232,9 +250,13 @@ class ConversationManager {
|
|
|
232
250
|
const metaParts = [agent, timestamp];
|
|
233
251
|
if (wd) metaParts.push(wd);
|
|
234
252
|
|
|
253
|
+
const streamingBadge = isStreaming
|
|
254
|
+
? '<span class="conversation-streaming-badge" title="Streaming in progress"><span class="streaming-dot"></span></span>'
|
|
255
|
+
: '';
|
|
256
|
+
|
|
235
257
|
li.innerHTML = `
|
|
236
258
|
<div class="conversation-item-content">
|
|
237
|
-
<div class="conversation-item-title">${this.escapeHtml(title)}</div>
|
|
259
|
+
<div class="conversation-item-title">${streamingBadge}${this.escapeHtml(title)}</div>
|
|
238
260
|
<div class="conversation-item-meta">${metaParts.join(' • ')}</div>
|
|
239
261
|
</div>
|
|
240
262
|
<button class="conversation-item-delete" title="Delete conversation" data-delete-conv="${conv.id}">
|
|
@@ -335,6 +357,12 @@ class ConversationManager {
|
|
|
335
357
|
this.updateConversation(msg.conversation.id, msg.conversation);
|
|
336
358
|
} else if (msg.type === 'conversation_deleted') {
|
|
337
359
|
this.deleteConversation(msg.conversationId);
|
|
360
|
+
} else if (msg.type === 'streaming_start' && msg.conversationId) {
|
|
361
|
+
this.streamingConversations.add(msg.conversationId);
|
|
362
|
+
this.render();
|
|
363
|
+
} else if ((msg.type === 'streaming_complete' || msg.type === 'streaming_error') && msg.conversationId) {
|
|
364
|
+
this.streamingConversations.delete(msg.conversationId);
|
|
365
|
+
this.render();
|
|
338
366
|
}
|
|
339
367
|
});
|
|
340
368
|
}
|
package/static/js/features.js
CHANGED
|
@@ -195,31 +195,31 @@
|
|
|
195
195
|
var chatArea = document.getElementById('output-scroll');
|
|
196
196
|
var execPanel = document.querySelector('.input-section');
|
|
197
197
|
var fileBrowser = document.getElementById('fileBrowserContainer');
|
|
198
|
-
var
|
|
198
|
+
var fileIframe = document.getElementById('fileBrowserIframe');
|
|
199
|
+
var voiceContainer = document.getElementById('voiceContainer');
|
|
199
200
|
|
|
200
201
|
if (!bar) return;
|
|
201
202
|
|
|
202
|
-
// Update active button
|
|
203
203
|
bar.querySelectorAll('.view-toggle-btn').forEach(function(btn) {
|
|
204
204
|
btn.classList.toggle('active', btn.dataset.view === view);
|
|
205
205
|
});
|
|
206
206
|
|
|
207
|
-
if (view === '
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
}
|
|
217
|
-
}
|
|
207
|
+
if (chatArea) chatArea.style.display = view === 'chat' ? '' : 'none';
|
|
208
|
+
if (execPanel) execPanel.style.display = view === 'chat' ? '' : 'none';
|
|
209
|
+
if (fileBrowser) fileBrowser.style.display = view === 'files' ? 'flex' : 'none';
|
|
210
|
+
if (voiceContainer) voiceContainer.style.display = view === 'voice' ? 'flex' : 'none';
|
|
211
|
+
|
|
212
|
+
if (view === 'files' && fileIframe && currentConversation) {
|
|
213
|
+
var src = BASE + '/files/' + currentConversation + '/';
|
|
214
|
+
if (fileIframe.src !== location.origin + src) {
|
|
215
|
+
fileIframe.src = src;
|
|
218
216
|
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (view === 'voice' && window.voiceModule) {
|
|
220
|
+
window.voiceModule.activate();
|
|
221
|
+
} else if (view !== 'voice' && window.voiceModule) {
|
|
222
|
+
window.voiceModule.deactivate();
|
|
223
223
|
}
|
|
224
224
|
}
|
|
225
225
|
|