@myrialabs/clopen 0.0.7 → 0.1.1
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/backend/index.ts +28 -10
- package/backend/lib/chat/stream-manager.ts +130 -10
- package/backend/lib/database/queries/message-queries.ts +47 -0
- package/backend/lib/engine/adapters/claude/stream.ts +65 -1
- package/backend/lib/engine/adapters/opencode/message-converter.ts +35 -77
- package/backend/lib/engine/types.ts +6 -0
- package/backend/lib/files/file-operations.ts +2 -2
- package/backend/lib/files/file-reading.ts +2 -2
- package/backend/lib/files/path-browsing.ts +2 -2
- package/backend/lib/terminal/pty-session-manager.ts +1 -1
- package/backend/lib/terminal/shell-utils.ts +4 -4
- package/backend/lib/terminal/stream-manager.ts +6 -3
- package/backend/ws/chat/background.ts +3 -0
- package/backend/ws/chat/stream.ts +43 -1
- package/backend/ws/terminal/session.ts +48 -0
- package/bin/clopen.ts +10 -0
- package/bun.lock +258 -383
- package/frontend/lib/components/chat/ChatInterface.svelte +8 -1
- package/frontend/lib/components/chat/formatters/MessageFormatter.svelte +20 -0
- package/frontend/lib/components/chat/formatters/TextMessage.svelte +3 -15
- package/frontend/lib/components/chat/formatters/Tools.svelte +15 -8
- package/frontend/lib/components/chat/input/ChatInput.svelte +70 -21
- package/frontend/lib/components/chat/input/components/LoadingIndicator.svelte +23 -11
- package/frontend/lib/components/chat/input/composables/use-chat-actions.svelte.ts +1 -1
- package/frontend/lib/components/chat/message/ChatMessage.svelte +13 -1
- package/frontend/lib/components/chat/message/ChatMessages.svelte +2 -2
- package/frontend/lib/components/chat/message/DateSeparator.svelte +1 -1
- package/frontend/lib/components/chat/message/MessageBubble.svelte +2 -2
- package/frontend/lib/components/chat/message/MessageHeader.svelte +14 -12
- package/frontend/lib/components/chat/tools/AgentTool.svelte +95 -0
- package/frontend/lib/components/chat/tools/AskUserQuestionTool.svelte +396 -0
- package/frontend/lib/components/chat/tools/BashTool.svelte +9 -4
- package/frontend/lib/components/chat/tools/EnterPlanModeTool.svelte +24 -0
- package/frontend/lib/components/chat/tools/ExitPlanModeTool.svelte +4 -7
- package/frontend/lib/components/chat/tools/{KillShellTool.svelte → TaskStopTool.svelte} +6 -6
- package/frontend/lib/components/chat/tools/index.ts +5 -2
- package/frontend/lib/components/checkpoint/TimelineModal.svelte +7 -2
- package/frontend/lib/components/history/HistoryModal.svelte +13 -5
- package/frontend/lib/components/workspace/DesktopNavigator.svelte +2 -1
- package/frontend/lib/components/workspace/MobileNavigator.svelte +2 -1
- package/frontend/lib/services/chat/chat.service.ts +146 -12
- package/frontend/lib/services/terminal/project.service.ts +65 -10
- package/frontend/lib/services/terminal/terminal.service.ts +19 -0
- package/frontend/lib/stores/core/app.svelte.ts +77 -0
- package/frontend/lib/stores/features/terminal.svelte.ts +10 -0
- package/frontend/lib/utils/chat/message-grouper.ts +94 -12
- package/frontend/lib/utils/chat/message-processor.ts +37 -4
- package/frontend/lib/utils/chat/tool-handler.ts +96 -5
- package/package.json +4 -5
- package/shared/constants/engines.ts +1 -1
- package/shared/types/database/schema.ts +1 -0
- package/shared/types/messaging/index.ts +15 -13
- package/shared/types/messaging/tool.ts +185 -361
- package/shared/utils/message-formatter.ts +1 -0
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
<script lang="ts">
|
|
13
13
|
import { sessionState, setCurrentSession, createNewChatSession, clearMessages, loadMessagesForSession } from '$frontend/lib/stores/core/sessions.svelte';
|
|
14
14
|
import { projectState } from '$frontend/lib/stores/core/projects.svelte';
|
|
15
|
-
import { appState } from '$frontend/lib/stores/core/app.svelte';
|
|
15
|
+
import { appState, getSessionProcessState } from '$frontend/lib/stores/core/app.svelte';
|
|
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';
|
|
@@ -310,11 +310,18 @@
|
|
|
310
310
|
</div>
|
|
311
311
|
{/if}
|
|
312
312
|
{#if isStreaming}
|
|
313
|
+
{#if getSessionProcessState(session.id).isWaitingInput}
|
|
314
|
+
<span class="shrink-0 flex items-center gap-1 px-1.5 py-0.5 text-3xs font-medium rounded-full bg-amber-100 dark:bg-amber-900/30 text-amber-600 dark:text-amber-400">
|
|
315
|
+
<span class="w-1.5 h-1.5 rounded-full bg-amber-500 animate-pulse"></span>
|
|
316
|
+
Input
|
|
317
|
+
</span>
|
|
318
|
+
{:else}
|
|
313
319
|
<span class="shrink-0 flex items-center gap-1 px-1.5 py-0.5 text-3xs font-medium rounded-full bg-violet-100 dark:bg-violet-900/30 text-violet-600 dark:text-violet-400">
|
|
314
320
|
<span class="w-1.5 h-1.5 rounded-full bg-violet-500 animate-pulse"></span>
|
|
315
321
|
AI
|
|
316
322
|
</span>
|
|
317
323
|
{/if}
|
|
324
|
+
{/if}
|
|
318
325
|
</button>
|
|
319
326
|
{/each}
|
|
320
327
|
</div>
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import type { SDKMessage, SDKPartialAssistantMessage } from '$shared/types/messaging';
|
|
11
11
|
import type { SDKMessageFormatter } from '$shared/types/database/schema';
|
|
12
|
+
import { getCompactSummary } from '$frontend/lib/utils/chat/message-grouper';
|
|
12
13
|
|
|
13
14
|
const { message }: { message: SDKMessageFormatter } = $props();
|
|
14
15
|
|
|
@@ -39,6 +40,25 @@
|
|
|
39
40
|
function parseContent() {
|
|
40
41
|
const elements: ContentElement[] = [];
|
|
41
42
|
|
|
43
|
+
// Handle compact boundary messages (conversation compaction indicator)
|
|
44
|
+
if (message.type === 'system' && (message as any).subtype === 'compact_boundary') {
|
|
45
|
+
const compactMeta = (message as any).compact_metadata;
|
|
46
|
+
const trigger = compactMeta?.trigger === 'manual' ? 'Manual' : 'Auto';
|
|
47
|
+
elements.push({
|
|
48
|
+
type: 'text',
|
|
49
|
+
content: `Context was compacted (${trigger}). Previous messages have been summarized to free up context space.`
|
|
50
|
+
});
|
|
51
|
+
// Include synthetic user content (continuation summary) via side-channel lookup
|
|
52
|
+
const summary = getCompactSummary(message);
|
|
53
|
+
if (summary) {
|
|
54
|
+
elements.push({
|
|
55
|
+
type: 'text',
|
|
56
|
+
content: summary
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return elements;
|
|
60
|
+
}
|
|
61
|
+
|
|
42
62
|
// Handle partial messages (streaming) — both text and reasoning
|
|
43
63
|
if (message.type === 'stream_event') {
|
|
44
64
|
if ('partialText' in message && message.partialText) {
|
|
@@ -91,8 +91,7 @@
|
|
|
91
91
|
const processedContent = $derived(processContent(content));
|
|
92
92
|
</script>
|
|
93
93
|
|
|
94
|
-
<div class="message-content">
|
|
95
|
-
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
|
94
|
+
<div class="message-content space-y-4 wrap-break-word">
|
|
96
95
|
{@html processedContent}
|
|
97
96
|
</div>
|
|
98
97
|
|
|
@@ -101,7 +100,6 @@
|
|
|
101
100
|
:global(.message-content) {
|
|
102
101
|
color: rgb(30 41 59);
|
|
103
102
|
line-height: 1.5;
|
|
104
|
-
margin-bottom: -1rem;
|
|
105
103
|
}
|
|
106
104
|
|
|
107
105
|
:global(.dark .message-content) {
|
|
@@ -109,21 +107,11 @@
|
|
|
109
107
|
}
|
|
110
108
|
|
|
111
109
|
/* Headings */
|
|
112
|
-
:global(.message-content
|
|
113
|
-
:global(.message-content h2:first-child),
|
|
114
|
-
:global(.message-content h3:first-child),
|
|
115
|
-
:global(.message-content h4:first-child),
|
|
116
|
-
:global(.message-content h5:first-child),
|
|
117
|
-
:global(.message-content h6:first-child) {
|
|
110
|
+
:global(.message-content *:first-child) {
|
|
118
111
|
margin-top: 0;
|
|
119
112
|
}
|
|
120
113
|
|
|
121
|
-
:global(.message-content
|
|
122
|
-
:global(.message-content h2:last-child),
|
|
123
|
-
:global(.message-content h3:last-child),
|
|
124
|
-
:global(.message-content h4:last-child),
|
|
125
|
-
:global(.message-content h5:last-child),
|
|
126
|
-
:global(.message-content h6:last-child) {
|
|
114
|
+
:global(.message-content *:last-child) {
|
|
127
115
|
margin-bottom: 0;
|
|
128
116
|
}
|
|
129
117
|
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
import Icon from '$frontend/lib/components/common/Icon.svelte';
|
|
3
3
|
import type { ToolInput } from '$shared/types/messaging';
|
|
4
4
|
import {
|
|
5
|
-
BashTool, BashOutputTool, EditTool, ExitPlanModeTool,
|
|
6
|
-
GlobTool, GrepTool,
|
|
5
|
+
BashTool, BashOutputTool, EditTool, EnterPlanModeTool, ExitPlanModeTool,
|
|
6
|
+
GlobTool, GrepTool, TaskStopTool, ListMcpResourcesTool,
|
|
7
7
|
NotebookEditTool,
|
|
8
|
-
ReadTool, ReadMcpResourceTool, TaskTool, TodoWriteTool,
|
|
9
|
-
WebFetchTool, WebSearchTool, WriteTool, CustomMcpTool
|
|
8
|
+
ReadTool, ReadMcpResourceTool, AgentTool, TaskTool, TodoWriteTool,
|
|
9
|
+
WebFetchTool, WebSearchTool, WriteTool, CustomMcpTool,
|
|
10
|
+
AskUserQuestionTool
|
|
10
11
|
} from '../tools';
|
|
11
12
|
|
|
12
13
|
const { toolInput }: { toolInput: ToolInput } = $props();
|
|
@@ -32,14 +33,16 @@
|
|
|
32
33
|
<BashOutputTool {toolInput} />
|
|
33
34
|
{:else if toolInput.name === 'Edit'}
|
|
34
35
|
<EditTool {toolInput} />
|
|
36
|
+
{:else if toolInput.name === 'EnterPlanMode'}
|
|
37
|
+
<EnterPlanModeTool {toolInput} />
|
|
35
38
|
{:else if toolInput.name === 'ExitPlanMode'}
|
|
36
39
|
<ExitPlanModeTool {toolInput} />
|
|
37
40
|
{:else if toolInput.name === 'Glob'}
|
|
38
41
|
<GlobTool {toolInput} />
|
|
39
42
|
{:else if toolInput.name === 'Grep'}
|
|
40
43
|
<GrepTool {toolInput} />
|
|
41
|
-
{:else if toolInput.name === '
|
|
42
|
-
<
|
|
44
|
+
{:else if toolInput.name === 'TaskStop'}
|
|
45
|
+
<TaskStopTool {toolInput} />
|
|
43
46
|
{:else if toolInput.name === 'ListMcpResources'}
|
|
44
47
|
<ListMcpResourcesTool {toolInput} />
|
|
45
48
|
{:else if toolInput.name === 'NotebookEdit'}
|
|
@@ -48,6 +51,8 @@
|
|
|
48
51
|
<ReadTool {toolInput} />
|
|
49
52
|
{:else if toolInput.name === 'ReadMcpResource'}
|
|
50
53
|
<ReadMcpResourceTool {toolInput} />
|
|
54
|
+
{:else if toolInput.name === 'Agent'}
|
|
55
|
+
<AgentTool {toolInput} />
|
|
51
56
|
{:else if toolInput.name === 'Task'}
|
|
52
57
|
<TaskTool {toolInput} />
|
|
53
58
|
{:else if toolInput.name === 'WebFetch'}
|
|
@@ -56,8 +61,10 @@
|
|
|
56
61
|
<WebSearchTool {toolInput} />
|
|
57
62
|
{:else if toolInput.name === 'Write'}
|
|
58
63
|
<WriteTool {toolInput} />
|
|
64
|
+
{:else if toolInput.name === 'AskUserQuestion'}
|
|
65
|
+
<AskUserQuestionTool {toolInput} />
|
|
59
66
|
{:else}
|
|
60
|
-
<!-- Generic fallback for unknown tools -->
|
|
67
|
+
<!-- Generic fallback for unknown tools (Config, EnterWorktree, etc.) -->
|
|
61
68
|
<div class="bg-slate-50 dark:bg-slate-900 border border-slate-200 dark:border-slate-700 rounded-lg p-4 mb-4">
|
|
62
69
|
<div class="flex items-center gap-2 mb-2">
|
|
63
70
|
<Icon name="lucide:wrench" class="text-green-600 dark:text-green-400 w-5 h-5" />
|
|
@@ -67,4 +74,4 @@
|
|
|
67
74
|
<pre class="text-xs text-slate-600 dark:text-slate-400 whitespace-pre-wrap font-mono">{JSON.stringify((toolInput as any).input, null, 2)}</pre>
|
|
68
75
|
</div>
|
|
69
76
|
</div>
|
|
70
|
-
{/if}
|
|
77
|
+
{/if}
|
|
@@ -146,6 +146,9 @@
|
|
|
146
146
|
if (chatBlockedReason === 'no-model') {
|
|
147
147
|
return 'No model selected. Please select a model to start chatting.';
|
|
148
148
|
}
|
|
149
|
+
if (appState.isWaitingInput) {
|
|
150
|
+
return 'Answer the question above to continue...';
|
|
151
|
+
}
|
|
149
152
|
return placeholderAnimation.placeholderText;
|
|
150
153
|
});
|
|
151
154
|
|
|
@@ -238,8 +241,10 @@
|
|
|
238
241
|
async function catchupActiveStream(status: any) {
|
|
239
242
|
if (!status?.streams?.length || !sessionState.currentSession?.id) return;
|
|
240
243
|
|
|
241
|
-
// Find the active stream
|
|
242
|
-
const activeStream = status.streams.find(
|
|
244
|
+
// Find the active stream for the current session
|
|
245
|
+
const activeStream = status.streams.find(
|
|
246
|
+
(s: any) => s.status === 'active' && s.chatSessionId === sessionState.currentSession?.id
|
|
247
|
+
);
|
|
243
248
|
if (!activeStream) return;
|
|
244
249
|
|
|
245
250
|
try {
|
|
@@ -248,27 +253,67 @@
|
|
|
248
253
|
});
|
|
249
254
|
|
|
250
255
|
if (streamState && streamState.status === 'active' && streamState.processId) {
|
|
251
|
-
//
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
256
|
+
// ── Inject reasoning stream_event (if available) ──
|
|
257
|
+
if (streamState.currentReasoningText) {
|
|
258
|
+
const hasReasoningStream = sessionState.messages.some(
|
|
259
|
+
(m: any) => m.type === 'stream_event' && m.metadata?.reasoning && m.processId === streamState.processId
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
if (!hasReasoningStream) {
|
|
263
|
+
const reasoningMessage = {
|
|
264
|
+
type: 'stream_event' as const,
|
|
265
|
+
processId: streamState.processId,
|
|
266
|
+
partialText: streamState.currentReasoningText,
|
|
267
|
+
metadata: { reasoning: true }
|
|
268
|
+
};
|
|
269
|
+
(sessionState.messages as any[]).push(reasoningMessage);
|
|
270
|
+
} else {
|
|
271
|
+
const existingReasoning = sessionState.messages.find(
|
|
272
|
+
(m: any) => m.type === 'stream_event' && m.metadata?.reasoning && m.processId === streamState.processId
|
|
273
|
+
);
|
|
274
|
+
if (existingReasoning) {
|
|
275
|
+
(existingReasoning as any).partialText = streamState.currentReasoningText;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
255
279
|
|
|
256
|
-
if
|
|
257
|
-
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
280
|
+
// ── Inject regular text stream_event (if available) ──
|
|
281
|
+
if (streamState.currentPartialText) {
|
|
282
|
+
const hasTextStream = sessionState.messages.some(
|
|
283
|
+
(m: any) => m.type === 'stream_event' && !m.metadata?.reasoning && m.processId === streamState.processId
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
if (!hasTextStream) {
|
|
287
|
+
const streamingMessage = {
|
|
288
|
+
type: 'stream_event' as const,
|
|
289
|
+
processId: streamState.processId,
|
|
290
|
+
partialText: streamState.currentPartialText,
|
|
291
|
+
metadata: {}
|
|
292
|
+
};
|
|
293
|
+
(sessionState.messages as any[]).push(streamingMessage);
|
|
294
|
+
} else {
|
|
295
|
+
const existingMsg = sessionState.messages.find(
|
|
296
|
+
(m: any) => m.type === 'stream_event' && !m.metadata?.reasoning && m.processId === streamState.processId
|
|
297
|
+
);
|
|
298
|
+
if (existingMsg) {
|
|
299
|
+
(existingMsg as any).partialText = streamState.currentPartialText;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// If neither text nor reasoning is available yet, inject an empty
|
|
305
|
+
// stream_event placeholder so the loading indicator is visible
|
|
306
|
+
if (!streamState.currentPartialText && !streamState.currentReasoningText) {
|
|
307
|
+
const hasAnyStream = sessionState.messages.some(
|
|
268
308
|
(m: any) => m.type === 'stream_event' && m.processId === streamState.processId
|
|
269
309
|
);
|
|
270
|
-
if (
|
|
271
|
-
(
|
|
310
|
+
if (!hasAnyStream) {
|
|
311
|
+
(sessionState.messages as any[]).push({
|
|
312
|
+
type: 'stream_event' as const,
|
|
313
|
+
processId: streamState.processId,
|
|
314
|
+
partialText: '',
|
|
315
|
+
metadata: {}
|
|
316
|
+
});
|
|
272
317
|
}
|
|
273
318
|
}
|
|
274
319
|
|
|
@@ -278,9 +323,13 @@
|
|
|
278
323
|
streamState.processId
|
|
279
324
|
);
|
|
280
325
|
|
|
326
|
+
// Detect if an interactive tool (e.g. AskUserQuestion) is pending in existing messages
|
|
327
|
+
chatService.detectPendingInteractiveTools();
|
|
328
|
+
|
|
281
329
|
debug.log('chat', 'Caught up with active stream:', {
|
|
282
330
|
processId: streamState.processId,
|
|
283
|
-
partialLength: streamState.currentPartialText?.length || 0
|
|
331
|
+
partialLength: streamState.currentPartialText?.length || 0,
|
|
332
|
+
reasoningLength: streamState.currentReasoningText?.length || 0
|
|
284
333
|
});
|
|
285
334
|
}
|
|
286
335
|
} catch (error) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { appState } from '$frontend/lib/stores/core/app.svelte';
|
|
3
|
+
import Icon from '$frontend/lib/components/common/Icon.svelte';
|
|
3
4
|
import { fly } from 'svelte/transition';
|
|
4
5
|
|
|
5
6
|
interface Props {
|
|
@@ -15,17 +16,28 @@
|
|
|
15
16
|
class="absolute z-20 {isWelcomeState ? '-top-16' : '-top-14'} left-0 right-0 flex justify-center pointer-events-none"
|
|
16
17
|
transition:fly={{ y: 100, duration: 300 }}
|
|
17
18
|
>
|
|
18
|
-
|
|
19
|
-
<!--
|
|
20
|
-
<
|
|
21
|
-
<
|
|
22
|
-
<
|
|
23
|
-
|
|
19
|
+
{#if appState.isWaitingInput}
|
|
20
|
+
<!-- Waiting for user input state -->
|
|
21
|
+
<div class="flex items-center gap-2.5 px-4 py-2 bg-amber-50 dark:bg-amber-900/30 rounded-full border border-amber-300 dark:border-amber-700 shadow-sm">
|
|
22
|
+
<Icon name="lucide:message-circle-question-mark" class="w-4 h-4 text-amber-600 dark:text-amber-400" />
|
|
23
|
+
<span class="text-sm font-medium text-amber-700 dark:text-amber-300">
|
|
24
|
+
Waiting for your input...
|
|
25
|
+
</span>
|
|
26
|
+
</div>
|
|
27
|
+
{:else}
|
|
28
|
+
<!-- Normal loading state -->
|
|
29
|
+
<div class="flex items-center gap-2.5 px-4 py-2 bg-slate-100 dark:bg-slate-800 rounded-full border border-slate-300 dark:border-slate-600 shadow-sm">
|
|
30
|
+
<!-- Simple spinner -->
|
|
31
|
+
<svg class="animate-spin h-4 w-4 text-slate-700 dark:text-slate-300" viewBox="0 0 24 24">
|
|
32
|
+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"></circle>
|
|
33
|
+
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
34
|
+
</svg>
|
|
24
35
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
36
|
+
<!-- Text with typewriter effect -->
|
|
37
|
+
<span class="text-sm font-medium text-slate-700 dark:text-slate-300 capitalize">
|
|
38
|
+
{visibleLoadingText}
|
|
39
|
+
</span>
|
|
40
|
+
</div>
|
|
41
|
+
{/if}
|
|
30
42
|
</div>
|
|
31
43
|
{/if}
|
|
@@ -34,7 +34,7 @@ export function useChatActions(params: ChatActionsParams) {
|
|
|
34
34
|
|
|
35
35
|
// Handle send message with SDK streaming
|
|
36
36
|
async function sendMessage(messageText: string, setMessageText: (value: string) => void) {
|
|
37
|
-
if ((!messageText.trim() && params.attachedFiles.length === 0) || appState.isLoading) return;
|
|
37
|
+
if ((!messageText.trim() && params.attachedFiles.length === 0) || appState.isLoading || appState.isWaitingInput) return;
|
|
38
38
|
|
|
39
39
|
// Initialize sound notifications on first user interaction (browser policy requirement)
|
|
40
40
|
soundNotification.initialize();
|
|
@@ -61,6 +61,10 @@
|
|
|
61
61
|
const shouldBeDimmed = $derived(shouldDimMessage(messageId));
|
|
62
62
|
|
|
63
63
|
const roleCategory = $derived.by(() => {
|
|
64
|
+
// Compact boundary messages (conversation compaction indicator)
|
|
65
|
+
if (message.type === 'system' && (message as any).subtype === 'compact_boundary') {
|
|
66
|
+
return 'compact';
|
|
67
|
+
}
|
|
64
68
|
// Reasoning messages (from both engines)
|
|
65
69
|
if (message.metadata?.reasoning) {
|
|
66
70
|
return 'reasoning';
|
|
@@ -164,7 +168,7 @@
|
|
|
164
168
|
// Detect agent processing status
|
|
165
169
|
// When stream is no longer active (not loading) and tools don't have results,
|
|
166
170
|
// mark them as failed (cancelled/interrupted) instead of perpetually "processing"
|
|
167
|
-
const agentStatus = $derived.by((): 'processing' | 'success' | 'error' | null => {
|
|
171
|
+
const agentStatus = $derived.by((): 'processing' | 'waiting' | 'success' | 'error' | null => {
|
|
168
172
|
if (roleCategory !== 'agent') return null;
|
|
169
173
|
|
|
170
174
|
if (message.type === 'assistant' && 'message' in message && Array.isArray(message.message.content)) {
|
|
@@ -176,6 +180,8 @@
|
|
|
176
180
|
const allHaveResults = toolUses.every((tool: any) => '$result' in tool && tool.$result);
|
|
177
181
|
|
|
178
182
|
if (!allHaveResults) {
|
|
183
|
+
// If stream is active and waiting for user input → show as waiting
|
|
184
|
+
if (appState.isWaitingInput) return 'waiting';
|
|
179
185
|
// If stream is still active, tools are processing
|
|
180
186
|
// If stream ended (not loading), tools were interrupted/cancelled → show as error
|
|
181
187
|
return appState.isLoading ? 'processing' : 'error';
|
|
@@ -191,6 +197,12 @@
|
|
|
191
197
|
|
|
192
198
|
const roleConfig = $derived.by((): { gradient: string; icon: IconName; name: string } => {
|
|
193
199
|
switch (roleCategory) {
|
|
200
|
+
case 'compact':
|
|
201
|
+
return {
|
|
202
|
+
gradient: 'from-amber-500 to-orange-600',
|
|
203
|
+
icon: 'lucide:layers',
|
|
204
|
+
name: 'System'
|
|
205
|
+
};
|
|
194
206
|
case 'reasoning':
|
|
195
207
|
return {
|
|
196
208
|
gradient: 'from-emerald-500 to-green-600',
|
|
@@ -48,8 +48,8 @@
|
|
|
48
48
|
|
|
49
49
|
// Process messages through grouping and embedding
|
|
50
50
|
const processedMessages = $derived.by(() => {
|
|
51
|
-
const { groups, toolUseMap } = groupMessages(sessionState.messages);
|
|
52
|
-
return embedToolResults(groups, toolUseMap);
|
|
51
|
+
const { groups, toolUseMap, subAgentMap } = groupMessages(sessionState.messages);
|
|
52
|
+
return embedToolResults(groups, toolUseMap, subAgentMap);
|
|
53
53
|
});
|
|
54
54
|
|
|
55
55
|
// Filter out messages with empty content arrays
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
</script>
|
|
63
63
|
|
|
64
64
|
<!-- Date Separator -->
|
|
65
|
-
<div class="flex items-center justify-center
|
|
65
|
+
<div class="flex items-center justify-center mt-6 mb-3 md:mt-8 md:mb-4 px-4 md:px-6 select-none">
|
|
66
66
|
<div class="relative flex items-center w-full max-w-md">
|
|
67
67
|
<!-- Left line -->
|
|
68
68
|
<div
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
isLastUserMessage?: boolean;
|
|
37
37
|
roleConfig: { gradient: string; icon: IconName; name: string };
|
|
38
38
|
roleCategory: 'user' | 'assistant' | 'agent' | string;
|
|
39
|
-
agentStatus: 'processing' | 'success' | 'error' | null;
|
|
39
|
+
agentStatus: 'processing' | 'waiting' | 'success' | 'error' | null;
|
|
40
40
|
senderName: string | null;
|
|
41
41
|
hasTokenUsageData: any;
|
|
42
42
|
formatTime: (timestamp?: string) => string;
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
|
|
75
75
|
<!-- Message Content -->
|
|
76
76
|
<div class="p-3 md:p-4">
|
|
77
|
-
<div class="max-w-none">
|
|
77
|
+
<div class="max-w-none space-y-4">
|
|
78
78
|
<!-- Content rendering using MessageFormatter component -->
|
|
79
79
|
<MessageFormatter {message} />
|
|
80
80
|
</div>
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
isLastUserMessage?: boolean;
|
|
36
36
|
roleConfig: { gradient: string; icon: IconName; name: string };
|
|
37
37
|
roleCategory: 'user' | 'assistant' | 'agent' | string;
|
|
38
|
-
agentStatus: 'processing' | 'success' | 'error' | null;
|
|
38
|
+
agentStatus: 'processing' | 'waiting' | 'success' | 'error' | null;
|
|
39
39
|
senderName: string | null;
|
|
40
40
|
hasTokenUsageData: any;
|
|
41
41
|
formatTime: (timestamp?: string) => string;
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
}
|
|
60
60
|
</script>
|
|
61
61
|
|
|
62
|
-
<div class="flex items-center justify-between gap-5 px-3 md:px-4 py-1 border-b border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-800">
|
|
62
|
+
<div class="flex items-center justify-between gap-5 h-8 px-3 md:px-4 py-1 border-b border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-800">
|
|
63
63
|
<!-- Timestamp and Type -->
|
|
64
64
|
<div class="flex items-center gap-2">
|
|
65
65
|
<span class="text-xs text-slate-500 dark:text-slate-400 font-medium">
|
|
@@ -76,6 +76,10 @@
|
|
|
76
76
|
<span title="Agent is processing...">
|
|
77
77
|
<Icon name="lucide:loader" class="w-3 h-3 text-violet-500 dark:text-violet-400 animate-spin" />
|
|
78
78
|
</span>
|
|
79
|
+
{:else if agentStatus === 'waiting'}
|
|
80
|
+
<span title="Waiting for your input...">
|
|
81
|
+
<Icon name="lucide:message-circle-question-mark" class="w-3 h-3 text-amber-500 dark:text-amber-400" />
|
|
82
|
+
</span>
|
|
79
83
|
{:else if agentStatus === 'success'}
|
|
80
84
|
<span title="Agent completed successfully">
|
|
81
85
|
<Icon name="lucide:check" class="w-3 h-3 text-green-500 dark:text-green-400" />
|
|
@@ -143,15 +147,13 @@
|
|
|
143
147
|
{/if}
|
|
144
148
|
|
|
145
149
|
<!-- Debug toggle button -->
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
</button>
|
|
155
|
-
{/if}
|
|
150
|
+
<button
|
|
151
|
+
onclick={onShowDebug}
|
|
152
|
+
class="inline-flex p-1.5 rounded-md hover:bg-slate-200 dark:hover:bg-slate-600 transition-colors opacity-60 hover:opacity-100"
|
|
153
|
+
aria-label="Show debug info"
|
|
154
|
+
title="Debug info"
|
|
155
|
+
>
|
|
156
|
+
<Icon name="lucide:bug" class="w-3.5 h-3.5" />
|
|
157
|
+
</button>
|
|
156
158
|
</div>
|
|
157
159
|
</div>
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { tick } from 'svelte';
|
|
3
|
+
import type { AgentToolInput, SubAgentActivity } from '$shared/types/messaging';
|
|
4
|
+
import { InfoLine } from './components';
|
|
5
|
+
import TextMessage from '../formatters/TextMessage.svelte';
|
|
6
|
+
|
|
7
|
+
const { toolInput }: { toolInput: AgentToolInput } = $props();
|
|
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;
|
|
13
|
+
|
|
14
|
+
let scrollContainer: HTMLDivElement | undefined = $state();
|
|
15
|
+
|
|
16
|
+
// Auto-scroll to bottom when new activities arrive
|
|
17
|
+
$effect(() => {
|
|
18
|
+
const _len = subMessages?.length ?? 0;
|
|
19
|
+
if (_len > 0 && scrollContainer) {
|
|
20
|
+
tick().then(() => {
|
|
21
|
+
if (scrollContainer) {
|
|
22
|
+
scrollContainer.scrollTop = scrollContainer.scrollHeight;
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
function getToolBrief(activity: SubAgentActivity): string {
|
|
29
|
+
if (!activity.toolInput) return '';
|
|
30
|
+
switch (activity.toolName) {
|
|
31
|
+
case 'Bash': return activity.toolInput.command || '';
|
|
32
|
+
case 'Read': return activity.toolInput.file_path || '';
|
|
33
|
+
case 'Write': return activity.toolInput.file_path || '';
|
|
34
|
+
case 'Edit': return activity.toolInput.file_path || '';
|
|
35
|
+
case 'Glob': return activity.toolInput.pattern || '';
|
|
36
|
+
case 'Grep': return activity.toolInput.pattern || '';
|
|
37
|
+
case 'WebFetch': return activity.toolInput.url || '';
|
|
38
|
+
case 'WebSearch': return activity.toolInput.query || '';
|
|
39
|
+
default: return '';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
</script>
|
|
43
|
+
|
|
44
|
+
<!-- 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="space-y-1">
|
|
47
|
+
<InfoLine icon="lucide:search" text={description} />
|
|
48
|
+
<InfoLine icon="lucide:bot" text="Using {subagentType} agent" />
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<!-- Sub-agent tool calls (separate from header) -->
|
|
53
|
+
{#if subMessages && subMessages.length > 0}
|
|
54
|
+
<div class="bg-white dark:bg-slate-800 rounded-md border border-slate-200/60 dark:border-slate-700/60 p-3">
|
|
55
|
+
<div class="text-xs text-slate-500 dark:text-slate-400 mb-2">
|
|
56
|
+
{toolUseCount} tool {toolUseCount === 1 ? 'call' : 'calls'}:
|
|
57
|
+
</div>
|
|
58
|
+
<div bind:this={scrollContainer} class="max-h-64 overflow-y-auto">
|
|
59
|
+
<ul class="list-disc pl-5 space-y-0.5">
|
|
60
|
+
{#each subMessages as activity}
|
|
61
|
+
{#if activity.type === 'tool_use'}
|
|
62
|
+
<li class="text-xs text-slate-600 dark:text-slate-400">
|
|
63
|
+
<span class="font-medium">{activity.toolName}</span>
|
|
64
|
+
{#if getToolBrief(activity)}
|
|
65
|
+
<span class="text-slate-400 dark:text-slate-500 ml-1">{getToolBrief(activity)}</span>
|
|
66
|
+
{/if}
|
|
67
|
+
</li>
|
|
68
|
+
{:else if activity.type === 'text'}
|
|
69
|
+
<li class="text-xs text-slate-600 dark:text-slate-400 line-clamp-2">
|
|
70
|
+
{activity.text}
|
|
71
|
+
</li>
|
|
72
|
+
{/if}
|
|
73
|
+
{/each}
|
|
74
|
+
</ul>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
{/if}
|
|
78
|
+
|
|
79
|
+
<!-- Tool Result -->
|
|
80
|
+
{#if toolInput.$result}
|
|
81
|
+
{@const resultContent = toolInput.$result.content as any}
|
|
82
|
+
<div class="mt-4">
|
|
83
|
+
{#if typeof resultContent === 'string'}
|
|
84
|
+
<TextMessage content={resultContent} />
|
|
85
|
+
{:else if Array.isArray(resultContent)}
|
|
86
|
+
{#each resultContent as block}
|
|
87
|
+
{#if typeof block === 'object' && block.type === 'text'}
|
|
88
|
+
<TextMessage content={block.text} />
|
|
89
|
+
{/if}
|
|
90
|
+
{/each}
|
|
91
|
+
{:else}
|
|
92
|
+
<TextMessage content={JSON.stringify(resultContent)} />
|
|
93
|
+
{/if}
|
|
94
|
+
</div>
|
|
95
|
+
{/if}
|