dylanebert commited on
Commit
ebb12a0
·
1 Parent(s): ec75a88

improved AI chat

Browse files
src/App.svelte CHANGED
@@ -1,6 +1,7 @@
1
  <script lang="ts">
2
  import { onMount, onDestroy } from 'svelte';
3
  import { consoleCapture } from './lib/services/console-capture';
 
4
  import { registerShortcuts, shortcuts } from './lib/config/shortcuts';
5
  import { loadingStore } from './lib/stores/loading';
6
  import AppHeader from './lib/components/layout/AppHeader.svelte';
@@ -11,23 +12,25 @@
11
 
12
  onMount(() => {
13
  loadingStore.startLoading();
14
-
15
  consoleCapture.setup();
 
16
  unregisterShortcuts = registerShortcuts(shortcuts);
17
-
18
  setTimeout(() => {
19
  loadingStore.setProgress(60);
20
  }, 100);
21
-
22
  requestAnimationFrame(() => {
23
  requestAnimationFrame(() => {
24
  loadingStore.finishLoading();
25
  });
26
  });
27
  });
28
-
29
  onDestroy(() => {
30
  consoleCapture.teardown();
 
31
  if (unregisterShortcuts) unregisterShortcuts();
32
  });
33
  </script>
 
1
  <script lang="ts">
2
  import { onMount, onDestroy } from 'svelte';
3
  import { consoleCapture } from './lib/services/console-capture';
4
+ import { consoleForwarder } from './lib/services/console-forward';
5
  import { registerShortcuts, shortcuts } from './lib/config/shortcuts';
6
  import { loadingStore } from './lib/stores/loading';
7
  import AppHeader from './lib/components/layout/AppHeader.svelte';
 
12
 
13
  onMount(() => {
14
  loadingStore.startLoading();
15
+
16
  consoleCapture.setup();
17
+ consoleForwarder.start();
18
  unregisterShortcuts = registerShortcuts(shortcuts);
19
+
20
  setTimeout(() => {
21
  loadingStore.setProgress(60);
22
  }, 100);
23
+
24
  requestAnimationFrame(() => {
25
  requestAnimationFrame(() => {
26
  loadingStore.finishLoading();
27
  });
28
  });
29
  });
30
+
31
  onDestroy(() => {
32
  consoleCapture.teardown();
33
+ consoleForwarder.stop();
34
  if (unregisterShortcuts) unregisterShortcuts();
35
  });
36
  </script>
src/lib/components/chat/ChatPanel.svelte CHANGED
@@ -6,6 +6,8 @@
6
  import ReasoningBlock from "./ReasoningBlock.svelte";
7
  import MarkdownRenderer from "./MarkdownRenderer.svelte";
8
  import ToolCallDisplay from "./ToolCallDisplay.svelte";
 
 
9
 
10
  let inputValue = "";
11
  let messagesContainer: HTMLDivElement;
@@ -197,29 +199,43 @@
197
  {#if $agentStore.messages.length === 0 && $isConnected}
198
  <div class="ready-message">Ready to Chat!</div>
199
  {/if}
 
 
 
 
 
 
 
 
 
 
200
  {#each $agentStore.messages as message}
201
- <div class="message {message.role}">
202
- {#if message.reasoning && message.role === "assistant"}
203
- <ReasoningBlock reasoning={message.reasoning} />
204
- {/if}
205
- <div class="message-content">
206
- {#if message.role === "assistant"}
207
- {@const parts = parseMessageContent(message.content.trim())}
208
- {#each parts as part}
209
- {#if part.type === 'text' && part.content.trim()}
210
- <MarkdownRenderer content={part.content} streaming={false} />
211
- {:else if part.type === 'tool' && part.toolName}
212
- <ToolCallDisplay toolName={part.toolName} parameters={part.params} />
 
 
 
 
 
 
 
213
  {/if}
214
- {/each}
215
- {#if message.streaming}
216
- <span class="cursor">▊</span>
217
  {/if}
218
- {:else}
219
- {message.content.trim()}
220
- {/if}
221
  </div>
222
- </div>
223
  {/each}
224
  {#if $agentStore.error}
225
  <div class="error-message">
 
6
  import ReasoningBlock from "./ReasoningBlock.svelte";
7
  import MarkdownRenderer from "./MarkdownRenderer.svelte";
8
  import ToolCallDisplay from "./ToolCallDisplay.svelte";
9
+ import ToolCallBlock from "./ToolCallBlock.svelte";
10
+ import InProgressBlock from "./InProgressBlock.svelte";
11
 
12
  let inputValue = "";
13
  let messagesContainer: HTMLDivElement;
 
199
  {#if $agentStore.messages.length === 0 && $isConnected}
200
  <div class="ready-message">Ready to Chat!</div>
201
  {/if}
202
+
203
+ {#if $agentStore.streamingStatus !== "idle" && (!$agentStore.streamingContent || $agentStore.streamingStatus === "thinking")}
204
+ <InProgressBlock
205
+ status={$agentStore.streamingStatus === "completing" ? "completing" : $agentStore.streamingStatus}
206
+ content={$agentStore.streamingContent}
207
+ startTime={$agentStore.thinkingStartTime || Date.now()}
208
+ isExpanded={false}
209
+ />
210
+ {/if}
211
+
212
  {#each $agentStore.messages as message}
213
+ {#if message.role === "tool" && message.toolExecutions}
214
+ <ToolCallBlock toolExecutions={message.toolExecutions} />
215
+ {:else}
216
+ <div class="message {message.role}">
217
+ {#if message.reasoning && message.role === "assistant"}
218
+ <ReasoningBlock reasoning={message.reasoning} />
219
+ {/if}
220
+ <div class="message-content">
221
+ {#if message.role === "assistant"}
222
+ {@const parts = parseMessageContent(message.content.trim())}
223
+ {#each parts as part}
224
+ {#if part.type === 'text' && part.content.trim()}
225
+ <MarkdownRenderer content={part.content} streaming={false} />
226
+ {:else if part.type === 'tool' && part.toolName}
227
+ <ToolCallDisplay toolName={part.toolName} parameters={part.params} />
228
+ {/if}
229
+ {/each}
230
+ {#if message.streaming}
231
+ <span class="cursor">▊</span>
232
  {/if}
233
+ {:else}
234
+ {message.content.trim()}
 
235
  {/if}
236
+ </div>
 
 
237
  </div>
238
+ {/if}
239
  {/each}
240
  {#if $agentStore.error}
241
  <div class="error-message">
src/lib/components/chat/InProgressBlock.svelte ADDED
@@ -0,0 +1,351 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { onMount, onDestroy } from "svelte";
3
+ import gsap from "gsap";
4
+
5
+ export let status: "thinking" | "streaming" | "completing" = "thinking";
6
+ export let content: string = "";
7
+ export let startTime: number = Date.now();
8
+ export let isExpanded: boolean = false;
9
+ export let autoCollapse: boolean = true;
10
+
11
+ let blockElement: HTMLDivElement;
12
+ let contentElement: HTMLDivElement;
13
+ let expandIcon: HTMLSpanElement;
14
+ let collapseTimeout: number | null = null;
15
+
16
+ $: duration = Date.now() - startTime;
17
+ $: formattedDuration = formatDuration(duration);
18
+
19
+ function formatDuration(ms: number): string {
20
+ if (ms < 1000) return `${ms}ms`;
21
+ return `${(ms / 1000).toFixed(1)}s`;
22
+ }
23
+
24
+ function toggleExpanded() {
25
+ isExpanded = !isExpanded;
26
+ updateExpandState();
27
+ }
28
+
29
+ function updateExpandState() {
30
+ if (!expandIcon || !contentElement) return;
31
+
32
+ if (isExpanded) {
33
+ gsap.to(expandIcon, {
34
+ rotation: 90,
35
+ duration: 0.2,
36
+ ease: "power2.out",
37
+ });
38
+
39
+ gsap.set(contentElement, { display: "block" });
40
+ gsap.fromTo(
41
+ contentElement,
42
+ { opacity: 0, maxHeight: 0 },
43
+ { opacity: 1, maxHeight: 500, duration: 0.3, ease: "power2.out" },
44
+ );
45
+ } else {
46
+ gsap.to(expandIcon, {
47
+ rotation: 0,
48
+ duration: 0.2,
49
+ ease: "power2.in",
50
+ });
51
+
52
+ gsap.to(contentElement, {
53
+ opacity: 0,
54
+ maxHeight: 0,
55
+ duration: 0.2,
56
+ ease: "power2.in",
57
+ onComplete: () => {
58
+ gsap.set(contentElement, { display: "none" });
59
+ },
60
+ });
61
+ }
62
+ }
63
+
64
+ function getStatusIcon() {
65
+ switch (status) {
66
+ case "thinking":
67
+ return "🤔";
68
+ case "streaming":
69
+ return "✍️";
70
+ case "completing":
71
+ return "✨";
72
+ default:
73
+ return "⚡";
74
+ }
75
+ }
76
+
77
+ function getStatusText() {
78
+ switch (status) {
79
+ case "thinking":
80
+ return "Thinking";
81
+ case "streaming":
82
+ return "Writing response";
83
+ case "completing":
84
+ return "Finalizing response";
85
+ default:
86
+ return "Processing";
87
+ }
88
+ }
89
+
90
+ $: if (status === "completing" && autoCollapse && isExpanded) {
91
+ if (collapseTimeout) clearTimeout(collapseTimeout);
92
+ collapseTimeout = window.setTimeout(() => {
93
+ isExpanded = false;
94
+ updateExpandState();
95
+ }, 500);
96
+ }
97
+
98
+ onMount(() => {
99
+ if (blockElement) {
100
+ gsap.fromTo(
101
+ blockElement,
102
+ { opacity: 0, y: -10 },
103
+ { opacity: 1, y: 0, duration: 0.3, ease: "power2.out" },
104
+ );
105
+ }
106
+
107
+ const interval = setInterval(() => {
108
+ duration = Date.now() - startTime;
109
+ }, 100);
110
+
111
+ return () => clearInterval(interval);
112
+ });
113
+
114
+ onDestroy(() => {
115
+ if (collapseTimeout) clearTimeout(collapseTimeout);
116
+ });
117
+ </script>
118
+
119
+ <div class="in-progress-block {status}" bind:this={blockElement}>
120
+ <button class="progress-header" on:click={toggleExpanded} aria-expanded={isExpanded}>
121
+ <span class="status-icon">
122
+ {#if status === "streaming"}
123
+ <span class="animated-icon">{getStatusIcon()}</span>
124
+ {:else if status === "thinking"}
125
+ <span class="spinning-icon">{getStatusIcon()}</span>
126
+ {:else}
127
+ {getStatusIcon()}
128
+ {/if}
129
+ </span>
130
+
131
+ <span class="status-text">
132
+ {getStatusText()}<span class="dots"></span>
133
+ </span>
134
+
135
+ <span class="progress-meta">
136
+ <span class="duration">{formattedDuration}</span>
137
+ {#if content}
138
+ <span class="char-count">{content.length} chars</span>
139
+ {/if}
140
+ </span>
141
+
142
+ <span bind:this={expandIcon} class="expand-icon">▶</span>
143
+ </button>
144
+
145
+ <div bind:this={contentElement} class="progress-content" style="display: none;">
146
+ {#if content}
147
+ <div class="content-wrapper">
148
+ <div class="content-text">{content}</div>
149
+ <span class="cursor">▊</span>
150
+ </div>
151
+ {:else}
152
+ <div class="waiting-message">Waiting for response to begin...</div>
153
+ {/if}
154
+ </div>
155
+ </div>
156
+
157
+ <style>
158
+ .in-progress-block {
159
+ margin: 0.4rem 0;
160
+ border-radius: 4px;
161
+ overflow: hidden;
162
+ border: 1px solid rgba(255, 210, 30, 0.2);
163
+ background: rgba(255, 210, 30, 0.05);
164
+ transition: all 0.2s ease;
165
+ }
166
+
167
+ .in-progress-block.thinking {
168
+ border-color: rgba(255, 210, 30, 0.3);
169
+ background: rgba(255, 210, 30, 0.08);
170
+ }
171
+
172
+ .in-progress-block.streaming {
173
+ border-color: rgba(65, 105, 225, 0.3);
174
+ background: rgba(65, 105, 225, 0.08);
175
+ }
176
+
177
+ .in-progress-block.completing {
178
+ border-color: rgba(0, 255, 0, 0.2);
179
+ background: rgba(0, 255, 0, 0.05);
180
+ }
181
+
182
+ .progress-header {
183
+ display: flex;
184
+ align-items: center;
185
+ gap: 0.5rem;
186
+ width: 100%;
187
+ padding: 0.5rem 0.75rem;
188
+ background: transparent;
189
+ border: none;
190
+ color: inherit;
191
+ font: inherit;
192
+ text-align: left;
193
+ cursor: pointer;
194
+ transition: background 0.2s ease;
195
+ }
196
+
197
+ .progress-header:hover {
198
+ background: rgba(255, 255, 255, 0.02);
199
+ }
200
+
201
+ .status-icon {
202
+ font-size: 1rem;
203
+ width: 1.5rem;
204
+ text-align: center;
205
+ }
206
+
207
+ .spinning-icon {
208
+ display: inline-block;
209
+ animation: spin 1s linear infinite;
210
+ }
211
+
212
+ .animated-icon {
213
+ display: inline-block;
214
+ animation: bounce 1s ease-in-out infinite;
215
+ }
216
+
217
+ @keyframes spin {
218
+ from {
219
+ transform: rotate(0deg);
220
+ }
221
+ to {
222
+ transform: rotate(360deg);
223
+ }
224
+ }
225
+
226
+ @keyframes bounce {
227
+ 0%,
228
+ 100% {
229
+ transform: translateY(0);
230
+ }
231
+ 50% {
232
+ transform: translateY(-2px);
233
+ }
234
+ }
235
+
236
+ .status-text {
237
+ flex: 1;
238
+ color: rgba(255, 255, 255, 0.9);
239
+ font-size: 0.875rem;
240
+ }
241
+
242
+ .dots::after {
243
+ content: "";
244
+ animation: dots 1.5s steps(4, end) infinite;
245
+ }
246
+
247
+ @keyframes dots {
248
+ 0%,
249
+ 20% {
250
+ content: "";
251
+ }
252
+ 40% {
253
+ content: ".";
254
+ }
255
+ 60% {
256
+ content: "..";
257
+ }
258
+ 80%,
259
+ 100% {
260
+ content: "...";
261
+ }
262
+ }
263
+
264
+ .progress-meta {
265
+ display: flex;
266
+ gap: 1rem;
267
+ align-items: center;
268
+ color: rgba(255, 255, 255, 0.4);
269
+ font-size: 0.75rem;
270
+ font-family: "Monaco", "Menlo", monospace;
271
+ }
272
+
273
+ .duration {
274
+ min-width: 3rem;
275
+ }
276
+
277
+ .char-count {
278
+ opacity: 0.7;
279
+ }
280
+
281
+ .expand-icon {
282
+ font-size: 0.7rem;
283
+ color: rgba(255, 255, 255, 0.4);
284
+ transition: transform 0.2s ease;
285
+ transform-origin: center;
286
+ }
287
+
288
+ .progress-content {
289
+ padding: 0.75rem;
290
+ background: rgba(0, 0, 0, 0.2);
291
+ border-top: 1px solid rgba(255, 255, 255, 0.05);
292
+ max-height: 300px;
293
+ overflow-y: auto;
294
+ }
295
+
296
+ .content-wrapper {
297
+ font-family: "Monaco", "Menlo", monospace;
298
+ font-size: 0.8rem;
299
+ line-height: 1.5;
300
+ color: rgba(255, 255, 255, 0.8);
301
+ }
302
+
303
+ .content-text {
304
+ white-space: pre-wrap;
305
+ word-wrap: break-word;
306
+ display: inline;
307
+ }
308
+
309
+ .cursor {
310
+ display: inline-block;
311
+ animation: blink 1s infinite;
312
+ color: rgba(65, 105, 225, 0.8);
313
+ font-weight: bold;
314
+ }
315
+
316
+ @keyframes blink {
317
+ 0%,
318
+ 50% {
319
+ opacity: 1;
320
+ }
321
+ 51%,
322
+ 100% {
323
+ opacity: 0;
324
+ }
325
+ }
326
+
327
+ .waiting-message {
328
+ text-align: center;
329
+ color: rgba(255, 255, 255, 0.4);
330
+ font-style: italic;
331
+ font-size: 0.85rem;
332
+ padding: 1rem;
333
+ }
334
+
335
+ ::-webkit-scrollbar {
336
+ width: 6px;
337
+ }
338
+
339
+ ::-webkit-scrollbar-track {
340
+ background: transparent;
341
+ }
342
+
343
+ ::-webkit-scrollbar-thumb {
344
+ background: rgba(255, 255, 255, 0.1);
345
+ border-radius: 3px;
346
+ }
347
+
348
+ ::-webkit-scrollbar-thumb:hover {
349
+ background: rgba(255, 255, 255, 0.2);
350
+ }
351
+ </style>
src/lib/components/chat/StreamingIndicator.svelte ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { onMount } from "svelte";
3
+ import gsap from "gsap";
4
+
5
+ export let status: "thinking" | "streaming" = "thinking";
6
+ export let content: string = "";
7
+ export let duration: number = 0;
8
+
9
+ let indicatorElement: HTMLDivElement;
10
+ let truncatedContent = "";
11
+ const maxPreviewLength = 100;
12
+
13
+ $: truncatedContent =
14
+ content.length > maxPreviewLength ? content.slice(0, maxPreviewLength) + "..." : content;
15
+
16
+ $: formattedDuration = duration > 0 ? `${(duration / 1000).toFixed(1)}s` : "";
17
+
18
+ onMount(() => {
19
+ if (indicatorElement) {
20
+ gsap.fromTo(
21
+ indicatorElement,
22
+ { opacity: 0, y: -5 },
23
+ { opacity: 1, y: 0, duration: 0.3, ease: "power2.out" },
24
+ );
25
+ }
26
+ });
27
+ </script>
28
+
29
+ <div class="streaming-indicator" bind:this={indicatorElement}>
30
+ <div class="indicator-header">
31
+ <span class="status-icon">
32
+ {#if status === "thinking"}
33
+ <span class="thinking-icon">🤔</span>
34
+ {:else}
35
+ <span class="streaming-icon">✍️</span>
36
+ {/if}
37
+ </span>
38
+ <span class="status-text">
39
+ {#if status === "thinking"}
40
+ Thinking<span class="dots"></span>
41
+ {:else}
42
+ Responding<span class="dots"></span>
43
+ {/if}
44
+ </span>
45
+ {#if formattedDuration}
46
+ <span class="duration">{formattedDuration}</span>
47
+ {/if}
48
+ </div>
49
+
50
+ {#if truncatedContent}
51
+ <div class="preview-content">
52
+ {truncatedContent}
53
+ </div>
54
+ {/if}
55
+ </div>
56
+
57
+ <style>
58
+ .streaming-indicator {
59
+ background: rgba(65, 105, 225, 0.08);
60
+ border: 1px solid rgba(65, 105, 225, 0.2);
61
+ border-radius: 4px;
62
+ padding: 0.5rem 0.75rem;
63
+ margin: 0.25rem 0;
64
+ font-size: 0.875rem;
65
+ }
66
+
67
+ .indicator-header {
68
+ display: flex;
69
+ align-items: center;
70
+ gap: 0.5rem;
71
+ color: rgba(255, 255, 255, 0.7);
72
+ }
73
+
74
+ .status-icon {
75
+ display: inline-flex;
76
+ align-items: center;
77
+ justify-content: center;
78
+ width: 1.2rem;
79
+ height: 1.2rem;
80
+ }
81
+
82
+ .thinking-icon {
83
+ animation: pulse 1.5s ease-in-out infinite;
84
+ }
85
+
86
+ .streaming-icon {
87
+ animation: write 1s ease-in-out infinite;
88
+ }
89
+
90
+ @keyframes pulse {
91
+ 0%,
92
+ 100% {
93
+ transform: scale(1);
94
+ opacity: 0.8;
95
+ }
96
+ 50% {
97
+ transform: scale(1.1);
98
+ opacity: 1;
99
+ }
100
+ }
101
+
102
+ @keyframes write {
103
+ 0%,
104
+ 100% {
105
+ transform: translateX(0);
106
+ }
107
+ 25% {
108
+ transform: translateX(-1px);
109
+ }
110
+ 75% {
111
+ transform: translateX(1px);
112
+ }
113
+ }
114
+
115
+ .status-text {
116
+ flex: 1;
117
+ }
118
+
119
+ .dots::after {
120
+ content: "";
121
+ animation: dots 1.5s steps(4, end) infinite;
122
+ }
123
+
124
+ @keyframes dots {
125
+ 0%,
126
+ 20% {
127
+ content: "";
128
+ }
129
+ 40% {
130
+ content: ".";
131
+ }
132
+ 60% {
133
+ content: "..";
134
+ }
135
+ 80%,
136
+ 100% {
137
+ content: "...";
138
+ }
139
+ }
140
+
141
+ .duration {
142
+ color: rgba(255, 255, 255, 0.4);
143
+ font-size: 0.75rem;
144
+ font-family: "Monaco", "Menlo", monospace;
145
+ }
146
+
147
+ .preview-content {
148
+ margin-top: 0.5rem;
149
+ padding-top: 0.5rem;
150
+ border-top: 1px solid rgba(255, 255, 255, 0.05);
151
+ color: rgba(255, 255, 255, 0.6);
152
+ font-family: "Monaco", "Menlo", monospace;
153
+ font-size: 0.8rem;
154
+ line-height: 1.4;
155
+ word-break: break-word;
156
+ }
157
+ </style>
src/lib/components/chat/ToolCallBlock.svelte ADDED
@@ -0,0 +1,373 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { onMount } from "svelte";
3
+ import gsap from "gsap";
4
+ import type { ToolExecution } from "../../stores/agent";
5
+
6
+ export let toolExecutions: ToolExecution[] = [];
7
+
8
+ let blockElement: HTMLDivElement;
9
+ let expandedTools: Set<string> = new Set();
10
+
11
+ const toolIcons: Record<string, string> = {
12
+ read_editor: "📄",
13
+ write_editor: "✏️",
14
+ observe_console: "📟",
15
+ default: "🔧",
16
+ };
17
+
18
+ const statusIcons: Record<string, string> = {
19
+ pending: "⏳",
20
+ running: "⚡",
21
+ completed: "✅",
22
+ error: "❌",
23
+ };
24
+
25
+ const statusMessages: Record<string, (name: string) => string> = {
26
+ read_editor: (name) => ({
27
+ pending: "Preparing to read code...",
28
+ running: "Reading editor content...",
29
+ completed: "Code read successfully",
30
+ error: "Failed to read code"
31
+ }[name] || name),
32
+ write_editor: (name) => ({
33
+ pending: "Preparing to write code...",
34
+ running: "Writing code and reloading game...",
35
+ completed: "Code updated successfully",
36
+ error: "Failed to write code"
37
+ }[name] || name),
38
+ observe_console: (name) => ({
39
+ pending: "Preparing to read console...",
40
+ running: "Reading console output...",
41
+ completed: "Console read successfully",
42
+ error: "Failed to read console"
43
+ }[name] || name),
44
+ };
45
+
46
+ function toggleTool(toolId: string) {
47
+ if (expandedTools.has(toolId)) {
48
+ expandedTools.delete(toolId);
49
+ } else {
50
+ expandedTools.add(toolId);
51
+ }
52
+ expandedTools = expandedTools;
53
+ }
54
+
55
+ function getStatusMessage(tool: ToolExecution): string {
56
+ const messageFunc = statusMessages[tool.name] || statusMessages.default;
57
+ return messageFunc ? messageFunc(tool.status) : `${tool.name}: ${tool.status}`;
58
+ }
59
+
60
+ function formatDuration(startTime: number, endTime?: number): string {
61
+ const duration = (endTime || Date.now()) - startTime;
62
+ if (duration < 1000) {
63
+ return `${duration}ms`;
64
+ }
65
+ return `${(duration / 1000).toFixed(1)}s`;
66
+ }
67
+
68
+ onMount(() => {
69
+ gsap.fromTo(
70
+ blockElement,
71
+ { opacity: 0, y: -10 },
72
+ { opacity: 1, y: 0, duration: 0.3, ease: "power2.out" }
73
+ );
74
+ });
75
+ </script>
76
+
77
+ <div class="tool-block" bind:this={blockElement}>
78
+ {#each toolExecutions as tool (tool.id)}
79
+ <div class="tool-item {tool.status}" class:expanded={expandedTools.has(tool.id)}>
80
+ <button
81
+ class="tool-item-header"
82
+ on:click={() => toggleTool(tool.id)}
83
+ aria-expanded={expandedTools.has(tool.id)}
84
+ >
85
+ <span class="tool-status-icon">
86
+ {#if tool.status === "running"}
87
+ <span class="spinner-small">{statusIcons[tool.status]}</span>
88
+ {:else}
89
+ {statusIcons[tool.status]}
90
+ {/if}
91
+ </span>
92
+ <span class="tool-icon">{toolIcons[tool.name] || toolIcons.default}</span>
93
+ <span class="tool-name">{getStatusMessage(tool)}</span>
94
+ <span class="tool-duration">
95
+ {formatDuration(tool.startTime, tool.endTime)}
96
+ </span>
97
+ <span class="expand-icon" class:rotated={expandedTools.has(tool.id)}>
98
+
99
+ </span>
100
+ </button>
101
+
102
+ {#if expandedTools.has(tool.id)}
103
+ <div class="tool-details">
104
+ {#if tool.args && Object.keys(tool.args).length > 0}
105
+ <div class="tool-section">
106
+ <div class="section-title">Parameters:</div>
107
+ <div class="params">
108
+ {#each Object.entries(tool.args) as [key, value]}
109
+ <div class="param">
110
+ <span class="param-key">{key}:</span>
111
+ <span class="param-value">
112
+ {#if typeof value === 'string' && value.length > 100}
113
+ <pre>{value}</pre>
114
+ {:else}
115
+ {JSON.stringify(value)}
116
+ {/if}
117
+ </span>
118
+ </div>
119
+ {/each}
120
+ </div>
121
+ </div>
122
+ {/if}
123
+
124
+ {#if tool.output}
125
+ <div class="tool-section">
126
+ <div class="section-title">Output:</div>
127
+ <pre class="tool-output">{tool.output}</pre>
128
+ </div>
129
+ {/if}
130
+
131
+ {#if tool.consoleOutput && tool.consoleOutput.length > 0}
132
+ <div class="tool-section">
133
+ <div class="section-title">Console Output:</div>
134
+ <div class="console-output">
135
+ {#each tool.consoleOutput as line}
136
+ <div class="console-line">{line}</div>
137
+ {/each}
138
+ </div>
139
+ </div>
140
+ {/if}
141
+
142
+ {#if tool.error}
143
+ <div class="tool-section error">
144
+ <div class="section-title">Error:</div>
145
+ <div class="error-message">{tool.error}</div>
146
+ </div>
147
+ {/if}
148
+
149
+ {#if tool.status === "running" && !tool.output}
150
+ <div class="tool-section">
151
+ <div class="loading-indicator">
152
+ <span class="loading-dots">Processing</span>
153
+ </div>
154
+ </div>
155
+ {/if}
156
+ </div>
157
+ {/if}
158
+ </div>
159
+ {/each}
160
+ </div>
161
+
162
+ <style>
163
+ .tool-block {
164
+ margin: 0.25rem 0;
165
+ display: flex;
166
+ flex-direction: column;
167
+ gap: 0.25rem;
168
+ }
169
+
170
+ .tool-item {
171
+ border-radius: 4px;
172
+ overflow: hidden;
173
+ transition: all 0.2s ease;
174
+ border: 1px solid rgba(65, 105, 225, 0.2);
175
+ background: rgba(65, 105, 225, 0.05);
176
+ }
177
+
178
+ .tool-item.running {
179
+ background: rgba(255, 210, 30, 0.08);
180
+ border: 1px solid rgba(255, 210, 30, 0.3);
181
+ }
182
+
183
+ .tool-item.completed {
184
+ background: rgba(0, 255, 0, 0.05);
185
+ border: 1px solid rgba(0, 255, 0, 0.2);
186
+ }
187
+
188
+ .tool-item.error {
189
+ background: rgba(255, 0, 0, 0.08);
190
+ border: 1px solid rgba(255, 0, 0, 0.3);
191
+ }
192
+
193
+ .tool-item-header {
194
+ display: flex;
195
+ align-items: center;
196
+ gap: 0.5rem;
197
+ width: 100%;
198
+ padding: 0.4rem 0.6rem;
199
+ background: transparent;
200
+ border: none;
201
+ color: inherit;
202
+ font: inherit;
203
+ text-align: left;
204
+ cursor: pointer;
205
+ transition: background 0.2s ease;
206
+ }
207
+
208
+ .tool-item-header:hover {
209
+ background: rgba(255, 255, 255, 0.02);
210
+ }
211
+
212
+ .tool-status-icon {
213
+ font-size: 0.9rem;
214
+ width: 1.2rem;
215
+ text-align: center;
216
+ }
217
+
218
+ .tool-icon {
219
+ font-size: 1rem;
220
+ }
221
+
222
+ .tool-name {
223
+ flex: 1;
224
+ color: rgba(255, 255, 255, 0.9);
225
+ font-size: 0.825rem;
226
+ }
227
+
228
+ .tool-duration {
229
+ color: rgba(255, 255, 255, 0.4);
230
+ font-size: 0.75rem;
231
+ font-family: "Monaco", "Menlo", monospace;
232
+ }
233
+
234
+ .expand-icon {
235
+ font-size: 0.7rem;
236
+ color: rgba(255, 255, 255, 0.4);
237
+ transition: transform 0.2s ease;
238
+ }
239
+
240
+ .expand-icon.rotated {
241
+ transform: rotate(90deg);
242
+ }
243
+
244
+ .tool-details {
245
+ padding: 0.75rem;
246
+ background: rgba(0, 0, 0, 0.2);
247
+ border-top: 1px solid rgba(255, 255, 255, 0.05);
248
+ }
249
+
250
+ .tool-section {
251
+ margin-bottom: 0.75rem;
252
+ }
253
+
254
+ .tool-section:last-child {
255
+ margin-bottom: 0;
256
+ }
257
+
258
+ .section-title {
259
+ color: rgba(255, 255, 255, 0.5);
260
+ font-size: 0.75rem;
261
+ font-weight: 600;
262
+ margin-bottom: 0.25rem;
263
+ text-transform: uppercase;
264
+ letter-spacing: 0.5px;
265
+ }
266
+
267
+ .params {
268
+ font-family: "Monaco", "Menlo", monospace;
269
+ font-size: 0.8rem;
270
+ }
271
+
272
+ .param {
273
+ display: flex;
274
+ gap: 0.5rem;
275
+ margin: 0.25rem 0;
276
+ }
277
+
278
+ .param-key {
279
+ color: rgba(255, 255, 255, 0.5);
280
+ }
281
+
282
+ .param-value {
283
+ color: rgba(255, 210, 30, 0.8);
284
+ word-break: break-all;
285
+ }
286
+
287
+ .param-value pre {
288
+ margin: 0;
289
+ padding: 0.5rem;
290
+ background: rgba(0, 0, 0, 0.3);
291
+ border-radius: 4px;
292
+ font-size: 0.75rem;
293
+ overflow-x: auto;
294
+ max-height: 200px;
295
+ }
296
+
297
+ .tool-output, .console-output {
298
+ background: rgba(0, 0, 0, 0.3);
299
+ border-radius: 4px;
300
+ padding: 0.5rem;
301
+ font-family: "Monaco", "Menlo", monospace;
302
+ font-size: 0.75rem;
303
+ color: rgba(255, 255, 255, 0.8);
304
+ overflow-x: auto;
305
+ max-height: 300px;
306
+ overflow-y: auto;
307
+ }
308
+
309
+ .console-line {
310
+ margin: 0.1rem 0;
311
+ }
312
+
313
+ .error-message {
314
+ color: #ff6b6b;
315
+ font-family: "Monaco", "Menlo", monospace;
316
+ font-size: 0.8rem;
317
+ padding: 0.5rem;
318
+ background: rgba(255, 0, 0, 0.1);
319
+ border-radius: 4px;
320
+ }
321
+
322
+ .loading-indicator {
323
+ text-align: center;
324
+ padding: 1rem;
325
+ color: rgba(255, 255, 255, 0.5);
326
+ font-size: 0.85rem;
327
+ }
328
+
329
+ .loading-dots::after {
330
+ content: "";
331
+ animation: dots 1.5s steps(4, end) infinite;
332
+ }
333
+
334
+ @keyframes dots {
335
+ 0%, 20% { content: ""; }
336
+ 40% { content: "."; }
337
+ 60% { content: ".."; }
338
+ 80%, 100% { content: "..."; }
339
+ }
340
+
341
+ .spinner {
342
+ display: inline-block;
343
+ animation: spin 1s linear infinite;
344
+ }
345
+
346
+ .spinner-small {
347
+ display: inline-block;
348
+ animation: spin 0.8s linear infinite;
349
+ }
350
+
351
+ @keyframes spin {
352
+ from { transform: rotate(0deg); }
353
+ to { transform: rotate(360deg); }
354
+ }
355
+
356
+ ::-webkit-scrollbar {
357
+ width: 6px;
358
+ height: 6px;
359
+ }
360
+
361
+ ::-webkit-scrollbar-track {
362
+ background: transparent;
363
+ }
364
+
365
+ ::-webkit-scrollbar-thumb {
366
+ background: rgba(255, 255, 255, 0.1);
367
+ border-radius: 3px;
368
+ }
369
+
370
+ ::-webkit-scrollbar-thumb:hover {
371
+ background: rgba(255, 255, 255, 0.2);
372
+ }
373
+ </style>
src/lib/components/chat/context.md CHANGED
@@ -7,13 +7,14 @@ AI chat interface with markdown rendering and tool visualization.
7
  - `ChatPanel.svelte` - Main chat UI with message display
8
  - `ReasoningBlock.svelte` - Collapsible AI thinking viewer
9
  - `MarkdownRenderer.svelte` - Markdown parser and renderer
10
- - `ToolCallDisplay.svelte` - Visual badges for tool calls
 
11
 
12
  ## Features
13
 
14
  - Real-time message streaming
15
  - Markdown formatting for assistant messages
16
- - Tool call visualization with icons
17
  - Collapsible thinking blocks (0.1s collapse)
18
  - Authentication integration
19
  - GSAP animations
 
7
  - `ChatPanel.svelte` - Main chat UI with message display
8
  - `ReasoningBlock.svelte` - Collapsible AI thinking viewer
9
  - `MarkdownRenderer.svelte` - Markdown parser and renderer
10
+ - `ToolCallDisplay.svelte` - Inline badges for [TOOL:] patterns
11
+ - `ToolCallBlock.svelte` - Expandable tool execution tracker
12
 
13
  ## Features
14
 
15
  - Real-time message streaming
16
  - Markdown formatting for assistant messages
17
+ - Expandable tool execution tracking with status
18
  - Collapsible thinking blocks (0.1s collapse)
19
  - Authentication integration
20
  - GSAP animations
src/lib/server/api.ts CHANGED
@@ -3,6 +3,7 @@ import type { WebSocket } from "ws";
3
  import { LangGraphAgent } from "./langgraph-agent";
4
  import { HumanMessage, AIMessage, BaseMessage } from "@langchain/core/messages";
5
  import { updateEditorContent } from "./tools";
 
6
 
7
  export interface WebSocketMessage {
8
  type:
@@ -13,7 +14,8 @@ export interface WebSocketMessage {
13
  | "auth"
14
  | "editor_update"
15
  | "editor_sync"
16
- | "tool_execution";
 
17
  payload: {
18
  content?: string;
19
  role?: string;
@@ -26,6 +28,8 @@ export interface WebSocketMessage {
26
  toolName?: string;
27
  toolArgs?: Record<string, unknown>;
28
  toolResult?: string;
 
 
29
  };
30
  timestamp: number;
31
  }
@@ -101,6 +105,21 @@ class WebSocketManager {
101
  }
102
  break;
103
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  case "chat":
105
  try {
106
  if (!connectionData?.agent) {
 
3
  import { LangGraphAgent } from "./langgraph-agent";
4
  import { HumanMessage, AIMessage, BaseMessage } from "@langchain/core/messages";
5
  import { updateEditorContent } from "./tools";
6
+ import { consoleBuffer } from "./console-buffer";
7
 
8
  export interface WebSocketMessage {
9
  type:
 
14
  | "auth"
15
  | "editor_update"
16
  | "editor_sync"
17
+ | "tool_execution"
18
+ | "console_sync";
19
  payload: {
20
  content?: string;
21
  role?: string;
 
28
  toolName?: string;
29
  toolArgs?: Record<string, unknown>;
30
  toolResult?: string;
31
+ id?: string;
32
+ type?: string;
33
  };
34
  timestamp: number;
35
  }
 
105
  }
106
  break;
107
 
108
+ case "console_sync":
109
+ if (
110
+ message.payload.id &&
111
+ message.payload.type &&
112
+ message.payload.message
113
+ ) {
114
+ consoleBuffer.addMessage({
115
+ id: message.payload.id,
116
+ type: message.payload.type as "log" | "warn" | "error" | "info",
117
+ message: message.payload.message,
118
+ timestamp: message.timestamp,
119
+ });
120
+ }
121
+ break;
122
+
123
  case "chat":
124
  try {
125
  if (!connectionData?.agent) {
src/lib/server/console-buffer.ts ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export interface ConsoleBufferMessage {
2
+ id: string;
3
+ type: "log" | "warn" | "error" | "info";
4
+ message: string;
5
+ timestamp: number;
6
+ }
7
+
8
+ export class ConsoleBuffer {
9
+ private static instance: ConsoleBuffer | null = null;
10
+ private messages: ConsoleBufferMessage[] = [];
11
+ private maxMessages = 100;
12
+ private lastReadTimestamp = 0;
13
+
14
+ private constructor() {}
15
+
16
+ static getInstance(): ConsoleBuffer {
17
+ if (!ConsoleBuffer.instance) {
18
+ ConsoleBuffer.instance = new ConsoleBuffer();
19
+ }
20
+ return ConsoleBuffer.instance;
21
+ }
22
+
23
+ addMessage(message: ConsoleBufferMessage): void {
24
+ this.messages.push(message);
25
+
26
+ if (this.messages.length > this.maxMessages) {
27
+ this.messages = this.messages.slice(-this.maxMessages);
28
+ }
29
+ }
30
+
31
+ getRecentMessages(since?: number): ConsoleBufferMessage[] {
32
+ const sinceTimestamp = since || this.lastReadTimestamp;
33
+ return this.messages.filter((msg) => msg.timestamp > sinceTimestamp);
34
+ }
35
+
36
+ getAllMessages(): ConsoleBufferMessage[] {
37
+ return [...this.messages];
38
+ }
39
+
40
+ markAsRead(): void {
41
+ if (this.messages.length > 0) {
42
+ this.lastReadTimestamp =
43
+ this.messages[this.messages.length - 1].timestamp;
44
+ }
45
+ }
46
+
47
+ clear(): void {
48
+ this.messages = [];
49
+ this.lastReadTimestamp = Date.now();
50
+ }
51
+
52
+ getGameStateFromMessages(): {
53
+ isLoading: boolean;
54
+ hasError: boolean;
55
+ lastError?: string;
56
+ isReady: boolean;
57
+ } {
58
+ const recentMessages = this.getRecentMessages(Date.now() - 3000);
59
+
60
+ const hasStartMessage = recentMessages.some(
61
+ (msg) =>
62
+ msg.message.includes("🎮 Starting game") ||
63
+ msg.message.includes("Starting game"),
64
+ );
65
+
66
+ const hasSuccessMessage = recentMessages.some(
67
+ (msg) =>
68
+ msg.message.includes("✅ Game started") ||
69
+ msg.message.includes("Game started!"),
70
+ );
71
+
72
+ const errorMessages = recentMessages.filter(
73
+ (msg) => msg.type === "error" || msg.message.includes("❌ Error"),
74
+ );
75
+
76
+ const lastError =
77
+ errorMessages.length > 0
78
+ ? errorMessages[errorMessages.length - 1].message
79
+ : undefined;
80
+
81
+ return {
82
+ isLoading:
83
+ hasStartMessage && !hasSuccessMessage && errorMessages.length === 0,
84
+ hasError: errorMessages.length > 0,
85
+ lastError,
86
+ isReady: hasSuccessMessage,
87
+ };
88
+ }
89
+ }
90
+
91
+ export const consoleBuffer = ConsoleBuffer.getInstance();
src/lib/server/context.md CHANGED
@@ -4,19 +4,21 @@ WebSocket server with React Agent pattern for AI-assisted game development.
4
 
5
  ## Key Components
6
 
7
- - **api.ts** - WebSocket routing with editor sync and tool feedback
8
  - **langgraph-agent.ts** - React Agent with conditional loops for tool execution
9
- - **tools.ts** - Editor read/write with WebSocket-based state sync
 
10
  - **documentation.ts** - VibeGame documentation loader
11
  - **prompts.ts** - Documentation formatting utilities
12
 
13
  ## Architecture
14
 
15
  React Agent pattern with conditional execution flow:
 
16
  - Agent node processes messages and executes tools
17
- - Conditional edges loop back after tool execution
18
- - Tool results feed back into agent for final response
19
- - Real-time tool execution feedback via WebSocket
20
 
21
  ## Message Types
22
 
@@ -24,6 +26,10 @@ React Agent pattern with conditional execution flow:
24
  - `chat` - User messages
25
  - `editor_sync` - Sync editor content with server
26
  - `editor_update` - Server updates client editor
27
- - `tool_execution` - Tool execution notifications
 
 
 
 
28
  - `stream` - Response streaming
29
  - `status` - Connection and processing status
 
4
 
5
  ## Key Components
6
 
7
+ - **api.ts** - WebSocket routing with editor sync, console forwarding, and tool feedback
8
  - **langgraph-agent.ts** - React Agent with conditional loops for tool execution
9
+ - **tools.ts** - Editor read/write with game reload feedback, console observation
10
+ - **console-buffer.ts** - Server-side console message storage and game state tracking
11
  - **documentation.ts** - VibeGame documentation loader
12
  - **prompts.ts** - Documentation formatting utilities
13
 
14
  ## Architecture
15
 
16
  React Agent pattern with conditional execution flow:
17
+
18
  - Agent node processes messages and executes tools
19
+ - Tool results include console feedback from game reload
20
+ - Console messages forwarded from client to server buffer
21
+ - Write operations wait for game state before returning
22
 
23
  ## Message Types
24
 
 
26
  - `chat` - User messages
27
  - `editor_sync` - Sync editor content with server
28
  - `editor_update` - Server updates client editor
29
+ - `console_sync` - Forward console messages to server
30
+ - `tool_start` - Tool begins execution with status
31
+ - `tool_output` - Streaming tool output chunks
32
+ - `tool_complete` - Tool execution completed
33
+ - `tool_error` - Tool execution failed
34
  - `stream` - Response streaming
35
  - `status` - Connection and processing status
src/lib/server/langgraph-agent.ts CHANGED
@@ -9,6 +9,7 @@ import {
9
  import {
10
  readEditorTool,
11
  writeEditorTool,
 
12
  setWebSocketConnection,
13
  } from "./tools";
14
  import { documentationService } from "./documentation";
@@ -27,7 +28,7 @@ const AgentState = Annotation.Root({
27
  export class LangGraphAgent {
28
  private client: InferenceClient | null = null;
29
  private graph!: ReturnType<typeof StateGraph.prototype.compile>;
30
- private model: string = "Qwen/Qwen2.5-Coder-32B-Instruct";
31
  private documentation: string = "";
32
  private ws: WebSocket | null = null;
33
 
@@ -122,7 +123,8 @@ ${this.documentation}
122
 
123
  AVAILABLE TOOLS:
124
  - read_editor: Read the current code in the editor
125
- - write_editor: Write new code to the editor
 
126
 
127
  TOOL USAGE FORMAT:
128
  To use a tool, format your response with the tool call in this exact format:
@@ -133,6 +135,7 @@ or with parameters:
133
  Example:
134
  To read the editor: [TOOL: read_editor]
135
  To write code: [TOOL: write_editor {"content": "<world>...</world>"}]
 
136
 
137
  IMPORTANT WORKFLOW:
138
  1. When asked to modify code, FIRST use read_editor to see the current code
@@ -191,12 +194,23 @@ Be concise, accurate, and focus on practical solutions.`;
191
  response: string,
192
  ): Array<{ name: string; args: Record<string, unknown> }> {
193
  const toolCalls = [];
194
- const toolRegex = /\[TOOL:\s*(\w+)(?:\s+({[^}]+}))?\]/g;
195
  let match;
196
 
197
  while ((match = toolRegex.exec(response)) !== null) {
198
  const toolName = match[1];
199
- const params = match[2] ? JSON.parse(match[2]) : {};
 
 
 
 
 
 
 
 
 
 
 
200
 
201
  toolCalls.push({
202
  name: toolName,
@@ -213,15 +227,18 @@ Be concise, accurate, and focus on practical solutions.`;
213
  const results = [];
214
 
215
  for (const call of toolCalls) {
 
 
216
  try {
217
  if (this.ws && this.ws.readyState === this.ws.OPEN) {
218
  this.ws.send(
219
  JSON.stringify({
220
- type: "tool_execution",
221
  payload: {
 
222
  toolName: call.name,
223
  toolArgs: call.args,
224
- message: `Executing ${call.name}...`,
225
  },
226
  timestamp: Date.now(),
227
  }),
@@ -233,6 +250,26 @@ Be concise, accurate, and focus on practical solutions.`;
233
  result = await readEditorTool.func("");
234
  } else if (call.name === "write_editor") {
235
  result = await writeEditorTool.func(call.args as { content: string });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
  } else {
237
  result = `Unknown tool: ${call.name}`;
238
  }
@@ -240,11 +277,12 @@ Be concise, accurate, and focus on practical solutions.`;
240
  if (this.ws && this.ws.readyState === this.ws.OPEN) {
241
  this.ws.send(
242
  JSON.stringify({
243
- type: "tool_execution",
244
  payload: {
 
245
  toolName: call.name,
246
  toolResult: result,
247
- message: `${call.name} completed`,
248
  },
249
  timestamp: Date.now(),
250
  }),
@@ -254,15 +292,30 @@ Be concise, accurate, and focus on practical solutions.`;
254
  results.push(
255
  new ToolMessage({
256
  content: result,
257
- tool_call_id: `${call.name}_${Date.now()}`,
258
  name: call.name,
259
  }),
260
  );
261
  } catch (error) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
262
  results.push(
263
  new ToolMessage({
264
  content: `Error executing ${call.name}: ${error}`,
265
- tool_call_id: `${call.name}_${Date.now()}`,
266
  name: call.name,
267
  }),
268
  );
 
9
  import {
10
  readEditorTool,
11
  writeEditorTool,
12
+ observeConsoleTool,
13
  setWebSocketConnection,
14
  } from "./tools";
15
  import { documentationService } from "./documentation";
 
28
  export class LangGraphAgent {
29
  private client: InferenceClient | null = null;
30
  private graph!: ReturnType<typeof StateGraph.prototype.compile>;
31
+ private model: string = "Qwen/Qwen3-Next-80B-A3B-Instruct";
32
  private documentation: string = "";
33
  private ws: WebSocket | null = null;
34
 
 
123
 
124
  AVAILABLE TOOLS:
125
  - read_editor: Read the current code in the editor
126
+ - write_editor: Write new code to the editor and wait for game to reload
127
+ - observe_console: Read recent console messages from the game
128
 
129
  TOOL USAGE FORMAT:
130
  To use a tool, format your response with the tool call in this exact format:
 
135
  Example:
136
  To read the editor: [TOOL: read_editor]
137
  To write code: [TOOL: write_editor {"content": "<world>...</world>"}]
138
+ To check console: [TOOL: observe_console]
139
 
140
  IMPORTANT WORKFLOW:
141
  1. When asked to modify code, FIRST use read_editor to see the current code
 
194
  response: string,
195
  ): Array<{ name: string; args: Record<string, unknown> }> {
196
  const toolCalls = [];
197
+ const toolRegex = /\[TOOL:\s*(\w+)(?:\s+({.*?}))?\]/gs;
198
  let match;
199
 
200
  while ((match = toolRegex.exec(response)) !== null) {
201
  const toolName = match[1];
202
+ let params = {};
203
+
204
+ if (match[2]) {
205
+ try {
206
+ params = JSON.parse(match[2]);
207
+ } catch {
208
+ console.error(
209
+ `Failed to parse tool parameters for ${toolName}: ${match[2]}`,
210
+ );
211
+ params = {};
212
+ }
213
+ }
214
 
215
  toolCalls.push({
216
  name: toolName,
 
227
  const results = [];
228
 
229
  for (const call of toolCalls) {
230
+ const toolId = `${call.name}_${Date.now()}`;
231
+
232
  try {
233
  if (this.ws && this.ws.readyState === this.ws.OPEN) {
234
  this.ws.send(
235
  JSON.stringify({
236
+ type: "tool_start",
237
  payload: {
238
+ toolId,
239
  toolName: call.name,
240
  toolArgs: call.args,
241
+ toolStatus: "running",
242
  },
243
  timestamp: Date.now(),
244
  }),
 
250
  result = await readEditorTool.func("");
251
  } else if (call.name === "write_editor") {
252
  result = await writeEditorTool.func(call.args as { content: string });
253
+
254
+ const consoleMatch = result.match(/Console output:\n([\s\S]*?)$/);
255
+ if (consoleMatch && this.ws && this.ws.readyState === this.ws.OPEN) {
256
+ const consoleLines = consoleMatch[1]
257
+ .split("\n")
258
+ .filter((line) => line.trim());
259
+ this.ws.send(
260
+ JSON.stringify({
261
+ type: "tool_output",
262
+ payload: {
263
+ toolId,
264
+ toolOutput: "",
265
+ consoleOutput: consoleLines,
266
+ },
267
+ timestamp: Date.now(),
268
+ }),
269
+ );
270
+ }
271
+ } else if (call.name === "observe_console") {
272
+ result = await observeConsoleTool.func("");
273
  } else {
274
  result = `Unknown tool: ${call.name}`;
275
  }
 
277
  if (this.ws && this.ws.readyState === this.ws.OPEN) {
278
  this.ws.send(
279
  JSON.stringify({
280
+ type: "tool_complete",
281
  payload: {
282
+ toolId,
283
  toolName: call.name,
284
  toolResult: result,
285
+ toolStatus: "completed",
286
  },
287
  timestamp: Date.now(),
288
  }),
 
292
  results.push(
293
  new ToolMessage({
294
  content: result,
295
+ tool_call_id: toolId,
296
  name: call.name,
297
  }),
298
  );
299
  } catch (error) {
300
+ if (this.ws && this.ws.readyState === this.ws.OPEN) {
301
+ this.ws.send(
302
+ JSON.stringify({
303
+ type: "tool_error",
304
+ payload: {
305
+ toolId,
306
+ toolName: call.name,
307
+ error: error instanceof Error ? error.message : String(error),
308
+ toolStatus: "error",
309
+ },
310
+ timestamp: Date.now(),
311
+ }),
312
+ );
313
+ }
314
+
315
  results.push(
316
  new ToolMessage({
317
  content: `Error executing ${call.name}: ${error}`,
318
+ tool_call_id: toolId,
319
  name: call.name,
320
  }),
321
  );
src/lib/server/tools.ts CHANGED
@@ -1,5 +1,6 @@
1
  import { DynamicStructuredTool, DynamicTool } from "@langchain/core/tools";
2
  import { z } from "zod";
 
3
 
4
  let currentEditorContent = `<canvas id="game-canvas"></canvas>
5
 
@@ -41,13 +42,15 @@ export const readEditorTool = new DynamicTool({
41
 
42
  export const writeEditorTool = new DynamicStructuredTool({
43
  name: "write_editor",
44
- description: "Write new code to the editor",
45
  schema: z.object({
46
  content: z.string().describe("The code content to write to the editor"),
47
  }),
48
  func: async (input: { content: string }) => {
49
  currentEditorContent = input.content;
50
 
 
 
51
  if (wsConnection) {
52
  wsConnection.send({
53
  type: "editor_update",
@@ -55,8 +58,61 @@ export const writeEditorTool = new DynamicStructuredTool({
55
  });
56
  }
57
 
58
- return "Code updated successfully in the editor";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  },
60
  });
61
 
62
- export const tools = [readEditorTool, writeEditorTool];
 
1
  import { DynamicStructuredTool, DynamicTool } from "@langchain/core/tools";
2
  import { z } from "zod";
3
+ import { consoleBuffer } from "./console-buffer";
4
 
5
  let currentEditorContent = `<canvas id="game-canvas"></canvas>
6
 
 
42
 
43
  export const writeEditorTool = new DynamicStructuredTool({
44
  name: "write_editor",
45
+ description: "Write new code to the editor and wait for game to reload",
46
  schema: z.object({
47
  content: z.string().describe("The code content to write to the editor"),
48
  }),
49
  func: async (input: { content: string }) => {
50
  currentEditorContent = input.content;
51
 
52
+ consoleBuffer.clear();
53
+
54
  if (wsConnection) {
55
  wsConnection.send({
56
  type: "editor_update",
 
58
  });
59
  }
60
 
61
+ const startTime = Date.now();
62
+ const maxWaitTime = 3000;
63
+
64
+ await new Promise((resolve) => setTimeout(resolve, 1000));
65
+
66
+ while (Date.now() - startTime < maxWaitTime) {
67
+ const gameState = consoleBuffer.getGameStateFromMessages();
68
+
69
+ if (gameState.isReady) {
70
+ const messages = consoleBuffer.getRecentMessages();
71
+ consoleBuffer.markAsRead();
72
+ return `Code updated successfully. Game reloaded without errors.\nRecent console output:\n${messages.map((m) => `[${m.type}] ${m.message}`).join("\n")}`;
73
+ }
74
+
75
+ if (gameState.hasError) {
76
+ const messages = consoleBuffer.getRecentMessages();
77
+ consoleBuffer.markAsRead();
78
+ return `Code updated but game failed to start.\nError: ${gameState.lastError}\nFull console output:\n${messages.map((m) => `[${m.type}] ${m.message}`).join("\n")}`;
79
+ }
80
+
81
+ await new Promise((resolve) => setTimeout(resolve, 100));
82
+ }
83
+
84
+ const messages = consoleBuffer.getRecentMessages();
85
+ consoleBuffer.markAsRead();
86
+ return `Code updated. Game reload status uncertain (timeout).\nConsole output:\n${messages.map((m) => `[${m.type}] ${m.message}`).join("\n")}`;
87
+ },
88
+ });
89
+
90
+ export const observeConsoleTool = new DynamicTool({
91
+ name: "observe_console",
92
+ description: "Read recent console messages from the game",
93
+ func: async () => {
94
+ const messages = consoleBuffer.getRecentMessages();
95
+ const gameState = consoleBuffer.getGameStateFromMessages();
96
+
97
+ consoleBuffer.markAsRead();
98
+
99
+ if (messages.length === 0) {
100
+ return "No new console messages.";
101
+ }
102
+
103
+ let output = "Console Messages:\n";
104
+ output += messages.map((msg) => `[${msg.type}] ${msg.message}`).join("\n");
105
+
106
+ output += "\n\nGame State:";
107
+ output += `\n- Loading: ${gameState.isLoading}`;
108
+ output += `\n- Ready: ${gameState.isReady}`;
109
+ output += `\n- Has Error: ${gameState.hasError}`;
110
+ if (gameState.lastError) {
111
+ output += `\n- Last Error: ${gameState.lastError}`;
112
+ }
113
+
114
+ return output;
115
  },
116
  });
117
 
118
+ export const tools = [readEditorTool, writeEditorTool, observeConsoleTool];
src/lib/services/console-forward.ts ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { consoleStore, type ConsoleMessage } from "../stores/console";
2
+ import { agentStore } from "../stores/agent";
3
+
4
+ export class ConsoleForwarder {
5
+ private static instance: ConsoleForwarder | null = null;
6
+ private unsubscribe: (() => void) | null = null;
7
+ private lastForwardedId: string | null = null;
8
+
9
+ private constructor() {}
10
+
11
+ static getInstance(): ConsoleForwarder {
12
+ if (!ConsoleForwarder.instance) {
13
+ ConsoleForwarder.instance = new ConsoleForwarder();
14
+ }
15
+ return ConsoleForwarder.instance;
16
+ }
17
+
18
+ start(): void {
19
+ if (this.unsubscribe) return;
20
+
21
+ this.unsubscribe = consoleStore.subscribe((state) => {
22
+ if (state.messages.length === 0) return;
23
+
24
+ const messagesToForward = this.lastForwardedId
25
+ ? state.messages.filter(
26
+ (msg) =>
27
+ msg.timestamp >
28
+ (state.messages.find((m) => m.id === this.lastForwardedId)
29
+ ?.timestamp || 0),
30
+ )
31
+ : state.messages;
32
+
33
+ if (messagesToForward.length > 0) {
34
+ this.forwardMessages(messagesToForward);
35
+ this.lastForwardedId =
36
+ messagesToForward[messagesToForward.length - 1].id;
37
+ }
38
+ });
39
+ }
40
+
41
+ private forwardMessages(messages: ConsoleMessage[]): void {
42
+ messages.forEach((message) => {
43
+ agentStore.sendRawMessage({
44
+ type: "console_sync",
45
+ payload: {
46
+ id: message.id,
47
+ type: message.type,
48
+ message: message.message,
49
+ timestamp: message.timestamp,
50
+ },
51
+ timestamp: Date.now(),
52
+ });
53
+ });
54
+ }
55
+
56
+ stop(): void {
57
+ if (this.unsubscribe) {
58
+ this.unsubscribe();
59
+ this.unsubscribe = null;
60
+ }
61
+ this.lastForwardedId = null;
62
+ }
63
+ }
64
+
65
+ export const consoleForwarder = ConsoleForwarder.getInstance();
src/lib/services/context.md CHANGED
@@ -16,6 +16,7 @@ services/
16
  ├── auth.ts # Hugging Face OAuth authentication
17
  ├── game-engine.ts # Game lifecycle management
18
  ├── console-capture.ts # Console interception
 
19
  └── html-parser.ts # Game HTML parsing
20
  ```
21
 
@@ -33,6 +34,8 @@ services/
33
  - `gameEngine.start()` - Start game with world content
34
  - `gameEngine.stop()` - Clean up game instance
35
  - `consoleCapture.setup()` - Begin console interception
 
 
36
  - `HTMLParser.extractGameContent()` - Parse world from HTML
37
 
38
  ## Dependencies
 
16
  ├── auth.ts # Hugging Face OAuth authentication
17
  ├── game-engine.ts # Game lifecycle management
18
  ├── console-capture.ts # Console interception
19
+ ├── console-forward.ts # WebSocket console forwarding
20
  └── html-parser.ts # Game HTML parsing
21
  ```
22
 
 
34
  - `gameEngine.start()` - Start game with world content
35
  - `gameEngine.stop()` - Clean up game instance
36
  - `consoleCapture.setup()` - Begin console interception
37
+ - `consoleForwarder.start()` - Start forwarding console to WebSocket
38
+ - `consoleForwarder.stop()` - Stop console forwarding
39
  - `HTMLParser.extractGameContent()` - Parse world from HTML
40
 
41
  ## Dependencies
src/lib/stores/agent.ts CHANGED
@@ -2,14 +2,28 @@ import { writable, derived, get } from "svelte/store";
2
  import { authStore } from "../services/auth";
3
  import { editorStore } from "./editor";
4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  export interface ChatMessage {
6
  id: string;
7
- role: "user" | "assistant" | "system";
8
  content: string;
9
  timestamp: number;
10
  streaming?: boolean;
11
  reasoning?: string;
12
  showReasoning?: boolean;
 
13
  }
14
 
15
  export interface AgentState {
@@ -18,6 +32,8 @@ export interface AgentState {
18
  messages: ChatMessage[];
19
  error: string | null;
20
  streamingContent: string;
 
 
21
  }
22
 
23
  function createAgentStore() {
@@ -27,6 +43,8 @@ function createAgentStore() {
27
  messages: [],
28
  error: null,
29
  streamingContent: "",
 
 
30
  });
31
 
32
  let ws: WebSocket | null = null;
@@ -121,14 +139,21 @@ function createAgentStore() {
121
  toolArgs?: Record<string, unknown>;
122
  toolResult?: string;
123
  message?: string;
 
 
 
 
124
  };
125
  }) {
126
  switch (message.type) {
127
  case "status":
128
  if (message.payload.processing !== undefined) {
 
129
  update((state) => ({
130
  ...state,
131
- processing: message.payload.processing as boolean,
 
 
132
  }));
133
  }
134
  if (message.payload.connected !== undefined) {
@@ -158,6 +183,7 @@ function createAgentStore() {
158
  ...state,
159
  streamingContent: newContent,
160
  messages: [...state.messages, streamMessage],
 
161
  };
162
  } else {
163
  const messages = state.messages.map((msg) => {
@@ -174,6 +200,7 @@ function createAgentStore() {
174
  ...state,
175
  streamingContent: newContent,
176
  messages,
 
177
  };
178
  }
179
  });
@@ -193,6 +220,8 @@ function createAgentStore() {
193
  ...state,
194
  streamingContent: "",
195
  messages,
 
 
196
  };
197
  } else {
198
  if (message.payload.role && message.payload.content) {
@@ -217,6 +246,8 @@ function createAgentStore() {
217
  ...state,
218
  error: message.payload.error || null,
219
  processing: false,
 
 
220
  }));
221
  break;
222
 
@@ -226,23 +257,121 @@ function createAgentStore() {
226
  }
227
  break;
228
 
229
- case "tool_execution":
230
  update((state) => {
231
- const toolMessage: ChatMessage = {
232
- id: `tool_${Date.now()}`,
233
- role: "system",
234
- content: `🔧 ${message.payload.toolName}: ${message.payload.message || "Executing..."}`,
235
- timestamp: Date.now(),
 
 
236
  };
237
 
238
- if (message.payload.toolResult) {
239
- toolMessage.content = `🔧 ${message.payload.toolName} completed`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
240
  }
 
 
241
 
242
- return {
243
- ...state,
244
- messages: [...state.messages, toolMessage],
245
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
246
  });
247
  break;
248
  }
@@ -276,12 +405,20 @@ function createAgentStore() {
276
  );
277
  }
278
 
 
 
 
 
 
 
279
  function clearMessages() {
280
  update((state) => ({
281
  ...state,
282
  messages: [],
283
  streamingContent: "",
284
  error: null,
 
 
285
  }));
286
  currentStreamId = null;
287
  }
@@ -313,6 +450,7 @@ function createAgentStore() {
313
  connect,
314
  disconnect,
315
  sendMessage,
 
316
  clearMessages,
317
  reauthenticate,
318
  };
 
2
  import { authStore } from "../services/auth";
3
  import { editorStore } from "./editor";
4
 
5
+ export interface ToolExecution {
6
+ id: string;
7
+ name: string;
8
+ status: "pending" | "running" | "completed" | "error";
9
+ args?: Record<string, unknown>;
10
+ output?: string;
11
+ error?: string;
12
+ startTime: number;
13
+ endTime?: number;
14
+ consoleOutput?: string[];
15
+ expanded?: boolean;
16
+ }
17
+
18
  export interface ChatMessage {
19
  id: string;
20
+ role: "user" | "assistant" | "system" | "tool";
21
  content: string;
22
  timestamp: number;
23
  streaming?: boolean;
24
  reasoning?: string;
25
  showReasoning?: boolean;
26
+ toolExecutions?: ToolExecution[];
27
  }
28
 
29
  export interface AgentState {
 
32
  messages: ChatMessage[];
33
  error: string | null;
34
  streamingContent: string;
35
+ streamingStatus: "idle" | "thinking" | "streaming" | "completing";
36
+ thinkingStartTime: number | null;
37
  }
38
 
39
  function createAgentStore() {
 
43
  messages: [],
44
  error: null,
45
  streamingContent: "",
46
+ streamingStatus: "idle",
47
+ thinkingStartTime: null,
48
  });
49
 
50
  let ws: WebSocket | null = null;
 
139
  toolArgs?: Record<string, unknown>;
140
  toolResult?: string;
141
  message?: string;
142
+ toolId?: string;
143
+ toolStatus?: "pending" | "running" | "completed" | "error";
144
+ toolOutput?: string;
145
+ consoleOutput?: string[];
146
  };
147
  }) {
148
  switch (message.type) {
149
  case "status":
150
  if (message.payload.processing !== undefined) {
151
+ const isProcessing = message.payload.processing as boolean;
152
  update((state) => ({
153
  ...state,
154
+ processing: isProcessing,
155
+ streamingStatus: isProcessing ? "thinking" : "idle",
156
+ thinkingStartTime: isProcessing ? Date.now() : null,
157
  }));
158
  }
159
  if (message.payload.connected !== undefined) {
 
183
  ...state,
184
  streamingContent: newContent,
185
  messages: [...state.messages, streamMessage],
186
+ streamingStatus: "streaming",
187
  };
188
  } else {
189
  const messages = state.messages.map((msg) => {
 
200
  ...state,
201
  streamingContent: newContent,
202
  messages,
203
+ streamingStatus: "streaming",
204
  };
205
  }
206
  });
 
220
  ...state,
221
  streamingContent: "",
222
  messages,
223
+ streamingStatus: "idle",
224
+ thinkingStartTime: null,
225
  };
226
  } else {
227
  if (message.payload.role && message.payload.content) {
 
246
  ...state,
247
  error: message.payload.error || null,
248
  processing: false,
249
+ streamingStatus: "idle",
250
+ thinkingStartTime: null,
251
  }));
252
  break;
253
 
 
257
  }
258
  break;
259
 
260
+ case "tool_start":
261
  update((state) => {
262
+ const toolExecution: ToolExecution = {
263
+ id: message.payload.toolId || `tool_${Date.now()}`,
264
+ name: message.payload.toolName || "unknown",
265
+ status: "running",
266
+ args: message.payload.toolArgs,
267
+ startTime: Date.now(),
268
+ expanded: false,
269
  };
270
 
271
+ let toolMessage = state.messages.find(
272
+ (msg) =>
273
+ msg.role === "tool" && msg.id === `tools_${currentStreamId}`,
274
+ );
275
+
276
+ if (!toolMessage) {
277
+ toolMessage = {
278
+ id: `tools_${currentStreamId || Date.now()}`,
279
+ role: "tool",
280
+ content: "",
281
+ timestamp: Date.now(),
282
+ toolExecutions: [toolExecution],
283
+ };
284
+ return {
285
+ ...state,
286
+ messages: [...state.messages, toolMessage],
287
+ };
288
+ } else {
289
+ const messages = state.messages.map((msg) => {
290
+ if (msg.id === toolMessage!.id) {
291
+ return {
292
+ ...msg,
293
+ toolExecutions: [
294
+ ...(msg.toolExecutions || []),
295
+ toolExecution,
296
+ ],
297
+ };
298
+ }
299
+ return msg;
300
+ });
301
+ return { ...state, messages };
302
  }
303
+ });
304
+ break;
305
 
306
+ case "tool_output":
307
+ update((state) => {
308
+ const messages = state.messages.map((msg) => {
309
+ if (msg.role === "tool" && msg.toolExecutions) {
310
+ const toolExecutions = msg.toolExecutions.map((exec) => {
311
+ if (exec.id === message.payload.toolId) {
312
+ return {
313
+ ...exec,
314
+ output:
315
+ (exec.output || "") + (message.payload.toolOutput || ""),
316
+ consoleOutput:
317
+ message.payload.consoleOutput || exec.consoleOutput,
318
+ };
319
+ }
320
+ return exec;
321
+ });
322
+ return { ...msg, toolExecutions };
323
+ }
324
+ return msg;
325
+ });
326
+ return { ...state, messages };
327
+ });
328
+ break;
329
+
330
+ case "tool_complete":
331
+ update((state) => {
332
+ const messages = state.messages.map((msg) => {
333
+ if (msg.role === "tool" && msg.toolExecutions) {
334
+ const toolExecutions = msg.toolExecutions.map((exec) => {
335
+ if (exec.id === message.payload.toolId) {
336
+ return {
337
+ ...exec,
338
+ status: "completed" as const,
339
+ output: message.payload.toolResult || exec.output,
340
+ endTime: Date.now(),
341
+ consoleOutput:
342
+ message.payload.consoleOutput || exec.consoleOutput,
343
+ };
344
+ }
345
+ return exec;
346
+ });
347
+ return { ...msg, toolExecutions };
348
+ }
349
+ return msg;
350
+ });
351
+ return { ...state, messages };
352
+ });
353
+ break;
354
+
355
+ case "tool_error":
356
+ update((state) => {
357
+ const messages = state.messages.map((msg) => {
358
+ if (msg.role === "tool" && msg.toolExecutions) {
359
+ const toolExecutions = msg.toolExecutions.map((exec) => {
360
+ if (exec.id === message.payload.toolId) {
361
+ return {
362
+ ...exec,
363
+ status: "error" as const,
364
+ error: message.payload.error || "Unknown error",
365
+ endTime: Date.now(),
366
+ };
367
+ }
368
+ return exec;
369
+ });
370
+ return { ...msg, toolExecutions };
371
+ }
372
+ return msg;
373
+ });
374
+ return { ...state, messages };
375
  });
376
  break;
377
  }
 
405
  );
406
  }
407
 
408
+ function sendRawMessage(message: unknown) {
409
+ if (ws && ws.readyState === WebSocket.OPEN) {
410
+ ws.send(JSON.stringify(message));
411
+ }
412
+ }
413
+
414
  function clearMessages() {
415
  update((state) => ({
416
  ...state,
417
  messages: [],
418
  streamingContent: "",
419
  error: null,
420
+ streamingStatus: "idle",
421
+ thinkingStartTime: null,
422
  }));
423
  currentStreamId = null;
424
  }
 
450
  connect,
451
  disconnect,
452
  sendMessage,
453
+ sendRawMessage,
454
  clearMessages,
455
  reauthenticate,
456
  };