@myrialabs/clopen 0.0.8 → 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 +9 -0
- 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/ws/chat/background.ts +3 -0
- package/backend/ws/chat/stream.ts +43 -1
- package/bin/clopen.ts +10 -0
- package/bun.lock +259 -381
- 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/stores/core/app.svelte.ts +77 -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 -4
- 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
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { AskUserQuestionToolInput } from '$shared/types/messaging';
|
|
3
|
+
import Icon from '$frontend/lib/components/common/Icon.svelte';
|
|
4
|
+
import ws from '$frontend/lib/utils/ws';
|
|
5
|
+
import { soundNotification, pushNotification } from '$frontend/lib/services/notification';
|
|
6
|
+
import { currentSessionId } from '$frontend/lib/stores/core/sessions.svelte';
|
|
7
|
+
import { appState, updateSessionProcessState } from '$frontend/lib/stores/core/app.svelte';
|
|
8
|
+
import { debug } from '$shared/utils/logger';
|
|
9
|
+
|
|
10
|
+
const { toolInput }: { toolInput: AskUserQuestionToolInput } = $props();
|
|
11
|
+
|
|
12
|
+
// Parse answers from the SDK's $result.content string using known question texts as anchors.
|
|
13
|
+
// Format: User has answered your questions: "q1"="a1", "q2"="a2", ... . You can now continue...
|
|
14
|
+
// Regex-based parsing breaks when answers contain quotes or "=", so we use
|
|
15
|
+
// the known question texts to delimit each answer's boundaries.
|
|
16
|
+
function parseResultAnswers(content: string, questions: { question: string }[]): Record<string, string> {
|
|
17
|
+
const answers: Record<string, string> = {};
|
|
18
|
+
|
|
19
|
+
// Try JSON parse first (future SDK versions might use JSON)
|
|
20
|
+
try {
|
|
21
|
+
const json = JSON.parse(content);
|
|
22
|
+
if (json?.answers && typeof json.answers === 'object') return json.answers;
|
|
23
|
+
} catch {
|
|
24
|
+
// Not JSON — parse human-readable format
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
for (let i = 0; i < questions.length; i++) {
|
|
28
|
+
const q = questions[i].question;
|
|
29
|
+
const searchStr = `"${q}"="`;
|
|
30
|
+
const startIdx = content.indexOf(searchStr);
|
|
31
|
+
if (startIdx === -1) continue;
|
|
32
|
+
|
|
33
|
+
const answerStart = startIdx + searchStr.length;
|
|
34
|
+
|
|
35
|
+
// Find the end of this answer:
|
|
36
|
+
// If there's a next question, use its marker as delimiter.
|
|
37
|
+
// Otherwise, use the SDK's closing marker.
|
|
38
|
+
let answerEnd = -1;
|
|
39
|
+
|
|
40
|
+
if (i < questions.length - 1) {
|
|
41
|
+
const nextQ = questions[i + 1].question;
|
|
42
|
+
const nextMarker = `", "${nextQ}"="`;
|
|
43
|
+
answerEnd = content.indexOf(nextMarker, answerStart);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (answerEnd === -1) {
|
|
47
|
+
const endMarker = '". You can now continue';
|
|
48
|
+
answerEnd = content.indexOf(endMarker, answerStart);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
answers[q] = answerEnd !== -1
|
|
52
|
+
? content.slice(answerStart, answerEnd)
|
|
53
|
+
: content.slice(answerStart);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return answers;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Detect whether the tool has a $result (answered or errored)
|
|
60
|
+
const hasResult = $derived(!!toolInput.$result?.content);
|
|
61
|
+
|
|
62
|
+
// Parse per-question answers from the result content
|
|
63
|
+
let parsedAnswers = $derived.by(() => {
|
|
64
|
+
if (!toolInput.$result?.content) return {};
|
|
65
|
+
return parseResultAnswers(toolInput.$result.content, toolInput.input.questions);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Detect error: $result exists but no answers could be parsed (error message instead of answer format)
|
|
69
|
+
const isError = $derived(hasResult && Object.keys(parsedAnswers).length === 0);
|
|
70
|
+
|
|
71
|
+
// Successfully answered: has result and parsed answers exist
|
|
72
|
+
const isAnswered = $derived(hasResult && !isError);
|
|
73
|
+
|
|
74
|
+
// Selection state per question
|
|
75
|
+
let selections = $state<Record<number, Set<string>>>({});
|
|
76
|
+
// Custom text input per question (for "Other" option)
|
|
77
|
+
let customInputs = $state<Record<number, string>>({});
|
|
78
|
+
// Track whether "Other" is the active selection per question
|
|
79
|
+
let otherActive = $state<Record<number, boolean>>({});
|
|
80
|
+
let isSubmitting = $state(false);
|
|
81
|
+
let hasSubmitted = $state(false);
|
|
82
|
+
|
|
83
|
+
// Tool was interrupted — metadata set by chat service when stream ends (error/cancel/complete)
|
|
84
|
+
const isInterrupted = $derived(!!toolInput.metadata?.interrupted);
|
|
85
|
+
|
|
86
|
+
// Initialize selections
|
|
87
|
+
$effect(() => {
|
|
88
|
+
if (!toolInput.input.questions) return;
|
|
89
|
+
const initial: Record<number, Set<string>> = {};
|
|
90
|
+
const initialCustom: Record<number, string> = {};
|
|
91
|
+
const initialOther: Record<number, boolean> = {};
|
|
92
|
+
for (let i = 0; i < toolInput.input.questions.length; i++) {
|
|
93
|
+
initial[i] = new Set();
|
|
94
|
+
initialCustom[i] = '';
|
|
95
|
+
initialOther[i] = false;
|
|
96
|
+
}
|
|
97
|
+
selections = initial;
|
|
98
|
+
customInputs = initialCustom;
|
|
99
|
+
otherActive = initialOther;
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Play notification sound when this tool appears and is not yet answered/errored/interrupted
|
|
103
|
+
$effect(() => {
|
|
104
|
+
if (!hasResult && !hasSubmitted && !isInterrupted) {
|
|
105
|
+
soundNotification.play().catch(() => {});
|
|
106
|
+
pushNotification.sendChatComplete('Claude is asking you a question. Please respond.').catch(() => {});
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
function toggleSelection(questionIdx: number, label: string, isMultiSelect: boolean) {
|
|
111
|
+
const current = selections[questionIdx] || new Set();
|
|
112
|
+
if (isMultiSelect) {
|
|
113
|
+
if (current.has(label)) {
|
|
114
|
+
current.delete(label);
|
|
115
|
+
} else {
|
|
116
|
+
current.add(label);
|
|
117
|
+
}
|
|
118
|
+
} else {
|
|
119
|
+
current.clear();
|
|
120
|
+
current.add(label);
|
|
121
|
+
// Single-select: deselect Other when picking a regular option
|
|
122
|
+
otherActive[questionIdx] = false;
|
|
123
|
+
customInputs[questionIdx] = '';
|
|
124
|
+
}
|
|
125
|
+
selections[questionIdx] = new Set(current);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function toggleOther(questionIdx: number, isMultiSelect: boolean) {
|
|
129
|
+
if (isMultiSelect) {
|
|
130
|
+
otherActive[questionIdx] = !otherActive[questionIdx];
|
|
131
|
+
} else {
|
|
132
|
+
selections[questionIdx] = new Set();
|
|
133
|
+
otherActive[questionIdx] = true;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function isSelected(questionIdx: number, label: string): boolean {
|
|
138
|
+
return selections[questionIdx]?.has(label) ?? false;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function submitAnswers() {
|
|
142
|
+
if (isSubmitting || hasSubmitted) return;
|
|
143
|
+
isSubmitting = true;
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const answers: Record<string, string> = {};
|
|
147
|
+
const questions = toolInput.input.questions;
|
|
148
|
+
|
|
149
|
+
for (let i = 0; i < questions.length; i++) {
|
|
150
|
+
const q = questions[i];
|
|
151
|
+
const selected = selections[i] || new Set();
|
|
152
|
+
const customText = customInputs[i]?.trim();
|
|
153
|
+
const isOther = otherActive[i];
|
|
154
|
+
|
|
155
|
+
if (q.multiSelect) {
|
|
156
|
+
const parts: string[] = Array.from(selected);
|
|
157
|
+
if (isOther && customText) {
|
|
158
|
+
parts.push(customText);
|
|
159
|
+
}
|
|
160
|
+
answers[q.question] = parts.join(', ');
|
|
161
|
+
} else {
|
|
162
|
+
if (isOther && customText) {
|
|
163
|
+
answers[q.question] = customText;
|
|
164
|
+
} else if (selected.size > 0) {
|
|
165
|
+
answers[q.question] = Array.from(selected).join(', ');
|
|
166
|
+
} else {
|
|
167
|
+
answers[q.question] = '';
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
debug.log('chat', 'Submitting AskUserQuestion answers:', answers);
|
|
173
|
+
|
|
174
|
+
ws.emit('chat:ask-user-answer', {
|
|
175
|
+
chatSessionId: currentSessionId(),
|
|
176
|
+
toolUseId: toolInput.id,
|
|
177
|
+
answers
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Clear waiting input status — SDK will resume processing
|
|
181
|
+
const sessId = currentSessionId();
|
|
182
|
+
if (sessId) updateSessionProcessState(sessId, { isWaitingInput: false });
|
|
183
|
+
appState.isWaitingInput = false;
|
|
184
|
+
|
|
185
|
+
hasSubmitted = true;
|
|
186
|
+
} catch (error) {
|
|
187
|
+
debug.error('chat', 'Failed to submit answers:', error);
|
|
188
|
+
} finally {
|
|
189
|
+
isSubmitting = false;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
</script>
|
|
193
|
+
|
|
194
|
+
{#if isError || isInterrupted}
|
|
195
|
+
<!-- Error/interrupted state — show questions as read-only with error indicator -->
|
|
196
|
+
<div class="space-y-3">
|
|
197
|
+
{#each toolInput.input.questions as question}
|
|
198
|
+
<div class="bg-white dark:bg-slate-800 rounded-lg border border-red-200/60 dark:border-red-800/40 p-4 space-y-2.5">
|
|
199
|
+
<div class="flex items-center gap-2">
|
|
200
|
+
<Icon name="lucide:circle-x" class="text-red-500 w-4 h-4 shrink-0" />
|
|
201
|
+
<span class="text-xs font-semibold px-2 py-0.5 rounded-full bg-slate-100 dark:bg-slate-700/50 text-slate-500 dark:text-slate-400">
|
|
202
|
+
{question.header}
|
|
203
|
+
</span>
|
|
204
|
+
</div>
|
|
205
|
+
<p class="text-sm text-slate-500 dark:text-slate-400">{question.question}</p>
|
|
206
|
+
</div>
|
|
207
|
+
{/each}
|
|
208
|
+
{#if isError && toolInput.$result?.content}
|
|
209
|
+
<p class="text-xs text-red-500 dark:text-red-400">{toolInput.$result.content}</p>
|
|
210
|
+
{:else}
|
|
211
|
+
<p class="text-xs text-red-500 dark:text-red-400">Session ended before question was answered</p>
|
|
212
|
+
{/if}
|
|
213
|
+
</div>
|
|
214
|
+
|
|
215
|
+
{:else if isAnswered}
|
|
216
|
+
<!-- Answered state — each question in its own card -->
|
|
217
|
+
<div class="space-y-3">
|
|
218
|
+
{#each toolInput.input.questions as question}
|
|
219
|
+
<div class="bg-white dark:bg-slate-800 rounded-lg border border-slate-200/60 dark:border-slate-700/60 p-4 space-y-2.5">
|
|
220
|
+
<div class="flex items-center gap-2">
|
|
221
|
+
<Icon name="lucide:circle-check" class="text-green-500 w-4 h-4 shrink-0" />
|
|
222
|
+
<span class="text-xs font-semibold px-2 py-0.5 rounded-full bg-blue-100 dark:bg-blue-800/50 text-blue-700 dark:text-blue-300">
|
|
223
|
+
{question.header}
|
|
224
|
+
</span>
|
|
225
|
+
</div>
|
|
226
|
+
<p class="text-sm font-medium text-slate-700 dark:text-slate-200">{question.question}</p>
|
|
227
|
+
<p class="text-sm text-green-700 dark:text-green-300 font-medium">
|
|
228
|
+
{parsedAnswers[question.question] || 'No answer'}
|
|
229
|
+
</p>
|
|
230
|
+
</div>
|
|
231
|
+
{/each}
|
|
232
|
+
</div>
|
|
233
|
+
|
|
234
|
+
{:else if hasSubmitted}
|
|
235
|
+
<!-- Submitted state — each question in its own card with local answer -->
|
|
236
|
+
<div class="space-y-3">
|
|
237
|
+
{#each toolInput.input.questions as question, idx}
|
|
238
|
+
{@const selected = selections[idx] || new Set()}
|
|
239
|
+
{@const customText = customInputs[idx]?.trim()}
|
|
240
|
+
{@const isOther = otherActive[idx]}
|
|
241
|
+
{@const localAnswer = question.multiSelect
|
|
242
|
+
? [...Array.from(selected), ...(isOther && customText ? [customText] : [])].join(', ')
|
|
243
|
+
: (isOther && customText ? customText : Array.from(selected).join(', '))
|
|
244
|
+
}
|
|
245
|
+
<div class="bg-white dark:bg-slate-800 rounded-lg border border-slate-200/60 dark:border-slate-700/60 p-4 space-y-2.5">
|
|
246
|
+
<div class="flex items-center gap-2">
|
|
247
|
+
<div class="w-4 h-4 border-2 border-blue-400 border-t-transparent rounded-full animate-spin shrink-0"></div>
|
|
248
|
+
<span class="text-xs font-semibold px-2 py-0.5 rounded-full bg-blue-100 dark:bg-blue-800/50 text-blue-700 dark:text-blue-300">
|
|
249
|
+
{question.header}
|
|
250
|
+
</span>
|
|
251
|
+
</div>
|
|
252
|
+
<p class="text-sm font-medium text-slate-700 dark:text-slate-200">{question.question}</p>
|
|
253
|
+
<p class="text-sm text-slate-500 dark:text-slate-400">
|
|
254
|
+
{localAnswer || 'No answer'}
|
|
255
|
+
</p>
|
|
256
|
+
</div>
|
|
257
|
+
{/each}
|
|
258
|
+
</div>
|
|
259
|
+
|
|
260
|
+
{:else}
|
|
261
|
+
<!-- Interactive form — each question in its own card -->
|
|
262
|
+
<div class="space-y-3">
|
|
263
|
+
{#each toolInput.input.questions as question, idx}
|
|
264
|
+
<div class="bg-white dark:bg-slate-800 rounded-lg border border-slate-200/60 dark:border-slate-700/60 p-4 space-y-3">
|
|
265
|
+
<!-- Question header badge -->
|
|
266
|
+
<div class="flex items-center gap-2">
|
|
267
|
+
<Icon name="lucide:message-circle-question-mark" class="text-blue-500 dark:text-blue-400 w-4 h-4 shrink-0" />
|
|
268
|
+
<span class="text-xs font-semibold px-2 py-0.5 rounded-full bg-blue-100 dark:bg-blue-800/50 text-blue-700 dark:text-blue-300">
|
|
269
|
+
{question.header}
|
|
270
|
+
</span>
|
|
271
|
+
</div>
|
|
272
|
+
|
|
273
|
+
<!-- Question text -->
|
|
274
|
+
<p class="text-sm font-medium text-slate-700 dark:text-slate-200">{question.question}</p>
|
|
275
|
+
|
|
276
|
+
<!-- Options -->
|
|
277
|
+
<div class="space-y-1.5">
|
|
278
|
+
{#each question.options as option}
|
|
279
|
+
<button
|
|
280
|
+
class="w-full text-left flex items-start gap-3 p-2.5 rounded-md border transition-colors
|
|
281
|
+
{isSelected(idx, option.label)
|
|
282
|
+
? 'border-blue-400 dark:border-blue-500 bg-blue-50 dark:bg-blue-900/30'
|
|
283
|
+
: 'border-slate-200 dark:border-slate-700 hover:border-blue-300 dark:hover:border-blue-600 hover:bg-slate-50 dark:hover:bg-slate-700/50'
|
|
284
|
+
}"
|
|
285
|
+
onclick={() => toggleSelection(idx, option.label, question.multiSelect)}
|
|
286
|
+
>
|
|
287
|
+
<div class="mt-0.5 shrink-0">
|
|
288
|
+
{#if question.multiSelect}
|
|
289
|
+
<div class="w-4 h-4 rounded border-2 flex items-center justify-center transition-colors
|
|
290
|
+
{isSelected(idx, option.label)
|
|
291
|
+
? 'border-blue-500 bg-blue-500'
|
|
292
|
+
: 'border-slate-300 dark:border-slate-600'
|
|
293
|
+
}"
|
|
294
|
+
>
|
|
295
|
+
{#if isSelected(idx, option.label)}
|
|
296
|
+
<Icon name="lucide:check" class="text-white w-3 h-3" />
|
|
297
|
+
{/if}
|
|
298
|
+
</div>
|
|
299
|
+
{:else}
|
|
300
|
+
<div class="w-4 h-4 rounded-full border-2 flex items-center justify-center transition-colors
|
|
301
|
+
{isSelected(idx, option.label)
|
|
302
|
+
? 'border-blue-500'
|
|
303
|
+
: 'border-slate-300 dark:border-slate-600'
|
|
304
|
+
}"
|
|
305
|
+
>
|
|
306
|
+
{#if isSelected(idx, option.label)}
|
|
307
|
+
<div class="w-2 h-2 rounded-full bg-blue-500"></div>
|
|
308
|
+
{/if}
|
|
309
|
+
</div>
|
|
310
|
+
{/if}
|
|
311
|
+
</div>
|
|
312
|
+
<div class="min-w-0">
|
|
313
|
+
<span class="text-sm font-medium text-slate-800 dark:text-slate-200">{option.label}</span>
|
|
314
|
+
<span class="text-sm text-slate-500 dark:text-slate-400"> — {option.description}</span>
|
|
315
|
+
</div>
|
|
316
|
+
</button>
|
|
317
|
+
{/each}
|
|
318
|
+
|
|
319
|
+
<!-- "Other" custom input option -->
|
|
320
|
+
<div
|
|
321
|
+
class="flex items-start gap-3 p-2.5 rounded-md border transition-colors cursor-pointer
|
|
322
|
+
{otherActive[idx]
|
|
323
|
+
? 'border-blue-400 dark:border-blue-500 bg-blue-50 dark:bg-blue-900/30'
|
|
324
|
+
: 'border-slate-200 dark:border-slate-700 hover:border-blue-300 dark:hover:border-blue-600 hover:bg-slate-50 dark:hover:bg-slate-700/50'
|
|
325
|
+
}"
|
|
326
|
+
onclick={() => toggleOther(idx, question.multiSelect)}
|
|
327
|
+
role="button"
|
|
328
|
+
tabindex="-1"
|
|
329
|
+
>
|
|
330
|
+
<div class="mt-0.5 shrink-0">
|
|
331
|
+
{#if question.multiSelect}
|
|
332
|
+
<div class="w-4 h-4 rounded border-2 flex items-center justify-center transition-colors
|
|
333
|
+
{otherActive[idx]
|
|
334
|
+
? 'border-blue-500 bg-blue-500'
|
|
335
|
+
: 'border-slate-300 dark:border-slate-600'
|
|
336
|
+
}"
|
|
337
|
+
>
|
|
338
|
+
{#if otherActive[idx]}
|
|
339
|
+
<Icon name="lucide:check" class="text-white w-3 h-3" />
|
|
340
|
+
{/if}
|
|
341
|
+
</div>
|
|
342
|
+
{:else}
|
|
343
|
+
<div class="w-4 h-4 rounded-full border-2 flex items-center justify-center transition-colors
|
|
344
|
+
{otherActive[idx]
|
|
345
|
+
? 'border-blue-500'
|
|
346
|
+
: 'border-slate-300 dark:border-slate-600'
|
|
347
|
+
}"
|
|
348
|
+
>
|
|
349
|
+
{#if otherActive[idx]}
|
|
350
|
+
<div class="w-2 h-2 rounded-full bg-blue-500"></div>
|
|
351
|
+
{/if}
|
|
352
|
+
</div>
|
|
353
|
+
{/if}
|
|
354
|
+
</div>
|
|
355
|
+
<div class="flex-1 min-w-0">
|
|
356
|
+
<input
|
|
357
|
+
type="text"
|
|
358
|
+
placeholder="Other (type your answer)..."
|
|
359
|
+
class="w-full text-sm bg-transparent border-none outline-none text-slate-700 dark:text-slate-200 placeholder-slate-400"
|
|
360
|
+
bind:value={customInputs[idx]}
|
|
361
|
+
onclick={(e) => e.stopPropagation()}
|
|
362
|
+
onfocus={() => {
|
|
363
|
+
if (!question.multiSelect) {
|
|
364
|
+
selections[idx] = new Set();
|
|
365
|
+
}
|
|
366
|
+
otherActive[idx] = true;
|
|
367
|
+
}}
|
|
368
|
+
/>
|
|
369
|
+
</div>
|
|
370
|
+
</div>
|
|
371
|
+
</div>
|
|
372
|
+
|
|
373
|
+
{#if question.multiSelect}
|
|
374
|
+
<p class="text-xs text-slate-400 dark:text-slate-500 italic">Multiple selections allowed</p>
|
|
375
|
+
{/if}
|
|
376
|
+
</div>
|
|
377
|
+
{/each}
|
|
378
|
+
|
|
379
|
+
<!-- Submit button -->
|
|
380
|
+
<div class="flex justify-end pt-1">
|
|
381
|
+
<button
|
|
382
|
+
class="px-4 py-1.5 text-sm font-medium rounded-md transition-colors
|
|
383
|
+
bg-blue-600 hover:bg-blue-700 text-white
|
|
384
|
+
disabled:opacity-50 disabled:cursor-not-allowed"
|
|
385
|
+
onclick={submitAnswers}
|
|
386
|
+
disabled={isSubmitting}
|
|
387
|
+
>
|
|
388
|
+
{#if isSubmitting}
|
|
389
|
+
Submitting...
|
|
390
|
+
{:else}
|
|
391
|
+
Submit Answer
|
|
392
|
+
{/if}
|
|
393
|
+
</button>
|
|
394
|
+
</div>
|
|
395
|
+
</div>
|
|
396
|
+
{/if}
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import type { BashToolInput
|
|
2
|
+
import type { BashToolInput } from '$shared/types/messaging';
|
|
3
3
|
import { TerminalCommand } from './components';
|
|
4
4
|
import CodeBlock from './components/CodeBlock.svelte';
|
|
5
5
|
|
|
6
|
+
/** Parsed background bash output (from XML-formatted BashOutput tool result) */
|
|
7
|
+
interface ParsedBashOutput {
|
|
8
|
+
status: 'running' | 'completed' | 'failed';
|
|
9
|
+
output: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
6
12
|
const { toolInput }: { toolInput: BashToolInput } = $props();
|
|
7
13
|
|
|
8
14
|
const command = toolInput.input.command || '';
|
|
@@ -10,13 +16,12 @@
|
|
|
10
16
|
const timeout = toolInput.input.timeout;
|
|
11
17
|
const isBackground = toolInput.input.run_in_background;
|
|
12
18
|
|
|
13
|
-
function parseBashOutputToolOutput(content: string):
|
|
19
|
+
function parseBashOutputToolOutput(content: string): ParsedBashOutput {
|
|
14
20
|
const statusMatch = content.match(/<status>(.*?)<\/status>/);
|
|
15
21
|
const stdoutMatch = content.match(/<stdout>(.*?)<\/stdout>/s);
|
|
16
|
-
const timestampMatch = content.match(/<timestamp>(.*?)<\/timestamp>/);
|
|
17
22
|
|
|
18
23
|
return {
|
|
19
|
-
status: statusMatch ? statusMatch[1] as
|
|
24
|
+
status: statusMatch ? statusMatch[1] as ParsedBashOutput['status'] : 'completed',
|
|
20
25
|
output: stdoutMatch ? stdoutMatch[1].trim() : ""
|
|
21
26
|
};
|
|
22
27
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { EnterPlanModeToolInput } from '$shared/types/messaging';
|
|
3
|
+
import { InfoLine } from './components';
|
|
4
|
+
import TextMessage from '../formatters/TextMessage.svelte';
|
|
5
|
+
|
|
6
|
+
const { toolInput }: { toolInput: EnterPlanModeToolInput } = $props();
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<div class="bg-white dark:bg-slate-800 rounded-md border border-slate-200/60 dark:border-slate-700/60 p-3">
|
|
10
|
+
<div class="flex gap-3">
|
|
11
|
+
<InfoLine icon="lucide:map" text="Entering plan mode" />
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
<!-- Tool Result -->
|
|
16
|
+
{#if toolInput.$result}
|
|
17
|
+
<div class="mt-4">
|
|
18
|
+
{#if typeof toolInput.$result.content === 'string'}
|
|
19
|
+
<TextMessage content={toolInput.$result.content} />
|
|
20
|
+
{:else}
|
|
21
|
+
<TextMessage content={JSON.stringify(toolInput.$result.content)} />
|
|
22
|
+
{/if}
|
|
23
|
+
</div>
|
|
24
|
+
{/if}
|
|
@@ -8,21 +8,18 @@
|
|
|
8
8
|
const plan = (toolInput.input as any).plan as string || '';
|
|
9
9
|
</script>
|
|
10
10
|
|
|
11
|
-
<div class="bg-white dark:bg-slate-800 rounded-md border border-slate-200/60 dark:border-slate-700/60 p-3">
|
|
11
|
+
<div class="bg-white dark:bg-slate-800 rounded-md border border-slate-200/60 dark:border-slate-700/60 p-3 mb-4">
|
|
12
12
|
<!-- Plan Info -->
|
|
13
|
-
<div class="flex gap-3 mb-2">
|
|
13
|
+
<div class="flex gap-3 mb-2.5">
|
|
14
14
|
<InfoLine icon="lucide:map" text="Exiting plan mode with proposed plan" />
|
|
15
15
|
</div>
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
<div class="border-t border-slate-200 dark:border-slate-700 pt-3">
|
|
19
|
-
<CodeBlock code={plan} type="neutral" label="Plan" />
|
|
20
|
-
</div>
|
|
17
|
+
<CodeBlock code={plan} type="neutral" />
|
|
21
18
|
</div>
|
|
22
19
|
|
|
23
20
|
<!-- Tool Result -->
|
|
24
21
|
{#if toolInput.$result}
|
|
25
|
-
<div class="
|
|
22
|
+
<div class="">
|
|
26
23
|
{#if typeof toolInput.$result.content === 'string'}
|
|
27
24
|
<TextMessage content={toolInput.$result.content} />
|
|
28
25
|
{:else}
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import type {
|
|
2
|
+
import type { TaskStopToolInput } from '$shared/types/messaging';
|
|
3
3
|
import { InfoLine } from './components';
|
|
4
4
|
import TextMessage from '../formatters/TextMessage.svelte';
|
|
5
5
|
|
|
6
|
-
const { toolInput }: { toolInput:
|
|
7
|
-
|
|
8
|
-
const
|
|
6
|
+
const { toolInput }: { toolInput: TaskStopToolInput } = $props();
|
|
7
|
+
|
|
8
|
+
const taskId = toolInput.input.task_id || toolInput.input.shell_id || 'unknown';
|
|
9
9
|
</script>
|
|
10
10
|
|
|
11
11
|
<div class="bg-white dark:bg-slate-800 rounded-md border border-slate-200/60 dark:border-slate-700/60 p-3">
|
|
12
12
|
<div class="flex gap-3">
|
|
13
|
-
<InfoLine icon="lucide:circle-x" text="
|
|
13
|
+
<InfoLine icon="lucide:circle-x" text="Stopping task: {taskId}" />
|
|
14
14
|
</div>
|
|
15
15
|
</div>
|
|
16
16
|
|
|
@@ -23,4 +23,4 @@
|
|
|
23
23
|
<TextMessage content={JSON.stringify(toolInput.$result.content)} />
|
|
24
24
|
{/if}
|
|
25
25
|
</div>
|
|
26
|
-
{/if}
|
|
26
|
+
{/if}
|
|
@@ -2,19 +2,22 @@
|
|
|
2
2
|
export { default as BashTool } from './BashTool.svelte';
|
|
3
3
|
export { default as BashOutputTool } from './BashOutputTool.svelte';
|
|
4
4
|
export { default as EditTool } from './EditTool.svelte';
|
|
5
|
+
export { default as EnterPlanModeTool } from './EnterPlanModeTool.svelte';
|
|
5
6
|
export { default as ExitPlanModeTool } from './ExitPlanModeTool.svelte';
|
|
6
7
|
export { default as GlobTool } from './GlobTool.svelte';
|
|
7
8
|
export { default as GrepTool } from './GrepTool.svelte';
|
|
8
|
-
export { default as
|
|
9
|
+
export { default as TaskStopTool } from './TaskStopTool.svelte';
|
|
9
10
|
export { default as ListMcpResourcesTool } from './ListMcpResourcesTool.svelte';
|
|
10
11
|
export { default as NotebookEditTool } from './NotebookEditTool.svelte';
|
|
11
12
|
export { default as ReadTool } from './ReadTool.svelte';
|
|
12
13
|
export { default as ReadMcpResourceTool } from './ReadMcpResourceTool.svelte';
|
|
14
|
+
export { default as AgentTool } from './AgentTool.svelte';
|
|
13
15
|
export { default as TaskTool } from './TaskTool.svelte';
|
|
14
16
|
export { default as TodoWriteTool } from './TodoWriteTool.svelte';
|
|
15
17
|
export { default as WebFetchTool } from './WebFetchTool.svelte';
|
|
16
18
|
export { default as WebSearchTool } from './WebSearchTool.svelte';
|
|
17
19
|
export { default as WriteTool } from './WriteTool.svelte';
|
|
20
|
+
export { default as AskUserQuestionTool } from './AskUserQuestionTool.svelte';
|
|
18
21
|
|
|
19
22
|
// Custom MCP Tools
|
|
20
23
|
export { default as CustomMcpTool } from './CustomMcpTool.svelte';
|
|
@@ -23,4 +26,4 @@ export { default as CustomMcpTool } from './CustomMcpTool.svelte';
|
|
|
23
26
|
export * from './components';
|
|
24
27
|
|
|
25
28
|
// Shared utilities
|
|
26
|
-
export * from '../shared';
|
|
29
|
+
export * from '../shared';
|
|
@@ -291,8 +291,13 @@
|
|
|
291
291
|
</div>
|
|
292
292
|
{:else if appState.isLoading}
|
|
293
293
|
<div class="flex items-center gap-2 px-3 py-1.5 bg-amber-50 dark:bg-amber-900/20 rounded-lg">
|
|
294
|
-
|
|
295
|
-
|
|
294
|
+
{#if appState.isWaitingInput}
|
|
295
|
+
<Icon name="lucide:message-circle-question-mark" class="w-3 h-3 text-amber-600 dark:text-amber-400" />
|
|
296
|
+
<span class="text-xs text-amber-600 dark:text-amber-400">Waiting for input...</span>
|
|
297
|
+
{:else}
|
|
298
|
+
<Icon name="lucide:loader" class="w-3 h-3 text-amber-600 dark:text-amber-400 animate-spin" />
|
|
299
|
+
<span class="text-xs text-amber-600 dark:text-amber-400">Chat in progress...</span>
|
|
300
|
+
{/if}
|
|
296
301
|
</div>
|
|
297
302
|
{/if}
|
|
298
303
|
</div>
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import Dialog from '$frontend/lib/components/common/Dialog.svelte';
|
|
13
13
|
import { presenceState } from '$frontend/lib/stores/core/presence.svelte';
|
|
14
14
|
import { userStore } from '$frontend/lib/stores/features/user.svelte';
|
|
15
|
+
import { getSessionProcessState } from '$frontend/lib/stores/core/app.svelte';
|
|
15
16
|
import { debug } from '$shared/utils/logger';
|
|
16
17
|
|
|
17
18
|
interface Props {
|
|
@@ -473,7 +474,7 @@
|
|
|
473
474
|
<Icon name="lucide:message-square" class="text-violet-600 dark:text-violet-400 w-4 h-4" />
|
|
474
475
|
{#if streaming}
|
|
475
476
|
<span
|
|
476
|
-
class="absolute -bottom-0.5 -right-0.5 w-2.5 h-2.5 rounded-full border-2 border-white dark:border-slate-900 bg-emerald-500"
|
|
477
|
+
class="absolute -bottom-0.5 -right-0.5 w-2.5 h-2.5 rounded-full border-2 border-white dark:border-slate-900 {getSessionProcessState(session.id).isWaitingInput ? 'bg-amber-500' : 'bg-emerald-500'}"
|
|
477
478
|
></span>
|
|
478
479
|
{:else}
|
|
479
480
|
<span
|
|
@@ -511,10 +512,17 @@
|
|
|
511
512
|
{/if}
|
|
512
513
|
</div>
|
|
513
514
|
{#if streaming}
|
|
514
|
-
|
|
515
|
-
<
|
|
516
|
-
|
|
517
|
-
|
|
515
|
+
{#if getSessionProcessState(session.id).isWaitingInput}
|
|
516
|
+
<p class="text-xs text-amber-500 dark:text-amber-400 mt-0.5 flex items-center gap-1.5">
|
|
517
|
+
<Icon name="lucide:message-circle-question-mark" class="w-3 h-3 shrink-0" />
|
|
518
|
+
Waiting for input...
|
|
519
|
+
</p>
|
|
520
|
+
{:else}
|
|
521
|
+
<p class="text-xs text-violet-500 dark:text-violet-400 mt-0.5 flex items-center gap-1.5">
|
|
522
|
+
<span class="inline-block w-3 h-3 border-2 border-violet-400 border-t-transparent rounded-full animate-spin shrink-0"></span>
|
|
523
|
+
Processing...
|
|
524
|
+
</p>
|
|
525
|
+
{/if}
|
|
518
526
|
{:else}
|
|
519
527
|
<p class="text-xs text-slate-400 dark:text-slate-500 truncate mt-0.5">
|
|
520
528
|
{getSessionSummary(session.id)}
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import TunnelModal from '$frontend/lib/components/tunnel/TunnelModal.svelte';
|
|
18
18
|
import ProjectUserAvatars from '$frontend/lib/components/common/ProjectUserAvatars.svelte';
|
|
19
19
|
import { sessionState } from '$frontend/lib/stores/core/sessions.svelte';
|
|
20
|
+
import { getSessionProcessState } from '$frontend/lib/stores/core/app.svelte';
|
|
20
21
|
import ws from '$frontend/lib/utils/ws';
|
|
21
22
|
|
|
22
23
|
// State
|
|
@@ -121,7 +122,7 @@
|
|
|
121
122
|
const hasActiveForSession = status.streams.some(
|
|
122
123
|
(s: any) => s.status === 'active' && s.chatSessionId === currentChatSessionId
|
|
123
124
|
);
|
|
124
|
-
if (hasActiveForSession) return 'bg-emerald-500';
|
|
125
|
+
if (hasActiveForSession) return currentChatSessionId && getSessionProcessState(currentChatSessionId).isWaitingInput ? 'bg-amber-500' : 'bg-emerald-500';
|
|
125
126
|
return 'bg-slate-500/30';
|
|
126
127
|
}
|
|
127
128
|
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import FolderBrowser from '$frontend/lib/components/common/FolderBrowser.svelte';
|
|
19
19
|
import ProjectUserAvatars from '$frontend/lib/components/common/ProjectUserAvatars.svelte';
|
|
20
20
|
import { sessionState } from '$frontend/lib/stores/core/sessions.svelte';
|
|
21
|
+
import { getSessionProcessState } from '$frontend/lib/stores/core/app.svelte';
|
|
21
22
|
import ws from '$frontend/lib/utils/ws';
|
|
22
23
|
import { debug } from '$shared/utils/logger';
|
|
23
24
|
|
|
@@ -77,7 +78,7 @@
|
|
|
77
78
|
const hasActiveForSession = status.streams.some(
|
|
78
79
|
(s: any) => s.status === 'active' && s.chatSessionId === currentChatSessionId
|
|
79
80
|
);
|
|
80
|
-
if (hasActiveForSession) return 'bg-emerald-500';
|
|
81
|
+
if (hasActiveForSession) return currentChatSessionId && getSessionProcessState(currentChatSessionId).isWaitingInput ? 'bg-amber-500' : 'bg-emerald-500';
|
|
81
82
|
return 'bg-slate-500/30';
|
|
82
83
|
}
|
|
83
84
|
|