dylanebert commited on
Commit
7ad6991
·
1 Parent(s): 66ed511

examples prompts, stop button

Browse files
eslint.config.js CHANGED
@@ -35,6 +35,8 @@ export default [
35
  clearInterval: "readonly",
36
  sessionStorage: "readonly",
37
  localStorage: "readonly",
 
 
38
  },
39
  },
40
  plugins: {
 
35
  clearInterval: "readonly",
36
  sessionStorage: "readonly",
37
  localStorage: "readonly",
38
+ AbortController: "readonly",
39
+ AbortSignal: "readonly",
40
  },
41
  },
42
  plugins: {
src/lib/components/chat/ChatPanel.svelte CHANGED
@@ -9,12 +9,15 @@
9
  import MarkdownRenderer from "./MarkdownRenderer.svelte";
10
  import InProgressBlock from "./InProgressBlock.svelte";
11
  import MessageSegment from "./MessageSegment.svelte";
 
12
 
13
  let inputValue = "";
14
  let messagesContainer: HTMLDivElement;
15
  let sendButton: HTMLButtonElement;
 
16
  let inputTextarea: HTMLTextAreaElement;
17
  let authPromptBtn: HTMLButtonElement;
 
18
 
19
  let hasConnected = false;
20
 
@@ -134,6 +137,54 @@
134
  }
135
  }
136
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
  function handleKeydown(event: KeyboardEvent) {
138
  if (event.key === "Enter" && !event.shiftKey) {
139
  event.preventDefault();
@@ -182,8 +233,8 @@
182
  }
183
 
184
  $: {
185
- if (sendButton) {
186
- if (!$authStore.isAuthenticated || !$isConnected || $isProcessing || !inputValue.trim()) {
187
  gsap.to(sendButton, {
188
  opacity: 0.3,
189
  duration: 0.2,
@@ -201,6 +252,24 @@
201
  </script>
202
 
203
  <div class="chat-panel">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
  {#if isUserScrolling}
205
  <button
206
  class="scroll-to-bottom"
@@ -224,7 +293,7 @@
224
  </div>
225
  {:else}
226
  {#if $agentStore.messages.length === 0 && $isConnected}
227
- <div class="ready-message">Ready to Chat!</div>
228
  {/if}
229
 
230
  {#each $agentStore.messages as message (message.id)}
@@ -282,19 +351,30 @@
282
  disabled={!$authStore.isAuthenticated || !$isConnected || $isProcessing}
283
  rows="1"
284
  />
285
- <button
286
- bind:this={sendButton}
287
- on:click={handleSubmit}
288
- on:mouseenter={handleButtonMouseEnter}
289
- on:mouseleave={handleButtonMouseLeave}
290
- on:mousedown={handleButtonMouseDown}
291
- on:mouseup={handleButtonMouseUp}
292
- disabled={!$authStore.isAuthenticated || !$isConnected || $isProcessing || !inputValue.trim()}
293
- class="send-btn"
294
- title="Send message"
295
- >
296
-
297
- </button>
 
 
 
 
 
 
 
 
 
 
 
298
  </div>
299
  </div>
300
 
@@ -309,6 +389,51 @@
309
  position: relative;
310
  }
311
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
312
  .scroll-to-bottom {
313
  position: absolute;
314
  bottom: 4.5rem;
@@ -459,6 +584,34 @@
459
  cursor: not-allowed;
460
  }
461
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
462
  ::-webkit-scrollbar {
463
  width: 6px;
464
  }
@@ -518,25 +671,4 @@
518
  transform: translateY(0);
519
  }
520
 
521
- .ready-message {
522
- color: rgba(255, 255, 255, 0.2);
523
- font-size: 0.875rem;
524
- text-align: center;
525
- font-style: italic;
526
- display: flex;
527
- align-items: center;
528
- justify-content: center;
529
- height: 100%;
530
- min-height: 200px;
531
- animation: gentle-fade 3s ease-in-out infinite;
532
- }
533
-
534
- @keyframes gentle-fade {
535
- 0%, 100% {
536
- opacity: 0.7;
537
- }
538
- 50% {
539
- opacity: 1;
540
- }
541
- }
542
  </style>
 
9
  import MarkdownRenderer from "./MarkdownRenderer.svelte";
10
  import InProgressBlock from "./InProgressBlock.svelte";
11
  import MessageSegment from "./MessageSegment.svelte";
12
+ import ExampleMessages from "./ExampleMessages.svelte";
13
 
14
  let inputValue = "";
15
  let messagesContainer: HTMLDivElement;
16
  let sendButton: HTMLButtonElement;
17
+ let stopButton: HTMLButtonElement;
18
  let inputTextarea: HTMLTextAreaElement;
19
  let authPromptBtn: HTMLButtonElement;
20
+ let clearButton: HTMLButtonElement;
21
 
22
  let hasConnected = false;
23
 
 
137
  }
138
  }
139
 
140
+ function handleStop() {
141
+ if ($isProcessing) {
142
+ if (stopButton) {
143
+ gsap.to(stopButton, {
144
+ scale: 0.9,
145
+ duration: 0.1,
146
+ ease: "power2.in",
147
+ onComplete: () => {
148
+ if (stopButton) {
149
+ gsap.to(stopButton, {
150
+ scale: 1,
151
+ duration: 0.2,
152
+ ease: "elastic.out(1, 0.5)",
153
+ });
154
+ }
155
+ },
156
+ });
157
+ }
158
+
159
+ agentService.stopConversation();
160
+ }
161
+ }
162
+
163
+ function handleExampleMessage(message: string) {
164
+ if ($authStore.isAuthenticated && $isConnected && !$isProcessing) {
165
+ agentService.sendMessage(message);
166
+ }
167
+ }
168
+
169
+ function handleClearConversation() {
170
+ if (clearButton) {
171
+ gsap.to(clearButton, {
172
+ scale: 0.9,
173
+ duration: 0.1,
174
+ ease: "power2.in",
175
+ onComplete: () => {
176
+ gsap.to(clearButton, {
177
+ scale: 1,
178
+ duration: 0.2,
179
+ ease: "elastic.out(1, 0.5)",
180
+ });
181
+ },
182
+ });
183
+ }
184
+
185
+ agentStore.clearMessages();
186
+ }
187
+
188
  function handleKeydown(event: KeyboardEvent) {
189
  if (event.key === "Enter" && !event.shiftKey) {
190
  event.preventDefault();
 
233
  }
234
 
235
  $: {
236
+ if (sendButton && !$isProcessing) {
237
+ if (!$authStore.isAuthenticated || !$isConnected || !inputValue.trim()) {
238
  gsap.to(sendButton, {
239
  opacity: 0.3,
240
  duration: 0.2,
 
252
  </script>
253
 
254
  <div class="chat-panel">
255
+ <div class="chat-header">
256
+ <span class="chat-title">Chat</span>
257
+ {#if $agentStore.messages.length > 0 && $authStore.isAuthenticated && !$isProcessing}
258
+ <button
259
+ bind:this={clearButton}
260
+ class="clear-button"
261
+ on:click={handleClearConversation}
262
+ title="Clear conversation"
263
+ transition:fade={{ duration: 200 }}
264
+ >
265
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
266
+ <path d="M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2m3 0v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6h14zM10 11v6M14 11v6" />
267
+ </svg>
268
+ <span>Clear</span>
269
+ </button>
270
+ {/if}
271
+ </div>
272
+
273
  {#if isUserScrolling}
274
  <button
275
  class="scroll-to-bottom"
 
293
  </div>
294
  {:else}
295
  {#if $agentStore.messages.length === 0 && $isConnected}
296
+ <ExampleMessages onSendMessage={handleExampleMessage} />
297
  {/if}
298
 
299
  {#each $agentStore.messages as message (message.id)}
 
351
  disabled={!$authStore.isAuthenticated || !$isConnected || $isProcessing}
352
  rows="1"
353
  />
354
+ {#if $isProcessing}
355
+ <button
356
+ bind:this={stopButton}
357
+ on:click={handleStop}
358
+ class="stop-btn"
359
+ title="Stop conversation"
360
+ >
361
+
362
+ </button>
363
+ {:else}
364
+ <button
365
+ bind:this={sendButton}
366
+ on:click={handleSubmit}
367
+ on:mouseenter={handleButtonMouseEnter}
368
+ on:mouseleave={handleButtonMouseLeave}
369
+ on:mousedown={handleButtonMouseDown}
370
+ on:mouseup={handleButtonMouseUp}
371
+ disabled={!$authStore.isAuthenticated || !$isConnected || !inputValue.trim()}
372
+ class="send-btn"
373
+ title="Send message"
374
+ >
375
+
376
+ </button>
377
+ {/if}
378
  </div>
379
  </div>
380
 
 
389
  position: relative;
390
  }
391
 
392
+ .chat-header {
393
+ display: flex;
394
+ align-items: center;
395
+ justify-content: space-between;
396
+ padding: 0.5rem 0.75rem;
397
+ background: rgba(0, 0, 0, 0.3);
398
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
399
+ min-height: 2.5rem;
400
+ }
401
+
402
+ .chat-title {
403
+ font-size: 0.875rem;
404
+ font-weight: 500;
405
+ color: rgba(255, 255, 255, 0.7);
406
+ }
407
+
408
+ .clear-button {
409
+ display: flex;
410
+ align-items: center;
411
+ gap: 0.25rem;
412
+ padding: 0.25rem 0.5rem;
413
+ background: transparent;
414
+ border: 1px solid rgba(255, 255, 255, 0.2);
415
+ border-radius: 4px;
416
+ color: rgba(255, 255, 255, 0.6);
417
+ font-size: 0.75rem;
418
+ cursor: pointer;
419
+ transition: all 0.2s ease;
420
+ }
421
+
422
+ .clear-button:hover {
423
+ background: rgba(244, 67, 54, 0.1);
424
+ border-color: rgba(244, 67, 54, 0.3);
425
+ color: rgba(244, 67, 54, 0.9);
426
+ }
427
+
428
+ .clear-button:active {
429
+ transform: scale(0.95);
430
+ }
431
+
432
+ .clear-button svg {
433
+ width: 14px;
434
+ height: 14px;
435
+ }
436
+
437
  .scroll-to-bottom {
438
  position: absolute;
439
  bottom: 4.5rem;
 
584
  cursor: not-allowed;
585
  }
586
 
587
+ .stop-btn {
588
+ padding: 0.4rem 0.8rem;
589
+ background: rgba(244, 67, 54, 0.2);
590
+ color: #f44336;
591
+ border: 1px solid rgba(244, 67, 54, 0.3);
592
+ border-radius: 4px;
593
+ cursor: pointer;
594
+ font-size: 1rem;
595
+ transform-origin: center;
596
+ will-change: transform;
597
+ animation: pulse 1.5s ease-in-out infinite;
598
+ }
599
+
600
+ @keyframes pulse {
601
+ 0%, 100% {
602
+ opacity: 1;
603
+ }
604
+ 50% {
605
+ opacity: 0.6;
606
+ }
607
+ }
608
+
609
+ .stop-btn:hover {
610
+ background: rgba(244, 67, 54, 0.3);
611
+ border-color: rgba(244, 67, 54, 0.5);
612
+ animation: none;
613
+ }
614
+
615
  ::-webkit-scrollbar {
616
  width: 6px;
617
  }
 
671
  transform: translateY(0);
672
  }
673
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
674
  </style>
src/lib/components/chat/ExampleMessages.svelte ADDED
@@ -0,0 +1,179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { onMount } from "svelte";
3
+ import { fade } from "svelte/transition";
4
+ import gsap from "gsap";
5
+
6
+ export let onSendMessage: (message: string) => void;
7
+
8
+ const examples = [
9
+ { icon: "🏀", text: "add another ball" },
10
+ { icon: "📝", text: "explain what the code does" },
11
+ { icon: "🎮", text: "make an obby" },
12
+ { icon: "⬆️", text: "increase the player jump height" }
13
+ ];
14
+
15
+ let exampleCards: HTMLButtonElement[] = [];
16
+
17
+ onMount(() => {
18
+ exampleCards.forEach((card, index) => {
19
+ if (!card) return;
20
+
21
+ gsap.fromTo(card, {
22
+ opacity: 0,
23
+ y: 15,
24
+ scale: 0.97
25
+ }, {
26
+ opacity: 1,
27
+ y: 0,
28
+ scale: 1,
29
+ duration: 0.2,
30
+ delay: index * 0.02,
31
+ ease: "power3.out"
32
+ });
33
+ });
34
+ });
35
+
36
+ function handleClick(text: string) {
37
+ onSendMessage(text);
38
+ }
39
+
40
+ function handleMouseEnter(event: MouseEvent) {
41
+ const card = event.currentTarget as HTMLButtonElement;
42
+ gsap.to(card, {
43
+ scale: 1.03,
44
+ boxShadow: "0 6px 24px rgba(65, 105, 225, 0.25)",
45
+ borderColor: "rgba(65, 105, 225, 0.5)",
46
+ duration: 0.1,
47
+ ease: "power3.out"
48
+ });
49
+ }
50
+
51
+ function handleMouseLeave(event: MouseEvent) {
52
+ const card = event.currentTarget as HTMLButtonElement;
53
+ gsap.to(card, {
54
+ scale: 1,
55
+ boxShadow: "0 2px 10px rgba(0, 0, 0, 0.2)",
56
+ borderColor: "rgba(255, 255, 255, 0.05)",
57
+ duration: 0.12,
58
+ ease: "power2.inOut"
59
+ });
60
+ }
61
+
62
+ function handleMouseDown(event: MouseEvent) {
63
+ const card = event.currentTarget as HTMLButtonElement;
64
+ gsap.to(card, {
65
+ scale: 0.97,
66
+ duration: 0.05,
67
+ ease: "power1.in"
68
+ });
69
+ }
70
+
71
+ function handleMouseUp(event: MouseEvent) {
72
+ const card = event.currentTarget as HTMLButtonElement;
73
+ gsap.to(card, {
74
+ scale: 1.03,
75
+ duration: 0.08,
76
+ ease: "back.out(1.7)"
77
+ });
78
+ }
79
+ </script>
80
+
81
+ <div class="examples-container" transition:fade={{ duration: 200 }}>
82
+ <div class="examples-header">
83
+ <span class="header-text">Try an example</span>
84
+ </div>
85
+ <div class="examples-grid">
86
+ {#each examples as example, i}
87
+ <button
88
+ bind:this={exampleCards[i]}
89
+ class="example-card"
90
+ on:click={() => handleClick(example.text)}
91
+ on:mouseenter={handleMouseEnter}
92
+ on:mouseleave={handleMouseLeave}
93
+ on:mousedown={handleMouseDown}
94
+ on:mouseup={handleMouseUp}
95
+ >
96
+ <span class="example-icon">{example.icon}</span>
97
+ <span class="example-text">{example.text}</span>
98
+ </button>
99
+ {/each}
100
+ </div>
101
+ </div>
102
+
103
+ <style>
104
+ .examples-container {
105
+ display: flex;
106
+ flex-direction: column;
107
+ align-items: center;
108
+ justify-content: center;
109
+ padding: 2rem 1rem;
110
+ height: 100%;
111
+ min-height: 300px;
112
+ gap: 1.5rem;
113
+ }
114
+
115
+ .examples-header {
116
+ color: rgba(255, 255, 255, 0.3);
117
+ font-size: 0.75rem;
118
+ text-transform: uppercase;
119
+ letter-spacing: 0.1em;
120
+ font-weight: 500;
121
+ }
122
+
123
+ .header-text {
124
+ animation: gentle-pulse 3s ease-in-out infinite;
125
+ }
126
+
127
+ @keyframes gentle-pulse {
128
+ 0%, 100% {
129
+ opacity: 0.5;
130
+ }
131
+ 50% {
132
+ opacity: 0.8;
133
+ }
134
+ }
135
+
136
+ .examples-grid {
137
+ display: grid;
138
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
139
+ gap: 0.75rem;
140
+ width: 100%;
141
+ max-width: 600px;
142
+ }
143
+
144
+ .example-card {
145
+ display: flex;
146
+ align-items: center;
147
+ gap: 0.5rem;
148
+ padding: 0.5rem 1rem;
149
+ background: rgba(255, 255, 255, 0.02);
150
+ border: 1px solid rgba(255, 255, 255, 0.05);
151
+ border-radius: 6px;
152
+ cursor: pointer;
153
+ transition: all 0.2s ease;
154
+ font-family: "Monaco", "Menlo", monospace;
155
+ font-size: 0.875rem;
156
+ color: rgba(255, 255, 255, 0.85);
157
+ will-change: transform, box-shadow;
158
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
159
+ }
160
+
161
+ .example-card:hover {
162
+ background: rgba(65, 105, 225, 0.05);
163
+ }
164
+
165
+ .example-icon {
166
+ font-size: 1rem;
167
+ opacity: 0.8;
168
+ }
169
+
170
+ .example-text {
171
+ flex: 1;
172
+ }
173
+
174
+ @media (max-width: 600px) {
175
+ .examples-grid {
176
+ grid-template-columns: 1fr;
177
+ }
178
+ }
179
+ </style>
src/lib/components/chat/context.md CHANGED
@@ -1,10 +1,11 @@
1
  # Chat Context
2
 
3
- AI chat interface with real-time streaming.
4
 
5
  ## Components
6
 
7
- - `ChatPanel.svelte` - Main chat UI with smooth scroll
 
8
  - `MessageSegment.svelte` - Renders text, tool invocations, and results
9
  - `StreamingText.svelte` - Optimized character streaming with state persistence
10
  - `ToolInvocation.svelte` - Tool execution and results display
@@ -19,3 +20,4 @@ AI chat interface with real-time streaming.
19
  - Batch character processing (3 chars/cycle) at 120 chars/sec
20
  - Tool invocations displayed through dedicated segments
21
  - Clean separation between text and tool content
 
 
1
  # Chat Context
2
 
3
+ AI chat interface with real-time streaming and conversation control.
4
 
5
  ## Components
6
 
7
+ - `ChatPanel.svelte` - Main chat UI with header bar, clear button, smooth scroll, stop button when processing
8
+ - `ExampleMessages.svelte` - Clickable prompt suggestions when chat is empty
9
  - `MessageSegment.svelte` - Renders text, tool invocations, and results
10
  - `StreamingText.svelte` - Optimized character streaming with state persistence
11
  - `ToolInvocation.svelte` - Tool execution and results display
 
20
  - Batch character processing (3 chars/cycle) at 120 chars/sec
21
  - Tool invocations displayed through dedicated segments
22
  - Clean separation between text and tool content
23
+ - Abortable conversations via stop button during processing
src/lib/components/layout/AppHeader.svelte CHANGED
@@ -66,6 +66,15 @@
66
  <span class="app-icon">🥕</span>
67
  <span class="app-name">VibeGame</span>
68
  </div>
 
 
 
 
 
 
 
 
 
69
  </div>
70
 
71
  <div class="header-center">
@@ -137,20 +146,21 @@
137
 
138
  .header-left {
139
  justify-content: flex-start;
 
140
  }
141
-
142
  .app-title {
143
  display: flex;
144
  align-items: center;
145
  gap: 0.5rem;
146
  user-select: none;
147
  }
148
-
149
  .app-icon {
150
  font-size: 1.25rem;
151
  line-height: 1;
152
  }
153
-
154
  .app-name {
155
  font-size: 0.875rem;
156
  font-weight: 600;
@@ -158,6 +168,44 @@
158
  letter-spacing: 0.025em;
159
  }
160
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
  .header-center {
162
  justify-content: center;
163
  }
 
66
  <span class="app-icon">🥕</span>
67
  <span class="app-name">VibeGame</span>
68
  </div>
69
+ <a href="https://github.com/dylanebert/VibeGame"
70
+ target="_blank"
71
+ rel="noopener noreferrer"
72
+ class="github-link"
73
+ aria-label="View on GitHub">
74
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
75
+ <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>
76
+ </svg>
77
+ </a>
78
  </div>
79
 
80
  <div class="header-center">
 
146
 
147
  .header-left {
148
  justify-content: flex-start;
149
+ gap: 0.75rem;
150
  }
151
+
152
  .app-title {
153
  display: flex;
154
  align-items: center;
155
  gap: 0.5rem;
156
  user-select: none;
157
  }
158
+
159
  .app-icon {
160
  font-size: 1.25rem;
161
  line-height: 1;
162
  }
163
+
164
  .app-name {
165
  font-size: 0.875rem;
166
  font-weight: 600;
 
168
  letter-spacing: 0.025em;
169
  }
170
 
171
+ .github-link {
172
+ display: flex;
173
+ align-items: center;
174
+ padding: 0.25rem;
175
+ border-radius: 0.25rem;
176
+ color: rgba(251, 248, 244, 0.5);
177
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
178
+ position: relative;
179
+ }
180
+
181
+ .github-link::before {
182
+ content: '';
183
+ position: absolute;
184
+ inset: 0;
185
+ border-radius: 0.25rem;
186
+ background: rgba(139, 115, 85, 0.08);
187
+ opacity: 0;
188
+ transition: opacity 0.2s ease-out;
189
+ }
190
+
191
+ .github-link:hover {
192
+ color: rgba(251, 248, 244, 0.9);
193
+ transform: scale(1.05);
194
+ }
195
+
196
+ .github-link:hover::before {
197
+ opacity: 1;
198
+ }
199
+
200
+ .github-link:active {
201
+ transform: scale(0.98);
202
+ }
203
+
204
+ .github-link svg {
205
+ width: 16px;
206
+ height: 16px;
207
+ }
208
+
209
  .header-center {
210
  justify-content: center;
211
  }
src/lib/server/api.ts CHANGED
@@ -18,7 +18,8 @@ export interface WebSocketMessage {
18
  | "editor_update"
19
  | "editor_sync"
20
  | "tool_execution"
21
- | "console_sync";
 
22
  payload: {
23
  content?: string;
24
  role?: string;
@@ -41,7 +42,12 @@ export interface WebSocketMessage {
41
  class WebSocketManager {
42
  private connections: Map<
43
  WebSocket,
44
- { token?: string; agent?: LangGraphAgent; messages?: BaseMessage[] }
 
 
 
 
 
45
  > = new Map();
46
 
47
  handleConnection(ws: WebSocket, _request: IncomingMessage) {
@@ -124,6 +130,18 @@ class WebSocketManager {
124
  }
125
  break;
126
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  case "chat":
128
  try {
129
  if (!connectionData?.agent) {
@@ -137,6 +155,8 @@ class WebSocketManager {
137
  throw new Error("No message content provided");
138
  }
139
 
 
 
140
  this.sendMessage(ws, {
141
  type: "status",
142
  payload: { processing: true },
@@ -170,6 +190,7 @@ class WebSocketManager {
170
  });
171
  },
172
  messageId,
 
173
  );
174
 
175
  connectionData.messages.push(new AIMessage(response));
@@ -190,10 +211,26 @@ class WebSocketManager {
190
  });
191
  } catch (error) {
192
  console.error("Error processing chat message:", error);
193
- this.sendError(
194
- ws,
195
- error instanceof Error ? error.message : "Unknown error",
196
- );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
  }
198
  break;
199
 
 
18
  | "editor_update"
19
  | "editor_sync"
20
  | "tool_execution"
21
+ | "console_sync"
22
+ | "abort";
23
  payload: {
24
  content?: string;
25
  role?: string;
 
42
  class WebSocketManager {
43
  private connections: Map<
44
  WebSocket,
45
+ {
46
+ token?: string;
47
+ agent?: LangGraphAgent;
48
+ messages?: BaseMessage[];
49
+ abortController?: AbortController;
50
+ }
51
  > = new Map();
52
 
53
  handleConnection(ws: WebSocket, _request: IncomingMessage) {
 
130
  }
131
  break;
132
 
133
+ case "abort":
134
+ if (connectionData?.abortController) {
135
+ connectionData.abortController.abort();
136
+
137
+ this.sendMessage(ws, {
138
+ type: "status",
139
+ payload: { processing: false, message: "Conversation stopped" },
140
+ timestamp: Date.now(),
141
+ });
142
+ }
143
+ break;
144
+
145
  case "chat":
146
  try {
147
  if (!connectionData?.agent) {
 
155
  throw new Error("No message content provided");
156
  }
157
 
158
+ connectionData.abortController = new AbortController();
159
+
160
  this.sendMessage(ws, {
161
  type: "status",
162
  payload: { processing: true },
 
190
  });
191
  },
192
  messageId,
193
+ connectionData.abortController.signal,
194
  );
195
 
196
  connectionData.messages.push(new AIMessage(response));
 
211
  });
212
  } catch (error) {
213
  console.error("Error processing chat message:", error);
214
+
215
+ if (error instanceof Error && error.name === "AbortError") {
216
+ this.sendMessage(ws, {
217
+ type: "stream_end",
218
+ payload: {
219
+ messageId: `msg_${Date.now()}`,
220
+ content: "",
221
+ },
222
+ timestamp: Date.now(),
223
+ });
224
+ } else {
225
+ this.sendError(
226
+ ws,
227
+ error instanceof Error ? error.message : "Unknown error",
228
+ );
229
+ }
230
+ } finally {
231
+ if (connectionData) {
232
+ connectionData.abortController = undefined;
233
+ }
234
  }
235
  break;
236
 
src/lib/server/context.md CHANGED
@@ -4,8 +4,8 @@ WebSocket server with LangGraph agent for AI-assisted game development.
4
 
5
  ## Key Components
6
 
7
- - **api.ts** - WebSocket message routing
8
- - **langgraph-agent.ts** - LangGraph agent with buffered streaming
9
  - **tools.ts** - Editor manipulation: full read/write, line-based reading, text/regex search, search-replace editing
10
  - **console-buffer.ts** - Console message storage
11
  - **documentation.ts** - VibeGame documentation loader
@@ -17,11 +17,13 @@ LangGraph state machine with real-time streaming:
17
  - Buffers and filters tool patterns from text segments
18
  - Tool invocations handled separately from text content
19
  - Explicit message IDs required for all segment operations
 
20
 
21
  ## Message Protocol
22
 
23
  - `auth` - HF token authentication
24
  - `chat` - User messages
 
25
  - `stream_start/token/end` - Legacy streaming
26
  - `segment_start/token/end` - Segment streaming
27
  - `editor_sync` - Sync editor content
 
4
 
5
  ## Key Components
6
 
7
+ - **api.ts** - WebSocket message routing with abort handling
8
+ - **langgraph-agent.ts** - LangGraph agent with buffered streaming and abort signals
9
  - **tools.ts** - Editor manipulation: full read/write, line-based reading, text/regex search, search-replace editing
10
  - **console-buffer.ts** - Console message storage
11
  - **documentation.ts** - VibeGame documentation loader
 
17
  - Buffers and filters tool patterns from text segments
18
  - Tool invocations handled separately from text content
19
  - Explicit message IDs required for all segment operations
20
+ - AbortController for canceling running conversations
21
 
22
  ## Message Protocol
23
 
24
  - `auth` - HF token authentication
25
  - `chat` - User messages
26
+ - `abort` - Stop running conversation
27
  - `stream_start/token/end` - Legacy streaming
28
  - `segment_start/token/end` - Segment streaming
29
  - `editor_sync` - Sync editor content
src/lib/server/langgraph-agent.ts CHANGED
@@ -72,10 +72,16 @@ export class LangGraphAgent {
72
  let currentSegmentContent = "";
73
  let buffer = "";
74
  const messageId = config?.metadata?.messageId;
 
 
 
75
 
76
  const toolRegex = /TOOL:\s*(\w+)\s+ARGS:\s*({[^}]*})/g;
77
 
78
- for await (const token of this.streamModelResponse(messages)) {
 
 
 
79
  fullResponse += token;
80
  config?.writer?.({ type: "token", content: token });
81
  buffer += token;
@@ -309,7 +315,10 @@ CRITICAL INSTRUCTIONS:
309
  VIBEGAME CONTEXT:
310
  ${this.documentation}
311
 
312
- The game auto-reloads on every change. The GAME import is automatically provided by the framework.
 
 
 
313
 
314
  AVAILABLE TOOLS:
315
  1. search_editor - Find text/patterns in code
@@ -340,6 +349,7 @@ IMPORTANT: You are an executor. Take action immediately using tools, don't expla
340
 
341
  private async *streamModelResponse(
342
  messages: Array<{ role: string; content: string }>,
 
343
  ): AsyncGenerator<string, string, unknown> {
344
  if (!this.client) {
345
  throw new Error("Agent not initialized");
@@ -355,6 +365,10 @@ IMPORTANT: You are an executor. Take action immediately using tools, don't expla
355
  });
356
 
357
  for await (const chunk of stream) {
 
 
 
 
358
  const token = chunk.choices[0]?.delta?.content || "";
359
  if (token) {
360
  fullContent += token;
@@ -622,6 +636,7 @@ IMPORTANT: You are an executor. Take action immediately using tools, don't expla
622
  messageHistory: BaseMessage[] = [],
623
  onStream?: (chunk: string) => void,
624
  messageId?: string,
 
625
  ): Promise<string> {
626
  if (!this.client) {
627
  throw new Error("Agent not initialized");
@@ -634,13 +649,17 @@ IMPORTANT: You are an executor. Take action immediately using tools, don't expla
634
  },
635
  {
636
  streamMode: ["custom", "updates"] as const,
637
- metadata: { messageId },
638
  },
639
  );
640
 
641
  let fullResponse = "";
642
 
643
  for await (const chunk of stream) {
 
 
 
 
644
  if (Array.isArray(chunk)) {
645
  const [mode, data] = chunk;
646
  if (mode === "custom" && data?.type === "token") {
 
72
  let currentSegmentContent = "";
73
  let buffer = "";
74
  const messageId = config?.metadata?.messageId;
75
+ const abortSignal = config?.metadata?.abortSignal as
76
+ | AbortSignal
77
+ | undefined;
78
 
79
  const toolRegex = /TOOL:\s*(\w+)\s+ARGS:\s*({[^}]*})/g;
80
 
81
+ for await (const token of this.streamModelResponse(
82
+ messages,
83
+ abortSignal,
84
+ )) {
85
  fullResponse += token;
86
  config?.writer?.({ type: "token", content: token });
87
  buffer += token;
 
315
  VIBEGAME CONTEXT:
316
  ${this.documentation}
317
 
318
+ IMPORTANT:
319
+ - The game auto-reloads on every change.
320
+ - The GAME import is automatically provided by the framework.
321
+ - The player is automatically created at [0, 0, 0] if not specified.
322
 
323
  AVAILABLE TOOLS:
324
  1. search_editor - Find text/patterns in code
 
349
 
350
  private async *streamModelResponse(
351
  messages: Array<{ role: string; content: string }>,
352
+ abortSignal?: AbortSignal,
353
  ): AsyncGenerator<string, string, unknown> {
354
  if (!this.client) {
355
  throw new Error("Agent not initialized");
 
365
  });
366
 
367
  for await (const chunk of stream) {
368
+ if (abortSignal?.aborted) {
369
+ throw new Error("AbortError");
370
+ }
371
+
372
  const token = chunk.choices[0]?.delta?.content || "";
373
  if (token) {
374
  fullContent += token;
 
636
  messageHistory: BaseMessage[] = [],
637
  onStream?: (chunk: string) => void,
638
  messageId?: string,
639
+ abortSignal?: AbortSignal,
640
  ): Promise<string> {
641
  if (!this.client) {
642
  throw new Error("Agent not initialized");
 
649
  },
650
  {
651
  streamMode: ["custom", "updates"] as const,
652
+ metadata: { messageId, abortSignal },
653
  },
654
  );
655
 
656
  let fullResponse = "";
657
 
658
  for await (const chunk of stream) {
659
+ if (abortSignal?.aborted) {
660
+ throw new Error("AbortError");
661
+ }
662
+
663
  if (Array.isArray(chunk)) {
664
  const [mode, data] = chunk;
665
  if (mode === "custom" && data?.type === "token") {
src/lib/services/agent.ts CHANGED
@@ -52,6 +52,18 @@ export class AgentService {
52
  });
53
  }
54
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  sendRawMessage(message: unknown): void {
56
  if (websocketService.isConnected()) {
57
  websocketService.send(message as WebSocketMessage);
 
52
  });
53
  }
54
 
55
+ stopConversation(): void {
56
+ if (!websocketService.isConnected()) {
57
+ return;
58
+ }
59
+
60
+ websocketService.send({
61
+ type: "abort",
62
+ payload: {},
63
+ timestamp: Date.now(),
64
+ });
65
+ }
66
+
67
  sendRawMessage(message: unknown): void {
68
  if (websocketService.isConnected()) {
69
  websocketService.send(message as WebSocketMessage);
src/lib/services/console-sync.ts CHANGED
@@ -30,7 +30,9 @@ export class ConsoleSyncService {
30
  firstArg.includes("[VibeGame] Console forwarding") ||
31
  firstArg.includes("[DEBUG]") ||
32
  firstArg.includes("hot updated:") ||
33
- firstArg.includes("using deprecated parameters for the initialization function")
 
 
34
  ) {
35
  return;
36
  }
 
30
  firstArg.includes("[VibeGame] Console forwarding") ||
31
  firstArg.includes("[DEBUG]") ||
32
  firstArg.includes("hot updated:") ||
33
+ firstArg.includes(
34
+ "using deprecated parameters for the initialization function",
35
+ )
36
  ) {
37
  return;
38
  }