@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.
Files changed (63) hide show
  1. package/CONTRIBUTING.md +40 -355
  2. package/README.md +46 -113
  3. package/backend/lib/engine/adapters/opencode/message-converter.ts +53 -2
  4. package/backend/lib/engine/adapters/opencode/stream.ts +89 -5
  5. package/backend/lib/mcp/config.ts +7 -3
  6. package/backend/lib/mcp/servers/helper.ts +25 -14
  7. package/backend/lib/mcp/servers/index.ts +7 -2
  8. package/backend/lib/project/status-manager.ts +221 -181
  9. package/frontend/lib/components/chat/ChatInterface.svelte +7 -0
  10. package/frontend/lib/components/chat/input/components/EngineModelPicker.svelte +16 -9
  11. package/frontend/lib/components/chat/message/ChatMessages.svelte +16 -4
  12. package/frontend/lib/components/chat/tools/AgentTool.svelte +12 -11
  13. package/frontend/lib/components/chat/tools/BashOutputTool.svelte +3 -3
  14. package/frontend/lib/components/chat/tools/BashTool.svelte +4 -4
  15. package/frontend/lib/components/chat/tools/CustomMcpTool.svelte +3 -1
  16. package/frontend/lib/components/chat/tools/EditTool.svelte +6 -6
  17. package/frontend/lib/components/chat/tools/ExitPlanModeTool.svelte +1 -1
  18. package/frontend/lib/components/chat/tools/GlobTool.svelte +12 -12
  19. package/frontend/lib/components/chat/tools/GrepTool.svelte +5 -5
  20. package/frontend/lib/components/chat/tools/ListMcpResourcesTool.svelte +1 -1
  21. package/frontend/lib/components/chat/tools/NotebookEditTool.svelte +6 -6
  22. package/frontend/lib/components/chat/tools/ReadMcpResourceTool.svelte +2 -2
  23. package/frontend/lib/components/chat/tools/ReadTool.svelte +4 -4
  24. package/frontend/lib/components/chat/tools/TaskStopTool.svelte +1 -1
  25. package/frontend/lib/components/chat/tools/TaskTool.svelte +1 -1
  26. package/frontend/lib/components/chat/tools/TodoWriteTool.svelte +4 -4
  27. package/frontend/lib/components/chat/tools/WebSearchTool.svelte +1 -1
  28. package/frontend/lib/components/chat/tools/WriteTool.svelte +3 -3
  29. package/frontend/lib/components/chat/tools/components/CodeBlock.svelte +3 -3
  30. package/frontend/lib/components/chat/tools/components/DiffBlock.svelte +2 -2
  31. package/frontend/lib/components/chat/tools/components/FileHeader.svelte +1 -1
  32. package/frontend/lib/components/chat/tools/components/InfoLine.svelte +2 -2
  33. package/frontend/lib/components/chat/tools/components/StatsBadges.svelte +1 -1
  34. package/frontend/lib/components/chat/tools/components/TerminalCommand.svelte +5 -5
  35. package/frontend/lib/components/common/Button.svelte +1 -1
  36. package/frontend/lib/components/common/Card.svelte +3 -3
  37. package/frontend/lib/components/common/Input.svelte +3 -3
  38. package/frontend/lib/components/common/LoadingSpinner.svelte +1 -1
  39. package/frontend/lib/components/common/Select.svelte +6 -6
  40. package/frontend/lib/components/common/Textarea.svelte +3 -3
  41. package/frontend/lib/components/files/FileViewer.svelte +1 -1
  42. package/frontend/lib/components/git/ChangesSection.svelte +2 -4
  43. package/frontend/lib/components/preview/browser/BrowserPreview.svelte +9 -29
  44. package/frontend/lib/components/preview/browser/components/Container.svelte +17 -0
  45. package/frontend/lib/components/preview/browser/components/VirtualCursor.svelte +2 -2
  46. package/frontend/lib/components/settings/appearance/AppearanceSettings.svelte +0 -6
  47. package/frontend/lib/components/settings/appearance/LayoutPresetSettings.svelte +15 -15
  48. package/frontend/lib/components/settings/appearance/LayoutPreview.svelte +2 -2
  49. package/frontend/lib/components/settings/engines/AIEnginesSettings.svelte +1 -1
  50. package/frontend/lib/components/workspace/DesktopNavigator.svelte +380 -383
  51. package/frontend/lib/components/workspace/MobileNavigator.svelte +391 -395
  52. package/frontend/lib/components/workspace/PanelHeader.svelte +623 -505
  53. package/frontend/lib/components/workspace/ViewMenu.svelte +9 -25
  54. package/frontend/lib/components/workspace/layout/split-pane/Layout.svelte +29 -4
  55. package/frontend/lib/components/workspace/panels/ChatPanel.svelte +3 -2
  56. package/frontend/lib/services/notification/global-stream-monitor.ts +77 -86
  57. package/frontend/lib/services/project/status.service.ts +160 -159
  58. package/frontend/lib/stores/ui/workspace.svelte.ts +326 -283
  59. package/package.json +1 -1
  60. package/scripts/pre-publish-check.sh +0 -142
  61. package/scripts/setup-hooks.sh +0 -134
  62. package/scripts/validate-branch-name.sh +0 -47
  63. 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
- // Store active users per project (shared with main endpoint)
10
- const projectUsers = new Map<string, Set<{ userId: string; userName: string; lastSeen: number }>>();
11
-
12
- // Cleanup inactive users after 5 minutes
13
- const USER_TIMEOUT = 5 * 60 * 1000;
14
-
15
- function cleanupInactiveUsers() {
16
- const now = Date.now();
17
- projectUsers.forEach((users, projectId) => {
18
- const activeUsers = new Set([...users].filter(user => now - user.lastSeen < USER_TIMEOUT));
19
- if (activeUsers.size === 0) {
20
- projectUsers.delete(projectId);
21
- } else {
22
- projectUsers.set(projectId, activeUsers);
23
- }
24
- });
25
- }
26
-
27
- // Get project status data
28
- export async function getProjectStatusData(projectId?: string) {
29
- cleanupInactiveUsers();
30
-
31
- if (projectId) {
32
- // Get status for specific project
33
- const allProjectStreams = streamManager.getProjectStreams(projectId);
34
- const users = projectUsers.get(projectId);
35
-
36
- // Filter to only count active streams
37
- const activeStreams = allProjectStreams.filter(s => s.status === 'active');
38
-
39
- // Get per-chat-session user presence from WS rooms
40
- const chatSessionUsers = ws.getProjectChatSessions(projectId);
41
-
42
- return {
43
- projectId,
44
- hasActiveStreams: activeStreams.length > 0,
45
- activeStreamCount: activeStreams.length,
46
- activeUsers: users ? [...users].map(u => ({
47
- userId: u.userId,
48
- userName: u.userName
49
- })) : [],
50
- streams: allProjectStreams.map(s => ({
51
- streamId: s.streamId,
52
- chatSessionId: s.chatSessionId,
53
- status: s.status,
54
- startedAt: s.startedAt,
55
- messagesCount: s.messages.length
56
- })),
57
- chatSessionUsers: Object.fromEntries(
58
- Array.from(chatSessionUsers.entries()).map(([csId, csUsers]) => [
59
- csId,
60
- csUsers.map(u => {
61
- // Resolve userName from projectUsers
62
- const projectUser = users ? [...users].find(pu => pu.userId === u.userId) : undefined;
63
- return { userId: u.userId, userName: projectUser?.userName || u.userId };
64
- })
65
- ])
66
- )
67
- };
68
- } else {
69
- // Get status for all projects
70
- const allProjects = new Map<string, any>();
71
-
72
- // Get all active streams grouped by project
73
- const allStreams = streamManager.getAllStreams();
74
- allStreams.forEach(stream => {
75
- if (stream.projectId) {
76
- if (!allProjects.has(stream.projectId)) {
77
- allProjects.set(stream.projectId, {
78
- projectId: stream.projectId,
79
- hasActiveStreams: false,
80
- activeStreamCount: 0,
81
- activeUsers: [],
82
- streams: []
83
- });
84
- }
85
-
86
- const projectData = allProjects.get(stream.projectId);
87
- if (stream.status === 'active') {
88
- projectData.hasActiveStreams = true;
89
- projectData.activeStreamCount++;
90
- }
91
- projectData.streams.push({
92
- streamId: stream.streamId,
93
- chatSessionId: stream.chatSessionId,
94
- status: stream.status,
95
- startedAt: stream.startedAt,
96
- messagesCount: stream.messages.length
97
- });
98
- }
99
- });
100
-
101
- // Add active users to each project
102
- projectUsers.forEach((users, projectId) => {
103
- if (!allProjects.has(projectId)) {
104
- allProjects.set(projectId, {
105
- projectId,
106
- hasActiveStreams: false,
107
- activeStreamCount: 0,
108
- activeUsers: [],
109
- streams: [],
110
- chatSessionUsers: {}
111
- });
112
- }
113
-
114
- const projectData = allProjects.get(projectId);
115
- projectData.activeUsers = [...users].map(u => ({
116
- userId: u.userId,
117
- userName: u.userName
118
- }));
119
- });
120
-
121
- // Add per-chat-session user presence to each project
122
- for (const [projectId, projectData] of allProjects) {
123
- const chatSessionUsers = ws.getProjectChatSessions(projectId);
124
- const users = projectUsers.get(projectId);
125
- projectData.chatSessionUsers = Object.fromEntries(
126
- Array.from(chatSessionUsers.entries()).map(([csId, csUsers]) => [
127
- csId,
128
- csUsers.map(u => {
129
- const projectUser = users ? [...users].find(pu => pu.userId === u.userId) : undefined;
130
- return { userId: u.userId, userName: projectUser?.userName || u.userId };
131
- })
132
- ])
133
- );
134
- }
135
-
136
- return [...allProjects.values()];
137
- }
138
- }
139
-
140
- // Update user presence
141
- export function updateUserPresence(projectId: string, userId: string, userName: string, action: string) {
142
- cleanupInactiveUsers();
143
-
144
- if (action === 'leave') {
145
- // Remove user from project
146
- const users = projectUsers.get(projectId);
147
- if (users) {
148
- const updatedUsers = new Set([...users].filter(u => u.userId !== userId));
149
- if (updatedUsers.size === 0) {
150
- projectUsers.delete(projectId);
151
- } else {
152
- projectUsers.set(projectId, updatedUsers);
153
- }
154
- }
155
- } else {
156
- // Add or update user presence
157
- if (!projectUsers.has(projectId)) {
158
- projectUsers.set(projectId, new Set());
159
- }
160
-
161
- const users = projectUsers.get(projectId)!;
162
- // Remove old entry if exists
163
- const updatedUsers = new Set([...users].filter(u => u.userId !== userId));
164
- // Add new entry with updated timestamp
165
- updatedUsers.add({
166
- userId,
167
- userName,
168
- lastSeen: Date.now()
169
- });
170
- projectUsers.set(projectId, updatedUsers);
171
- }
172
-
173
- // Return current users for the project
174
- const currentUsers = projectUsers.get(projectId);
175
- return {
176
- projectId,
177
- activeUsers: currentUsers ? [...currentUsers].map(u => ({
178
- userId: u.userId,
179
- userName: u.userName
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
- // - New session (no messages): apply Settings defaults
150
- // - Existing session (has messages): restore from session's persisted engine/model
151
- // Reads are done outside untrack (tracked), writes inside untrack (not tracked)
152
- // to prevent UpdatedAtError from circular chatModelState read-write.
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 (!started) {
166
- // New session (no messages): apply Settings defaults
167
- initChatModel(sEngine, sModel, sMemory || {});
168
- } else if (sessionEngine && sessionModel) {
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, or tool result 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 as any).$subMessages as SubAgentActivity[] | undefined;
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 _len = subMessages?.length ?? 0;
19
- if (_len > 0 && scrollContainer) {
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 toolInput.$result}
81
- {@const resultContent = toolInput.$result.content as any}
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 { server, tool } = parseMcpToolName(toolInput.name);
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