dylanebert commited on
Commit
eb1a39a
·
1 Parent(s): 794cf6c
PLAN.md DELETED
@@ -1,870 +0,0 @@
1
- # AI Agent Development Plan - Refined Strategy
2
-
3
- ## Overview
4
-
5
- Iterative AI-driven game development system using **browser-native TypeScript architecture** with streaming AI agents for real-time VibeGame game modification and self-correcting feedback loops.
6
-
7
- ## Current Status
8
-
9
- **Phase 1 - Foundation COMPLETED** ✅
10
-
11
- - ✅ WebSocket server integrated with Vite
12
- - ✅ HuggingFace Inference API connected
13
- - ✅ Real-time streaming chat interface
14
- - ✅ Agent state management with Svelte stores
15
- - ✅ Bidirectional communication working
16
-
17
- **Next: Tool Implementation** 🚀
18
-
19
- ## Core Strategy: Small, Testable Iterations
20
-
21
- ### Guiding Principles
22
-
23
- 1. **Start Ultra-Simple** - First tool in ~10 lines of code
24
- 2. **Test Continuously** - Every iteration delivers testable value
25
- 3. **Fail Visibly** - Errors shown immediately in UI
26
- 4. **Incremental Complexity** - Add features one at a time
27
- 5. **User-Centric Progress** - Each day delivers visible improvement
28
-
29
- ### Development Phases
30
-
31
- - **Week 1**: Make it Work (basic tools)
32
- - **Week 2**: Make it Good (error handling)
33
- - **Week 3**: Make it Fast (optimization)
34
-
35
- ## Architecture Evolution
36
-
37
- ### Current Architecture (Simple)
38
-
39
- ```
40
- Browser → WebSocket → Agent → Tools → Stores → UI
41
- ```
42
-
43
- ### Target Architecture (Advanced)
44
-
45
- ```
46
- Browser → SSE/WebSocket → Composite Agent System → MCP Tools → Stores → UI
47
- ├── Base Model (Complex reasoning)
48
- ├── Quick Edit Model (Fast edits)
49
- └── AutoFix Model (Error correction)
50
- ```
51
-
52
- ## Implementation Phases
53
-
54
- ### Phase 1: Minimal Viable Tools (Days 1-7)
55
-
56
- **Goal:** Agent can read and edit game code
57
-
58
- #### Day 1: First Tool - Read Game Code
59
-
60
- - [ ] Create `src/lib/tools/registry.ts` with simple tool interface
61
- - [ ] Implement `read-game-code.ts` (returns editor content)
62
- - [ ] Update agent-runner to detect tool calls
63
- - **Test:** "What's in the game?" → Agent describes scene
64
-
65
- #### Day 2: Tool Execution
66
-
67
- - [ ] Parse tool requests from agent responses
68
- - [ ] Execute tools and return results
69
- - [ ] Add tool context to agent
70
- - **Test:** Agent accurately describes game elements
71
-
72
- #### Day 3: Edit Game Code
73
-
74
- - [ ] Implement `edit-game-code.ts` with string replacement
75
- - [ ] Connect to editor store
76
- - [ ] Basic XML validation
77
- - **Test:** "Add a red box" → Box appears
78
-
79
- #### Day 4: Console Reading
80
-
81
- - [ ] Implement `read-console.ts`
82
- - [ ] Filter and format messages
83
- - **Test:** Agent reports console errors
84
-
85
- #### Day 5-6: Tool Chaining
86
-
87
- - [ ] Multiple tools per response
88
- - [ ] Simple sequencing
89
- - [ ] Context preservation
90
- - **Test:** Multi-step operations work
91
-
92
- #### Day 7: Testing & Stabilization
93
-
94
- - [ ] Fix discovered bugs
95
- - [ ] Improve error messages
96
- - [ ] Add logging
97
- - **Success Metrics:**
98
- - ✅ Agent can describe game content
99
- - ✅ Agent can add/modify elements
100
- - ✅ Agent can read console
101
- - ✅ 5 basic operations work reliably
102
-
103
- ### Phase 2: Error Detection & Correction (Days 8-14)
104
-
105
- **Goal:** Agent detects and fixes simple errors
106
-
107
- #### Day 8: Error Pattern Recognition
108
-
109
- - [ ] Parse common VibeGame errors
110
- - [ ] Categorize: syntax, physics, missing components
111
- - **Test:** Agent identifies "no ground" error
112
-
113
- #### Day 9: Simple Self-Correction
114
-
115
- - [ ] Single retry on error
116
- - [ ] Generate fix attempts
117
- - **Test:** Agent adds missing ground
118
-
119
- #### Day 10: Undo/Redo
120
-
121
- - [ ] Edit history in editor store
122
- - [ ] Rollback on failure
123
- - **Test:** Failed edits can be undone
124
-
125
- #### Day 11: Validation Tool
126
-
127
- - [ ] Check required components
128
- - [ ] Pre-validate changes
129
- - **Test:** Prevents invalid states
130
-
131
- #### Day 12-13: Better Prompting
132
-
133
- - [ ] VibeGame-specific examples
134
- - [ ] Few-shot learning
135
- - **Test:** Higher first-attempt success
136
-
137
- #### Day 14: Polish
138
-
139
- - [ ] Clear error messages
140
- - [ ] Better explanations
141
- - **Success Metrics:**
142
- - ✅ Agent detects common errors
143
- - ✅ Agent fixes simple mistakes
144
- - ✅ Edit history prevents data loss
145
- - ✅ 80% success rate on common tasks
146
-
147
- ### Phase 3: Advanced Features (Days 15-21)
148
-
149
- **Goal:** Faster, smarter, more capable
150
-
151
- #### Day 15: Code-Based Tools
152
-
153
- - [ ] Agent generates TypeScript for tools
154
- - [ ] Controlled execution environment
155
- - **Test:** More flexible tool usage
156
-
157
- #### Day 16: Streaming Improvements
158
-
159
- - [ ] Migrate to Server-Sent Events (SSE)
160
- - [ ] Real-time tool visualization
161
- - **Test:** Better feedback
162
-
163
- #### Day 17: Quick Edit Model
164
-
165
- - [ ] Route simple edits to Qwen3-Coder-7B
166
- - [ ] Instant text/color changes
167
- - **Test:** Sub-second simple edits
168
-
169
- #### Day 18: Context Management
170
-
171
- - [ ] Sliding window for long chats
172
- - [ ] Iteration summarization
173
- - **Test:** 50+ message sessions
174
-
175
- #### Day 19: Template System
176
-
177
- - [ ] Pre-built game templates
178
- - [ ] Quick start patterns
179
- - **Test:** "Make a platformer" works
180
-
181
- #### Day 20: Thinking Visualization
182
-
183
- - [ ] Show reasoning process
184
- - [ ] Chain-of-thought UI
185
- - **Test:** Users understand logic
186
-
187
- #### Day 21: Final Polish
188
-
189
- - [ ] Performance optimization
190
- - [ ] Documentation
191
- - **Success Metrics:**
192
- - ✅ Sub-second simple edits
193
- - ✅ Complex multi-step tasks work
194
- - ✅ Users understand process
195
- - ✅ Production-ready quality
196
-
197
- ## Tool Specifications
198
-
199
- ### Core Tool Interface (MCP-Compatible)
200
-
201
- ```typescript
202
- interface Tool {
203
- name: string;
204
- description: string;
205
- execute: (params: any) => Promise<ToolResult>;
206
- }
207
-
208
- interface ToolResult {
209
- success: boolean;
210
- data?: any;
211
- error?: string;
212
- }
213
- ```
214
-
215
- ### Initial Tool Set
216
-
217
- 1. **read_game_code** - Get current editor content
218
- 2. **edit_game_code** - Modify via search/replace
219
- 3. **read_console** - Get console messages
220
- 4. **validate_game** - Check VibeGame rules
221
-
222
- ### Future Tools
223
-
224
- 5. **list_entities** - Get all scene entities
225
- 6. **add_entity** - Add new entity
226
- 7. **remove_entity** - Delete entity
227
- 8. **test_physics** - Simulate and check
228
- 9. **reset_game** - Restart scene
229
-
230
- ## Model Strategy
231
-
232
- ### Phase 1: Single Model
233
-
234
- - **Primary:** DeepSeek-R1-Distill-Qwen-1.5B (current)
235
- - Simple prompting, basic tool use
236
-
237
- ### Phase 2: Fallback Chain
238
-
239
- - **Primary:** DeepSeek-R1-Distill
240
- - **Fallback:** Qwen3-Coder-7B
241
- - Error recovery and retry logic
242
-
243
- ### Phase 3: Composite System
244
-
245
- - **Complex Tasks:** ERNIE-4.5-21B-A3B-Thinking (128K context)
246
- - **Quick Edits:** Qwen3-Coder-7B-Instruct
247
- - **Error Fix:** Dedicated correction model
248
- - **Streaming Post-Processor:** Real-time validation
249
-
250
- ## Key Improvements from Research
251
-
252
- ### From v0/Vercel
253
-
254
- - Composite model architecture
255
- - Streaming post-processing
256
- - Quick edit optimization
257
- - Real-time error correction
258
-
259
- ### From Smolagents
260
-
261
- - Code-based tools (TypeScript instead of JSON)
262
- - Direct execution without translation
263
- - Simpler debugging
264
-
265
- ### From Modern Thinking Models
266
-
267
- - 128K context windows
268
- - Chain-of-thought reasoning
269
- - Sparse MoE activation (3B active params)
270
-
271
- ### From MCP/Agentic Web
272
-
273
- - Standardized tool protocols
274
- - Browser-native execution
275
- - SSE for streaming
276
- - Direct store integration
277
-
278
- ## Testing Strategy
279
-
280
- ### Unit Tests
281
-
282
- - Individual tool validation
283
- - Mock stores for isolation
284
-
285
- ### Integration Tests
286
-
287
- - Full flow: request → tools → response
288
- - Multi-tool sequences
289
- - Error recovery
290
-
291
- ### User Tests
292
-
293
- - "Add jumping platforms"
294
- - "Fix the bouncing balls"
295
- - "Create a coin collector"
296
-
297
- ## Deployment
298
-
299
- ### Environment Variables
300
-
301
- ```env
302
- HF_TOKEN=<user_token>
303
- MODEL_PRIMARY=deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B
304
- MODEL_QUICK=Qwen/Qwen3-Coder-7B-Instruct
305
- MAX_ITERATIONS=10
306
- ITERATION_TIMEOUT=30000
307
- PORT=7860
308
- ```
309
-
310
- ### Single Container (HuggingFace Spaces)
311
-
312
- ```dockerfile
313
- FROM oven/bun:1-alpine
314
- WORKDIR /app
315
- COPY package.json bun.lockb ./
316
- RUN bun install --frozen-lockfile
317
- COPY . .
318
- RUN bun run build
319
- EXPOSE 7860
320
- CMD ["bun", "run", "preview", "--host", "0.0.0.0", "--port", "7860"]
321
- ```
322
-
323
- ## Implementation Details
324
-
325
- ### Tool System Architecture
326
-
327
- #### Pattern Recognition in Agent Responses
328
-
329
- The agent will naturally include tool calls in its responses using a simple pattern:
330
-
331
- ```
332
- [TOOL: tool_name {"param": "value"}]
333
- ```
334
-
335
- Example agent response:
336
-
337
- ```
338
- I'll check what's currently in your game.
339
-
340
- [TOOL: read_game_code]
341
-
342
- Based on the code, I can see you have a ground platform and a red ball...
343
- ```
344
-
345
- #### Complete Tool Registry Implementation
346
-
347
- ```typescript
348
- // src/lib/tools/registry.ts - FULL CODE TO CREATE
349
-
350
- export interface Tool {
351
- name: string;
352
- description: string;
353
- execute: (params: any) => Promise<ToolResult>;
354
- }
355
-
356
- export interface ToolResult {
357
- success: boolean;
358
- data?: any;
359
- error?: string;
360
- }
361
-
362
- export interface ToolCall {
363
- tool: string;
364
- parameters?: any;
365
- }
366
-
367
- class ToolRegistry {
368
- private tools: Map<string, Tool> = new Map();
369
-
370
- register(tool: Tool) {
371
- if (this.tools.has(tool.name)) {
372
- console.warn(`Tool ${tool.name} already registered, overwriting`);
373
- }
374
- this.tools.set(tool.name, tool);
375
- console.log(`Registered tool: ${tool.name}`);
376
- }
377
-
378
- async execute(toolName: string, params?: any): Promise<ToolResult> {
379
- const tool = this.tools.get(toolName);
380
-
381
- if (!tool) {
382
- return {
383
- success: false,
384
- error: `Tool '${toolName}' not found. Available: ${Array.from(this.tools.keys()).join(", ")}`,
385
- };
386
- }
387
-
388
- try {
389
- console.log(`Executing tool: ${toolName}`, params);
390
- const result = await tool.execute(params);
391
- console.log(`Tool ${toolName} result:`, result);
392
- return result;
393
- } catch (error) {
394
- console.error(`Tool ${toolName} error:`, error);
395
- return {
396
- success: false,
397
- error: error instanceof Error ? error.message : "Unknown error",
398
- };
399
- }
400
- }
401
-
402
- getTools(): Tool[] {
403
- return Array.from(this.tools.values());
404
- }
405
-
406
- getToolNames(): string[] {
407
- return Array.from(this.tools.keys());
408
- }
409
-
410
- getToolDescriptions(): string {
411
- return Array.from(this.tools.values())
412
- .map((tool) => `- ${tool.name}: ${tool.description}`)
413
- .join("\n");
414
- }
415
- }
416
-
417
- // Global registry instance
418
- export const toolRegistry = new ToolRegistry();
419
-
420
- // Helper function to parse tool calls from agent response
421
- export function parseToolCalls(response: string): ToolCall[] {
422
- const toolCalls: ToolCall[] = [];
423
-
424
- // Pattern: [TOOL: tool_name] or [TOOL: tool_name {"param": "value"}]
425
- const toolPattern = /\[TOOL:\s*(\w+)(?:\s+({[^}]+}))?\]/g;
426
-
427
- let match;
428
- while ((match = toolPattern.exec(response)) !== null) {
429
- const toolName = match[1];
430
- const paramsStr = match[2];
431
-
432
- let parameters = undefined;
433
- if (paramsStr) {
434
- try {
435
- parameters = JSON.parse(paramsStr);
436
- } catch (e) {
437
- console.warn(`Failed to parse tool parameters: ${paramsStr}`);
438
- }
439
- }
440
-
441
- toolCalls.push({ tool: toolName, parameters });
442
- }
443
-
444
- return toolCalls;
445
- }
446
-
447
- // Helper to format tool results for agent context
448
- export function formatToolResult(toolName: string, result: ToolResult): string {
449
- if (result.success) {
450
- return `[TOOL_RESULT: ${toolName}]\n${JSON.stringify(result.data, null, 2)}\n[/TOOL_RESULT]`;
451
- } else {
452
- return `[TOOL_ERROR: ${toolName}]\n${result.error}\n[/TOOL_ERROR]`;
453
- }
454
- }
455
- ```
456
-
457
- #### Tool Implementation Examples
458
-
459
- **read-game-code.ts** (Ultra-simple first tool):
460
-
461
- ```typescript
462
- import { editorStore } from "$lib/stores/editor";
463
- import { get } from "svelte/store";
464
-
465
- export const readGameCodeTool: Tool = {
466
- name: "read_game_code",
467
- description: "Get the current game code from the editor",
468
- execute: async () => {
469
- const state = get(editorStore);
470
- return {
471
- success: true,
472
- data: {
473
- content: state.content,
474
- language: state.language,
475
- },
476
- };
477
- },
478
- };
479
- ```
480
-
481
- **edit-game-code.ts** (Simple string replacement):
482
-
483
- ```typescript
484
- export const editGameCodeTool: Tool = {
485
- name: "edit_game_code",
486
- description: "Edit game code using search and replace",
487
- execute: async (params: { search: string; replace: string }) => {
488
- const state = get(editorStore);
489
- const newContent = state.content.replace(params.search, params.replace);
490
- editorStore.setContent(newContent);
491
- return {
492
- success: true,
493
- data: { modified: true, newContent },
494
- };
495
- },
496
- };
497
- ```
498
-
499
- #### Agent Runner Updates
500
-
501
- Add to `src/lib/server/agent-runner.ts`:
502
-
503
- ```typescript
504
- import { parseToolCalls, toolRegistry, formatToolResult } from '../tools/registry';
505
-
506
- async processMessage(message: string, onStream?: (chunk: string) => void) {
507
- // ... existing code ...
508
-
509
- // After getting response, check for tool calls
510
- const toolCalls = parseToolCalls(fullResponse);
511
-
512
- if (toolCalls.length > 0) {
513
- // Execute tools and add results to context
514
- for (const call of toolCalls) {
515
- const result = await toolRegistry.execute(call.tool, call.parameters);
516
- const formatted = formatToolResult(call.tool, result);
517
-
518
- // Add to message history for context
519
- this.messageHistory.push({
520
- id: this.generateId(),
521
- role: 'system',
522
- content: formatted,
523
- timestamp: Date.now()
524
- });
525
- }
526
-
527
- // Continue conversation with tool results
528
- // (In phase 2, we'll make this recursive for self-correction)
529
- }
530
- }
531
- ```
532
-
533
- #### System Prompt Enhancement
534
-
535
- ```typescript
536
- const systemPrompt = `You are an AI assistant helping to build games with VibeGame.
537
-
538
- Available tools:
539
- ${toolRegistry.getToolDescriptions()}
540
-
541
- To use a tool, include in your response: [TOOL: tool_name {"param": "value"}]
542
-
543
- Example:
544
- - To read game code: [TOOL: read_game_code]
545
- - To edit: [TOOL: edit_game_code {"search": "old text", "replace": "new text"}]
546
-
547
- Always validate changes and ensure the game remains playable.
548
- Use small, incremental changes rather than large rewrites.`;
549
- ```
550
-
551
- ### Testing Scenarios
552
-
553
- #### Phase 1 Test Cases
554
-
555
- 1. **"What's in the game?"**
556
- - Expected: Agent uses `read_game_code`, describes scene elements
557
-
558
- 2. **"Add a blue box at position 5,2,0"**
559
- - Expected: Agent uses `edit_game_code` to insert `<dynamic-part>`
560
-
561
- 3. **"Change the ball color to green"**
562
- - Expected: Agent finds and replaces color attribute
563
-
564
- 4. **"What errors are in the console?"**
565
- - Expected: Agent uses `read_console` tool
566
-
567
- 5. **"Check the code and fix any issues"**
568
- - Expected: Agent chains multiple tools
569
-
570
- ### Current File Structure
571
-
572
- ```
573
- src/lib/
574
- ├── server/
575
- │ ├── agent-runner.ts (✅ exists - needs tool integration)
576
- │ └── api.ts (✅ exists - WebSocket handler)
577
- ├── tools/
578
- │ └── (empty directory - needs implementation)
579
- └── stores/
580
- ├── agent.ts (✅ exists)
581
- ├── editor.ts (✅ exists)
582
- ├── console.ts (✅ exists)
583
- ├── game.ts (✅ exists)
584
- └── ui.ts (✅ exists)
585
- ```
586
-
587
- ### Target File Structure After Implementation
588
-
589
- ```
590
- src/lib/
591
- ├── server/
592
- │ ├── agent-runner.ts (updated with tool execution)
593
- │ └── api.ts (existing WebSocket handler)
594
- ├── tools/
595
- │ ├── registry.ts (to create)
596
- │ ├── read-game-code.ts (to create)
597
- │ ├── edit-game-code.ts (to create)
598
- │ ├── read-console.ts (to create)
599
- │ └── index.ts (to create - exports all tools)
600
- └── stores/
601
- └── (all existing)
602
- ```
603
-
604
- ## Next Immediate Steps
605
-
606
- 1. ✅ Update PLAN.md with implementation details
607
- 2. ⏳ Create tool registry (`src/lib/tools/registry.ts`)
608
- 3. ⏳ Implement read-game-code tool
609
- 4. ⏳ Update agent-runner for tool execution
610
- 5. ⏳ Test in UI with "What's in the game?"
611
-
612
- ## Success Criteria
613
-
614
- ### Week 1
615
-
616
- - [ ] Basic tools working
617
- - [ ] Agent can modify games
618
- - [ ] Console feedback visible
619
-
620
- ### Week 2
621
-
622
- - [ ] Error detection working
623
- - [ ] Self-correction attempts
624
- - [ ] Higher success rate
625
-
626
- ### Week 3
627
-
628
- - [ ] Fast response times
629
- - [ ] Advanced features
630
- - [ ] Production ready
631
-
632
- ## Model Configuration Details
633
-
634
- ### HuggingFace Inference API Setup
635
-
636
- #### Available Models (via Serverless API)
637
-
638
- ```typescript
639
- // Phase 1: Simple model
640
- const MODELS = {
641
- primary: "deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B",
642
-
643
- // Phase 2: Fallback chain
644
- fallback: "Qwen/Qwen2.5-Coder-7B-Instruct",
645
-
646
- // Phase 3: Thinking models (require special handling)
647
- thinking: {
648
- ernie: "baidu/ERNIE-4.5-21B-A3B-Thinking", // Apache 2.0, 128K context
649
- qwen: "Qwen/Qwen3-Next-80B-A3B-Thinking", // Longer thinking chains
650
- },
651
- };
652
- ```
653
-
654
- #### Thinking Model Integration
655
-
656
- For thinking models, parse and display reasoning:
657
-
658
- ```typescript
659
- // Response may contain thinking process
660
- const thinkingPattern = /<thinking>(.*?)<\/thinking>/s;
661
- const match = response.match(thinkingPattern);
662
- if (match) {
663
- // Store thinking for UI display
664
- const thinking = match[1];
665
- // Extract actual response
666
- const answer = response.replace(thinkingPattern, "");
667
- }
668
- ```
669
-
670
- ### Advanced Features Implementation
671
-
672
- #### Server-Sent Events (SSE) Migration
673
-
674
- Replace WebSocket with SSE for simpler streaming:
675
-
676
- ```typescript
677
- // src/lib/server/sse.ts
678
- export function createSSEStream(req: Request) {
679
- const stream = new ReadableStream({
680
- start(controller) {
681
- controller.enqueue('data: {"type":"connected"}\n\n');
682
- },
683
- async pull(controller) {
684
- // Stream agent responses
685
- const chunk = await getNextChunk();
686
- controller.enqueue(`data: ${JSON.stringify(chunk)}\n\n`);
687
- },
688
- });
689
-
690
- return new Response(stream, {
691
- headers: {
692
- "Content-Type": "text/event-stream",
693
- "Cache-Control": "no-cache",
694
- Connection: "keep-alive",
695
- },
696
- });
697
- }
698
- ```
699
-
700
- #### Code-Based Tool Execution (Smolagents-inspired)
701
-
702
- Allow agent to write TypeScript code for tools:
703
-
704
- ```typescript
705
- // Agent generates code like:
706
- const code = `
707
- const editor = await tools.readGameCode();
708
- const content = editor.data.content;
709
- const modified = content.replace('color="#ff0000"', 'color="#00ff00"');
710
- await tools.editGameCode({ search: content, replace: modified });
711
- return "Changed red to green";
712
- `;
713
-
714
- // Execute in sandboxed context
715
- const sandbox = {
716
- tools: toolRegistry,
717
- console: { log: (...args) => consoleStore.addMessage("log", args.join(" ")) },
718
- };
719
- const result = await executeInSandbox(code, sandbox);
720
- ```
721
-
722
- #### Composite Model Router
723
-
724
- Route requests based on complexity:
725
-
726
- ```typescript
727
- class ModelRouter {
728
- async route(message: string, context: AgentMessage[]): Promise<string> {
729
- const complexity = this.assessComplexity(message);
730
-
731
- if (complexity === "simple") {
732
- // Text changes, color modifications
733
- return this.quickEditModel(message);
734
- } else if (complexity === "complex") {
735
- // Multi-step reasoning, debugging
736
- return this.thinkingModel(message);
737
- } else {
738
- // Default
739
- return this.primaryModel(message);
740
- }
741
- }
742
-
743
- private assessComplexity(message: string): "simple" | "medium" | "complex" {
744
- // Simple heuristics
745
- if (message.match(/change|color|text|rename/i)) return "simple";
746
- if (message.match(/debug|fix|error|why/i)) return "complex";
747
- return "medium";
748
- }
749
- }
750
- ```
751
-
752
- ## Common Patterns & Solutions
753
-
754
- ### Pattern: Adding Game Elements
755
-
756
- ```typescript
757
- // User: "Add a platform at 0,3,0"
758
- // Agent response with tool:
759
- `I'll add a platform at position 0,3,0 for you.
760
-
761
- [TOOL: edit_game_code {"search": "</world>", "replace": " <static-part pos=\"0 3 0\" shape=\"box\" size=\"3 0.5 3\" color=\"#808080\"></static-part>\n</world>"}]
762
-
763
- I've added a gray platform at position (0, 3, 0).`;
764
- ```
765
-
766
- ### Pattern: Debugging Errors
767
-
768
- ```typescript
769
- // User: "The player keeps falling"
770
- // Agent workflow:
771
- 1. [TOOL: read_game_code] - Check for ground
772
- 2. [TOOL: read_console] - Check for physics errors
773
- 3. Identify: No ground platform
774
- 4. [TOOL: edit_game_code] - Add ground
775
- 5. [TOOL: validate_game] - Verify fix
776
- ```
777
-
778
- ### Pattern: Complex Modifications
779
-
780
- ```typescript
781
- // User: "Make this into a platformer"
782
- // Agent uses template:
783
- const platformerTemplate = `
784
- <!-- Platforms -->
785
- <static-part pos="-5 2 0" shape="box" size="3 0.5 3" color="#808080"></static-part>
786
- <static-part pos="0 4 0" shape="box" size="3 0.5 3" color="#808080"></static-part>
787
- <static-part pos="5 6 0" shape="box" size="3 0.5 3" color="#808080"></static-part>
788
-
789
- <!-- Moving platform -->
790
- <kinematic-part pos="0 3 5" shape="box" size="4 0.5 4" color="#4169e1">
791
- <tween target="body.pos-x" from="-10" to="10" duration="5" loop="ping-pong"></tween>
792
- </kinematic-part>
793
- `;
794
- ```
795
-
796
- ## Troubleshooting Guide
797
-
798
- ### Issue: Agent doesn't use tools
799
-
800
- **Solution**: Enhance system prompt with examples
801
-
802
- ```typescript
803
- const systemPrompt = `
804
- IMPORTANT: You have tools available. Use them!
805
-
806
- Examples of when to use tools:
807
- - User asks "what's in the game?" → Use [TOOL: read_game_code]
808
- - User says "add a box" → Use [TOOL: edit_game_code]
809
- - User mentions "error" → Use [TOOL: read_console]
810
- `;
811
- ```
812
-
813
- ### Issue: Tool execution fails
814
-
815
- **Solution**: Add error recovery
816
-
817
- ```typescript
818
- try {
819
- const result = await tool.execute(params);
820
- if (!result.success) {
821
- // Retry with adjusted parameters
822
- const adjusted = this.adjustParameters(params, result.error);
823
- return await tool.execute(adjusted);
824
- }
825
- } catch (error) {
826
- // Fallback to manual instruction
827
- return {
828
- success: false,
829
- error: `Tool failed. Manual fix: ${this.getManualInstructions(tool.name)}`,
830
- };
831
- }
832
- ```
833
-
834
- ### Issue: Context overflow
835
-
836
- **Solution**: Implement sliding window
837
-
838
- ```typescript
839
- class ContextManager {
840
- private maxTokens = 4000;
841
-
842
- summarize(messages: AgentMessage[]): AgentMessage[] {
843
- if (this.countTokens(messages) < this.maxTokens) {
844
- return messages;
845
- }
846
-
847
- // Keep system, first user, and last N messages
848
- const summary = this.createSummary(messages.slice(1, -5));
849
- return [
850
- messages[0], // system
851
- { role: "system", content: `Previous conversation summary: ${summary}` },
852
- ...messages.slice(-5), // recent context
853
- ];
854
- }
855
- }
856
- ```
857
-
858
- ## Resources
859
-
860
- - [HuggingFace Inference API](https://huggingface.co/docs/api-inference/index)
861
- - [MCP Specification](https://modelcontextprotocol.io/specification)
862
- - [VibeGame Docs](llms.txt)
863
- - [Smolagents](https://github.com/huggingface/smolagents)
864
- - [AI SDK](https://ai-sdk.dev)
865
- - [ERNIE-4.5 Model](https://huggingface.co/baidu/ERNIE-4.5-21B-A3B-Thinking)
866
- - [Qwen3-Next Model](https://huggingface.co/Qwen/Qwen3-Next-80B-A3B-Thinking)
867
-
868
- ---
869
-
870
- _This plan provides a complete roadmap for implementing an AI agent system for VibeGame game development. Start with Phase 1 for immediate value, then progressively add sophistication. Each iteration is designed to be completed in hours, not days, with immediate testing in the UI._
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bun.lock CHANGED
@@ -6,8 +6,10 @@
6
  "dependencies": {
7
  "@huggingface/hub": "^2.6.3",
8
  "@huggingface/inference": "^4.8.0",
 
9
  "@types/node": "^24.3.3",
10
  "gsap": "^3.13.0",
 
11
  "monaco-editor": "^0.50.0",
12
  "svelte-splitpanes": "^8.0.5",
13
  "vibegame": "^0.1.1",
@@ -15,6 +17,7 @@
15
  "devDependencies": {
16
  "@eslint/js": "^9.33.0",
17
  "@sveltejs/vite-plugin-svelte": "^3.1.1",
 
18
  "@types/ws": "^8.18.1",
19
  "@typescript-eslint/eslint-plugin": "^8.40.0",
20
  "@typescript-eslint/parser": "^8.40.0",
@@ -180,16 +183,22 @@
180
 
181
  "@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/[email protected]", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^3.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "vite": "^5.0.0" } }, "sha512-9QX28IymvBlSCqsCll5t0kQVxipsfhFFL+L2t3nTWfXnddYwxBuAEtTtlaVQpRz9c37BhJjltSeY4AJSC03SSg=="],
182
 
 
 
183
  "@types/estree": ["@types/[email protected]", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
184
 
185
  "@types/json-schema": ["@types/[email protected]", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
186
 
187
  "@types/json5": ["@types/[email protected]", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="],
188
 
 
 
189
  "@types/node": ["@types/[email protected]", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-GKBNHjoNw3Kra1Qg5UXttsY5kiWMEfoHq2TmXb+b1rcm6N7B3wTrFYIf/oSZ1xNQ+hVVijgLkiDZh7jRRsh+Gw=="],
190
 
191
  "@types/pug": ["@types/[email protected]", "", {}, "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA=="],
192
 
 
 
193
  "@types/ws": ["@types/[email protected]", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
194
 
195
  "@typescript-eslint/eslint-plugin": ["@typescript-eslint/[email protected]", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.43.0", "@typescript-eslint/type-utils": "8.43.0", "@typescript-eslint/utils": "8.43.0", "@typescript-eslint/visitor-keys": "8.43.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.43.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-8tg+gt7ENL7KewsKMKDHXR1vm8tt9eMxjJBYINf6swonlWgkYn5NwyIgXpbbDxTNU5DgpDFfj95prcTq2clIQQ=="],
@@ -258,6 +267,8 @@
258
 
259
  "buffer-crc32": ["[email protected]", "", {}, "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w=="],
260
 
 
 
261
  "call-bind": ["[email protected]", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],
262
 
263
  "call-bind-apply-helpers": ["[email protected]", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
@@ -284,6 +295,8 @@
284
 
285
  "css-tree": ["[email protected]", "", { "dependencies": { "mdn-data": "2.0.30", "source-map-js": "^1.0.1" } }, "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw=="],
286
 
 
 
287
  "data-view-buffer": ["[email protected]", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="],
288
 
289
  "data-view-byte-length": ["[email protected]", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="],
@@ -526,6 +539,8 @@
526
 
527
  "magic-string": ["[email protected]", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="],
528
 
 
 
529
  "math-intrinsics": ["[email protected]", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
530
 
531
  "mdn-data": ["[email protected]", "", {}, "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA=="],
 
6
  "dependencies": {
7
  "@huggingface/hub": "^2.6.3",
8
  "@huggingface/inference": "^4.8.0",
9
+ "@types/marked": "^6.0.0",
10
  "@types/node": "^24.3.3",
11
  "gsap": "^3.13.0",
12
+ "marked": "^16.2.1",
13
  "monaco-editor": "^0.50.0",
14
  "svelte-splitpanes": "^8.0.5",
15
  "vibegame": "^0.1.1",
 
17
  "devDependencies": {
18
  "@eslint/js": "^9.33.0",
19
  "@sveltejs/vite-plugin-svelte": "^3.1.1",
20
+ "@types/bun": "^1.2.21",
21
  "@types/ws": "^8.18.1",
22
  "@typescript-eslint/eslint-plugin": "^8.40.0",
23
  "@typescript-eslint/parser": "^8.40.0",
 
183
 
184
  "@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/[email protected]", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^3.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "vite": "^5.0.0" } }, "sha512-9QX28IymvBlSCqsCll5t0kQVxipsfhFFL+L2t3nTWfXnddYwxBuAEtTtlaVQpRz9c37BhJjltSeY4AJSC03SSg=="],
185
 
186
+ "@types/bun": ["@types/[email protected]", "", { "dependencies": { "bun-types": "1.2.21" } }, "sha512-NiDnvEqmbfQ6dmZ3EeUO577s4P5bf4HCTXtI6trMc6f6RzirY5IrF3aIookuSpyslFzrnvv2lmEWv5HyC1X79A=="],
187
+
188
  "@types/estree": ["@types/[email protected]", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
189
 
190
  "@types/json-schema": ["@types/[email protected]", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
191
 
192
  "@types/json5": ["@types/[email protected]", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="],
193
 
194
+ "@types/marked": ["@types/[email protected]", "", { "dependencies": { "marked": "*" } }, "sha512-jmjpa4BwUsmhxcfsgUit/7A9KbrC48Q0q8KvnY107ogcjGgTFDlIL3RpihNpx2Mu1hM4mdFQjoVc4O6JoGKHsA=="],
195
+
196
  "@types/node": ["@types/[email protected]", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-GKBNHjoNw3Kra1Qg5UXttsY5kiWMEfoHq2TmXb+b1rcm6N7B3wTrFYIf/oSZ1xNQ+hVVijgLkiDZh7jRRsh+Gw=="],
197
 
198
  "@types/pug": ["@types/[email protected]", "", {}, "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA=="],
199
 
200
+ "@types/react": ["@types/[email protected]", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ=="],
201
+
202
  "@types/ws": ["@types/[email protected]", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
203
 
204
  "@typescript-eslint/eslint-plugin": ["@typescript-eslint/[email protected]", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.43.0", "@typescript-eslint/type-utils": "8.43.0", "@typescript-eslint/utils": "8.43.0", "@typescript-eslint/visitor-keys": "8.43.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.43.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-8tg+gt7ENL7KewsKMKDHXR1vm8tt9eMxjJBYINf6swonlWgkYn5NwyIgXpbbDxTNU5DgpDFfj95prcTq2clIQQ=="],
 
267
 
268
  "buffer-crc32": ["[email protected]", "", {}, "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w=="],
269
 
270
+ "bun-types": ["[email protected]", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-sa2Tj77Ijc/NTLS0/Odjq/qngmEPZfbfnOERi0KRUYhT9R8M4VBioWVmMWE5GrYbKMc+5lVybXygLdibHaqVqw=="],
271
+
272
  "call-bind": ["[email protected]", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],
273
 
274
  "call-bind-apply-helpers": ["[email protected]", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
 
295
 
296
  "css-tree": ["[email protected]", "", { "dependencies": { "mdn-data": "2.0.30", "source-map-js": "^1.0.1" } }, "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw=="],
297
 
298
+ "csstype": ["[email protected]", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
299
+
300
  "data-view-buffer": ["[email protected]", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="],
301
 
302
  "data-view-byte-length": ["[email protected]", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="],
 
539
 
540
  "magic-string": ["[email protected]", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="],
541
 
542
+ "marked": ["[email protected]", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-r3UrXED9lMlHF97jJByry90cwrZBBvZmjG1L68oYfuPMW+uDTnuMbyJDymCWwbTE+f+3LhpNDKfpR3a3saFyjA=="],
543
+
544
  "math-intrinsics": ["[email protected]", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
545
 
546
  "mdn-data": ["[email protected]", "", {}, "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA=="],
favicon.ico ADDED
index.html CHANGED
@@ -3,7 +3,8 @@
3
  <head>
4
  <meta charset="UTF-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <title>🥕 VibeGame</title>
 
7
  <style>
8
  body {
9
  margin: 0;
 
3
  <head>
4
  <meta charset="UTF-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <link rel="icon" href="/favicon.ico" />
7
+ <title>VibeGame</title>
8
  <style>
9
  body {
10
  margin: 0;
package.json CHANGED
@@ -20,6 +20,7 @@
20
  "devDependencies": {
21
  "@eslint/js": "^9.33.0",
22
  "@sveltejs/vite-plugin-svelte": "^3.1.1",
 
23
  "@types/ws": "^8.18.1",
24
  "@typescript-eslint/eslint-plugin": "^8.40.0",
25
  "@typescript-eslint/parser": "^8.40.0",
@@ -37,8 +38,10 @@
37
  "dependencies": {
38
  "@huggingface/hub": "^2.6.3",
39
  "@huggingface/inference": "^4.8.0",
 
40
  "@types/node": "^24.3.3",
41
  "gsap": "^3.13.0",
 
42
  "monaco-editor": "^0.50.0",
43
  "svelte-splitpanes": "^8.0.5",
44
  "vibegame": "^0.1.1"
 
20
  "devDependencies": {
21
  "@eslint/js": "^9.33.0",
22
  "@sveltejs/vite-plugin-svelte": "^3.1.1",
23
+ "@types/bun": "^1.2.21",
24
  "@types/ws": "^8.18.1",
25
  "@typescript-eslint/eslint-plugin": "^8.40.0",
26
  "@typescript-eslint/parser": "^8.40.0",
 
38
  "dependencies": {
39
  "@huggingface/hub": "^2.6.3",
40
  "@huggingface/inference": "^4.8.0",
41
+ "@types/marked": "^6.0.0",
42
  "@types/node": "^24.3.3",
43
  "gsap": "^3.13.0",
44
+ "marked": "^16.2.1",
45
  "monaco-editor": "^0.50.0",
46
  "svelte-splitpanes": "^8.0.5",
47
  "vibegame": "^0.1.1"
src/lib/components/chat/ChatPanel.svelte CHANGED
@@ -3,6 +3,9 @@
3
  import { agentStore, isConnected, isProcessing } from "../../stores/agent";
4
  import { authStore } from "../../services/auth";
5
  import gsap from "gsap";
 
 
 
6
 
7
  let inputValue = "";
8
  let messagesContainer: HTMLDivElement;
@@ -12,6 +15,40 @@
12
 
13
  let hasConnected = false;
14
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  onMount(() => {
16
  if ($authStore.isAuthenticated && !$authStore.loading) {
17
  agentStore.connect();
@@ -162,10 +199,24 @@
162
  {/if}
163
  {#each $agentStore.messages as message}
164
  <div class="message {message.role}">
 
 
 
165
  <div class="message-content">
166
- {message.content.trim()}
167
- {#if message.streaming}
168
- <span class="cursor">▊</span>
 
 
 
 
 
 
 
 
 
 
 
169
  {/if}
170
  </div>
171
  </div>
@@ -232,7 +283,7 @@
232
  .message {
233
  padding: 0.4rem 0.6rem;
234
  border-radius: 3px;
235
- font-size: 0.75rem;
236
  animation: fadeIn 0.2s ease-out;
237
  }
238
 
@@ -260,13 +311,16 @@
260
  }
261
 
262
  .message-content {
 
 
 
 
 
 
263
  color: rgba(255, 255, 255, 0.9);
264
  line-height: 1.5;
265
  white-space: pre-line;
266
  word-wrap: break-word;
267
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Monaco", "Menlo", monospace;
268
- margin: 0;
269
- display: block;
270
  }
271
 
272
  .cursor {
@@ -290,7 +344,7 @@
290
  border-radius: 3px;
291
  padding: 0.4rem 0.6rem;
292
  color: #ff9999;
293
- font-size: 0.75rem;
294
  animation: fadeIn 0.2s ease-out;
295
  }
296
 
@@ -311,7 +365,7 @@
311
  padding: 0.4rem 0.6rem;
312
  resize: none;
313
  font-family: "Monaco", "Menlo", monospace;
314
- font-size: 0.8rem;
315
  line-height: 1.2;
316
  }
317
 
@@ -408,7 +462,7 @@
408
 
409
  .ready-message {
410
  color: rgba(255, 255, 255, 0.2);
411
- font-size: 0.75rem;
412
  text-align: center;
413
  font-style: italic;
414
  display: flex;
 
3
  import { agentStore, isConnected, isProcessing } from "../../stores/agent";
4
  import { authStore } from "../../services/auth";
5
  import gsap from "gsap";
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;
 
15
 
16
  let hasConnected = false;
17
 
18
+ const TOOL_PATTERN = /\[TOOL:\s*(\w+)(?:\s+({[^}]+}))?\]/g;
19
+
20
+ function parseMessageContent(content: string) {
21
+ const parts: Array<{ type: 'text' | 'tool', content: string, toolName?: string, params?: any }> = [];
22
+ let lastIndex = 0;
23
+
24
+ const matches = Array.from(content.matchAll(TOOL_PATTERN));
25
+
26
+ for (const match of matches) {
27
+ const [fullMatch, toolName, paramsStr] = match;
28
+ const index = match.index!;
29
+
30
+ if (index > lastIndex) {
31
+ parts.push({ type: 'text', content: content.slice(lastIndex, index) });
32
+ }
33
+
34
+ let params = null;
35
+ if (paramsStr) {
36
+ try {
37
+ params = JSON.parse(paramsStr);
38
+ } catch {}
39
+ }
40
+
41
+ parts.push({ type: 'tool', content: fullMatch, toolName, params });
42
+ lastIndex = index + fullMatch.length;
43
+ }
44
+
45
+ if (lastIndex < content.length) {
46
+ parts.push({ type: 'text', content: content.slice(lastIndex) });
47
+ }
48
+
49
+ return parts.length > 0 ? parts : [{ type: 'text' as const, content }];
50
+ }
51
+
52
  onMount(() => {
53
  if ($authStore.isAuthenticated && !$authStore.loading) {
54
  agentStore.connect();
 
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>
 
283
  .message {
284
  padding: 0.4rem 0.6rem;
285
  border-radius: 3px;
286
+ font-size: 0.875rem;
287
  animation: fadeIn 0.2s ease-out;
288
  }
289
 
 
311
  }
312
 
313
  .message-content {
314
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Monaco", "Menlo", monospace;
315
+ margin: 0;
316
+ display: block;
317
+ }
318
+
319
+ .message.user .message-content {
320
  color: rgba(255, 255, 255, 0.9);
321
  line-height: 1.5;
322
  white-space: pre-line;
323
  word-wrap: break-word;
 
 
 
324
  }
325
 
326
  .cursor {
 
344
  border-radius: 3px;
345
  padding: 0.4rem 0.6rem;
346
  color: #ff9999;
347
+ font-size: 0.875rem;
348
  animation: fadeIn 0.2s ease-out;
349
  }
350
 
 
365
  padding: 0.4rem 0.6rem;
366
  resize: none;
367
  font-family: "Monaco", "Menlo", monospace;
368
+ font-size: 0.875rem;
369
  line-height: 1.2;
370
  }
371
 
 
462
 
463
  .ready-message {
464
  color: rgba(255, 255, 255, 0.2);
465
+ font-size: 0.875rem;
466
  text-align: center;
467
  font-style: italic;
468
  display: flex;
src/lib/components/chat/MarkdownRenderer.svelte ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { marked } from "marked";
3
+
4
+ export let content: string;
5
+ export let streaming: boolean = false;
6
+
7
+ let htmlContent = "";
8
+
9
+ const renderer = new marked.Renderer();
10
+
11
+ renderer.code = ({ text, lang }) => {
12
+ const language = lang || "text";
13
+ return `<pre><code class="language-${language}">${escapeHtml(text)}</code></pre>`;
14
+ };
15
+
16
+ renderer.link = ({ href, title, tokens }) => {
17
+ const firstToken = tokens?.[0];
18
+ const text = (firstToken && 'text' in firstToken) ? firstToken.text : '';
19
+ return `<a href="${href}" target="_blank" rel="noopener noreferrer" title="${title || text}">${text}</a>`;
20
+ };
21
+
22
+ function escapeHtml(text: string): string {
23
+ const div = document.createElement("div");
24
+ div.textContent = text;
25
+ return div.innerHTML;
26
+ }
27
+
28
+ marked.setOptions({
29
+ renderer,
30
+ breaks: true,
31
+ gfm: true,
32
+ });
33
+
34
+ $: if (content) {
35
+ try {
36
+ htmlContent = marked.parse(content) as string;
37
+ } catch {
38
+ htmlContent = escapeHtml(content);
39
+ }
40
+ }
41
+ </script>
42
+
43
+ <div class="markdown-content">
44
+ {@html htmlContent}
45
+ {#if streaming}
46
+ <span class="cursor">▊</span>
47
+ {/if}
48
+ </div>
49
+
50
+ <style>
51
+ .markdown-content {
52
+ color: rgba(255, 255, 255, 0.9);
53
+ line-height: 1.6;
54
+ word-wrap: break-word;
55
+ }
56
+
57
+ .markdown-content :global(p) {
58
+ margin: 0 0 0.5rem 0;
59
+ }
60
+
61
+ .markdown-content :global(p:last-child) {
62
+ margin-bottom: 0;
63
+ }
64
+
65
+ .markdown-content :global(h1),
66
+ .markdown-content :global(h2),
67
+ .markdown-content :global(h3),
68
+ .markdown-content :global(h4),
69
+ .markdown-content :global(h5),
70
+ .markdown-content :global(h6) {
71
+ margin: 0.75rem 0 0.5rem 0;
72
+ font-weight: 600;
73
+ color: rgba(255, 255, 255, 1);
74
+ }
75
+
76
+ .markdown-content :global(h1) {
77
+ font-size: 1.25rem;
78
+ }
79
+
80
+ .markdown-content :global(h2) {
81
+ font-size: 1.125rem;
82
+ }
83
+
84
+ .markdown-content :global(h3) {
85
+ font-size: 1rem;
86
+ }
87
+
88
+ .markdown-content :global(pre) {
89
+ background: rgba(0, 0, 0, 0.4);
90
+ border: 1px solid rgba(255, 255, 255, 0.1);
91
+ border-radius: 4px;
92
+ padding: 0.75rem;
93
+ margin: 0.5rem 0;
94
+ overflow-x: auto;
95
+ }
96
+
97
+ .markdown-content :global(code) {
98
+ font-family: "Monaco", "Menlo", "Consolas", monospace;
99
+ font-size: 0.85rem;
100
+ background: rgba(0, 0, 0, 0.3);
101
+ padding: 0.125rem 0.25rem;
102
+ border-radius: 3px;
103
+ color: rgba(255, 210, 30, 0.9);
104
+ }
105
+
106
+ .markdown-content :global(pre code) {
107
+ background: none;
108
+ padding: 0;
109
+ color: rgba(255, 255, 255, 0.9);
110
+ }
111
+
112
+ .markdown-content :global(ul),
113
+ .markdown-content :global(ol) {
114
+ margin: 0.5rem 0;
115
+ padding-left: 1.5rem;
116
+ }
117
+
118
+ .markdown-content :global(li) {
119
+ margin: 0.25rem 0;
120
+ }
121
+
122
+ .markdown-content :global(blockquote) {
123
+ border-left: 3px solid rgba(255, 255, 255, 0.3);
124
+ padding-left: 0.75rem;
125
+ margin: 0.5rem 0;
126
+ color: rgba(255, 255, 255, 0.7);
127
+ font-style: italic;
128
+ }
129
+
130
+ .markdown-content :global(a) {
131
+ color: rgba(65, 105, 225, 0.9);
132
+ text-decoration: none;
133
+ border-bottom: 1px solid transparent;
134
+ transition: border-color 0.2s;
135
+ }
136
+
137
+ .markdown-content :global(a:hover) {
138
+ border-bottom-color: rgba(65, 105, 225, 0.5);
139
+ }
140
+
141
+ .markdown-content :global(strong) {
142
+ font-weight: 600;
143
+ color: rgba(255, 255, 255, 1);
144
+ }
145
+
146
+ .markdown-content :global(em) {
147
+ font-style: italic;
148
+ }
149
+
150
+ .markdown-content :global(hr) {
151
+ border: none;
152
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
153
+ margin: 1rem 0;
154
+ }
155
+
156
+ .markdown-content :global(table) {
157
+ border-collapse: collapse;
158
+ width: 100%;
159
+ margin: 0.5rem 0;
160
+ }
161
+
162
+ .markdown-content :global(th),
163
+ .markdown-content :global(td) {
164
+ border: 1px solid rgba(255, 255, 255, 0.1);
165
+ padding: 0.5rem;
166
+ text-align: left;
167
+ }
168
+
169
+ .markdown-content :global(th) {
170
+ background: rgba(0, 0, 0, 0.3);
171
+ font-weight: 600;
172
+ }
173
+
174
+ .cursor {
175
+ animation: blink 1s infinite;
176
+ }
177
+
178
+ @keyframes blink {
179
+ 0%,
180
+ 50% {
181
+ opacity: 1;
182
+ }
183
+ 51%,
184
+ 100% {
185
+ opacity: 0;
186
+ }
187
+ }
188
+ </style>
src/lib/components/chat/ReasoningBlock.svelte ADDED
@@ -0,0 +1,237 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { onMount } from "svelte";
3
+ import gsap from "gsap";
4
+
5
+ export let reasoning: string;
6
+ export let isExpanded = false;
7
+
8
+ let iconElement: HTMLSpanElement;
9
+ let contentElement: HTMLDivElement;
10
+
11
+ function toggleExpanded() {
12
+ isExpanded = !isExpanded;
13
+
14
+ if (!iconElement || !contentElement) return;
15
+
16
+ if (isExpanded) {
17
+ // Expand - quick but smooth
18
+ gsap.to(iconElement, {
19
+ rotation: 180,
20
+ duration: 0.15,
21
+ ease: "power2.out"
22
+ });
23
+
24
+ // Show content
25
+ gsap.set(contentElement, {
26
+ display: 'block'
27
+ });
28
+
29
+ gsap.fromTo(contentElement, {
30
+ opacity: 0,
31
+ maxHeight: 0,
32
+ y: -10
33
+ }, {
34
+ opacity: 1,
35
+ maxHeight: 500, // Large enough for content
36
+ y: 0,
37
+ duration: 0.2,
38
+ ease: "power2.out"
39
+ });
40
+
41
+ } else {
42
+ // Collapse - nearly instant
43
+ gsap.to(iconElement, {
44
+ rotation: 0,
45
+ duration: 0.1,
46
+ ease: "power2.in"
47
+ });
48
+
49
+ gsap.to(contentElement, {
50
+ opacity: 0,
51
+ maxHeight: 0,
52
+ y: -5,
53
+ duration: 0.1,
54
+ ease: "power2.in",
55
+ onComplete: () => {
56
+ gsap.set(contentElement, { display: 'none' });
57
+ }
58
+ });
59
+ }
60
+ }
61
+
62
+ onMount(() => {
63
+ if (iconElement) {
64
+ gsap.set(iconElement, {
65
+ transformOrigin: "center",
66
+ rotation: isExpanded ? 180 : 0
67
+ });
68
+ }
69
+
70
+ if (contentElement) {
71
+ gsap.set(contentElement, {
72
+ display: isExpanded ? 'block' : 'none',
73
+ opacity: isExpanded ? 1 : 0,
74
+ maxHeight: isExpanded ? 500 : 0,
75
+ y: isExpanded ? 0 : -10
76
+ });
77
+ }
78
+ });
79
+ </script>
80
+
81
+ <div class="reasoning-block">
82
+ <button
83
+ class="reasoning-toggle"
84
+ on:click={toggleExpanded}
85
+ title={isExpanded ? "Hide AI thinking" : "Show AI thinking"}
86
+ aria-expanded={isExpanded}
87
+ aria-label="Toggle thinking visibility"
88
+ >
89
+ <div class="toggle-content">
90
+ <span bind:this={iconElement} class="toggle-icon">
91
+ <svg width="12" height="12" viewBox="0 0 12 12" fill="currentColor">
92
+ <path d="M3 4.5L6 7.5L9 4.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
93
+ </svg>
94
+ </span>
95
+ <span class="toggle-label">
96
+ <span class="label-main">Thinking</span>
97
+ <span class="label-hint">{isExpanded ? "Click to hide" : "Click to view"}</span>
98
+ </span>
99
+ </div>
100
+ </button>
101
+
102
+ <div
103
+ bind:this={contentElement}
104
+ class="reasoning-content"
105
+ >
106
+ <div class="content-inner">
107
+ <pre class="reasoning-text">{reasoning}</pre>
108
+ </div>
109
+ </div>
110
+ </div>
111
+
112
+ <style>
113
+ .reasoning-block {
114
+ margin: 0.75rem 0;
115
+ border-radius: 8px;
116
+ background: linear-gradient(135deg, rgba(70, 70, 80, 0.15) 0%, rgba(60, 60, 70, 0.15) 100%);
117
+ border: 1px solid rgba(120, 120, 140, 0.15);
118
+ overflow: hidden;
119
+ transition: border-color 0.2s ease, background 0.2s ease;
120
+ }
121
+
122
+ .reasoning-block:hover {
123
+ border-color: rgba(140, 140, 160, 0.25);
124
+ background: linear-gradient(135deg, rgba(75, 75, 85, 0.2) 0%, rgba(65, 65, 75, 0.2) 100%);
125
+ }
126
+
127
+ .reasoning-toggle {
128
+ width: 100%;
129
+ padding: 0.75rem 1rem;
130
+ background: transparent;
131
+ border: none;
132
+ color: rgba(255, 255, 255, 0.7);
133
+ cursor: pointer;
134
+ font-family: inherit;
135
+ text-align: left;
136
+ transition: all 0.2s ease;
137
+ }
138
+
139
+ .reasoning-toggle:hover {
140
+ background: rgba(255, 255, 255, 0.03);
141
+ color: rgba(255, 255, 255, 0.9);
142
+ }
143
+
144
+ .reasoning-toggle:focus-visible {
145
+ outline: 2px solid rgba(140, 140, 200, 0.5);
146
+ outline-offset: -2px;
147
+ }
148
+
149
+ .toggle-content {
150
+ display: flex;
151
+ align-items: center;
152
+ gap: 0.75rem;
153
+ }
154
+
155
+ .toggle-icon {
156
+ display: flex;
157
+ align-items: center;
158
+ justify-content: center;
159
+ width: 20px;
160
+ height: 20px;
161
+ border-radius: 4px;
162
+ background: rgba(255, 255, 255, 0.05);
163
+ color: rgba(255, 255, 255, 0.5);
164
+ transition: background 0.2s ease, color 0.2s ease;
165
+ }
166
+
167
+ .reasoning-toggle:hover .toggle-icon {
168
+ background: rgba(255, 255, 255, 0.08);
169
+ color: rgba(255, 255, 255, 0.7);
170
+ }
171
+
172
+ .toggle-label {
173
+ display: flex;
174
+ flex-direction: column;
175
+ gap: 0.125rem;
176
+ }
177
+
178
+ .label-main {
179
+ font-size: 0.875rem;
180
+ font-weight: 500;
181
+ letter-spacing: 0.01em;
182
+ }
183
+
184
+ .label-hint {
185
+ font-size: 0.7rem;
186
+ color: rgba(255, 255, 255, 0.4);
187
+ transition: color 0.2s ease;
188
+ }
189
+
190
+ .reasoning-toggle:hover .label-hint {
191
+ color: rgba(255, 255, 255, 0.5);
192
+ }
193
+
194
+ .reasoning-content {
195
+ border-top: 1px solid rgba(100, 100, 100, 0.15);
196
+ background: rgba(30, 30, 35, 0.3);
197
+ overflow: hidden;
198
+ }
199
+
200
+ .content-inner {
201
+ padding: 0.75rem 1rem;
202
+ }
203
+
204
+ .reasoning-text {
205
+ margin: 0;
206
+ padding: 0.75rem;
207
+ background: rgba(20, 20, 25, 0.4);
208
+ border-radius: 6px;
209
+ font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;
210
+ font-size: 0.75rem;
211
+ line-height: 1.5;
212
+ color: rgba(255, 255, 255, 0.6);
213
+ white-space: pre-wrap;
214
+ word-wrap: break-word;
215
+ max-height: 350px;
216
+ overflow-y: auto;
217
+ }
218
+
219
+ .reasoning-text::-webkit-scrollbar {
220
+ width: 8px;
221
+ }
222
+
223
+ .reasoning-text::-webkit-scrollbar-track {
224
+ background: rgba(30, 30, 35, 0.5);
225
+ border-radius: 4px;
226
+ }
227
+
228
+ .reasoning-text::-webkit-scrollbar-thumb {
229
+ background: rgba(100, 100, 110, 0.4);
230
+ border-radius: 4px;
231
+ transition: background 0.2s ease;
232
+ }
233
+
234
+ .reasoning-text::-webkit-scrollbar-thumb:hover {
235
+ background: rgba(120, 120, 130, 0.5);
236
+ }
237
+ </style>
src/lib/components/chat/ToolCallDisplay.svelte ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { onMount } from "svelte";
3
+ import gsap from "gsap";
4
+
5
+ export let toolName: string;
6
+ export let parameters: any = null;
7
+
8
+ let element: HTMLDivElement;
9
+
10
+ onMount(() => {
11
+ gsap.fromTo(
12
+ element,
13
+ { opacity: 0, scale: 0.9, y: -5 },
14
+ { opacity: 1, scale: 1, y: 0, duration: 0.3, ease: "back.out(1.5)" }
15
+ );
16
+ });
17
+
18
+ const toolIcons: Record<string, string> = {
19
+ read_game_code: "📄",
20
+ edit_game_code: "✏️",
21
+ read_console: "📟",
22
+ clear_console: "🧹",
23
+ run_game: "▶️",
24
+ stop_game: "⏹",
25
+ default: "🔧",
26
+ };
27
+
28
+ $: icon = toolIcons[toolName] || toolIcons.default;
29
+ </script>
30
+
31
+ <div class="tool-call" bind:this={element}>
32
+ <span class="tool-icon">{icon}</span>
33
+ <span class="tool-name">{toolName}</span>
34
+ {#if parameters && Object.keys(parameters).length > 0}
35
+ <span class="tool-params">
36
+ {#each Object.entries(parameters) as [key, value]}
37
+ <span class="param">
38
+ <span class="param-key">{key}:</span>
39
+ <span class="param-value">{typeof value === 'string' && value.length > 20 ? value.substring(0, 20) + '...' : value}</span>
40
+ </span>
41
+ {/each}
42
+ </span>
43
+ {/if}
44
+ </div>
45
+
46
+ <style>
47
+ .tool-call {
48
+ display: inline-flex;
49
+ align-items: center;
50
+ gap: 0.5rem;
51
+ background: rgba(65, 105, 225, 0.1);
52
+ border: 1px solid rgba(65, 105, 225, 0.3);
53
+ border-radius: 4px;
54
+ padding: 0.25rem 0.5rem;
55
+ margin: 0.25rem 0;
56
+ font-size: 0.8rem;
57
+ font-family: "Monaco", "Menlo", monospace;
58
+ }
59
+
60
+ .tool-icon {
61
+ font-size: 1rem;
62
+ line-height: 1;
63
+ }
64
+
65
+ .tool-name {
66
+ color: rgba(65, 105, 225, 1);
67
+ font-weight: 600;
68
+ }
69
+
70
+ .tool-params {
71
+ display: flex;
72
+ gap: 0.5rem;
73
+ color: rgba(255, 255, 255, 0.6);
74
+ font-size: 0.75rem;
75
+ }
76
+
77
+ .param {
78
+ display: flex;
79
+ gap: 0.25rem;
80
+ }
81
+
82
+ .param-key {
83
+ color: rgba(255, 255, 255, 0.5);
84
+ }
85
+
86
+ .param-value {
87
+ color: rgba(255, 210, 30, 0.7);
88
+ }
89
+ </style>
src/lib/components/chat/context.md CHANGED
@@ -1,14 +1,19 @@
1
  # Chat Context
2
 
3
- AI assistant interface with authentication-gated input
4
 
5
  ## Components
6
 
7
- - `ChatPanel.svelte` - Chat UI with auth-aware input controls
 
 
 
8
 
9
  ## Features
10
 
11
- - Authentication-required input (visible but disabled when not logged in)
12
- - Real-time message streaming when connected
13
- - Breathing input animation when ready
14
- - GSAP-animated button interactions
 
 
 
1
  # Chat Context
2
 
3
+ AI chat interface with markdown rendering and tool visualization.
4
 
5
  ## Components
6
 
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
src/lib/components/layout/SplitView.svelte CHANGED
@@ -1,355 +1,427 @@
1
  <script lang="ts">
2
- import { onMount } from 'svelte';
3
- import { Splitpanes, Pane } from 'svelte-splitpanes';
4
- import { uiStore } from '../../stores/ui';
5
- import CodeEditor from '../editor/CodeEditor.svelte';
6
- import GameCanvas from '../game/GameCanvas.svelte';
7
- import ConsolePanel from '../console/ConsolePanel.svelte';
8
- import ChatPanel from '../chat/ChatPanel.svelte';
9
- import gsap from 'gsap';
10
-
11
- $: viewMode = $uiStore.viewMode;
12
-
13
- let userMainPaneSizes = { editor: 0.4, preview: 0.6 };
14
- let userEditorPaneSizes = { code: 0.7, chat: 0.3 };
15
- let userPreviewPaneSizes = { game: 0.75, console: 0.25 };
16
-
17
- onMount(() => {
18
- gsap.fromTo(".editor-pane",
19
- { opacity: 0, x: -20 },
20
- { opacity: 1, x: 0, duration: 0.6, delay: 0.1, ease: "power3.out" }
21
- );
22
-
23
- gsap.fromTo(".preview-pane",
24
- { opacity: 0, x: 20 },
25
- { opacity: 1, x: 0, duration: 0.6, delay: 0.2, ease: "power3.out" }
26
- );
27
-
28
- const resetCursor = () => {
29
- document.body.style.cursor = '';
30
- };
31
- document.addEventListener('mouseup', resetCursor);
32
-
33
- return () => {
34
- document.removeEventListener('mouseup', resetCursor);
35
- };
36
- });
37
-
38
- $: if (viewMode) {
39
- handleViewModeAnimation(viewMode);
40
- }
41
-
42
- function handleViewModeAnimation(mode: 'code' | 'preview') {
43
- const mainPanes = document.querySelector('.main-splitpanes')?.querySelectorAll(':scope > .splitpanes__pane');
44
- if (!mainPanes || mainPanes.length < 2) return;
45
-
46
- const editorPaneEl = mainPanes[0] as HTMLElement;
47
- const previewPaneEl = mainPanes[1] as HTMLElement;
48
- const mainSplitter = document.querySelector('.main-splitpanes > .splitpanes__splitter') as HTMLElement;
49
-
50
- if (!editorPaneEl || !previewPaneEl) return;
51
-
52
- const editorSplitpanes = editorPaneEl.querySelectorAll('.splitpanes__pane');
53
- const codePane = editorSplitpanes[0] as HTMLElement;
54
- const chatPane = editorSplitpanes[1] as HTMLElement;
55
-
56
- const previewSplitpanes = previewPaneEl.querySelectorAll('.splitpanes__pane');
57
- const gamePane = previewSplitpanes[0] as HTMLElement;
58
- const consolePane = previewSplitpanes[1] as HTMLElement;
59
- const consoleSplitter = previewPaneEl.querySelector('.splitpanes__splitter') as HTMLElement;
60
-
61
- if (mode === 'preview') {
62
- const editorWidth = editorPaneEl.offsetWidth;
63
- const previewWidth = previewPaneEl.offsetWidth;
64
- const totalWidth = editorWidth + previewWidth;
65
-
66
- if (totalWidth > 0) {
67
- userMainPaneSizes.editor = editorWidth / totalWidth;
68
- userMainPaneSizes.preview = previewWidth / totalWidth;
69
- }
70
-
71
- if (codePane && chatPane) {
72
- const codeHeight = codePane.offsetHeight;
73
- const chatHeight = chatPane.offsetHeight;
74
- const totalEditorHeight = codeHeight + chatHeight;
75
-
76
- if (totalEditorHeight > 0) {
77
- userEditorPaneSizes.code = codeHeight / totalEditorHeight;
78
- userEditorPaneSizes.chat = chatHeight / totalEditorHeight;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  }
80
- }
81
-
82
- if (gamePane && consolePane) {
83
- const gameHeight = gamePane.offsetHeight;
84
- const consoleHeight = consolePane.offsetHeight;
85
- const totalHeight = gameHeight + consoleHeight;
86
-
87
- if (totalHeight > 0) {
88
- userPreviewPaneSizes.game = gameHeight / totalHeight;
89
- userPreviewPaneSizes.console = consoleHeight / totalHeight;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  }
91
- }
92
-
93
- gsap.set(editorPaneEl, { flexGrow: userMainPaneSizes.editor, flexBasis: `${userMainPaneSizes.editor * 100}%` });
94
- gsap.set(previewPaneEl, { flexGrow: userMainPaneSizes.preview, flexBasis: `${userMainPaneSizes.preview * 100}%` });
95
-
96
- if (codePane && chatPane) {
97
- gsap.set(codePane, { flexGrow: userEditorPaneSizes.code, flexBasis: `${userEditorPaneSizes.code * 100}%` });
98
- gsap.set(chatPane, { flexGrow: userEditorPaneSizes.chat, flexBasis: `${userEditorPaneSizes.chat * 100}%` });
99
- }
100
-
101
- if (gamePane && consolePane) {
102
- gsap.set(gamePane, { flexGrow: userPreviewPaneSizes.game, flexBasis: `${userPreviewPaneSizes.game * 100}%` });
103
- gsap.set(consolePane, { flexGrow: userPreviewPaneSizes.console, flexBasis: `${userPreviewPaneSizes.console * 100}%` });
104
- }
105
  }
106
-
107
- if (mode === 'preview') {
108
- gsap.timeline({ ease: "power2.out" })
109
- .to(editorPaneEl, {
110
- opacity: 0,
111
- duration: 0.15,
112
- })
113
- .to(consolePane, {
114
- opacity: 0,
115
- duration: 0.15,
116
- }, "-=0.15")
117
- .to(editorPaneEl, {
118
- flexGrow: 0,
119
- flexBasis: "0%",
120
- duration: 0.2,
121
- ease: "power2.inOut",
122
- onComplete: () => {
123
- editorPaneEl.style.display = 'none';
124
- if (mainSplitter) mainSplitter.style.display = 'none';
125
- }
126
- }, "-=0.1")
127
- .to(consolePane, {
128
- flexGrow: 0,
129
- flexBasis: "0%",
130
- duration: 0.2,
131
- ease: "power2.inOut",
132
- onComplete: () => {
133
- consolePane.style.display = 'none';
134
- if (consoleSplitter) consoleSplitter.style.display = 'none';
135
- }
136
- }, "-=0.2")
137
- .to(gamePane, {
138
- flexGrow: 1,
139
- flexBasis: "100%",
140
- duration: 0.2,
141
- ease: "power2.inOut",
142
- }, "-=0.2")
143
- .to(previewPaneEl, {
144
- flexGrow: 1,
145
- flexBasis: "100%",
146
- duration: 0.2,
147
- ease: "power2.inOut",
148
- }, "-=0.2");
149
- } else {
150
- editorPaneEl.style.display = '';
151
- if (mainSplitter) mainSplitter.style.display = '';
152
- consolePane.style.display = '';
153
- if (consoleSplitter) consoleSplitter.style.display = '';
154
-
155
- gsap.timeline({ ease: "power2.out" })
156
- .set(editorPaneEl, {
157
- opacity: 0,
158
- flexGrow: 0,
159
- flexBasis: "0%"
160
- })
161
- .set(consolePane, {
162
- opacity: 0,
163
- flexGrow: 0,
164
- flexBasis: "0%"
165
- })
166
- .to([editorPaneEl, previewPaneEl], {
167
- flexGrow: (i) => i === 0 ? userMainPaneSizes.editor : userMainPaneSizes.preview,
168
- flexBasis: (i) => i === 0 ? `${userMainPaneSizes.editor * 100}%` : `${userMainPaneSizes.preview * 100}%`,
169
- duration: 0.2,
170
- ease: "power2.inOut",
171
- })
172
- .to([codePane, chatPane], {
173
- flexGrow: (i) => i === 0 ? userEditorPaneSizes.code : userEditorPaneSizes.chat,
174
- flexBasis: (i) => i === 0 ? `${userEditorPaneSizes.code * 100}%` : `${userEditorPaneSizes.chat * 100}%`,
175
- duration: 0.2,
176
- ease: "power2.inOut",
177
- }, "-=0.2")
178
- .to([gamePane, consolePane], {
179
- flexGrow: (i) => i === 0 ? userPreviewPaneSizes.game : userPreviewPaneSizes.console,
180
- flexBasis: (i) => i === 0 ? `${userPreviewPaneSizes.game * 100}%` : `${userPreviewPaneSizes.console * 100}%`,
181
- duration: 0.2,
182
- ease: "power2.inOut",
183
- }, "-=0.2")
184
- .to(consolePane, {
185
- opacity: 1,
186
- duration: 0.15,
187
- }, "-=0.15")
188
- .to(editorPaneEl, {
189
- opacity: 1,
190
- duration: 0.15,
191
- onComplete: () => {
192
- window.dispatchEvent(new Event('resize'));
193
- }
194
- }, "-=0.15");
195
- }
196
- }
197
  </script>
198
 
199
  <div class="editor-layout">
200
- <Splitpanes class="main-splitpanes" theme="modern-theme">
201
- <Pane minSize={20} size={40} class="editor-pane">
202
- <div class="editor-panel">
203
- <Splitpanes horizontal class="editor-splitpanes" theme="modern-theme">
204
- <Pane minSize={30} size={70} class="code-pane">
205
- <CodeEditor />
206
- </Pane>
207
- <Pane minSize={15} size={30} class="chat-pane">
208
- <ChatPanel />
209
- </Pane>
210
- </Splitpanes>
211
- </div>
212
- </Pane>
213
- <Pane minSize={25} size={60} class="preview-pane">
214
- <div class="preview-panel">
215
- <Splitpanes horizontal class="preview-splitpanes" theme="modern-theme">
216
- <Pane minSize={30} class="game-pane">
217
- <GameCanvas />
218
- </Pane>
219
- <Pane minSize={15} size={25} class="console-pane">
220
- <ConsolePanel />
221
- </Pane>
222
- </Splitpanes>
223
- </div>
224
- </Pane>
225
- </Splitpanes>
226
  </div>
227
 
228
  <style>
229
- .editor-layout {
230
- flex: 1;
231
- display: flex;
232
- min-height: 0;
233
- background: rgba(139, 115, 85, 0.02);
234
- }
235
-
236
- :global(.main-splitpanes) {
237
- width: 100%;
238
- height: 100%;
239
- }
240
-
241
- :global(.editor-splitpanes) {
242
- width: 100%;
243
- height: 100%;
244
- }
245
-
246
- :global(.preview-splitpanes) {
247
- width: 100%;
248
- height: 100%;
249
- }
250
-
251
- :global(.editor-pane) {
252
- display: flex;
253
- overflow: hidden;
254
- }
255
-
256
- :global(.preview-pane) {
257
- display: flex;
258
- overflow: hidden;
259
- }
260
-
261
- :global(.code-pane) {
262
- display: flex;
263
- overflow: hidden;
264
- }
265
-
266
- :global(.game-pane) {
267
- display: flex;
268
- overflow: hidden;
269
- }
270
-
271
- :global(.console-pane) {
272
- display: flex;
273
- overflow: hidden;
274
- }
275
-
276
- :global(.chat-pane) {
277
- display: flex;
278
- overflow: hidden;
279
- }
280
-
281
- .editor-panel {
282
- width: 100%;
283
- height: 100%;
284
- display: flex;
285
- flex-direction: column;
286
- background: rgba(11, 10, 9, 0.3);
287
- min-width: 0;
288
- overflow: hidden;
289
- }
290
-
291
- .preview-panel {
292
- width: 100%;
293
- height: 100%;
294
- display: flex;
295
- flex-direction: column;
296
- background: rgba(11, 10, 9, 0.3);
297
- min-width: 0;
298
- overflow: hidden;
299
- }
300
-
301
- :global(.splitpanes.modern-theme .splitpanes__splitter) {
302
- background: rgba(139, 115, 85, 0.06);
303
- position: relative;
304
- transition: background 0.2s;
305
- }
306
-
307
- :global(.splitpanes.modern-theme .splitpanes__splitter:hover) {
308
- background: rgba(139, 115, 85, 0.12);
309
- }
310
-
311
- :global(.splitpanes.modern-theme .splitpanes__splitter:before) {
312
- content: '';
313
- position: absolute;
314
- z-index: 1;
315
- transition: opacity 0.2s;
316
- background: rgba(124, 152, 133, 0.1);
317
- opacity: 0;
318
- }
319
-
320
- :global(.splitpanes.modern-theme .splitpanes__splitter:hover:before) {
321
- opacity: 1;
322
- }
323
-
324
- :global(.splitpanes--vertical.modern-theme > .splitpanes__splitter) {
325
- width: 1px;
326
- cursor: col-resize;
327
- }
328
-
329
- :global(.splitpanes--vertical.modern-theme > .splitpanes__splitter:before) {
330
- left: -3px;
331
- right: -3px;
332
- height: 100%;
333
- cursor: col-resize;
334
- }
335
-
336
- :global(.splitpanes--horizontal.modern-theme > .splitpanes__splitter) {
337
- height: 1px;
338
- cursor: row-resize;
339
- }
340
-
341
- :global(.splitpanes--horizontal.modern-theme > .splitpanes__splitter:before) {
342
- top: -3px;
343
- bottom: -3px;
344
- width: 100%;
345
- cursor: row-resize;
346
- }
347
-
348
- :global(body.splitpanes--dragging) {
349
- cursor: inherit;
350
- }
351
-
352
- :global(body:not(.splitpanes--dragging) .splitpanes__splitter:not(:hover)) {
353
- cursor: default;
354
- }
355
  </style>
 
1
  <script lang="ts">
2
+ import { onMount } from "svelte";
3
+ import { Splitpanes, Pane } from "svelte-splitpanes";
4
+ import { uiStore } from "../../stores/ui";
5
+ import CodeEditor from "../editor/CodeEditor.svelte";
6
+ import GameCanvas from "../game/GameCanvas.svelte";
7
+ import ConsolePanel from "../console/ConsolePanel.svelte";
8
+ import ChatPanel from "../chat/ChatPanel.svelte";
9
+ import gsap from "gsap";
10
+
11
+ $: viewMode = $uiStore.viewMode;
12
+
13
+ let userMainPaneSizes = { editor: 0.4, preview: 0.6 };
14
+ let userEditorPaneSizes = { code: 0.5, chat: 0.5 };
15
+ let userPreviewPaneSizes = { game: 0.75, console: 0.25 };
16
+
17
+ onMount(() => {
18
+ gsap.fromTo(
19
+ ".editor-pane",
20
+ { opacity: 0, x: -20 },
21
+ { opacity: 1, x: 0, duration: 0.6, delay: 0.1, ease: "power3.out" },
22
+ );
23
+
24
+ gsap.fromTo(
25
+ ".preview-pane",
26
+ { opacity: 0, x: 20 },
27
+ { opacity: 1, x: 0, duration: 0.6, delay: 0.2, ease: "power3.out" },
28
+ );
29
+
30
+ const resetCursor = () => {
31
+ document.body.style.cursor = "";
32
+ };
33
+ document.addEventListener("mouseup", resetCursor);
34
+
35
+ return () => {
36
+ document.removeEventListener("mouseup", resetCursor);
37
+ };
38
+ });
39
+
40
+ $: if (viewMode) {
41
+ handleViewModeAnimation(viewMode);
42
+ }
43
+
44
+ function handleViewModeAnimation(mode: "code" | "preview") {
45
+ const mainPanes = document
46
+ .querySelector(".main-splitpanes")
47
+ ?.querySelectorAll(":scope > .splitpanes__pane");
48
+ if (!mainPanes || mainPanes.length < 2) return;
49
+
50
+ const editorPaneEl = mainPanes[0] as HTMLElement;
51
+ const previewPaneEl = mainPanes[1] as HTMLElement;
52
+ const mainSplitter = document.querySelector(
53
+ ".main-splitpanes > .splitpanes__splitter",
54
+ ) as HTMLElement;
55
+
56
+ if (!editorPaneEl || !previewPaneEl) return;
57
+
58
+ const editorSplitpanes = editorPaneEl.querySelectorAll(".splitpanes__pane");
59
+ const codePane = editorSplitpanes[0] as HTMLElement;
60
+ const chatPane = editorSplitpanes[1] as HTMLElement;
61
+
62
+ const previewSplitpanes = previewPaneEl.querySelectorAll(".splitpanes__pane");
63
+ const gamePane = previewSplitpanes[0] as HTMLElement;
64
+ const consolePane = previewSplitpanes[1] as HTMLElement;
65
+ const consoleSplitter = previewPaneEl.querySelector(".splitpanes__splitter") as HTMLElement;
66
+
67
+ if (mode === "preview") {
68
+ const editorWidth = editorPaneEl.offsetWidth;
69
+ const previewWidth = previewPaneEl.offsetWidth;
70
+ const totalWidth = editorWidth + previewWidth;
71
+
72
+ if (totalWidth > 0) {
73
+ userMainPaneSizes.editor = editorWidth / totalWidth;
74
+ userMainPaneSizes.preview = previewWidth / totalWidth;
75
+ }
76
+
77
+ if (codePane && chatPane) {
78
+ const codeHeight = codePane.offsetHeight;
79
+ const chatHeight = chatPane.offsetHeight;
80
+ const totalEditorHeight = codeHeight + chatHeight;
81
+
82
+ if (totalEditorHeight > 0) {
83
+ userEditorPaneSizes.code = codeHeight / totalEditorHeight;
84
+ userEditorPaneSizes.chat = chatHeight / totalEditorHeight;
85
+ }
86
+ }
87
+
88
+ if (gamePane && consolePane) {
89
+ const gameHeight = gamePane.offsetHeight;
90
+ const consoleHeight = consolePane.offsetHeight;
91
+ const totalHeight = gameHeight + consoleHeight;
92
+
93
+ if (totalHeight > 0) {
94
+ userPreviewPaneSizes.game = gameHeight / totalHeight;
95
+ userPreviewPaneSizes.console = consoleHeight / totalHeight;
96
+ }
97
+ }
98
+
99
+ gsap.set(editorPaneEl, {
100
+ flexGrow: userMainPaneSizes.editor,
101
+ flexBasis: `${userMainPaneSizes.editor * 100}%`,
102
+ });
103
+ gsap.set(previewPaneEl, {
104
+ flexGrow: userMainPaneSizes.preview,
105
+ flexBasis: `${userMainPaneSizes.preview * 100}%`,
106
+ });
107
+
108
+ if (codePane && chatPane) {
109
+ gsap.set(codePane, {
110
+ flexGrow: userEditorPaneSizes.code,
111
+ flexBasis: `${userEditorPaneSizes.code * 100}%`,
112
+ });
113
+ gsap.set(chatPane, {
114
+ flexGrow: userEditorPaneSizes.chat,
115
+ flexBasis: `${userEditorPaneSizes.chat * 100}%`,
116
+ });
117
+ }
118
+
119
+ if (gamePane && consolePane) {
120
+ gsap.set(gamePane, {
121
+ flexGrow: userPreviewPaneSizes.game,
122
+ flexBasis: `${userPreviewPaneSizes.game * 100}%`,
123
+ });
124
+ gsap.set(consolePane, {
125
+ flexGrow: userPreviewPaneSizes.console,
126
+ flexBasis: `${userPreviewPaneSizes.console * 100}%`,
127
+ });
128
+ }
129
  }
130
+
131
+ if (mode === "preview") {
132
+ gsap.timeline({ ease: "power2.out" })
133
+ .to(editorPaneEl, {
134
+ opacity: 0,
135
+ duration: 0.15,
136
+ })
137
+ .to(
138
+ consolePane,
139
+ {
140
+ opacity: 0,
141
+ duration: 0.15,
142
+ },
143
+ "-=0.15",
144
+ )
145
+ .to(
146
+ editorPaneEl,
147
+ {
148
+ flexGrow: 0,
149
+ flexBasis: "0%",
150
+ duration: 0.2,
151
+ ease: "power2.inOut",
152
+ onComplete: () => {
153
+ editorPaneEl.style.display = "none";
154
+ if (mainSplitter) mainSplitter.style.display = "none";
155
+ },
156
+ },
157
+ "-=0.1",
158
+ )
159
+ .to(
160
+ consolePane,
161
+ {
162
+ flexGrow: 0,
163
+ flexBasis: "0%",
164
+ duration: 0.2,
165
+ ease: "power2.inOut",
166
+ onComplete: () => {
167
+ consolePane.style.display = "none";
168
+ if (consoleSplitter) consoleSplitter.style.display = "none";
169
+ },
170
+ },
171
+ "-=0.2",
172
+ )
173
+ .to(
174
+ gamePane,
175
+ {
176
+ flexGrow: 1,
177
+ flexBasis: "100%",
178
+ duration: 0.2,
179
+ ease: "power2.inOut",
180
+ },
181
+ "-=0.2",
182
+ )
183
+ .to(
184
+ previewPaneEl,
185
+ {
186
+ flexGrow: 1,
187
+ flexBasis: "100%",
188
+ duration: 0.2,
189
+ ease: "power2.inOut",
190
+ },
191
+ "-=0.2",
192
+ );
193
+ } else {
194
+ editorPaneEl.style.display = "";
195
+ if (mainSplitter) mainSplitter.style.display = "";
196
+ consolePane.style.display = "";
197
+ if (consoleSplitter) consoleSplitter.style.display = "";
198
+
199
+ gsap.timeline({ ease: "power2.out" })
200
+ .set(editorPaneEl, {
201
+ opacity: 0,
202
+ flexGrow: 0,
203
+ flexBasis: "0%",
204
+ })
205
+ .set(consolePane, {
206
+ opacity: 0,
207
+ flexGrow: 0,
208
+ flexBasis: "0%",
209
+ })
210
+ .to([editorPaneEl, previewPaneEl], {
211
+ flexGrow: (i) =>
212
+ i === 0 ? userMainPaneSizes.editor : userMainPaneSizes.preview,
213
+ flexBasis: (i) =>
214
+ i === 0
215
+ ? `${userMainPaneSizes.editor * 100}%`
216
+ : `${userMainPaneSizes.preview * 100}%`,
217
+ duration: 0.2,
218
+ ease: "power2.inOut",
219
+ })
220
+ .to(
221
+ [codePane, chatPane],
222
+ {
223
+ flexGrow: (i) =>
224
+ i === 0 ? userEditorPaneSizes.code : userEditorPaneSizes.chat,
225
+ flexBasis: (i) =>
226
+ i === 0
227
+ ? `${userEditorPaneSizes.code * 100}%`
228
+ : `${userEditorPaneSizes.chat * 100}%`,
229
+ duration: 0.2,
230
+ ease: "power2.inOut",
231
+ },
232
+ "-=0.2",
233
+ )
234
+ .to(
235
+ [gamePane, consolePane],
236
+ {
237
+ flexGrow: (i) =>
238
+ i === 0 ? userPreviewPaneSizes.game : userPreviewPaneSizes.console,
239
+ flexBasis: (i) =>
240
+ i === 0
241
+ ? `${userPreviewPaneSizes.game * 100}%`
242
+ : `${userPreviewPaneSizes.console * 100}%`,
243
+ duration: 0.2,
244
+ ease: "power2.inOut",
245
+ },
246
+ "-=0.2",
247
+ )
248
+ .to(
249
+ consolePane,
250
+ {
251
+ opacity: 1,
252
+ duration: 0.15,
253
+ },
254
+ "-=0.15",
255
+ )
256
+ .to(
257
+ editorPaneEl,
258
+ {
259
+ opacity: 1,
260
+ duration: 0.15,
261
+ onComplete: () => {
262
+ window.dispatchEvent(new Event("resize"));
263
+ },
264
+ },
265
+ "-=0.15",
266
+ );
267
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
268
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
269
  </script>
270
 
271
  <div class="editor-layout">
272
+ <Splitpanes class="main-splitpanes" theme="modern-theme">
273
+ <Pane minSize={20} size={40} class="editor-pane">
274
+ <div class="editor-panel">
275
+ <Splitpanes horizontal class="editor-splitpanes" theme="modern-theme">
276
+ <Pane minSize={30} size={userEditorPaneSizes.code * 100} class="code-pane">
277
+ <CodeEditor />
278
+ </Pane>
279
+ <Pane minSize={15} size={userEditorPaneSizes.chat * 100} class="chat-pane">
280
+ <ChatPanel />
281
+ </Pane>
282
+ </Splitpanes>
283
+ </div>
284
+ </Pane>
285
+ <Pane minSize={25} size={60} class="preview-pane">
286
+ <div class="preview-panel">
287
+ <Splitpanes horizontal class="preview-splitpanes" theme="modern-theme">
288
+ <Pane minSize={30} class="game-pane">
289
+ <GameCanvas />
290
+ </Pane>
291
+ <Pane minSize={15} size={25} class="console-pane">
292
+ <ConsolePanel />
293
+ </Pane>
294
+ </Splitpanes>
295
+ </div>
296
+ </Pane>
297
+ </Splitpanes>
298
  </div>
299
 
300
  <style>
301
+ .editor-layout {
302
+ flex: 1;
303
+ display: flex;
304
+ min-height: 0;
305
+ background: rgba(139, 115, 85, 0.02);
306
+ }
307
+
308
+ :global(.main-splitpanes) {
309
+ width: 100%;
310
+ height: 100%;
311
+ }
312
+
313
+ :global(.editor-splitpanes) {
314
+ width: 100%;
315
+ height: 100%;
316
+ }
317
+
318
+ :global(.preview-splitpanes) {
319
+ width: 100%;
320
+ height: 100%;
321
+ }
322
+
323
+ :global(.editor-pane) {
324
+ display: flex;
325
+ overflow: hidden;
326
+ }
327
+
328
+ :global(.preview-pane) {
329
+ display: flex;
330
+ overflow: hidden;
331
+ }
332
+
333
+ :global(.code-pane) {
334
+ display: flex;
335
+ overflow: hidden;
336
+ }
337
+
338
+ :global(.game-pane) {
339
+ display: flex;
340
+ overflow: hidden;
341
+ }
342
+
343
+ :global(.console-pane) {
344
+ display: flex;
345
+ overflow: hidden;
346
+ }
347
+
348
+ :global(.chat-pane) {
349
+ display: flex;
350
+ overflow: hidden;
351
+ }
352
+
353
+ .editor-panel {
354
+ width: 100%;
355
+ height: 100%;
356
+ display: flex;
357
+ flex-direction: column;
358
+ background: rgba(11, 10, 9, 0.3);
359
+ min-width: 0;
360
+ overflow: hidden;
361
+ }
362
+
363
+ .preview-panel {
364
+ width: 100%;
365
+ height: 100%;
366
+ display: flex;
367
+ flex-direction: column;
368
+ background: rgba(11, 10, 9, 0.3);
369
+ min-width: 0;
370
+ overflow: hidden;
371
+ }
372
+
373
+ :global(.splitpanes.modern-theme .splitpanes__splitter) {
374
+ background: rgba(139, 115, 85, 0.06);
375
+ position: relative;
376
+ transition: background 0.2s;
377
+ }
378
+
379
+ :global(.splitpanes.modern-theme .splitpanes__splitter:hover) {
380
+ background: rgba(139, 115, 85, 0.12);
381
+ }
382
+
383
+ :global(.splitpanes.modern-theme .splitpanes__splitter:before) {
384
+ content: "";
385
+ position: absolute;
386
+ z-index: 1;
387
+ transition: opacity 0.2s;
388
+ background: rgba(124, 152, 133, 0.1);
389
+ opacity: 0;
390
+ }
391
+
392
+ :global(.splitpanes.modern-theme .splitpanes__splitter:hover:before) {
393
+ opacity: 1;
394
+ }
395
+
396
+ :global(.splitpanes--vertical.modern-theme > .splitpanes__splitter) {
397
+ width: 1px;
398
+ cursor: col-resize;
399
+ }
400
+
401
+ :global(.splitpanes--vertical.modern-theme > .splitpanes__splitter:before) {
402
+ left: -3px;
403
+ right: -3px;
404
+ height: 100%;
405
+ cursor: col-resize;
406
+ }
407
+
408
+ :global(.splitpanes--horizontal.modern-theme > .splitpanes__splitter) {
409
+ height: 1px;
410
+ cursor: row-resize;
411
+ }
412
+
413
+ :global(.splitpanes--horizontal.modern-theme > .splitpanes__splitter:before) {
414
+ top: -3px;
415
+ bottom: -3px;
416
+ width: 100%;
417
+ cursor: row-resize;
418
+ }
419
+
420
+ :global(body.splitpanes--dragging) {
421
+ cursor: inherit;
422
+ }
423
+
424
+ :global(body:not(.splitpanes--dragging) .splitpanes__splitter:not(:hover)) {
425
+ cursor: default;
426
+ }
427
  </style>
src/lib/server/agent-config.ts ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export interface AgentConfig {
2
+ model?: string;
3
+ maxIterations?: number;
4
+ temperature?: number;
5
+ maxTokens?: number;
6
+ }
7
+
8
+ export const DEFAULT_CONFIG: Required<AgentConfig> = {
9
+ model: "Qwen/Qwen3-Next-80B-A3B-Instruct",
10
+ maxIterations: 10,
11
+ temperature: 0.5,
12
+ maxTokens: 1200,
13
+ };
14
+
15
+ export const mergeConfig = (
16
+ config: AgentConfig = {},
17
+ ): Required<AgentConfig> => {
18
+ return { ...DEFAULT_CONFIG, ...config };
19
+ };
src/lib/server/agent-runner.test.ts ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { describe, expect, test, beforeAll } from "bun:test";
2
+ import { AgentRunner } from "./agent-runner";
3
+ import "../tools"; // Import to register tools
4
+
5
+ describe("AgentRunner Tool Execution", () => {
6
+ beforeAll(() => {
7
+ // Create runner instance for test context
8
+ new AgentRunner({
9
+ model: "test-model",
10
+ temperature: 0.7,
11
+ maxTokens: 1000,
12
+ });
13
+ });
14
+
15
+ describe("Tool Detection", () => {
16
+ test("should detect tool calls in response", async () => {
17
+ const { parseToolCalls } = await import("../tools");
18
+
19
+ // Test various agent responses
20
+ const responses = [
21
+ {
22
+ text: "I'll read the game code for you.\n\n[TOOL: read_game_code]\n\nAnalyzing...",
23
+ expected: ["read_game_code"],
24
+ },
25
+ {
26
+ text: "Let me check that. [TOOL: read_game_code] Now I can see the code.",
27
+ expected: ["read_game_code"],
28
+ },
29
+ {
30
+ text: "No tools needed for this response.",
31
+ expected: [],
32
+ },
33
+ ];
34
+
35
+ for (const { text, expected } of responses) {
36
+ const calls = parseToolCalls(text);
37
+ expect(calls.map((c) => c.tool)).toEqual(expected);
38
+ }
39
+ });
40
+
41
+ test("should format tool results correctly", async () => {
42
+ const { formatToolResult } = await import("../tools");
43
+
44
+ const successResult = {
45
+ success: true,
46
+ data: { content: "<world>test</world>", language: "html" },
47
+ };
48
+
49
+ const formatted = formatToolResult("read_game_code", successResult);
50
+ expect(formatted).toContain("[TOOL_RESULT: read_game_code]");
51
+ expect(formatted).toContain("<world>test</world>");
52
+ expect(formatted).toContain("[/TOOL_RESULT]");
53
+ });
54
+ });
55
+
56
+ describe("System Prompt", () => {
57
+ test("should include tool instructions", async () => {
58
+ const { toolRegistry } = await import("../tools");
59
+ const descriptions = toolRegistry.getToolDescriptions();
60
+
61
+ // System prompt should mention the tool
62
+ expect(descriptions).toContain("read_game_code");
63
+ expect(descriptions).toContain(
64
+ "Get the current game code from the editor",
65
+ );
66
+ });
67
+ });
68
+ });
69
+
70
+ // Integration test that would require mocking HuggingFace API
71
+ describe("Full Tool Flow (Mock)", () => {
72
+ test("should execute tool when detected in response", async () => {
73
+ // This test demonstrates the expected flow
74
+ // In production, this would make actual API calls
75
+
76
+ const mockResponse = "I'll check the game code.\n\n[TOOL: read_game_code]";
77
+ const { parseToolCalls, toolRegistry } = await import("../tools");
78
+
79
+ // 1. Parse tool calls from response
80
+ const calls = parseToolCalls(mockResponse);
81
+ expect(calls).toHaveLength(1);
82
+ expect(calls[0].tool).toBe("read_game_code");
83
+
84
+ // 2. Execute the tool
85
+ const result = await toolRegistry.execute(
86
+ calls[0].tool,
87
+ calls[0].parameters,
88
+ );
89
+ expect(result.success).toBe(true);
90
+ expect(result.data).toBeDefined();
91
+
92
+ // 3. Format result for context
93
+ const { formatToolResult } = await import("../tools");
94
+ const formatted = formatToolResult(calls[0].tool, result);
95
+ expect(formatted).toContain("[TOOL_RESULT:");
96
+
97
+ // 4. In the real flow, this formatted result would be added to message history
98
+ // and a follow-up API call would be made to process it
99
+ });
100
+ });
src/lib/server/agent-runner.ts CHANGED
@@ -1,32 +1,30 @@
1
  import { HfInference } from "@huggingface/inference";
 
 
 
 
 
 
 
 
 
2
 
3
  export interface AgentMessage {
4
  id: string;
5
  role: "user" | "assistant" | "system";
6
  content: string;
7
  timestamp: number;
8
- }
9
-
10
- export interface AgentConfig {
11
- model?: string;
12
- maxIterations?: number;
13
- temperature?: number;
14
- maxTokens?: number;
15
  }
16
 
17
  export class AgentRunner {
18
  private client: HfInference | null = null;
19
- private config: AgentConfig;
20
  private messageHistory: AgentMessage[] = [];
 
21
 
22
  constructor(config: AgentConfig = {}) {
23
- this.config = {
24
- model: "deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B",
25
- maxIterations: 10,
26
- temperature: 0.7,
27
- maxTokens: 1000,
28
- ...config,
29
- };
30
  }
31
 
32
  async initialize(hfToken?: string) {
@@ -35,94 +33,144 @@ export class AgentRunner {
35
  "Hugging Face authentication required. Please sign in to continue.",
36
  );
37
  }
38
-
39
  this.client = new HfInference(hfToken);
40
  }
41
 
42
  async processMessage(
43
  message: string,
44
- onStream?: (chunk: string) => void,
45
  ): Promise<string> {
46
  if (!this.client) {
47
  throw new Error("AgentRunner not initialized. Call initialize() first.");
48
  }
49
 
50
- const userMessage: AgentMessage = {
51
- id: this.generateId(),
52
- role: "user",
53
- content: message,
54
- timestamp: Date.now(),
55
- };
56
-
57
- this.messageHistory.push(userMessage);
58
-
59
- try {
60
- const systemPrompt = `You are an AI assistant helping to build games with VibeGame.
61
- You can read and modify game code using XML syntax.
62
- Always validate changes and ensure the game remains playable.
63
- Use small, incremental changes rather than large rewrites.`;
64
-
65
- const messages = [
66
- { role: "system", content: systemPrompt },
67
- ...this.messageHistory.map((msg) => ({
68
- role: msg.role,
69
- content: msg.content,
70
- })),
71
- ];
72
-
73
- let fullResponse = "";
74
-
75
- const stream = await this.client.chatCompletionStream({
76
- model: this.config.model!,
77
- messages: messages as Array<{ role: string; content: string }>,
78
- temperature: this.config.temperature,
79
- max_tokens: this.config.maxTokens,
80
- });
81
 
82
- for await (const chunk of stream) {
83
- if (chunk.choices && chunk.choices.length > 0) {
84
- const delta = chunk.choices[0].delta?.content;
85
- if (delta) {
86
- fullResponse += delta;
87
- if (onStream) {
88
- onStream(delta);
89
- }
90
- }
91
- }
92
- }
93
-
94
- const assistantMessage: AgentMessage = {
95
- id: this.generateId(),
96
- role: "assistant",
97
- content: fullResponse,
98
- timestamp: Date.now(),
99
- };
100
 
101
- this.messageHistory.push(assistantMessage);
 
102
 
103
- return fullResponse;
104
- } catch (error) {
105
- console.error("Error processing message:", error);
106
 
107
- if (error instanceof Error && error.message.includes("rate limit")) {
108
- throw new Error(
109
- "Hugging Face API rate limit exceeded. Please provide an API token or wait before retrying.",
110
- );
111
- }
112
 
113
- throw error;
 
 
114
  }
 
 
 
 
 
 
115
  }
116
 
117
  getHistory(): AgentMessage[] {
118
  return [...this.messageHistory];
119
  }
120
 
121
- clearHistory() {
122
- this.messageHistory = [];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  }
124
 
125
  private generateId(): string {
126
- return `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
127
  }
128
  }
 
1
  import { HfInference } from "@huggingface/inference";
2
+ import { toolRegistry, toolExecutor } from "../tools";
3
+ import { documentationService } from "./documentation";
4
+ import {
5
+ buildSystemPrompt,
6
+ buildToolFollowUpPrompt,
7
+ formatDocumentation,
8
+ } from "./prompts";
9
+ import { mergeConfig, type AgentConfig } from "./agent-config";
10
+ import { UniversalReasoningExtractor } from "./reasoning-extractor";
11
 
12
  export interface AgentMessage {
13
  id: string;
14
  role: "user" | "assistant" | "system";
15
  content: string;
16
  timestamp: number;
17
+ reasoning?: string;
 
 
 
 
 
 
18
  }
19
 
20
  export class AgentRunner {
21
  private client: HfInference | null = null;
22
+ private config: Required<AgentConfig>;
23
  private messageHistory: AgentMessage[] = [];
24
+ private reasoningExtractor = new UniversalReasoningExtractor();
25
 
26
  constructor(config: AgentConfig = {}) {
27
+ this.config = mergeConfig(config);
 
 
 
 
 
 
28
  }
29
 
30
  async initialize(hfToken?: string) {
 
33
  "Hugging Face authentication required. Please sign in to continue.",
34
  );
35
  }
 
36
  this.client = new HfInference(hfToken);
37
  }
38
 
39
  async processMessage(
40
  message: string,
41
+ onStream?: (chunk: string, reasoning?: string) => void,
42
  ): Promise<string> {
43
  if (!this.client) {
44
  throw new Error("AgentRunner not initialized. Call initialize() first.");
45
  }
46
 
47
+ this.addUserMessage(message);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
 
49
+ const documentation = await this.loadDocumentation();
50
+ const systemPrompt = this.buildPrompt(documentation);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
 
52
+ this.reasoningExtractor.reset();
53
+ const response = await this.streamCompletion(systemPrompt, onStream);
54
 
55
+ const extracted = this.reasoningExtractor.extract(response);
56
+ this.addAssistantMessage(extracted.content, extracted.reasoning);
 
57
 
58
+ const toolResults = await this.executeToolsIfNeeded(extracted.content);
 
 
 
 
59
 
60
+ if (toolResults.length > 0) {
61
+ this.addToolResults(toolResults);
62
+ return await this.handleToolFollowUp(documentation, onStream);
63
  }
64
+
65
+ return extracted.content;
66
+ }
67
+
68
+ clearHistory() {
69
+ this.messageHistory = [];
70
  }
71
 
72
  getHistory(): AgentMessage[] {
73
  return [...this.messageHistory];
74
  }
75
 
76
+ private async loadDocumentation(): Promise<string> {
77
+ const fullDocs = await documentationService.load();
78
+ return formatDocumentation(fullDocs || "");
79
+ }
80
+
81
+ private buildPrompt(documentation: string): string {
82
+ const tools = toolRegistry.getToolDescriptions();
83
+ return buildSystemPrompt(tools, documentation);
84
+ }
85
+
86
+ private async streamCompletion(
87
+ systemPrompt: string,
88
+ onStream?: (chunk: string, reasoning?: string) => void,
89
+ ): Promise<string> {
90
+ const messages = this.buildMessages(systemPrompt);
91
+
92
+ const stream = await this.client!.chatCompletionStream({
93
+ model: this.config.model,
94
+ messages: messages as Array<{ role: string; content: string }>,
95
+ temperature: this.config.temperature,
96
+ max_tokens: this.config.maxTokens,
97
+ });
98
+
99
+ let fullResponse = "";
100
+ for await (const chunk of stream) {
101
+ const delta = chunk.choices?.[0]?.delta?.content;
102
+ if (delta) {
103
+ fullResponse += delta;
104
+
105
+ const result = this.reasoningExtractor.stream(delta);
106
+ if (result.partial) {
107
+ onStream?.(result.partial, result.reasoning);
108
+ } else if (result.reasoning) {
109
+ onStream?.("", result.reasoning);
110
+ }
111
+ }
112
+ }
113
+
114
+ return fullResponse;
115
+ }
116
+
117
+ private buildMessages(systemPrompt: string) {
118
+ return [
119
+ { role: "system", content: systemPrompt },
120
+ ...this.messageHistory.map((msg) => ({
121
+ role: msg.role,
122
+ content: msg.content,
123
+ })),
124
+ ];
125
+ }
126
+
127
+ private async executeToolsIfNeeded(response: string) {
128
+ return toolExecutor.executeFromResponse(response);
129
+ }
130
+
131
+ private addToolResults(results: { formatted: string }[]) {
132
+ for (const result of results) {
133
+ this.messageHistory.push({
134
+ id: this.generateId(),
135
+ role: "system",
136
+ content: result.formatted,
137
+ timestamp: Date.now(),
138
+ });
139
+ }
140
+ }
141
+
142
+ private async handleToolFollowUp(
143
+ documentation: string,
144
+ onStream?: (chunk: string, reasoning?: string) => void,
145
+ ): Promise<string> {
146
+ const followUpPrompt = buildToolFollowUpPrompt(documentation);
147
+ this.reasoningExtractor.reset();
148
+ const response = await this.streamCompletion(followUpPrompt, onStream);
149
+ const extracted = this.reasoningExtractor.extract(response);
150
+ this.addAssistantMessage(extracted.content, extracted.reasoning);
151
+ return extracted.content;
152
+ }
153
+
154
+ private addUserMessage(content: string) {
155
+ this.messageHistory.push({
156
+ id: this.generateId(),
157
+ role: "user",
158
+ content,
159
+ timestamp: Date.now(),
160
+ });
161
+ }
162
+
163
+ private addAssistantMessage(content: string, reasoning?: string) {
164
+ this.messageHistory.push({
165
+ id: this.generateId(),
166
+ role: "assistant",
167
+ content,
168
+ timestamp: Date.now(),
169
+ reasoning,
170
+ });
171
  }
172
 
173
  private generateId(): string {
174
+ return `msg-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
175
  }
176
  }
src/lib/server/api.ts CHANGED
@@ -8,6 +8,7 @@ export interface WebSocketMessage {
8
  content?: string;
9
  role?: string;
10
  chunk?: string;
 
11
  error?: string;
12
  processing?: boolean;
13
  connected?: boolean;
@@ -102,10 +103,10 @@ class WebSocketManager {
102
 
103
  const response = await connectionData.agent.processMessage(
104
  userMessage,
105
- (chunk: string) => {
106
  this.sendMessage(ws, {
107
  type: "stream",
108
- payload: { chunk },
109
  timestamp: Date.now(),
110
  });
111
  },
@@ -162,4 +163,7 @@ class WebSocketManager {
162
 
163
  export const wsManager = new WebSocketManager();
164
 
165
- export async function initializeAgent() {}
 
 
 
 
8
  content?: string;
9
  role?: string;
10
  chunk?: string;
11
+ reasoning?: string;
12
  error?: string;
13
  processing?: boolean;
14
  connected?: boolean;
 
103
 
104
  const response = await connectionData.agent.processMessage(
105
  userMessage,
106
+ (chunk: string, reasoning?: string) => {
107
  this.sendMessage(ws, {
108
  type: "stream",
109
+ payload: { chunk, reasoning },
110
  timestamp: Date.now(),
111
  });
112
  },
 
163
 
164
  export const wsManager = new WebSocketManager();
165
 
166
+ export async function initializeAgent() {
167
+ // Initialize tools when server starts
168
+ await import("../tools");
169
+ }
src/lib/server/context.md CHANGED
@@ -1,20 +1,25 @@
1
  # Server Context
2
 
3
- WebSocket server for AI agent communication.
4
 
5
  ## Components
6
 
7
- - `agent-runner.ts` - HuggingFace inference client with streaming support
8
- - `api.ts` - WebSocket manager with per-connection agent instances
 
 
 
 
9
 
10
  ## Architecture
11
 
12
- Vite plugin providing:
13
 
14
- - WebSocket endpoint at `/ws`
15
- - Per-connection authentication via HuggingFace tokens
16
- - Message streaming for chat responses
 
17
 
18
  ## Integration
19
 
20
- Frontend connection via `src/lib/stores/agent.ts` store.
 
1
  # Server Context
2
 
3
+ WebSocket server and AI agent orchestration with reasoning extraction.
4
 
5
  ## Components
6
 
7
+ - `api.ts` - WebSocket connection management
8
+ - `agent-runner.ts` - Message flow orchestration with reasoning filtering
9
+ - `agent-config.ts` - Configuration constants
10
+ - `prompts.ts` - Prompt templates
11
+ - `documentation.ts` - Docs loading service
12
+ - `reasoning-extractor.ts` - Filters thinking tags from model output
13
 
14
  ## Architecture
15
 
16
+ Clean separation of concerns:
17
 
18
+ - Agent runner orchestrates message flow and filters reasoning
19
+ - Reasoning extractor handles streaming tag extraction
20
+ - Tools execute via registry pattern
21
+ - Prompts and config externalized
22
 
23
  ## Integration
24
 
25
+ Frontend via `src/lib/stores/agent.ts` store with reasoning support.
src/lib/server/documentation.ts ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { readFile } from "fs/promises";
2
+ import { join } from "path";
3
+
4
+ export class DocumentationService {
5
+ private cache: string | null = null;
6
+ private readonly docsPath: string;
7
+
8
+ constructor(filename: string = "llms.txt") {
9
+ this.docsPath = join(process.cwd(), filename);
10
+ }
11
+
12
+ async load(): Promise<string | null> {
13
+ if (this.cache !== null) {
14
+ return this.cache;
15
+ }
16
+
17
+ try {
18
+ this.cache = await readFile(this.docsPath, "utf-8");
19
+ return this.cache;
20
+ } catch {
21
+ return null;
22
+ }
23
+ }
24
+
25
+ clearCache(): void {
26
+ this.cache = null;
27
+ }
28
+ }
29
+
30
+ export const documentationService = new DocumentationService();
src/lib/server/hint.test.ts ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ describe("Message Enhancement for Tool Usage", () => {
4
+ test("should detect requests to read game code", () => {
5
+ const testCases = [
6
+ { input: "read the game code", shouldTrigger: true },
7
+ { input: "show me the game", shouldTrigger: true },
8
+ { input: "what's in the game?", shouldTrigger: true },
9
+ { input: "check the code", shouldTrigger: true },
10
+ { input: "view the editor", shouldTrigger: true },
11
+ { input: "see the scene", shouldTrigger: true },
12
+ { input: "display the game code", shouldTrigger: true },
13
+ { input: "look at the code", shouldTrigger: true },
14
+ { input: "get the game", shouldTrigger: true },
15
+ { input: "game code please", shouldTrigger: true },
16
+ { input: "hello there", shouldTrigger: false },
17
+ { input: "add a box", shouldTrigger: false },
18
+ { input: "change the color", shouldTrigger: false },
19
+ ];
20
+
21
+ const pattern =
22
+ /\b(read|show|view|check|see|what's in|what is in|get|display|look at)\b.*\b(game|code|editor|scene)\b/i;
23
+ const reversePattern =
24
+ /\b(game|code|editor|scene)\b.*\b(read|show|view|check|see|please)\b/i;
25
+
26
+ for (const { input, shouldTrigger } of testCases) {
27
+ const matches = pattern.test(input) || reversePattern.test(input);
28
+ expect(matches).toBe(shouldTrigger);
29
+ }
30
+ });
31
+ });
src/lib/server/prompts.ts ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const SYSTEM_PROMPTS = {
2
+ main: (
3
+ tools: string,
4
+ documentation: string,
5
+ ) => `You are an expert VibeGame developer assistant. VibeGame is a declarative 3D game engine using XML-like syntax, similar to Roblox but with "vibe coding" - focusing on quick, creative game development.
6
+
7
+ ENGINE CONTEXT:
8
+ VibeGame is a custom engine not in your training data. It uses declarative XML syntax for scene definition with Entity-Component-System (ECS) architecture.
9
+
10
+ ${documentation}
11
+
12
+ AVAILABLE TOOLS:
13
+ ${tools}
14
+
15
+ TOOL USAGE:
16
+ - Execute tools with: [TOOL: tool_name] or [TOOL: tool_name {"param": "value"}]
17
+ - read_game_code: Get current editor content
18
+ - write_game_code: Update the editor with new code
19
+ - read_console_output: Check console for errors, logs, and game output
20
+
21
+ WORKFLOW:
22
+ 1. Read the current code to understand the game
23
+ 2. Make changes using write_game_code
24
+ 3. Check console output for errors or success
25
+ 4. Iterate based on feedback
26
+
27
+ Be helpful, accurate, and adapt to what the user needs. Focus on working code and practical solutions.`,
28
+
29
+ toolFollowUp: (
30
+ documentation: string,
31
+ ) => `Based on the tool execution result, provide relevant analysis of what you found.
32
+
33
+ ${documentation}`,
34
+ };
35
+
36
+ export const formatDocumentation = (fullDocs: string): string => {
37
+ return `VIBEGAME REFERENCE:
38
+ ${fullDocs}`;
39
+ };
40
+
41
+ export const buildSystemPrompt = (
42
+ tools: string,
43
+ documentation: string,
44
+ ): string => {
45
+ return SYSTEM_PROMPTS.main(tools, documentation);
46
+ };
47
+
48
+ export const buildToolFollowUpPrompt = (documentation: string): string => {
49
+ return SYSTEM_PROMPTS.toolFollowUp(documentation);
50
+ };
src/lib/server/reasoning-extractor.test.ts ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { describe, test, expect } from "bun:test";
2
+ import { UniversalReasoningExtractor } from "./reasoning-extractor";
3
+
4
+ describe("UniversalReasoningExtractor", () => {
5
+ test("extracts Qwen thinking tags", () => {
6
+ const extractor = new UniversalReasoningExtractor();
7
+ const input = `<thinking>
8
+ Let me analyze this step by step:
9
+ 1. First, I need to understand the problem
10
+ 2. Then I'll formulate a solution
11
+ 3. Finally, I'll provide the answer
12
+ </thinking>
13
+
14
+ The answer to your question is 42.`;
15
+
16
+ const result = extractor.extract(input);
17
+ expect(result.content).toBe("The answer to your question is 42.");
18
+ expect(result.reasoning).toContain("Let me analyze this step by step");
19
+ });
20
+
21
+ test("extracts DeepSeek R1 think tags", () => {
22
+ const extractor = new UniversalReasoningExtractor();
23
+ const input = `<think>
24
+ Hmm, this is an interesting problem.
25
+ I should consider multiple approaches.
26
+ </think>
27
+
28
+ Here's the solution to your problem.`;
29
+
30
+ const result = extractor.extract(input);
31
+ expect(result.content).toBe("Here's the solution to your problem.");
32
+ expect(result.reasoning).toContain("interesting problem");
33
+ });
34
+
35
+ test("handles interleaved reasoning tags", () => {
36
+ const extractor = new UniversalReasoningExtractor();
37
+ const input = `<thinking>
38
+ First reasoning block
39
+ </thinking>
40
+ Some content here.
41
+ <reasoning>
42
+ Second reasoning block
43
+ </reasoning>
44
+ Final answer.`;
45
+
46
+ const result = extractor.extract(input);
47
+ expect(result.content).toBe("Some content here.\n\nFinal answer.");
48
+ expect(result.reasoning).toContain("First reasoning block");
49
+ expect(result.reasoning).toContain("Second reasoning block");
50
+ });
51
+
52
+ test("streams content correctly", () => {
53
+ const extractor = new UniversalReasoningExtractor();
54
+
55
+ const chunks = [
56
+ "Hello, ",
57
+ "<think",
58
+ "ing>",
59
+ "This is my ",
60
+ "reasoning process",
61
+ "</thinking>",
62
+ " Here is the answer.",
63
+ ];
64
+
65
+ let finalContent = "";
66
+ let finalReasoning = "";
67
+
68
+ for (const chunk of chunks) {
69
+ const result = extractor.stream(chunk);
70
+ if (result.partial) {
71
+ finalContent += result.partial;
72
+ }
73
+ if (result.reasoning) {
74
+ finalReasoning = result.reasoning;
75
+ }
76
+ }
77
+
78
+ expect(finalContent).toBe("Hello, Here is the answer.");
79
+ expect(finalReasoning).toBe("This is my reasoning process");
80
+ });
81
+
82
+ test("handles multiple reasoning blocks", () => {
83
+ const extractor = new UniversalReasoningExtractor();
84
+ const input = `<thinking>First thought</thinking>
85
+ Some content here.
86
+ <reasoning>Second thought</reasoning>
87
+ More content.`;
88
+
89
+ const result = extractor.extract(input);
90
+ expect(result.content).toBe("Some content here.\n\nMore content.");
91
+ expect(result.reasoning).toContain("First thought");
92
+ expect(result.reasoning).toContain("Second thought");
93
+ });
94
+
95
+ test("returns original text when no reasoning tags", () => {
96
+ const extractor = new UniversalReasoningExtractor();
97
+ const input = "This is just regular text without any reasoning tags.";
98
+
99
+ const result = extractor.extract(input);
100
+ expect(result.content).toBe(input);
101
+ expect(result.reasoning).toBe("");
102
+ });
103
+
104
+ test("reset clears internal state", () => {
105
+ const extractor = new UniversalReasoningExtractor();
106
+
107
+ extractor.stream("<thinking>Test");
108
+ extractor.reset();
109
+
110
+ const result = extractor.stream("Normal text");
111
+ expect(result.partial).toBe("Normal text");
112
+ expect(result.reasoning).toBeUndefined();
113
+ });
114
+ });
src/lib/server/reasoning-extractor.ts ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export interface ExtractedContent {
2
+ reasoning: string;
3
+ content: string;
4
+ }
5
+
6
+ export interface StreamResult {
7
+ partial: string;
8
+ reasoning?: string;
9
+ complete: boolean;
10
+ }
11
+
12
+ export interface ReasoningExtractor {
13
+ patterns: RegExp[];
14
+ extract(text: string): ExtractedContent;
15
+ stream(chunk: string): StreamResult;
16
+ reset(): void;
17
+ }
18
+
19
+ export class UniversalReasoningExtractor implements ReasoningExtractor {
20
+ patterns = [
21
+ /<thinking>(.*?)<\/thinking>/s,
22
+ /<think>(.*?)<\/think>/s,
23
+ /<reasoning>(.*?)<\/reasoning>/s,
24
+ ];
25
+
26
+ private buffer = "";
27
+ private inReasoning = false;
28
+ private reasoningBuffer = "";
29
+ private currentTag: string | null = null;
30
+
31
+ extract(text: string): ExtractedContent {
32
+ let reasoning = "";
33
+ let cleanContent = text;
34
+
35
+ for (const pattern of this.patterns) {
36
+ const matches = Array.from(
37
+ text.matchAll(new RegExp(pattern.source, "gs")),
38
+ );
39
+
40
+ for (const match of matches) {
41
+ if (match[1]) {
42
+ reasoning += match[1].trim() + "\n";
43
+ cleanContent = cleanContent.replace(match[0], "");
44
+ }
45
+ }
46
+ }
47
+
48
+ return {
49
+ reasoning: reasoning.trim(),
50
+ content: cleanContent.trim(),
51
+ };
52
+ }
53
+
54
+ stream(chunk: string): StreamResult {
55
+ this.buffer += chunk;
56
+
57
+ const result: StreamResult = {
58
+ partial: "",
59
+ complete: false,
60
+ };
61
+
62
+ // Check for opening tags
63
+ if (!this.inReasoning) {
64
+ const openingMatch = this.buffer.match(/<(thinking|think|reasoning)>/);
65
+ if (openingMatch) {
66
+ const beforeTag = this.buffer.substring(0, openingMatch.index);
67
+ result.partial = beforeTag;
68
+ this.buffer = this.buffer.substring(
69
+ openingMatch.index! + openingMatch[0].length,
70
+ );
71
+ this.inReasoning = true;
72
+ this.currentTag = openingMatch[1];
73
+ return result;
74
+ }
75
+ }
76
+
77
+ // Check for closing tags
78
+ if (this.inReasoning && this.currentTag) {
79
+ const closingTag = `</${this.currentTag}>`;
80
+ const closingIndex = this.buffer.indexOf(closingTag);
81
+
82
+ if (closingIndex !== -1) {
83
+ this.reasoningBuffer += this.buffer.substring(0, closingIndex);
84
+ result.reasoning = this.reasoningBuffer.trim();
85
+ this.reasoningBuffer = "";
86
+ this.buffer = this.buffer.substring(closingIndex + closingTag.length);
87
+ this.inReasoning = false;
88
+ this.currentTag = null;
89
+
90
+ // Process any remaining content
91
+ const nextResult = this.stream("");
92
+ result.partial = nextResult.partial;
93
+ if (nextResult.reasoning) {
94
+ result.reasoning =
95
+ (result.reasoning || "") + "\n" + nextResult.reasoning;
96
+ }
97
+ return result;
98
+ } else {
99
+ // Still in reasoning, buffer it
100
+ this.reasoningBuffer += this.buffer;
101
+ this.buffer = "";
102
+ return result;
103
+ }
104
+ }
105
+
106
+ // No reasoning tags, output as content
107
+ if (!this.inReasoning && this.buffer.length > 0) {
108
+ // Check if we might be in the middle of a tag
109
+ const lastAngle = this.buffer.lastIndexOf("<");
110
+ if (lastAngle !== -1 && !this.buffer.substring(lastAngle).includes(">")) {
111
+ // Might be incomplete tag, output everything before it
112
+ result.partial = this.buffer.substring(0, lastAngle);
113
+ this.buffer = this.buffer.substring(lastAngle);
114
+ } else {
115
+ // Safe to output everything
116
+ result.partial = this.buffer;
117
+ this.buffer = "";
118
+ }
119
+ }
120
+
121
+ result.complete = !this.inReasoning && this.buffer.length === 0;
122
+ return result;
123
+ }
124
+
125
+ reset(): void {
126
+ this.buffer = "";
127
+ this.inReasoning = false;
128
+ this.reasoningBuffer = "";
129
+ this.currentTag = null;
130
+ }
131
+ }
src/lib/stores/agent.ts CHANGED
@@ -7,6 +7,8 @@ export interface ChatMessage {
7
  content: string;
8
  timestamp: number;
9
  streaming?: boolean;
 
 
10
  }
11
 
12
  export interface AgentState {
@@ -95,6 +97,7 @@ function createAgentStore() {
95
  processing?: boolean;
96
  connected?: boolean;
97
  chunk?: string;
 
98
  role?: string;
99
  content?: string;
100
  error?: string;
@@ -129,6 +132,7 @@ function createAgentStore() {
129
  content: newContent,
130
  timestamp: Date.now(),
131
  streaming: true,
 
132
  };
133
  return {
134
  ...state,
@@ -138,7 +142,11 @@ function createAgentStore() {
138
  } else {
139
  const messages = state.messages.map((msg) => {
140
  if (msg.id === currentStreamId) {
141
- return { ...msg, content: newContent };
 
 
 
 
142
  }
143
  return msg;
144
  });
 
7
  content: string;
8
  timestamp: number;
9
  streaming?: boolean;
10
+ reasoning?: string;
11
+ showReasoning?: boolean;
12
  }
13
 
14
  export interface AgentState {
 
97
  processing?: boolean;
98
  connected?: boolean;
99
  chunk?: string;
100
+ reasoning?: string;
101
  role?: string;
102
  content?: string;
103
  error?: string;
 
132
  content: newContent,
133
  timestamp: Date.now(),
134
  streaming: true,
135
+ reasoning: message.payload.reasoning,
136
  };
137
  return {
138
  ...state,
 
142
  } else {
143
  const messages = state.messages.map((msg) => {
144
  if (msg.id === currentStreamId) {
145
+ return {
146
+ ...msg,
147
+ content: newContent,
148
+ reasoning: message.payload.reasoning || msg.reasoning,
149
+ };
150
  }
151
  return msg;
152
  });
src/lib/tools/context.md ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Tools Context
2
+
3
+ AI agent tools for game manipulation and feedback.
4
+
5
+ ## Structure
6
+
7
+ - `registry.ts` - Tool registration
8
+ - `parser.ts` - Tool call parsing
9
+ - `executor.ts` - Execution flow
10
+ - `read-game-code.ts` - Editor content reader
11
+ - `write-game-code.ts` - Editor content writer
12
+ - `read-console-output.ts` - Console message reader
13
+ - `index.ts` - Auto-registration
14
+
15
+ ## Architecture
16
+
17
+ Clean separation:
18
+
19
+ - Registry handles registration only
20
+ - Parser extracts tool calls from responses
21
+ - Executor manages execution flow
22
+ - Individual tools implement business logic
23
+
24
+ ## Pattern
25
+
26
+ Agent includes: `[TOOL: tool_name {"param": "value"}]`
27
+ Results formatted: `[TOOL_RESULT: name]` or `[TOOL_ERROR: name]`
28
+
29
+ ## Available Tools
30
+
31
+ 1. **read_game_code** - Returns editor content
32
+ 2. **write_game_code** - Updates editor with new code
33
+ 3. **read_console_output** - Reads console messages with filtering
34
+
35
+ ## Workflow
36
+
37
+ Read code → Write changes → Check console → Iterate on feedback
src/lib/tools/executor.ts ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { toolRegistry } from "./registry";
2
+ import { parseToolCalls, formatToolResult, type ToolCall } from "./parser";
3
+
4
+ export interface ToolExecutionResult {
5
+ toolName: string;
6
+ formatted: string;
7
+ raw: {
8
+ success: boolean;
9
+ data?: unknown;
10
+ error?: string;
11
+ };
12
+ }
13
+
14
+ export class ToolExecutor {
15
+ async executeFromResponse(response: string): Promise<ToolExecutionResult[]> {
16
+ const toolCalls = parseToolCalls(response);
17
+
18
+ if (toolCalls.length === 0) {
19
+ return [];
20
+ }
21
+
22
+ return Promise.all(toolCalls.map((call) => this.executeSingle(call)));
23
+ }
24
+
25
+ private async executeSingle(call: ToolCall): Promise<ToolExecutionResult> {
26
+ const result = await toolRegistry.execute(call.tool, call.parameters);
27
+
28
+ return {
29
+ toolName: call.tool,
30
+ formatted: formatToolResult(call.tool, result),
31
+ raw: result,
32
+ };
33
+ }
34
+ }
35
+
36
+ export const toolExecutor = new ToolExecutor();
src/lib/tools/index.ts ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export { toolRegistry } from "./registry";
2
+ export type { Tool, ToolResult } from "./registry";
3
+
4
+ export { parseToolCalls, formatToolResult } from "./parser";
5
+ export type { ToolCall } from "./parser";
6
+
7
+ export { toolExecutor } from "./executor";
8
+ export type { ToolExecutionResult } from "./executor";
9
+
10
+ import { toolRegistry } from "./registry";
11
+ import { readGameCodeTool } from "./read-game-code";
12
+ import { writeGameCodeTool } from "./write-game-code";
13
+ import { readConsoleOutputTool } from "./read-console-output";
14
+
15
+ toolRegistry.register(readGameCodeTool);
16
+ toolRegistry.register(writeGameCodeTool);
17
+ toolRegistry.register(readConsoleOutputTool);
src/lib/tools/parser.ts ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export interface ToolCall {
2
+ tool: string;
3
+ parameters?: unknown;
4
+ }
5
+
6
+ const TOOL_PATTERN = /\[TOOL:\s*(\w+)(?:\s+({[^}]*}))?\]/g;
7
+
8
+ export const parseToolCalls = (response: string): ToolCall[] => {
9
+ const toolCalls: ToolCall[] = [];
10
+ let match;
11
+
12
+ while ((match = TOOL_PATTERN.exec(response)) !== null) {
13
+ const toolName = match[1];
14
+ const paramsStr = match[2];
15
+
16
+ toolCalls.push({
17
+ tool: toolName,
18
+ parameters: paramsStr ? tryParseJson(paramsStr) : undefined,
19
+ });
20
+ }
21
+
22
+ return toolCalls;
23
+ };
24
+
25
+ const tryParseJson = (str: string): unknown => {
26
+ try {
27
+ return JSON.parse(str);
28
+ } catch {
29
+ return undefined;
30
+ }
31
+ };
32
+
33
+ export const formatToolResult = (
34
+ toolName: string,
35
+ result: { success: boolean; data?: unknown; error?: string },
36
+ ): string => {
37
+ if (result.success) {
38
+ return `[TOOL_RESULT: ${toolName}]\n${JSON.stringify(result.data, null, 2)}\n[/TOOL_RESULT]`;
39
+ }
40
+ return `[TOOL_ERROR: ${toolName}]\n${result.error}\n[/TOOL_ERROR]`;
41
+ };
src/lib/tools/read-console-output.ts ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { get } from "svelte/store";
2
+ import { consoleStore } from "../stores/console";
3
+ import type { Tool } from "./registry";
4
+
5
+ interface ReadConsoleParams {
6
+ limit?: number;
7
+ type?: "log" | "warn" | "error" | "info";
8
+ }
9
+
10
+ export const readConsoleOutputTool: Tool = {
11
+ name: "read_console_output",
12
+ description: "Read messages from the game console",
13
+ execute: async (params: unknown) => {
14
+ const { limit = 10, type } = (params || {}) as ReadConsoleParams;
15
+
16
+ try {
17
+ const state = get(consoleStore);
18
+ let messages = state.messages;
19
+
20
+ if (type) {
21
+ messages = messages.filter((msg) => msg.type === type);
22
+ }
23
+
24
+ if (limit > 0) {
25
+ messages = messages.slice(-limit);
26
+ }
27
+
28
+ return {
29
+ success: true,
30
+ data: {
31
+ messages: messages.map((msg) => ({
32
+ type: msg.type,
33
+ message: msg.message,
34
+ timestamp: msg.timestamp,
35
+ })),
36
+ total: messages.length,
37
+ },
38
+ };
39
+ } catch (error) {
40
+ return {
41
+ success: false,
42
+ error:
43
+ error instanceof Error ? error.message : "Failed to read console",
44
+ };
45
+ }
46
+ },
47
+ };
src/lib/tools/read-game-code.ts ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { get } from "svelte/store";
2
+ import { editorStore } from "../stores/editor";
3
+ import type { Tool } from "./registry";
4
+
5
+ export const readGameCodeTool: Tool = {
6
+ name: "read_game_code",
7
+ description: "Get the current game code from the editor",
8
+ execute: async () => {
9
+ const state = get(editorStore);
10
+ return {
11
+ success: true,
12
+ data: {
13
+ content: state.content,
14
+ language: state.language,
15
+ },
16
+ };
17
+ },
18
+ };
src/lib/tools/registry.ts ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export interface Tool {
2
+ name: string;
3
+ description: string;
4
+ execute: (params: unknown) => Promise<ToolResult>;
5
+ }
6
+
7
+ export interface ToolResult {
8
+ success: boolean;
9
+ data?: unknown;
10
+ error?: string;
11
+ }
12
+
13
+ class ToolRegistry {
14
+ private tools: Map<string, Tool> = new Map();
15
+
16
+ register(tool: Tool): void {
17
+ this.tools.set(tool.name, tool);
18
+ }
19
+
20
+ async execute(toolName: string, params?: unknown): Promise<ToolResult> {
21
+ const tool = this.tools.get(toolName);
22
+
23
+ if (!tool) {
24
+ return {
25
+ success: false,
26
+ error: `Tool '${toolName}' not found. Available: ${this.getToolNames().join(", ")}`,
27
+ };
28
+ }
29
+
30
+ try {
31
+ return await tool.execute(params);
32
+ } catch (error) {
33
+ return {
34
+ success: false,
35
+ error: error instanceof Error ? error.message : "Unknown error",
36
+ };
37
+ }
38
+ }
39
+
40
+ getToolNames(): string[] {
41
+ return Array.from(this.tools.keys());
42
+ }
43
+
44
+ getToolDescriptions(): string {
45
+ return Array.from(this.tools.values())
46
+ .map((tool) => `- ${tool.name}: ${tool.description}`)
47
+ .join("\n");
48
+ }
49
+ }
50
+
51
+ export const toolRegistry = new ToolRegistry();
src/lib/tools/tools-integration.test.ts ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { describe, it, expect, beforeEach } from "bun:test";
2
+ import { get } from "svelte/store";
3
+ import { toolRegistry } from "./registry";
4
+ import { editorStore } from "../stores/editor";
5
+ import { consoleStore } from "../stores/console";
6
+ import "./index"; // Import to register tools
7
+
8
+ describe("Tool Integration", () => {
9
+ beforeEach(() => {
10
+ // Reset stores
11
+ editorStore.setContent("");
12
+ consoleStore.reset();
13
+ });
14
+
15
+ describe("write_game_code", () => {
16
+ it("should update editor content", async () => {
17
+ const testCode = `<world canvas="#game-canvas">
18
+ <static-part pos="0 0 0" shape="box" size="10 1 10" color="#90ee90"></static-part>
19
+ </world>`;
20
+
21
+ const result = await toolRegistry.execute("write_game_code", {
22
+ content: testCode,
23
+ });
24
+
25
+ expect(result.success).toBe(true);
26
+ expect(get(editorStore).content).toBe(testCode);
27
+ });
28
+
29
+ it("should handle invalid input", async () => {
30
+ const result = await toolRegistry.execute("write_game_code", {
31
+ content: 123, // Invalid type
32
+ });
33
+
34
+ expect(result.success).toBe(false);
35
+ expect(result.error).toContain("Content must be a string");
36
+ });
37
+ });
38
+
39
+ describe("read_game_code", () => {
40
+ it("should read current editor content", async () => {
41
+ const testCode = "<world>Test</world>";
42
+ editorStore.setContent(testCode);
43
+
44
+ const result = await toolRegistry.execute("read_game_code");
45
+
46
+ expect(result.success).toBe(true);
47
+ expect(result.data).toEqual({
48
+ content: testCode,
49
+ language: "html",
50
+ });
51
+ });
52
+ });
53
+
54
+ describe("read_console_output", () => {
55
+ it("should read console messages", async () => {
56
+ consoleStore.addMessage("log", "Game started");
57
+ consoleStore.addMessage("error", "Entity not found");
58
+ consoleStore.addMessage("warn", "Low performance");
59
+
60
+ const result = await toolRegistry.execute("read_console_output", {
61
+ limit: 2,
62
+ });
63
+
64
+ expect(result.success).toBe(true);
65
+ const data = result.data as { messages: Array<{ message: string }> };
66
+ expect(data.messages).toHaveLength(2);
67
+ expect(data.messages[1].message).toBe("Low performance");
68
+ });
69
+
70
+ it("should filter by type", async () => {
71
+ consoleStore.addMessage("log", "Info 1");
72
+ consoleStore.addMessage("error", "Error 1");
73
+ consoleStore.addMessage("log", "Info 2");
74
+
75
+ const result = await toolRegistry.execute("read_console_output", {
76
+ type: "error",
77
+ });
78
+
79
+ expect(result.success).toBe(true);
80
+ const data = result.data as { messages: Array<{ message: string }> };
81
+ expect(data.messages).toHaveLength(1);
82
+ expect(data.messages[0].message).toBe("Error 1");
83
+ });
84
+ });
85
+
86
+ describe("Tool workflow", () => {
87
+ it("should support a complete edit cycle", async () => {
88
+ // 1. Write some code
89
+ const code = `<world canvas="#game-canvas">
90
+ <dynamic-part pos="0 5 0" shape="sphere" size="1" color="#ff0000"></dynamic-part>
91
+ </world>`;
92
+
93
+ await toolRegistry.execute("write_game_code", { content: code });
94
+
95
+ // 2. Read it back
96
+ const readResult = await toolRegistry.execute("read_game_code");
97
+ expect((readResult.data as { content: string }).content).toBe(code);
98
+
99
+ // 3. Simulate console output
100
+ consoleStore.addMessage("log", "Entity created: dynamic-part");
101
+ consoleStore.addMessage("info", "Physics initialized");
102
+
103
+ // 4. Read console
104
+ const consoleResult = await toolRegistry.execute("read_console_output");
105
+ expect(consoleResult.success).toBe(true);
106
+ const messages = (consoleResult.data as { messages: unknown[] }).messages;
107
+ expect(messages.length).toBeGreaterThan(0);
108
+ });
109
+ });
110
+ });
src/lib/tools/write-game-code.ts ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { editorStore } from "../stores/editor";
2
+ import type { Tool } from "./registry";
3
+
4
+ interface WriteGameCodeParams {
5
+ content: string;
6
+ }
7
+
8
+ export const writeGameCodeTool: Tool = {
9
+ name: "write_game_code",
10
+ description: "Update the game code in the editor",
11
+ execute: async (params: unknown) => {
12
+ const { content } = params as WriteGameCodeParams;
13
+
14
+ if (typeof content !== "string") {
15
+ return {
16
+ success: false,
17
+ error: "Content must be a string",
18
+ };
19
+ }
20
+
21
+ try {
22
+ editorStore.setContent(content);
23
+
24
+ return {
25
+ success: true,
26
+ data: {
27
+ message: "Code updated successfully",
28
+ length: content.length,
29
+ },
30
+ };
31
+ } catch (error) {
32
+ return {
33
+ success: false,
34
+ error: error instanceof Error ? error.message : "Failed to update code",
35
+ };
36
+ }
37
+ },
38
+ };
tsconfig.json CHANGED
@@ -18,7 +18,7 @@
18
  "noUnusedParameters": true,
19
  "noImplicitReturns": true,
20
  "noFallthroughCasesInSwitch": true,
21
- "types": ["vite/client", "node"]
22
  },
23
  "include": ["src/**/*", "src/**/*.svelte", "vite.config.ts"],
24
  "exclude": ["node_modules", "dist"]
 
18
  "noUnusedParameters": true,
19
  "noImplicitReturns": true,
20
  "noFallthroughCasesInSwitch": true,
21
+ "types": ["vite/client", "node", "bun"]
22
  },
23
  "include": ["src/**/*", "src/**/*.svelte", "vite.config.ts"],
24
  "exclude": ["node_modules", "dist"]