@agent-link/server 0.1.145 → 0.1.147
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 +1 -1
- package/web/app.js +24 -51
- package/web/modules/appHelpers.js +84 -0
- package/web/modules/backgroundRouting.js +258 -0
- package/web/modules/connection.js +4 -303
- package/web/style.css +32 -0
package/package.json
CHANGED
package/web/app.js
CHANGED
|
@@ -19,6 +19,7 @@ import { createConnection } from './modules/connection.js';
|
|
|
19
19
|
import { createFileBrowser } from './modules/fileBrowser.js';
|
|
20
20
|
import { createFilePreview } from './modules/filePreview.js';
|
|
21
21
|
import { createTeam } from './modules/team.js';
|
|
22
|
+
import { createScrollManager, createHighlightScheduler, formatUsage } from './modules/appHelpers.js';
|
|
22
23
|
|
|
23
24
|
// ── App ─────────────────────────────────────────────────────────────────────
|
|
24
25
|
const App = {
|
|
@@ -117,6 +118,8 @@ const App = {
|
|
|
117
118
|
const sidebarView = ref('sessions'); // 'sessions' | 'files' | 'preview' (mobile only)
|
|
118
119
|
const isMobile = ref(window.innerWidth <= 768);
|
|
119
120
|
const workdirMenuOpen = ref(false);
|
|
121
|
+
const teamsCollapsed = ref(false);
|
|
122
|
+
const chatsCollapsed = ref(false);
|
|
120
123
|
|
|
121
124
|
// Team creation state
|
|
122
125
|
const teamInstruction = ref('');
|
|
@@ -231,38 +234,10 @@ const App = {
|
|
|
231
234
|
applyTheme();
|
|
232
235
|
|
|
233
236
|
// ── Scroll management ──
|
|
234
|
-
|
|
235
|
-
let _userScrolledUp = false;
|
|
236
|
-
|
|
237
|
-
function onMessageListScroll(e) {
|
|
238
|
-
const el = e.target;
|
|
239
|
-
_userScrolledUp = (el.scrollHeight - el.scrollTop - el.clientHeight) > 80;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
function scrollToBottom(force) {
|
|
243
|
-
if (_userScrolledUp && !force) return;
|
|
244
|
-
if (_scrollTimer) return;
|
|
245
|
-
_scrollTimer = setTimeout(() => {
|
|
246
|
-
_scrollTimer = null;
|
|
247
|
-
const el = document.querySelector('.message-list');
|
|
248
|
-
if (el) el.scrollTop = el.scrollHeight;
|
|
249
|
-
}, 50);
|
|
250
|
-
}
|
|
237
|
+
const { onScroll: onMessageListScroll, scrollToBottom, cleanup: cleanupScroll } = createScrollManager('.message-list');
|
|
251
238
|
|
|
252
239
|
// ── Highlight.js scheduling ──
|
|
253
|
-
|
|
254
|
-
function scheduleHighlight() {
|
|
255
|
-
if (_hlTimer) return;
|
|
256
|
-
_hlTimer = setTimeout(() => {
|
|
257
|
-
_hlTimer = null;
|
|
258
|
-
if (typeof hljs !== 'undefined') {
|
|
259
|
-
document.querySelectorAll('pre code:not([data-highlighted])').forEach(block => {
|
|
260
|
-
hljs.highlightElement(block);
|
|
261
|
-
block.dataset.highlighted = 'true';
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
}, 300);
|
|
265
|
-
}
|
|
240
|
+
const { scheduleHighlight, cleanup: cleanupHighlight } = createHighlightScheduler();
|
|
266
241
|
|
|
267
242
|
// ── Create module instances ──
|
|
268
243
|
|
|
@@ -475,25 +450,10 @@ const App = {
|
|
|
475
450
|
document.title = name ? `${name} — AgentLink` : 'AgentLink';
|
|
476
451
|
});
|
|
477
452
|
|
|
478
|
-
// ── Usage formatting ──
|
|
479
|
-
function formatTokens(n) {
|
|
480
|
-
if (n >= 1000) return (n / 1000).toFixed(1) + 'k';
|
|
481
|
-
return String(n);
|
|
482
|
-
}
|
|
483
|
-
function formatUsage(u) {
|
|
484
|
-
if (!u) return '';
|
|
485
|
-
const pct = u.contextWindow ? Math.round(u.inputTokens / u.contextWindow * 100) : 0;
|
|
486
|
-
const ctx = formatTokens(u.inputTokens) + ' / ' + formatTokens(u.contextWindow) + ' (' + pct + '%)';
|
|
487
|
-
const cost = '$' + u.totalCost.toFixed(2);
|
|
488
|
-
const model = u.model.replace(/^claude-/, '').replace(/-\d{8}$/, '').replace(/-1m$/, '');
|
|
489
|
-
const dur = (u.durationMs / 1000).toFixed(1) + 's';
|
|
490
|
-
return 'Context ' + ctx + ' \u00b7 Cost ' + cost + ' \u00b7 ' + model + ' \u00b7 ' + dur;
|
|
491
|
-
}
|
|
492
|
-
|
|
493
453
|
// ── Lifecycle ──
|
|
494
454
|
onMounted(() => { connect(scheduleHighlight); });
|
|
495
455
|
onUnmounted(() => {
|
|
496
|
-
closeWs(); streaming.cleanup();
|
|
456
|
+
closeWs(); streaming.cleanup(); cleanupScroll(); cleanupHighlight();
|
|
497
457
|
window.removeEventListener('resize', _resizeHandler);
|
|
498
458
|
document.removeEventListener('click', _workdirMenuClickHandler);
|
|
499
459
|
document.removeEventListener('keydown', _workdirMenuKeyHandler);
|
|
@@ -603,6 +563,7 @@ const App = {
|
|
|
603
563
|
// File preview
|
|
604
564
|
previewPanelOpen, previewPanelWidth, previewFile, previewLoading, previewMarkdownRendered, filePreview,
|
|
605
565
|
workdirMenuOpen,
|
|
566
|
+
teamsCollapsed, chatsCollapsed,
|
|
606
567
|
toggleWorkdirMenu() { workdirMenuOpen.value = !workdirMenuOpen.value; },
|
|
607
568
|
workdirMenuBrowse() {
|
|
608
569
|
workdirMenuOpen.value = false;
|
|
@@ -885,10 +846,14 @@ const App = {
|
|
|
885
846
|
|
|
886
847
|
<!-- Teams section -->
|
|
887
848
|
<div class="sidebar-section sidebar-teams">
|
|
888
|
-
<div class="sidebar-section-header">
|
|
849
|
+
<div class="sidebar-section-header" @click="teamsCollapsed = !teamsCollapsed" style="cursor: pointer;">
|
|
889
850
|
<span>Teams History</span>
|
|
851
|
+
<button class="sidebar-collapse-btn" :title="teamsCollapsed ? 'Expand' : 'Collapse'">
|
|
852
|
+
<svg :class="{ collapsed: teamsCollapsed }" viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6z"/></svg>
|
|
853
|
+
</button>
|
|
890
854
|
</div>
|
|
891
855
|
|
|
856
|
+
<div v-show="!teamsCollapsed">
|
|
892
857
|
<button class="new-conversation-btn" @click="newTeam" :disabled="isTeamActive">
|
|
893
858
|
<svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
|
|
894
859
|
New team
|
|
@@ -932,16 +897,23 @@ const App = {
|
|
|
932
897
|
</div>
|
|
933
898
|
</div>
|
|
934
899
|
</div>
|
|
900
|
+
</div>
|
|
935
901
|
</div>
|
|
936
902
|
|
|
937
903
|
<div class="sidebar-section sidebar-sessions">
|
|
938
|
-
<div class="sidebar-section-header">
|
|
904
|
+
<div class="sidebar-section-header" @click="chatsCollapsed = !chatsCollapsed" style="cursor: pointer;">
|
|
939
905
|
<span>Chat History</span>
|
|
940
|
-
<
|
|
941
|
-
<
|
|
942
|
-
|
|
906
|
+
<span class="sidebar-section-header-actions">
|
|
907
|
+
<button class="sidebar-refresh-btn" @click.stop="requestSessionList" title="Refresh" :disabled="loadingSessions">
|
|
908
|
+
<svg :class="{ spinning: loadingSessions }" viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M17.65 6.35A7.958 7.958 0 0 0 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08A5.99 5.99 0 0 1 12 18c-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>
|
|
909
|
+
</button>
|
|
910
|
+
<button class="sidebar-collapse-btn" :title="chatsCollapsed ? 'Expand' : 'Collapse'">
|
|
911
|
+
<svg :class="{ collapsed: chatsCollapsed }" viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6z"/></svg>
|
|
912
|
+
</button>
|
|
913
|
+
</span>
|
|
943
914
|
</div>
|
|
944
915
|
|
|
916
|
+
<div v-show="!chatsCollapsed">
|
|
945
917
|
<button class="new-conversation-btn" @click="newConversation">
|
|
946
918
|
<svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
|
|
947
919
|
New conversation
|
|
@@ -1002,6 +974,7 @@ const App = {
|
|
|
1002
974
|
</div>
|
|
1003
975
|
</div>
|
|
1004
976
|
</div>
|
|
977
|
+
</div>
|
|
1005
978
|
</div>
|
|
1006
979
|
|
|
1007
980
|
<div v-if="serverVersion || agentVersion" class="sidebar-version-footer">
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// ── UI utility functions for the main App component ──────────────────────────
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Create scroll management functions.
|
|
5
|
+
* @param {string} selector - CSS selector for the scrollable element
|
|
6
|
+
* @returns {{ onScroll, scrollToBottom, cleanup }}
|
|
7
|
+
*/
|
|
8
|
+
export function createScrollManager(selector) {
|
|
9
|
+
let _scrollTimer = null;
|
|
10
|
+
let _userScrolledUp = false;
|
|
11
|
+
|
|
12
|
+
function onScroll(e) {
|
|
13
|
+
const el = e.target;
|
|
14
|
+
_userScrolledUp = (el.scrollHeight - el.scrollTop - el.clientHeight) > 80;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function scrollToBottom(force) {
|
|
18
|
+
if (_userScrolledUp && !force) return;
|
|
19
|
+
if (_scrollTimer) return;
|
|
20
|
+
_scrollTimer = setTimeout(() => {
|
|
21
|
+
_scrollTimer = null;
|
|
22
|
+
const el = document.querySelector(selector);
|
|
23
|
+
if (el) el.scrollTop = el.scrollHeight;
|
|
24
|
+
}, 50);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function cleanup() {
|
|
28
|
+
if (_scrollTimer) { clearTimeout(_scrollTimer); _scrollTimer = null; }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return { onScroll, scrollToBottom, cleanup };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Create a debounced highlight.js scheduler.
|
|
36
|
+
* @returns {{ scheduleHighlight, cleanup }}
|
|
37
|
+
*/
|
|
38
|
+
export function createHighlightScheduler() {
|
|
39
|
+
let _hlTimer = null;
|
|
40
|
+
|
|
41
|
+
function scheduleHighlight() {
|
|
42
|
+
if (_hlTimer) return;
|
|
43
|
+
_hlTimer = setTimeout(() => {
|
|
44
|
+
_hlTimer = null;
|
|
45
|
+
if (typeof hljs !== 'undefined') {
|
|
46
|
+
document.querySelectorAll('pre code:not([data-highlighted])').forEach(block => {
|
|
47
|
+
hljs.highlightElement(block);
|
|
48
|
+
block.dataset.highlighted = 'true';
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}, 300);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function cleanup() {
|
|
55
|
+
if (_hlTimer) { clearTimeout(_hlTimer); _hlTimer = null; }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return { scheduleHighlight, cleanup };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Format a token count for display (e.g. 1500 → "1.5k").
|
|
63
|
+
* @param {number} n
|
|
64
|
+
* @returns {string}
|
|
65
|
+
*/
|
|
66
|
+
export function formatTokens(n) {
|
|
67
|
+
if (n >= 1000) return (n / 1000).toFixed(1) + 'k';
|
|
68
|
+
return String(n);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Format a usage stats object into a human-readable summary string.
|
|
73
|
+
* @param {object|null} u - Usage stats from turn_completed
|
|
74
|
+
* @returns {string}
|
|
75
|
+
*/
|
|
76
|
+
export function formatUsage(u) {
|
|
77
|
+
if (!u) return '';
|
|
78
|
+
const pct = u.contextWindow ? Math.round(u.inputTokens / u.contextWindow * 100) : 0;
|
|
79
|
+
const ctx = formatTokens(u.inputTokens) + ' / ' + formatTokens(u.contextWindow) + ' (' + pct + '%)';
|
|
80
|
+
const cost = '$' + u.totalCost.toFixed(2);
|
|
81
|
+
const model = u.model.replace(/^claude-/, '').replace(/-\d{8}$/, '').replace(/-1m$/, '');
|
|
82
|
+
const dur = (u.durationMs / 1000).toFixed(1) + 's';
|
|
83
|
+
return 'Context ' + ctx + ' \u00b7 Cost ' + cost + ' \u00b7 ' + model + ' \u00b7 ' + dur;
|
|
84
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
// ── History batch building & background conversation routing ──────────────────
|
|
2
|
+
import { isContextSummary } from './messageHelpers.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Convert a history array (from conversation_resumed) into a batch of UI messages.
|
|
6
|
+
* @param {Array} history - Array of {role, content, ...} from the agent
|
|
7
|
+
* @param {() => number} nextId - Function that returns the next message ID
|
|
8
|
+
* @returns {Array} Batch of UI message objects
|
|
9
|
+
*/
|
|
10
|
+
export function buildHistoryBatch(history, nextId) {
|
|
11
|
+
const batch = [];
|
|
12
|
+
for (const h of history) {
|
|
13
|
+
if (h.role === 'user') {
|
|
14
|
+
if (isContextSummary(h.content)) {
|
|
15
|
+
batch.push({
|
|
16
|
+
id: nextId(), role: 'context-summary',
|
|
17
|
+
content: h.content, contextExpanded: false,
|
|
18
|
+
timestamp: h.timestamp ? new Date(h.timestamp) : new Date(),
|
|
19
|
+
});
|
|
20
|
+
} else if (h.isCommandOutput) {
|
|
21
|
+
batch.push({
|
|
22
|
+
id: nextId(), role: 'system',
|
|
23
|
+
content: h.content, isCommandOutput: true,
|
|
24
|
+
timestamp: h.timestamp ? new Date(h.timestamp) : new Date(),
|
|
25
|
+
});
|
|
26
|
+
} else {
|
|
27
|
+
batch.push({
|
|
28
|
+
id: nextId(), role: 'user',
|
|
29
|
+
content: h.content,
|
|
30
|
+
timestamp: h.timestamp ? new Date(h.timestamp) : new Date(),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
} else if (h.role === 'assistant') {
|
|
34
|
+
const last = batch[batch.length - 1];
|
|
35
|
+
if (last && last.role === 'assistant' && !last.isStreaming) {
|
|
36
|
+
last.content += '\n\n' + h.content;
|
|
37
|
+
} else {
|
|
38
|
+
batch.push({
|
|
39
|
+
id: nextId(), role: 'assistant',
|
|
40
|
+
content: h.content, isStreaming: false,
|
|
41
|
+
timestamp: h.timestamp ? new Date(h.timestamp) : new Date(),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
} else if (h.role === 'tool') {
|
|
45
|
+
batch.push({
|
|
46
|
+
id: nextId(), role: 'tool',
|
|
47
|
+
toolId: h.toolId || '', toolName: h.toolName || 'unknown',
|
|
48
|
+
toolInput: h.toolInput || '', hasResult: true,
|
|
49
|
+
expanded: (h.toolName === 'Edit' || h.toolName === 'TodoWrite' || h.toolName === 'Agent'),
|
|
50
|
+
timestamp: h.timestamp ? new Date(h.timestamp) : new Date(),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return batch;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Finalize the last streaming assistant message in a message array.
|
|
59
|
+
* @param {Array} msgs - Array of message objects
|
|
60
|
+
*/
|
|
61
|
+
export function finalizeLastStreaming(msgs) {
|
|
62
|
+
const last = msgs.length > 0 ? msgs[msgs.length - 1] : null;
|
|
63
|
+
if (last && last.role === 'assistant' && last.isStreaming) {
|
|
64
|
+
last.isStreaming = false;
|
|
65
|
+
if (isContextSummary(last.content)) {
|
|
66
|
+
last.role = 'context-summary';
|
|
67
|
+
last.contextExpanded = false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Route a message to a background (non-foreground) conversation's cache.
|
|
74
|
+
* @param {object} deps - Dependencies: conversationCache, processingConversations, sidebar, wsSend
|
|
75
|
+
* @param {string} convId - The conversation ID
|
|
76
|
+
* @param {object} msg - The incoming message
|
|
77
|
+
*/
|
|
78
|
+
export function routeToBackgroundConversation(deps, convId, msg) {
|
|
79
|
+
const { conversationCache, processingConversations, sidebar, wsSend } = deps;
|
|
80
|
+
const cache = conversationCache.value[convId];
|
|
81
|
+
if (!cache) return;
|
|
82
|
+
|
|
83
|
+
if (msg.type === 'session_started') {
|
|
84
|
+
cache.claudeSessionId = msg.claudeSessionId;
|
|
85
|
+
sidebar.requestSessionList();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (msg.type === 'conversation_resumed') {
|
|
90
|
+
cache.claudeSessionId = msg.claudeSessionId;
|
|
91
|
+
if (msg.history && Array.isArray(msg.history)) {
|
|
92
|
+
const nextId = () => ++cache.messageIdCounter;
|
|
93
|
+
cache.messages = buildHistoryBatch(msg.history, nextId);
|
|
94
|
+
if (cache.toolMsgMap) cache.toolMsgMap.clear();
|
|
95
|
+
}
|
|
96
|
+
cache.loadingHistory = false;
|
|
97
|
+
if (msg.isCompacting) {
|
|
98
|
+
cache.isCompacting = true;
|
|
99
|
+
cache.isProcessing = true;
|
|
100
|
+
processingConversations.value[convId] = true;
|
|
101
|
+
cache.messages.push({
|
|
102
|
+
id: ++cache.messageIdCounter, role: 'system',
|
|
103
|
+
content: 'Context compacting...', isCompactStart: true,
|
|
104
|
+
timestamp: new Date(),
|
|
105
|
+
});
|
|
106
|
+
} else if (msg.isProcessing) {
|
|
107
|
+
cache.isProcessing = true;
|
|
108
|
+
processingConversations.value[convId] = true;
|
|
109
|
+
cache.messages.push({
|
|
110
|
+
id: ++cache.messageIdCounter, role: 'system',
|
|
111
|
+
content: 'Agent is processing...',
|
|
112
|
+
timestamp: new Date(),
|
|
113
|
+
});
|
|
114
|
+
} else {
|
|
115
|
+
cache.messages.push({
|
|
116
|
+
id: ++cache.messageIdCounter, role: 'system',
|
|
117
|
+
content: 'Session restored. You can continue the conversation.',
|
|
118
|
+
timestamp: new Date(),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (msg.type === 'claude_output') {
|
|
125
|
+
if (!cache.isProcessing) {
|
|
126
|
+
cache.isProcessing = true;
|
|
127
|
+
processingConversations.value[convId] = true;
|
|
128
|
+
}
|
|
129
|
+
const data = msg.data;
|
|
130
|
+
if (!data) return;
|
|
131
|
+
if (data.type === 'content_block_delta' && data.delta) {
|
|
132
|
+
const msgs = cache.messages;
|
|
133
|
+
const last = msgs.length > 0 ? msgs[msgs.length - 1] : null;
|
|
134
|
+
if (last && last.role === 'assistant' && last.isStreaming) {
|
|
135
|
+
last.content += data.delta;
|
|
136
|
+
} else {
|
|
137
|
+
msgs.push({
|
|
138
|
+
id: ++cache.messageIdCounter, role: 'assistant',
|
|
139
|
+
content: data.delta, isStreaming: true, timestamp: new Date(),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
} else if (data.type === 'tool_use' && data.tools) {
|
|
143
|
+
const msgs = cache.messages;
|
|
144
|
+
finalizeLastStreaming(msgs);
|
|
145
|
+
for (const tool of data.tools) {
|
|
146
|
+
const toolMsg = {
|
|
147
|
+
id: ++cache.messageIdCounter, role: 'tool',
|
|
148
|
+
toolId: tool.id, toolName: tool.name || 'unknown',
|
|
149
|
+
toolInput: tool.input ? JSON.stringify(tool.input, null, 2) : '',
|
|
150
|
+
hasResult: false, expanded: (tool.name === 'Edit' || tool.name === 'TodoWrite' || tool.name === 'Agent'),
|
|
151
|
+
timestamp: new Date(),
|
|
152
|
+
};
|
|
153
|
+
msgs.push(toolMsg);
|
|
154
|
+
if (tool.id) {
|
|
155
|
+
if (!cache.toolMsgMap) cache.toolMsgMap = new Map();
|
|
156
|
+
cache.toolMsgMap.set(tool.id, toolMsg);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
} else if (data.type === 'user' && data.tool_use_result) {
|
|
160
|
+
const result = data.tool_use_result;
|
|
161
|
+
const results = Array.isArray(result) ? result : [result];
|
|
162
|
+
const tMap = cache.toolMsgMap || new Map();
|
|
163
|
+
for (const r of results) {
|
|
164
|
+
const toolMsg = tMap.get(r.tool_use_id);
|
|
165
|
+
if (toolMsg) {
|
|
166
|
+
toolMsg.toolOutput = typeof r.content === 'string'
|
|
167
|
+
? r.content : JSON.stringify(r.content, null, 2);
|
|
168
|
+
toolMsg.hasResult = true;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
} else if (msg.type === 'turn_completed' || msg.type === 'execution_cancelled') {
|
|
173
|
+
finalizeLastStreaming(cache.messages);
|
|
174
|
+
cache.isProcessing = false;
|
|
175
|
+
cache.isCompacting = false;
|
|
176
|
+
if (msg.usage) cache.usageStats = msg.usage;
|
|
177
|
+
if (cache.toolMsgMap) cache.toolMsgMap.clear();
|
|
178
|
+
processingConversations.value[convId] = false;
|
|
179
|
+
if (msg.type === 'execution_cancelled') {
|
|
180
|
+
cache.needsResume = true;
|
|
181
|
+
cache.messages.push({
|
|
182
|
+
id: ++cache.messageIdCounter, role: 'system',
|
|
183
|
+
content: 'Generation stopped.', timestamp: new Date(),
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
sidebar.requestSessionList();
|
|
187
|
+
if (cache.queuedMessages && cache.queuedMessages.length > 0) {
|
|
188
|
+
const queued = cache.queuedMessages.shift();
|
|
189
|
+
cache.messages.push({
|
|
190
|
+
id: ++cache.messageIdCounter, role: 'user', status: 'sent',
|
|
191
|
+
content: queued.content, attachments: queued.attachments,
|
|
192
|
+
timestamp: new Date(),
|
|
193
|
+
});
|
|
194
|
+
cache.isProcessing = true;
|
|
195
|
+
processingConversations.value[convId] = true;
|
|
196
|
+
wsSend(queued.payload);
|
|
197
|
+
}
|
|
198
|
+
} else if (msg.type === 'context_compaction') {
|
|
199
|
+
if (msg.status === 'started') {
|
|
200
|
+
cache.isCompacting = true;
|
|
201
|
+
cache.messages.push({
|
|
202
|
+
id: ++cache.messageIdCounter, role: 'system',
|
|
203
|
+
content: 'Context compacting...', isCompactStart: true,
|
|
204
|
+
timestamp: new Date(),
|
|
205
|
+
});
|
|
206
|
+
} else if (msg.status === 'completed') {
|
|
207
|
+
cache.isCompacting = false;
|
|
208
|
+
const startMsg = [...cache.messages].reverse().find(m => m.isCompactStart && !m.compactDone);
|
|
209
|
+
if (startMsg) {
|
|
210
|
+
startMsg.content = 'Context compacted';
|
|
211
|
+
startMsg.compactDone = true;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
} else if (msg.type === 'error') {
|
|
215
|
+
finalizeLastStreaming(cache.messages);
|
|
216
|
+
cache.messages.push({
|
|
217
|
+
id: ++cache.messageIdCounter, role: 'system',
|
|
218
|
+
content: msg.message, isError: true, timestamp: new Date(),
|
|
219
|
+
});
|
|
220
|
+
cache.isProcessing = false;
|
|
221
|
+
cache.isCompacting = false;
|
|
222
|
+
processingConversations.value[convId] = false;
|
|
223
|
+
} else if (msg.type === 'command_output') {
|
|
224
|
+
finalizeLastStreaming(cache.messages);
|
|
225
|
+
cache.messages.push({
|
|
226
|
+
id: ++cache.messageIdCounter, role: 'system',
|
|
227
|
+
content: msg.content, isCommandOutput: true, timestamp: new Date(),
|
|
228
|
+
});
|
|
229
|
+
} else if (msg.type === 'ask_user_question') {
|
|
230
|
+
const msgs = cache.messages;
|
|
231
|
+
finalizeLastStreaming(msgs);
|
|
232
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
233
|
+
const m = msgs[i];
|
|
234
|
+
if (m.role === 'tool' && m.toolName === 'AskUserQuestion') {
|
|
235
|
+
msgs.splice(i, 1);
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
if (m.role === 'user') break;
|
|
239
|
+
}
|
|
240
|
+
const questions = msg.questions || [];
|
|
241
|
+
const selectedAnswers = {};
|
|
242
|
+
const customTexts = {};
|
|
243
|
+
for (let i = 0; i < questions.length; i++) {
|
|
244
|
+
selectedAnswers[i] = questions[i].multiSelect ? [] : null;
|
|
245
|
+
customTexts[i] = '';
|
|
246
|
+
}
|
|
247
|
+
msgs.push({
|
|
248
|
+
id: ++cache.messageIdCounter,
|
|
249
|
+
role: 'ask-question',
|
|
250
|
+
requestId: msg.requestId,
|
|
251
|
+
questions,
|
|
252
|
+
answered: false,
|
|
253
|
+
selectedAnswers,
|
|
254
|
+
customTexts,
|
|
255
|
+
timestamp: new Date(),
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// ── WebSocket connection, message routing, reconnection ──────────────────────
|
|
2
2
|
import { encrypt, decrypt, isEncrypted, decodeKey } from '../encryption.js';
|
|
3
3
|
import { isContextSummary } from './messageHelpers.js';
|
|
4
|
+
import { buildHistoryBatch, finalizeLastStreaming, routeToBackgroundConversation } from './backgroundRouting.js';
|
|
4
5
|
|
|
5
6
|
const MAX_RECONNECT_ATTEMPTS = 50;
|
|
6
7
|
const RECONNECT_BASE_DELAY = 1000;
|
|
@@ -55,265 +56,7 @@ export function createConnection(deps) {
|
|
|
55
56
|
function clearToolMsgMap() { toolMsgMap.clear(); }
|
|
56
57
|
|
|
57
58
|
// ── Background conversation routing ──
|
|
58
|
-
//
|
|
59
|
-
// update its cached state directly (no streaming animation).
|
|
60
|
-
function routeToBackgroundConversation(convId, msg) {
|
|
61
|
-
const cache = conversationCache.value[convId];
|
|
62
|
-
if (!cache) return; // no cache entry — discard
|
|
63
|
-
|
|
64
|
-
if (msg.type === 'session_started') {
|
|
65
|
-
// Claude session ID captured for background conversation
|
|
66
|
-
cache.claudeSessionId = msg.claudeSessionId;
|
|
67
|
-
sidebar.requestSessionList();
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (msg.type === 'conversation_resumed') {
|
|
72
|
-
cache.claudeSessionId = msg.claudeSessionId;
|
|
73
|
-
if (msg.history && Array.isArray(msg.history)) {
|
|
74
|
-
const batch = [];
|
|
75
|
-
for (const h of msg.history) {
|
|
76
|
-
if (h.role === 'user') {
|
|
77
|
-
if (isContextSummary(h.content)) {
|
|
78
|
-
batch.push({
|
|
79
|
-
id: ++cache.messageIdCounter, role: 'context-summary',
|
|
80
|
-
content: h.content, contextExpanded: false,
|
|
81
|
-
timestamp: h.timestamp ? new Date(h.timestamp) : new Date(),
|
|
82
|
-
});
|
|
83
|
-
} else if (h.isCommandOutput) {
|
|
84
|
-
batch.push({
|
|
85
|
-
id: ++cache.messageIdCounter, role: 'system',
|
|
86
|
-
content: h.content, isCommandOutput: true,
|
|
87
|
-
timestamp: h.timestamp ? new Date(h.timestamp) : new Date(),
|
|
88
|
-
});
|
|
89
|
-
} else {
|
|
90
|
-
batch.push({
|
|
91
|
-
id: ++cache.messageIdCounter, role: 'user',
|
|
92
|
-
content: h.content,
|
|
93
|
-
timestamp: h.timestamp ? new Date(h.timestamp) : new Date(),
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
} else if (h.role === 'assistant') {
|
|
97
|
-
const last = batch[batch.length - 1];
|
|
98
|
-
if (last && last.role === 'assistant' && !last.isStreaming) {
|
|
99
|
-
last.content += '\n\n' + h.content;
|
|
100
|
-
} else {
|
|
101
|
-
batch.push({
|
|
102
|
-
id: ++cache.messageIdCounter, role: 'assistant',
|
|
103
|
-
content: h.content, isStreaming: false,
|
|
104
|
-
timestamp: h.timestamp ? new Date(h.timestamp) : new Date(),
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
} else if (h.role === 'tool') {
|
|
108
|
-
batch.push({
|
|
109
|
-
id: ++cache.messageIdCounter, role: 'tool',
|
|
110
|
-
toolId: h.toolId || '', toolName: h.toolName || 'unknown',
|
|
111
|
-
toolInput: h.toolInput || '', hasResult: true,
|
|
112
|
-
expanded: (h.toolName === 'Edit' || h.toolName === 'TodoWrite' || h.toolName === 'Agent'),
|
|
113
|
-
timestamp: h.timestamp ? new Date(h.timestamp) : new Date(),
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
cache.messages = batch;
|
|
118
|
-
if (cache.toolMsgMap) cache.toolMsgMap.clear();
|
|
119
|
-
}
|
|
120
|
-
cache.loadingHistory = false;
|
|
121
|
-
if (msg.isCompacting) {
|
|
122
|
-
cache.isCompacting = true;
|
|
123
|
-
cache.isProcessing = true;
|
|
124
|
-
processingConversations.value[convId] = true;
|
|
125
|
-
cache.messages.push({
|
|
126
|
-
id: ++cache.messageIdCounter, role: 'system',
|
|
127
|
-
content: 'Context compacting...', isCompactStart: true,
|
|
128
|
-
timestamp: new Date(),
|
|
129
|
-
});
|
|
130
|
-
} else if (msg.isProcessing) {
|
|
131
|
-
cache.isProcessing = true;
|
|
132
|
-
processingConversations.value[convId] = true;
|
|
133
|
-
cache.messages.push({
|
|
134
|
-
id: ++cache.messageIdCounter, role: 'system',
|
|
135
|
-
content: 'Agent is processing...',
|
|
136
|
-
timestamp: new Date(),
|
|
137
|
-
});
|
|
138
|
-
} else {
|
|
139
|
-
cache.messages.push({
|
|
140
|
-
id: ++cache.messageIdCounter, role: 'system',
|
|
141
|
-
content: 'Session restored. You can continue the conversation.',
|
|
142
|
-
timestamp: new Date(),
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
if (msg.type === 'claude_output') {
|
|
149
|
-
// Safety net: restore processing state if output arrives after reconnect
|
|
150
|
-
if (!cache.isProcessing) {
|
|
151
|
-
cache.isProcessing = true;
|
|
152
|
-
processingConversations.value[convId] = true;
|
|
153
|
-
}
|
|
154
|
-
const data = msg.data;
|
|
155
|
-
if (!data) return;
|
|
156
|
-
if (data.type === 'content_block_delta' && data.delta) {
|
|
157
|
-
// Append text to last assistant message (or create new one)
|
|
158
|
-
const msgs = cache.messages;
|
|
159
|
-
const last = msgs.length > 0 ? msgs[msgs.length - 1] : null;
|
|
160
|
-
if (last && last.role === 'assistant' && last.isStreaming) {
|
|
161
|
-
last.content += data.delta;
|
|
162
|
-
} else {
|
|
163
|
-
msgs.push({
|
|
164
|
-
id: ++cache.messageIdCounter, role: 'assistant',
|
|
165
|
-
content: data.delta, isStreaming: true, timestamp: new Date(),
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
} else if (data.type === 'tool_use' && data.tools) {
|
|
169
|
-
// Finalize streaming message
|
|
170
|
-
const msgs = cache.messages;
|
|
171
|
-
const last = msgs.length > 0 ? msgs[msgs.length - 1] : null;
|
|
172
|
-
if (last && last.role === 'assistant' && last.isStreaming) {
|
|
173
|
-
last.isStreaming = false;
|
|
174
|
-
if (isContextSummary(last.content)) {
|
|
175
|
-
last.role = 'context-summary';
|
|
176
|
-
last.contextExpanded = false;
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
for (const tool of data.tools) {
|
|
180
|
-
const toolMsg = {
|
|
181
|
-
id: ++cache.messageIdCounter, role: 'tool',
|
|
182
|
-
toolId: tool.id, toolName: tool.name || 'unknown',
|
|
183
|
-
toolInput: tool.input ? JSON.stringify(tool.input, null, 2) : '',
|
|
184
|
-
hasResult: false, expanded: (tool.name === 'Edit' || tool.name === 'TodoWrite' || tool.name === 'Agent'),
|
|
185
|
-
timestamp: new Date(),
|
|
186
|
-
};
|
|
187
|
-
msgs.push(toolMsg);
|
|
188
|
-
if (tool.id) {
|
|
189
|
-
if (!cache.toolMsgMap) cache.toolMsgMap = new Map();
|
|
190
|
-
cache.toolMsgMap.set(tool.id, toolMsg);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
} else if (data.type === 'user' && data.tool_use_result) {
|
|
194
|
-
const result = data.tool_use_result;
|
|
195
|
-
const results = Array.isArray(result) ? result : [result];
|
|
196
|
-
const tMap = cache.toolMsgMap || new Map();
|
|
197
|
-
for (const r of results) {
|
|
198
|
-
const toolMsg = tMap.get(r.tool_use_id);
|
|
199
|
-
if (toolMsg) {
|
|
200
|
-
toolMsg.toolOutput = typeof r.content === 'string'
|
|
201
|
-
? r.content : JSON.stringify(r.content, null, 2);
|
|
202
|
-
toolMsg.hasResult = true;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
} else if (msg.type === 'turn_completed' || msg.type === 'execution_cancelled') {
|
|
207
|
-
// Finalize streaming message
|
|
208
|
-
const msgs = cache.messages;
|
|
209
|
-
const last = msgs.length > 0 ? msgs[msgs.length - 1] : null;
|
|
210
|
-
if (last && last.role === 'assistant' && last.isStreaming) {
|
|
211
|
-
last.isStreaming = false;
|
|
212
|
-
if (isContextSummary(last.content)) {
|
|
213
|
-
last.role = 'context-summary';
|
|
214
|
-
last.contextExpanded = false;
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
cache.isProcessing = false;
|
|
218
|
-
cache.isCompacting = false;
|
|
219
|
-
if (msg.usage) cache.usageStats = msg.usage;
|
|
220
|
-
if (cache.toolMsgMap) cache.toolMsgMap.clear();
|
|
221
|
-
processingConversations.value[convId] = false;
|
|
222
|
-
if (msg.type === 'execution_cancelled') {
|
|
223
|
-
cache.needsResume = true;
|
|
224
|
-
cache.messages.push({
|
|
225
|
-
id: ++cache.messageIdCounter, role: 'system',
|
|
226
|
-
content: 'Generation stopped.', timestamp: new Date(),
|
|
227
|
-
});
|
|
228
|
-
}
|
|
229
|
-
sidebar.requestSessionList();
|
|
230
|
-
// Dequeue next message for this background conversation
|
|
231
|
-
if (cache.queuedMessages && cache.queuedMessages.length > 0) {
|
|
232
|
-
const queued = cache.queuedMessages.shift();
|
|
233
|
-
cache.messages.push({
|
|
234
|
-
id: ++cache.messageIdCounter, role: 'user', status: 'sent',
|
|
235
|
-
content: queued.content, attachments: queued.attachments,
|
|
236
|
-
timestamp: new Date(),
|
|
237
|
-
});
|
|
238
|
-
cache.isProcessing = true;
|
|
239
|
-
processingConversations.value[convId] = true;
|
|
240
|
-
wsSend(queued.payload);
|
|
241
|
-
}
|
|
242
|
-
} else if (msg.type === 'context_compaction') {
|
|
243
|
-
if (msg.status === 'started') {
|
|
244
|
-
cache.isCompacting = true;
|
|
245
|
-
cache.messages.push({
|
|
246
|
-
id: ++cache.messageIdCounter, role: 'system',
|
|
247
|
-
content: 'Context compacting...', isCompactStart: true,
|
|
248
|
-
timestamp: new Date(),
|
|
249
|
-
});
|
|
250
|
-
} else if (msg.status === 'completed') {
|
|
251
|
-
cache.isCompacting = false;
|
|
252
|
-
const startMsg = [...cache.messages].reverse().find(m => m.isCompactStart && !m.compactDone);
|
|
253
|
-
if (startMsg) {
|
|
254
|
-
startMsg.content = 'Context compacted';
|
|
255
|
-
startMsg.compactDone = true;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
} else if (msg.type === 'error') {
|
|
259
|
-
// Finalize streaming
|
|
260
|
-
const msgs = cache.messages;
|
|
261
|
-
const last = msgs.length > 0 ? msgs[msgs.length - 1] : null;
|
|
262
|
-
if (last && last.role === 'assistant' && last.isStreaming) {
|
|
263
|
-
last.isStreaming = false;
|
|
264
|
-
}
|
|
265
|
-
cache.messages.push({
|
|
266
|
-
id: ++cache.messageIdCounter, role: 'system',
|
|
267
|
-
content: msg.message, isError: true, timestamp: new Date(),
|
|
268
|
-
});
|
|
269
|
-
cache.isProcessing = false;
|
|
270
|
-
cache.isCompacting = false;
|
|
271
|
-
processingConversations.value[convId] = false;
|
|
272
|
-
} else if (msg.type === 'command_output') {
|
|
273
|
-
const msgs = cache.messages;
|
|
274
|
-
const last = msgs.length > 0 ? msgs[msgs.length - 1] : null;
|
|
275
|
-
if (last && last.role === 'assistant' && last.isStreaming) {
|
|
276
|
-
last.isStreaming = false;
|
|
277
|
-
}
|
|
278
|
-
cache.messages.push({
|
|
279
|
-
id: ++cache.messageIdCounter, role: 'system',
|
|
280
|
-
content: msg.content, isCommandOutput: true, timestamp: new Date(),
|
|
281
|
-
});
|
|
282
|
-
} else if (msg.type === 'ask_user_question') {
|
|
283
|
-
// Finalize streaming
|
|
284
|
-
const msgs = cache.messages;
|
|
285
|
-
const last = msgs.length > 0 ? msgs[msgs.length - 1] : null;
|
|
286
|
-
if (last && last.role === 'assistant' && last.isStreaming) {
|
|
287
|
-
last.isStreaming = false;
|
|
288
|
-
}
|
|
289
|
-
// Remove AskUserQuestion tool msg
|
|
290
|
-
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
291
|
-
const m = msgs[i];
|
|
292
|
-
if (m.role === 'tool' && m.toolName === 'AskUserQuestion') {
|
|
293
|
-
msgs.splice(i, 1);
|
|
294
|
-
break;
|
|
295
|
-
}
|
|
296
|
-
if (m.role === 'user') break;
|
|
297
|
-
}
|
|
298
|
-
const questions = msg.questions || [];
|
|
299
|
-
const selectedAnswers = {};
|
|
300
|
-
const customTexts = {};
|
|
301
|
-
for (let i = 0; i < questions.length; i++) {
|
|
302
|
-
selectedAnswers[i] = questions[i].multiSelect ? [] : null;
|
|
303
|
-
customTexts[i] = '';
|
|
304
|
-
}
|
|
305
|
-
msgs.push({
|
|
306
|
-
id: ++cache.messageIdCounter,
|
|
307
|
-
role: 'ask-question',
|
|
308
|
-
requestId: msg.requestId,
|
|
309
|
-
questions,
|
|
310
|
-
answered: false,
|
|
311
|
-
selectedAnswers,
|
|
312
|
-
customTexts,
|
|
313
|
-
timestamp: new Date(),
|
|
314
|
-
});
|
|
315
|
-
}
|
|
316
|
-
}
|
|
59
|
+
// Delegated to backgroundRouting.js module.
|
|
317
60
|
|
|
318
61
|
function wsSend(msg) {
|
|
319
62
|
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
|
@@ -493,7 +236,7 @@ export function createConnection(deps) {
|
|
|
493
236
|
if (msg.conversationId && currentConversationId
|
|
494
237
|
&& currentConversationId.value
|
|
495
238
|
&& msg.conversationId !== currentConversationId.value) {
|
|
496
|
-
routeToBackgroundConversation(msg.conversationId, msg);
|
|
239
|
+
routeToBackgroundConversation({ conversationCache, processingConversations, sidebar, wsSend }, msg.conversationId, msg);
|
|
497
240
|
return;
|
|
498
241
|
}
|
|
499
242
|
|
|
@@ -738,49 +481,7 @@ export function createConnection(deps) {
|
|
|
738
481
|
} else if (msg.type === 'conversation_resumed') {
|
|
739
482
|
currentClaudeSessionId.value = msg.claudeSessionId;
|
|
740
483
|
if (msg.history && Array.isArray(msg.history)) {
|
|
741
|
-
|
|
742
|
-
for (const h of msg.history) {
|
|
743
|
-
if (h.role === 'user') {
|
|
744
|
-
if (isContextSummary(h.content)) {
|
|
745
|
-
batch.push({
|
|
746
|
-
id: streaming.nextId(), role: 'context-summary',
|
|
747
|
-
content: h.content, contextExpanded: false,
|
|
748
|
-
timestamp: h.timestamp ? new Date(h.timestamp) : new Date(),
|
|
749
|
-
});
|
|
750
|
-
} else if (h.isCommandOutput) {
|
|
751
|
-
batch.push({
|
|
752
|
-
id: streaming.nextId(), role: 'system',
|
|
753
|
-
content: h.content, isCommandOutput: true,
|
|
754
|
-
timestamp: h.timestamp ? new Date(h.timestamp) : new Date(),
|
|
755
|
-
});
|
|
756
|
-
} else {
|
|
757
|
-
batch.push({
|
|
758
|
-
id: streaming.nextId(), role: 'user',
|
|
759
|
-
content: h.content,
|
|
760
|
-
timestamp: h.timestamp ? new Date(h.timestamp) : new Date(),
|
|
761
|
-
});
|
|
762
|
-
}
|
|
763
|
-
} else if (h.role === 'assistant') {
|
|
764
|
-
const last = batch[batch.length - 1];
|
|
765
|
-
if (last && last.role === 'assistant' && !last.isStreaming) {
|
|
766
|
-
last.content += '\n\n' + h.content;
|
|
767
|
-
} else {
|
|
768
|
-
batch.push({
|
|
769
|
-
id: streaming.nextId(), role: 'assistant',
|
|
770
|
-
content: h.content, isStreaming: false,
|
|
771
|
-
timestamp: h.timestamp ? new Date(h.timestamp) : new Date(),
|
|
772
|
-
});
|
|
773
|
-
}
|
|
774
|
-
} else if (h.role === 'tool') {
|
|
775
|
-
batch.push({
|
|
776
|
-
id: streaming.nextId(), role: 'tool',
|
|
777
|
-
toolId: h.toolId || '', toolName: h.toolName || 'unknown',
|
|
778
|
-
toolInput: h.toolInput || '', hasResult: true,
|
|
779
|
-
expanded: (h.toolName === 'Edit' || h.toolName === 'TodoWrite' || h.toolName === 'Agent'), timestamp: h.timestamp ? new Date(h.timestamp) : new Date(),
|
|
780
|
-
});
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
messages.value = batch;
|
|
484
|
+
messages.value = buildHistoryBatch(msg.history, () => streaming.nextId());
|
|
784
485
|
toolMsgMap.clear();
|
|
785
486
|
}
|
|
786
487
|
loadingHistory.value = false;
|
package/web/style.css
CHANGED
|
@@ -419,6 +419,38 @@ body {
|
|
|
419
419
|
cursor: not-allowed;
|
|
420
420
|
}
|
|
421
421
|
|
|
422
|
+
.sidebar-section-header-actions {
|
|
423
|
+
display: flex;
|
|
424
|
+
align-items: center;
|
|
425
|
+
gap: 2px;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
.sidebar-collapse-btn {
|
|
429
|
+
display: flex;
|
|
430
|
+
align-items: center;
|
|
431
|
+
justify-content: center;
|
|
432
|
+
width: 24px;
|
|
433
|
+
height: 24px;
|
|
434
|
+
background: none;
|
|
435
|
+
border: none;
|
|
436
|
+
border-radius: 4px;
|
|
437
|
+
color: var(--text-secondary);
|
|
438
|
+
cursor: pointer;
|
|
439
|
+
transition: color 0.15s;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
.sidebar-collapse-btn:hover {
|
|
443
|
+
color: var(--text-primary);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
.sidebar-collapse-btn svg {
|
|
447
|
+
transition: transform 0.2s ease;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
.sidebar-collapse-btn svg.collapsed {
|
|
451
|
+
transform: rotate(-90deg);
|
|
452
|
+
}
|
|
453
|
+
|
|
422
454
|
@keyframes spin {
|
|
423
455
|
from { transform: rotate(0deg); }
|
|
424
456
|
to { transform: rotate(360deg); }
|