@myrialabs/clopen 0.1.2 → 0.1.4
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/CONTRIBUTING.md +40 -355
- package/README.md +46 -113
- package/backend/lib/engine/adapters/opencode/message-converter.ts +53 -2
- package/backend/lib/engine/adapters/opencode/stream.ts +89 -5
- package/backend/lib/mcp/config.ts +7 -3
- package/backend/lib/mcp/servers/helper.ts +25 -14
- package/backend/lib/mcp/servers/index.ts +7 -2
- package/backend/lib/project/status-manager.ts +221 -181
- package/frontend/lib/components/chat/ChatInterface.svelte +7 -0
- package/frontend/lib/components/chat/input/components/EngineModelPicker.svelte +16 -9
- package/frontend/lib/components/chat/message/ChatMessages.svelte +16 -4
- package/frontend/lib/components/chat/tools/AgentTool.svelte +12 -11
- package/frontend/lib/components/chat/tools/BashOutputTool.svelte +3 -3
- package/frontend/lib/components/chat/tools/BashTool.svelte +4 -4
- package/frontend/lib/components/chat/tools/CustomMcpTool.svelte +3 -1
- package/frontend/lib/components/chat/tools/EditTool.svelte +6 -6
- package/frontend/lib/components/chat/tools/ExitPlanModeTool.svelte +1 -1
- package/frontend/lib/components/chat/tools/GlobTool.svelte +12 -12
- package/frontend/lib/components/chat/tools/GrepTool.svelte +5 -5
- package/frontend/lib/components/chat/tools/ListMcpResourcesTool.svelte +1 -1
- package/frontend/lib/components/chat/tools/NotebookEditTool.svelte +6 -6
- package/frontend/lib/components/chat/tools/ReadMcpResourceTool.svelte +2 -2
- package/frontend/lib/components/chat/tools/ReadTool.svelte +4 -4
- package/frontend/lib/components/chat/tools/TaskStopTool.svelte +1 -1
- package/frontend/lib/components/chat/tools/TaskTool.svelte +1 -1
- package/frontend/lib/components/chat/tools/TodoWriteTool.svelte +4 -4
- package/frontend/lib/components/chat/tools/WebSearchTool.svelte +1 -1
- package/frontend/lib/components/chat/tools/WriteTool.svelte +3 -3
- package/frontend/lib/components/chat/tools/components/CodeBlock.svelte +3 -3
- package/frontend/lib/components/chat/tools/components/DiffBlock.svelte +2 -2
- package/frontend/lib/components/chat/tools/components/FileHeader.svelte +1 -1
- package/frontend/lib/components/chat/tools/components/InfoLine.svelte +2 -2
- package/frontend/lib/components/chat/tools/components/StatsBadges.svelte +1 -1
- package/frontend/lib/components/chat/tools/components/TerminalCommand.svelte +5 -5
- package/frontend/lib/components/common/Button.svelte +1 -1
- package/frontend/lib/components/common/Card.svelte +3 -3
- package/frontend/lib/components/common/Input.svelte +3 -3
- package/frontend/lib/components/common/LoadingSpinner.svelte +1 -1
- package/frontend/lib/components/common/Select.svelte +6 -6
- package/frontend/lib/components/common/Textarea.svelte +3 -3
- package/frontend/lib/components/files/FileViewer.svelte +1 -1
- package/frontend/lib/components/git/ChangesSection.svelte +2 -4
- package/frontend/lib/components/preview/browser/BrowserPreview.svelte +9 -29
- package/frontend/lib/components/preview/browser/components/Container.svelte +17 -0
- package/frontend/lib/components/preview/browser/components/VirtualCursor.svelte +2 -2
- package/frontend/lib/components/settings/appearance/AppearanceSettings.svelte +0 -6
- package/frontend/lib/components/settings/appearance/LayoutPresetSettings.svelte +15 -15
- package/frontend/lib/components/settings/appearance/LayoutPreview.svelte +2 -2
- package/frontend/lib/components/settings/engines/AIEnginesSettings.svelte +1 -1
- package/frontend/lib/components/workspace/DesktopNavigator.svelte +380 -383
- package/frontend/lib/components/workspace/MobileNavigator.svelte +391 -395
- package/frontend/lib/components/workspace/PanelHeader.svelte +623 -505
- package/frontend/lib/components/workspace/ViewMenu.svelte +9 -25
- package/frontend/lib/components/workspace/layout/split-pane/Layout.svelte +29 -4
- package/frontend/lib/components/workspace/panels/ChatPanel.svelte +3 -2
- package/frontend/lib/services/notification/global-stream-monitor.ts +77 -86
- package/frontend/lib/services/project/status.service.ts +160 -159
- package/frontend/lib/stores/ui/workspace.svelte.ts +326 -283
- package/package.json +1 -1
- package/scripts/pre-publish-check.sh +0 -142
- package/scripts/setup-hooks.sh +0 -134
- package/scripts/validate-branch-name.sh +0 -47
- package/scripts/validate-commit-msg.sh +0 -42
|
@@ -1,182 +1,222 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Project Status Data Service
|
|
3
|
-
* Shared logic for getting project status data
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { streamManager } from '../chat/stream-manager.js';
|
|
7
|
-
import { ws } from '../utils/ws.js';
|
|
8
|
-
|
|
9
|
-
//
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Project Status Data Service
|
|
3
|
+
* Shared logic for getting project status data
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { streamManager, type StreamState } from '../chat/stream-manager.js';
|
|
7
|
+
import { ws } from '../utils/ws.js';
|
|
8
|
+
|
|
9
|
+
// Interactive tools that block the stream waiting for user input
|
|
10
|
+
const INTERACTIVE_TOOLS = new Set(['AskUserQuestion']);
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Check if an active stream is waiting for user input.
|
|
14
|
+
* Scans stream messages for unanswered interactive tool_use blocks.
|
|
15
|
+
* This is the backend single source of truth — works even when the
|
|
16
|
+
* user is on a different project and not receiving chat events.
|
|
17
|
+
*/
|
|
18
|
+
function detectStreamWaitingInput(stream: StreamState): boolean {
|
|
19
|
+
if (stream.status !== 'active') return false;
|
|
20
|
+
|
|
21
|
+
const answeredToolIds = new Set<string>();
|
|
22
|
+
for (const event of stream.messages) {
|
|
23
|
+
const msg = event.message;
|
|
24
|
+
if (!msg || (msg as any).type !== 'user' || !(msg as any).content) continue;
|
|
25
|
+
const content = Array.isArray((msg as any).content) ? (msg as any).content : [];
|
|
26
|
+
for (const item of content) {
|
|
27
|
+
if (item.type === 'tool_result' && item.tool_use_id) {
|
|
28
|
+
answeredToolIds.add(item.tool_use_id);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
for (const event of stream.messages) {
|
|
34
|
+
const msg = event.message;
|
|
35
|
+
if (!msg || (msg as any).type !== 'assistant' || !(msg as any).content) continue;
|
|
36
|
+
const content = Array.isArray((msg as any).content) ? (msg as any).content : [];
|
|
37
|
+
if (content.some((item: any) =>
|
|
38
|
+
item.type === 'tool_use' && INTERACTIVE_TOOLS.has(item.name) && item.id && !answeredToolIds.has(item.id)
|
|
39
|
+
)) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Store active users per project (shared with main endpoint)
|
|
48
|
+
const projectUsers = new Map<string, Set<{ userId: string; userName: string; lastSeen: number }>>();
|
|
49
|
+
|
|
50
|
+
// Cleanup inactive users after 5 minutes
|
|
51
|
+
const USER_TIMEOUT = 5 * 60 * 1000;
|
|
52
|
+
|
|
53
|
+
function cleanupInactiveUsers() {
|
|
54
|
+
const now = Date.now();
|
|
55
|
+
projectUsers.forEach((users, projectId) => {
|
|
56
|
+
const activeUsers = new Set([...users].filter(user => now - user.lastSeen < USER_TIMEOUT));
|
|
57
|
+
if (activeUsers.size === 0) {
|
|
58
|
+
projectUsers.delete(projectId);
|
|
59
|
+
} else {
|
|
60
|
+
projectUsers.set(projectId, activeUsers);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Get project status data
|
|
66
|
+
export async function getProjectStatusData(projectId?: string) {
|
|
67
|
+
cleanupInactiveUsers();
|
|
68
|
+
|
|
69
|
+
if (projectId) {
|
|
70
|
+
// Get status for specific project
|
|
71
|
+
const allProjectStreams = streamManager.getProjectStreams(projectId);
|
|
72
|
+
const users = projectUsers.get(projectId);
|
|
73
|
+
|
|
74
|
+
// Filter to only count active streams
|
|
75
|
+
const activeStreams = allProjectStreams.filter(s => s.status === 'active');
|
|
76
|
+
|
|
77
|
+
// Get per-chat-session user presence from WS rooms
|
|
78
|
+
const chatSessionUsers = ws.getProjectChatSessions(projectId);
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
projectId,
|
|
82
|
+
hasActiveStreams: activeStreams.length > 0,
|
|
83
|
+
activeStreamCount: activeStreams.length,
|
|
84
|
+
activeUsers: users ? [...users].map(u => ({
|
|
85
|
+
userId: u.userId,
|
|
86
|
+
userName: u.userName
|
|
87
|
+
})) : [],
|
|
88
|
+
streams: allProjectStreams.map(s => ({
|
|
89
|
+
streamId: s.streamId,
|
|
90
|
+
chatSessionId: s.chatSessionId,
|
|
91
|
+
status: s.status,
|
|
92
|
+
startedAt: s.startedAt,
|
|
93
|
+
messagesCount: s.messages.length,
|
|
94
|
+
isWaitingInput: detectStreamWaitingInput(s)
|
|
95
|
+
})),
|
|
96
|
+
chatSessionUsers: Object.fromEntries(
|
|
97
|
+
Array.from(chatSessionUsers.entries()).map(([csId, csUsers]) => [
|
|
98
|
+
csId,
|
|
99
|
+
csUsers.map(u => {
|
|
100
|
+
// Resolve userName from projectUsers
|
|
101
|
+
const projectUser = users ? [...users].find(pu => pu.userId === u.userId) : undefined;
|
|
102
|
+
return { userId: u.userId, userName: projectUser?.userName || u.userId };
|
|
103
|
+
})
|
|
104
|
+
])
|
|
105
|
+
)
|
|
106
|
+
};
|
|
107
|
+
} else {
|
|
108
|
+
// Get status for all projects
|
|
109
|
+
const allProjects = new Map<string, any>();
|
|
110
|
+
|
|
111
|
+
// Get all active streams grouped by project
|
|
112
|
+
const allStreams = streamManager.getAllStreams();
|
|
113
|
+
allStreams.forEach(stream => {
|
|
114
|
+
if (stream.projectId) {
|
|
115
|
+
if (!allProjects.has(stream.projectId)) {
|
|
116
|
+
allProjects.set(stream.projectId, {
|
|
117
|
+
projectId: stream.projectId,
|
|
118
|
+
hasActiveStreams: false,
|
|
119
|
+
activeStreamCount: 0,
|
|
120
|
+
activeUsers: [],
|
|
121
|
+
streams: []
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const projectData = allProjects.get(stream.projectId);
|
|
126
|
+
if (stream.status === 'active') {
|
|
127
|
+
projectData.hasActiveStreams = true;
|
|
128
|
+
projectData.activeStreamCount++;
|
|
129
|
+
}
|
|
130
|
+
projectData.streams.push({
|
|
131
|
+
streamId: stream.streamId,
|
|
132
|
+
chatSessionId: stream.chatSessionId,
|
|
133
|
+
status: stream.status,
|
|
134
|
+
startedAt: stream.startedAt,
|
|
135
|
+
messagesCount: stream.messages.length,
|
|
136
|
+
isWaitingInput: detectStreamWaitingInput(stream)
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Add active users to each project
|
|
142
|
+
projectUsers.forEach((users, projectId) => {
|
|
143
|
+
if (!allProjects.has(projectId)) {
|
|
144
|
+
allProjects.set(projectId, {
|
|
145
|
+
projectId,
|
|
146
|
+
hasActiveStreams: false,
|
|
147
|
+
activeStreamCount: 0,
|
|
148
|
+
activeUsers: [],
|
|
149
|
+
streams: [],
|
|
150
|
+
chatSessionUsers: {}
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const projectData = allProjects.get(projectId);
|
|
155
|
+
projectData.activeUsers = [...users].map(u => ({
|
|
156
|
+
userId: u.userId,
|
|
157
|
+
userName: u.userName
|
|
158
|
+
}));
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Add per-chat-session user presence to each project
|
|
162
|
+
for (const [projectId, projectData] of allProjects) {
|
|
163
|
+
const chatSessionUsers = ws.getProjectChatSessions(projectId);
|
|
164
|
+
const users = projectUsers.get(projectId);
|
|
165
|
+
projectData.chatSessionUsers = Object.fromEntries(
|
|
166
|
+
Array.from(chatSessionUsers.entries()).map(([csId, csUsers]) => [
|
|
167
|
+
csId,
|
|
168
|
+
csUsers.map(u => {
|
|
169
|
+
const projectUser = users ? [...users].find(pu => pu.userId === u.userId) : undefined;
|
|
170
|
+
return { userId: u.userId, userName: projectUser?.userName || u.userId };
|
|
171
|
+
})
|
|
172
|
+
])
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return [...allProjects.values()];
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Update user presence
|
|
181
|
+
export function updateUserPresence(projectId: string, userId: string, userName: string, action: string) {
|
|
182
|
+
cleanupInactiveUsers();
|
|
183
|
+
|
|
184
|
+
if (action === 'leave') {
|
|
185
|
+
// Remove user from project
|
|
186
|
+
const users = projectUsers.get(projectId);
|
|
187
|
+
if (users) {
|
|
188
|
+
const updatedUsers = new Set([...users].filter(u => u.userId !== userId));
|
|
189
|
+
if (updatedUsers.size === 0) {
|
|
190
|
+
projectUsers.delete(projectId);
|
|
191
|
+
} else {
|
|
192
|
+
projectUsers.set(projectId, updatedUsers);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
// Add or update user presence
|
|
197
|
+
if (!projectUsers.has(projectId)) {
|
|
198
|
+
projectUsers.set(projectId, new Set());
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const users = projectUsers.get(projectId)!;
|
|
202
|
+
// Remove old entry if exists
|
|
203
|
+
const updatedUsers = new Set([...users].filter(u => u.userId !== userId));
|
|
204
|
+
// Add new entry with updated timestamp
|
|
205
|
+
updatedUsers.add({
|
|
206
|
+
userId,
|
|
207
|
+
userName,
|
|
208
|
+
lastSeen: Date.now()
|
|
209
|
+
});
|
|
210
|
+
projectUsers.set(projectId, updatedUsers);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Return current users for the project
|
|
214
|
+
const currentUsers = projectUsers.get(projectId);
|
|
215
|
+
return {
|
|
216
|
+
projectId,
|
|
217
|
+
activeUsers: currentUsers ? [...currentUsers].map(u => ({
|
|
218
|
+
userId: u.userId,
|
|
219
|
+
userName: u.userName
|
|
220
|
+
})) : []
|
|
221
|
+
};
|
|
182
222
|
}
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import { presenceState } from '$frontend/lib/stores/core/presence.svelte';
|
|
17
17
|
import { userStore } from '$frontend/lib/stores/features/user.svelte';
|
|
18
18
|
import { addNotification } from '$frontend/lib/stores/ui/notification.svelte';
|
|
19
|
+
import { chatService } from '$frontend/lib/services/chat/chat.service';
|
|
19
20
|
import { onMount } from 'svelte';
|
|
20
21
|
import { fade } from 'svelte/transition';
|
|
21
22
|
import ChatMessages from './message/ChatMessages.svelte';
|
|
@@ -214,6 +215,12 @@
|
|
|
214
215
|
return;
|
|
215
216
|
}
|
|
216
217
|
|
|
218
|
+
// Reset frontend state without killing the backend stream
|
|
219
|
+
// The old session's stream continues running in the background
|
|
220
|
+
if (appState.isLoading) {
|
|
221
|
+
chatService.resetForSessionSwitch();
|
|
222
|
+
}
|
|
223
|
+
|
|
217
224
|
// Clear messages for the local view
|
|
218
225
|
clearMessages();
|
|
219
226
|
|
|
@@ -146,10 +146,15 @@
|
|
|
146
146
|
});
|
|
147
147
|
|
|
148
148
|
// Initialize model picker based on session state:
|
|
149
|
-
// -
|
|
150
|
-
// -
|
|
151
|
-
//
|
|
152
|
-
//
|
|
149
|
+
// - Session with persisted engine/model: restore from session (highest priority)
|
|
150
|
+
// - New session (no messages, no persisted engine/model): apply Settings defaults
|
|
151
|
+
// - Legacy session without engine/model: fall back to Settings defaults
|
|
152
|
+
//
|
|
153
|
+
// IMPORTANT: Session engine/model is checked BEFORE hasStartedChat because
|
|
154
|
+
// when switching sessions, messages load asynchronously AFTER the session is set.
|
|
155
|
+
// If we checked hasStartedChat first, there's a window where messages haven't
|
|
156
|
+
// loaded yet (hasStartedChat=false) → defaults would be applied, overriding the
|
|
157
|
+
// session's actual engine/model selection.
|
|
153
158
|
$effect(() => {
|
|
154
159
|
const session = sessionState.currentSession;
|
|
155
160
|
const _sessionId = session?.id;
|
|
@@ -162,12 +167,14 @@
|
|
|
162
167
|
const sessionAccountId = session?.claude_account_id;
|
|
163
168
|
|
|
164
169
|
untrack(() => {
|
|
165
|
-
if (
|
|
166
|
-
//
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
// Existing session with persisted engine/model: restore
|
|
170
|
+
if (sessionEngine && sessionModel) {
|
|
171
|
+
// Session has persisted engine/model: always restore from session.
|
|
172
|
+
// This works for both existing sessions (has messages) and sessions
|
|
173
|
+
// where messages are still loading asynchronously.
|
|
170
174
|
restoreChatModelFromSession(sessionEngine, sessionModel, sessionAccountId);
|
|
175
|
+
} else if (!started) {
|
|
176
|
+
// New session (no messages, no persisted engine/model): apply Settings defaults
|
|
177
|
+
initChatModel(sEngine, sModel, sMemory || {});
|
|
171
178
|
} else {
|
|
172
179
|
// Existing session without engine/model (pre-migration or not yet set):
|
|
173
180
|
// fall back to Settings defaults
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
let hasInitiallyScrolled = $state(false);
|
|
29
29
|
let isContentReady = $state(false);
|
|
30
30
|
let lastToolResultsHash = $state('');
|
|
31
|
+
let lastSubAgentHash = $state('');
|
|
31
32
|
// Prevent scroll events from overriding isUserAtBottom during programmatic scroll
|
|
32
33
|
let scrollLockUntil = 0;
|
|
33
34
|
|
|
@@ -254,8 +255,10 @@
|
|
|
254
255
|
// Check for tool result updates ($result additions) across all messages
|
|
255
256
|
let toolResultChanged = false;
|
|
256
257
|
let currentToolResultsHash = '';
|
|
258
|
+
let subAgentChanged = false;
|
|
259
|
+
let currentSubAgentHash = '';
|
|
257
260
|
|
|
258
|
-
// Build a hash of all tool results to detect changes
|
|
261
|
+
// Build a hash of all tool results and sub-agent activities to detect changes
|
|
259
262
|
for (const message of filteredMessages) {
|
|
260
263
|
if ('message' in message) {
|
|
261
264
|
const messageContent = (message as any).message?.content;
|
|
@@ -264,6 +267,9 @@
|
|
|
264
267
|
if (item?.type === 'tool_use' && item.$result) {
|
|
265
268
|
currentToolResultsHash += `${item.id}:${JSON.stringify(item.$result).length}|`;
|
|
266
269
|
}
|
|
270
|
+
if (item?.type === 'tool_use' && item.$subMessages) {
|
|
271
|
+
currentSubAgentHash += `${item.id}:${item.$subMessages.length}|`;
|
|
272
|
+
}
|
|
267
273
|
}
|
|
268
274
|
}
|
|
269
275
|
}
|
|
@@ -275,6 +281,12 @@
|
|
|
275
281
|
lastToolResultsHash = currentToolResultsHash;
|
|
276
282
|
}
|
|
277
283
|
|
|
284
|
+
// Check if sub-agent activities have changed
|
|
285
|
+
if (currentSubAgentHash !== lastSubAgentHash) {
|
|
286
|
+
subAgentChanged = true;
|
|
287
|
+
lastSubAgentHash = currentSubAgentHash;
|
|
288
|
+
}
|
|
289
|
+
|
|
278
290
|
// When message count decreases (e.g. after undo/reload), scroll to bottom
|
|
279
291
|
if (isMessageCountDecreased && hasInitiallyScrolled) {
|
|
280
292
|
requestAnimationFrame(() => {
|
|
@@ -284,14 +296,14 @@
|
|
|
284
296
|
return;
|
|
285
297
|
}
|
|
286
298
|
|
|
287
|
-
// Scroll on new message, partial text changes,
|
|
288
|
-
if ((isNewMessage || partialTextChanged || toolResultChanged) && isUserAtBottom) {
|
|
299
|
+
// Scroll on new message, partial text changes, tool result changes, or sub-agent updates
|
|
300
|
+
if ((isNewMessage || partialTextChanged || toolResultChanged || subAgentChanged) && isUserAtBottom) {
|
|
289
301
|
requestAnimationFrame(() => {
|
|
290
302
|
if (editModeState.isEditing) return;
|
|
291
303
|
|
|
292
304
|
// During streaming, scroll immediately (instant via scrollMessagesToBottom)
|
|
293
305
|
// For non-streaming changes, add a small delay for smooth UX
|
|
294
|
-
if (partialTextChanged || hasStreamingMessage) {
|
|
306
|
+
if (partialTextChanged || hasStreamingMessage || subAgentChanged) {
|
|
295
307
|
scrollMessagesToBottom(false);
|
|
296
308
|
} else {
|
|
297
309
|
const delay = toolResultChanged ? 50 : 100;
|
|
@@ -6,17 +6,18 @@
|
|
|
6
6
|
|
|
7
7
|
const { toolInput }: { toolInput: AgentToolInput } = $props();
|
|
8
8
|
|
|
9
|
-
const description = toolInput.input.description || '';
|
|
10
|
-
const subagentType = toolInput.input.subagent_type || 'general-purpose';
|
|
11
|
-
const subMessages = (toolInput
|
|
12
|
-
const toolUseCount = subMessages?.filter(a => a.type === 'tool_use').length ?? 0;
|
|
9
|
+
const description = $derived(toolInput.input.description || '');
|
|
10
|
+
const subagentType = $derived(toolInput.input.subagent_type || 'general-purpose');
|
|
11
|
+
const subMessages = $derived(toolInput.$subMessages);
|
|
12
|
+
const toolUseCount = $derived(subMessages?.filter(a => a.type === 'tool_use').length ?? 0);
|
|
13
|
+
const result = $derived(toolInput.$result);
|
|
13
14
|
|
|
14
15
|
let scrollContainer: HTMLDivElement | undefined = $state();
|
|
15
16
|
|
|
16
17
|
// Auto-scroll to bottom when new activities arrive
|
|
17
18
|
$effect(() => {
|
|
18
|
-
const
|
|
19
|
-
if (
|
|
19
|
+
const len = subMessages?.length ?? 0;
|
|
20
|
+
if (len > 0 && scrollContainer) {
|
|
20
21
|
tick().then(() => {
|
|
21
22
|
if (scrollContainer) {
|
|
22
23
|
scrollContainer.scrollTop = scrollContainer.scrollHeight;
|
|
@@ -42,7 +43,7 @@
|
|
|
42
43
|
</script>
|
|
43
44
|
|
|
44
45
|
<!-- Header card -->
|
|
45
|
-
<div class="bg-white dark:bg-slate-800 rounded-md border border-slate-200/60 dark:border-slate-700/60 p-3">
|
|
46
|
+
<div class="bg-white dark:bg-slate-800 rounded-md border border-slate-200/60 dark:border-slate-700/60 p-3 mb-2">
|
|
46
47
|
<div class="space-y-1">
|
|
47
48
|
<InfoLine icon="lucide:search" text={description} />
|
|
48
49
|
<InfoLine icon="lucide:bot" text="Using {subagentType} agent" />
|
|
@@ -55,7 +56,7 @@
|
|
|
55
56
|
<div class="text-xs text-slate-500 dark:text-slate-400 mb-2">
|
|
56
57
|
{toolUseCount} tool {toolUseCount === 1 ? 'call' : 'calls'}:
|
|
57
58
|
</div>
|
|
58
|
-
<div bind:this={scrollContainer} class="max-h-64 overflow-y-auto">
|
|
59
|
+
<div bind:this={scrollContainer} class="max-h-64 overflow-y-auto wrap-break-word">
|
|
59
60
|
<ul class="list-disc pl-5 space-y-0.5">
|
|
60
61
|
{#each subMessages as activity}
|
|
61
62
|
{#if activity.type === 'tool_use'}
|
|
@@ -77,8 +78,8 @@
|
|
|
77
78
|
{/if}
|
|
78
79
|
|
|
79
80
|
<!-- Tool Result -->
|
|
80
|
-
{#if
|
|
81
|
-
{@const resultContent =
|
|
81
|
+
<!-- {#if result}
|
|
82
|
+
{@const resultContent = result.content as any}
|
|
82
83
|
<div class="mt-4">
|
|
83
84
|
{#if typeof resultContent === 'string'}
|
|
84
85
|
<TextMessage content={resultContent} />
|
|
@@ -92,4 +93,4 @@
|
|
|
92
93
|
<TextMessage content={JSON.stringify(resultContent)} />
|
|
93
94
|
{/if}
|
|
94
95
|
</div>
|
|
95
|
-
{/if}
|
|
96
|
+
{/if} -->
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
|
|
6
6
|
const { toolInput }: { toolInput: BashOutputToolInput } = $props();
|
|
7
7
|
|
|
8
|
-
const taskId = toolInput.input.task_id;
|
|
9
|
-
const block = toolInput.input.block;
|
|
10
|
-
const timeout = toolInput.input.timeout;
|
|
8
|
+
const taskId = $derived(toolInput.input.task_id);
|
|
9
|
+
const block = $derived(toolInput.input.block);
|
|
10
|
+
const timeout = $derived(toolInput.input.timeout);
|
|
11
11
|
</script>
|
|
12
12
|
|
|
13
13
|
<div class="bg-white dark:bg-slate-800 rounded-md border border-slate-200/60 dark:border-slate-700/60 p-3">
|
|
@@ -11,10 +11,10 @@
|
|
|
11
11
|
|
|
12
12
|
const { toolInput }: { toolInput: BashToolInput } = $props();
|
|
13
13
|
|
|
14
|
-
const command = toolInput.input.command || '';
|
|
15
|
-
const description = toolInput.input.description;
|
|
16
|
-
const timeout = toolInput.input.timeout;
|
|
17
|
-
const isBackground = toolInput.input.run_in_background;
|
|
14
|
+
const command = $derived(toolInput.input.command || '');
|
|
15
|
+
const description = $derived(toolInput.input.description);
|
|
16
|
+
const timeout = $derived(toolInput.input.timeout);
|
|
17
|
+
const isBackground = $derived(toolInput.input.run_in_background);
|
|
18
18
|
|
|
19
19
|
function parseBashOutputToolOutput(content: string): ParsedBashOutput {
|
|
20
20
|
const statusMatch = content.match(/<status>(.*?)<\/status>/);
|
|
@@ -25,7 +25,9 @@
|
|
|
25
25
|
};
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
const
|
|
28
|
+
const parsedToolName = $derived(parseMcpToolName(toolInput.name));
|
|
29
|
+
const server = $derived(parsedToolName.server);
|
|
30
|
+
const tool = $derived(parsedToolName.tool);
|
|
29
31
|
|
|
30
32
|
// Format server name for display (e.g., "weather-service" -> "Weather Service")
|
|
31
33
|
const serverDisplayName = $derived.by(() => {
|
|
@@ -5,13 +5,13 @@
|
|
|
5
5
|
|
|
6
6
|
const { toolInput }: { toolInput: EditToolInput } = $props();
|
|
7
7
|
|
|
8
|
-
const filePath = toolInput.input.file_path || '';
|
|
9
|
-
const fileName = filePath.split(/[/\\]/).pop() || filePath || 'unknown';
|
|
10
|
-
const oldString = toolInput.input.old_string || '';
|
|
11
|
-
const newString = toolInput.input.new_string || '';
|
|
12
|
-
const replaceAll = toolInput.input.replace_all || false;
|
|
8
|
+
const filePath = $derived(toolInput.input.file_path || '');
|
|
9
|
+
const fileName = $derived(filePath.split(/[/\\]/).pop() || filePath || 'unknown');
|
|
10
|
+
const oldString = $derived(toolInput.input.old_string || '');
|
|
11
|
+
const newString = $derived(toolInput.input.new_string || '');
|
|
12
|
+
const replaceAll = $derived(toolInput.input.replace_all || false);
|
|
13
13
|
|
|
14
|
-
const badges = replaceAll ? [{ text: 'Replace All', color: 'bg-amber-100 dark:bg-amber-900 text-amber-700 dark:text-amber-300' }] : [];
|
|
14
|
+
const badges = $derived(replaceAll ? [{ text: 'Replace All', color: 'bg-amber-100 dark:bg-amber-900 text-amber-700 dark:text-amber-300' }] : []);
|
|
15
15
|
</script>
|
|
16
16
|
|
|
17
17
|
<FileHeader
|