@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.
Files changed (49) hide show
  1. package/backend/index.ts +9 -0
  2. package/backend/lib/chat/stream-manager.ts +130 -10
  3. package/backend/lib/database/queries/message-queries.ts +47 -0
  4. package/backend/lib/engine/adapters/claude/stream.ts +65 -1
  5. package/backend/lib/engine/adapters/opencode/message-converter.ts +35 -77
  6. package/backend/lib/engine/types.ts +6 -0
  7. package/backend/lib/files/file-operations.ts +2 -2
  8. package/backend/lib/files/file-reading.ts +2 -2
  9. package/backend/lib/files/path-browsing.ts +2 -2
  10. package/backend/lib/terminal/pty-session-manager.ts +1 -1
  11. package/backend/lib/terminal/shell-utils.ts +4 -4
  12. package/backend/ws/chat/background.ts +3 -0
  13. package/backend/ws/chat/stream.ts +43 -1
  14. package/bin/clopen.ts +10 -0
  15. package/bun.lock +259 -381
  16. package/frontend/lib/components/chat/ChatInterface.svelte +8 -1
  17. package/frontend/lib/components/chat/formatters/MessageFormatter.svelte +20 -0
  18. package/frontend/lib/components/chat/formatters/TextMessage.svelte +3 -15
  19. package/frontend/lib/components/chat/formatters/Tools.svelte +15 -8
  20. package/frontend/lib/components/chat/input/ChatInput.svelte +70 -21
  21. package/frontend/lib/components/chat/input/components/LoadingIndicator.svelte +23 -11
  22. package/frontend/lib/components/chat/input/composables/use-chat-actions.svelte.ts +1 -1
  23. package/frontend/lib/components/chat/message/ChatMessage.svelte +13 -1
  24. package/frontend/lib/components/chat/message/ChatMessages.svelte +2 -2
  25. package/frontend/lib/components/chat/message/DateSeparator.svelte +1 -1
  26. package/frontend/lib/components/chat/message/MessageBubble.svelte +2 -2
  27. package/frontend/lib/components/chat/message/MessageHeader.svelte +14 -12
  28. package/frontend/lib/components/chat/tools/AgentTool.svelte +95 -0
  29. package/frontend/lib/components/chat/tools/AskUserQuestionTool.svelte +396 -0
  30. package/frontend/lib/components/chat/tools/BashTool.svelte +9 -4
  31. package/frontend/lib/components/chat/tools/EnterPlanModeTool.svelte +24 -0
  32. package/frontend/lib/components/chat/tools/ExitPlanModeTool.svelte +4 -7
  33. package/frontend/lib/components/chat/tools/{KillShellTool.svelte → TaskStopTool.svelte} +6 -6
  34. package/frontend/lib/components/chat/tools/index.ts +5 -2
  35. package/frontend/lib/components/checkpoint/TimelineModal.svelte +7 -2
  36. package/frontend/lib/components/history/HistoryModal.svelte +13 -5
  37. package/frontend/lib/components/workspace/DesktopNavigator.svelte +2 -1
  38. package/frontend/lib/components/workspace/MobileNavigator.svelte +2 -1
  39. package/frontend/lib/services/chat/chat.service.ts +146 -12
  40. package/frontend/lib/stores/core/app.svelte.ts +77 -0
  41. package/frontend/lib/utils/chat/message-grouper.ts +94 -12
  42. package/frontend/lib/utils/chat/message-processor.ts +37 -4
  43. package/frontend/lib/utils/chat/tool-handler.ts +96 -5
  44. package/package.json +4 -4
  45. package/shared/constants/engines.ts +1 -1
  46. package/shared/types/database/schema.ts +1 -0
  47. package/shared/types/messaging/index.ts +15 -13
  48. package/shared/types/messaging/tool.ts +185 -361
  49. 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, BashOutputToolOutput } from '$shared/types/messaging';
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): BashOutputToolOutput {
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 BashOutputToolOutput['status'] : 'completed',
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
- <!-- Plan Content -->
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="mt-4 bg-white dark:bg-slate-800 rounded-md border border-slate-200/60 dark:border-slate-700/60 p-3">
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 { KillShellToolInput } from '$shared/types/messaging';
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: KillShellToolInput } = $props();
7
-
8
- const shellId = toolInput.input.shell_id;
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="Terminating shell process: {shellId}" />
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 KillShellTool } from './KillShellTool.svelte';
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
- <Icon name="lucide:loader" class="w-3 h-3 text-amber-600 dark:text-amber-400 animate-spin" />
295
- <span class="text-xs text-amber-600 dark:text-amber-400">Chat in progress...</span>
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
- <p class="text-xs text-violet-500 dark:text-violet-400 mt-0.5 flex items-center gap-1.5">
515
- <span class="inline-block w-3 h-3 border-2 border-violet-400 border-t-transparent rounded-full animate-spin shrink-0"></span>
516
- Processing...
517
- </p>
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