dylanebert commited on
Commit
db9635c
·
1 Parent(s): 712a0e0

improved prompting/UX

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. agents.md +0 -88
  2. bun.lock +18 -9
  3. layers/structure.md +1 -1
  4. llms.txt +2430 -0
  5. package.json +4 -3
  6. src/App.svelte +7 -1
  7. src/lib/components/about/About.svelte +310 -0
  8. src/lib/components/about/context.md +31 -0
  9. src/lib/components/chat/ChatPanel.svelte +17 -11
  10. src/lib/components/chat/ExampleMessages.svelte +117 -148
  11. src/lib/components/chat/ExampleRow.svelte +183 -0
  12. src/lib/components/chat/Message.svelte +43 -11
  13. src/lib/components/chat/MessageContent.svelte +4 -3
  14. src/lib/components/chat/MessageInput.svelte +81 -39
  15. src/lib/components/chat/MessageList.svelte +60 -29
  16. src/lib/components/chat/TextRenderer.svelte +66 -4
  17. src/lib/components/chat/context.md +16 -15
  18. src/lib/components/chat/segments/TodoSegment.svelte +120 -8
  19. src/lib/components/chat/segments/ToolBlock.svelte +143 -4
  20. src/lib/components/game/GameCanvas.svelte +11 -3
  21. src/lib/components/game/context.md +3 -3
  22. src/lib/components/layout/AppHeader.svelte +118 -21
  23. src/lib/components/layout/SplitView.svelte +1 -1
  24. src/lib/components/layout/context.md +5 -4
  25. src/lib/config/animations.ts +0 -43
  26. src/lib/config/context.md +2 -5
  27. src/lib/controllers/animation-controller.ts +0 -190
  28. src/lib/controllers/context.md +3 -6
  29. src/lib/models/context.md +3 -2
  30. src/lib/models/segment-view.ts +0 -3
  31. src/lib/server/console-buffer.ts +83 -8
  32. src/lib/server/context.md +11 -9
  33. src/lib/server/documentation.ts +1 -1
  34. src/lib/server/langgraph-agent.ts +344 -200
  35. src/lib/server/mcp-client.ts +147 -315
  36. src/lib/server/tools.ts +20 -7
  37. src/lib/services/content-manager.ts +58 -2
  38. src/lib/services/context.md +3 -3
  39. src/lib/services/game-engine.ts +5 -0
  40. src/lib/services/message-handler.ts +25 -7
  41. src/lib/services/segment-formatter.ts +0 -3
  42. src/lib/services/virtual-fs.ts +188 -8
  43. src/lib/stores/context.md +1 -1
  44. src/lib/stores/ui.ts +28 -2
  45. src/lib/utils/context.md +32 -0
  46. src/lib/utils/tool-call-parser.ts +88 -0
  47. src/lib/utils/tool-parser-htmlparser2.ts +205 -0
  48. tests/api.test.ts +0 -229
  49. tests/console-buffer.test.ts +0 -162
  50. tests/langgraph-agent.test.ts +0 -133
agents.md DELETED
@@ -1,88 +0,0 @@
1
- # VibeGame Engine Context
2
-
3
- You are a VibeGame specialist. VibeGame is a 3D game engine with declarative XML syntax and ECS architecture. Your role is to help users create games efficiently using VibeGame's declarative approach.
4
-
5
- ## Core Architecture
6
-
7
- **ECS Pattern**: Entities (IDs) + Components (data) + Systems (logic)
8
- **Declarative XML**: Game entities defined in `<world>` tags
9
- **Auto-Creation**: Engine provides player, camera, lighting by default
10
-
11
- ## Essential Syntax
12
-
13
- ```xml
14
- <world canvas="#game-canvas" sky="#87ceeb">
15
- <!-- REQUIRED: Ground to prevent falling -->
16
- <static-part pos="0 -0.5 0" shape="box" size="20 1 20" color="#90ee90"></static-part>
17
-
18
- <!-- Physics objects -->
19
- <dynamic-part pos="0 5 0" shape="sphere" size="1" color="#ff0000"></dynamic-part>
20
- <kinematic-part pos="5 2 0" shape="box" size="3 0.5 3" color="#0000ff">
21
- <tween target="body.pos-y" from="2" to="5" duration="3" loop="ping-pong"></tween>
22
- </kinematic-part>
23
- </world>
24
- ```
25
-
26
- ## Key Recipes & Components
27
-
28
- - `<static-part>` - Immovable (grounds, walls)
29
- - `<dynamic-part>` - Gravity-affected (balls, crates)
30
- - `<kinematic-part>` - Script-controlled (moving platforms)
31
- - `<player>`, `<camera>` - Auto-created if missing
32
- - `<entity>` - Base with custom components
33
-
34
- ## Critical Rules
35
-
36
- ⚠️ **Physics Override**: Body position overrides transform position. Always use `pos` on physics entities.
37
-
38
- ```xml
39
- <!-- ✅ CORRECT -->
40
- <dynamic-part pos="0 5 0" shape="sphere"></dynamic-part>
41
-
42
- <!-- ❌ WRONG: Transform ignored -->
43
- <entity transform="pos: 0 5 0" body collider></entity>
44
- ```
45
-
46
- ## Component Syntax
47
-
48
- ```xml
49
- <!-- Bare attributes = defaults -->
50
- <entity transform body collider renderer></entity>
51
-
52
- <!-- Override properties -->
53
- <entity transform="pos: 0 5 0" body="type: dynamic; mass: 10" collider renderer></entity>
54
- ```
55
-
56
- **Shorthands**: `pos`, `color`, `size` auto-expand to matching component properties.
57
-
58
- ## Development Commands
59
-
60
- - `bun dev` - Start development server
61
- - `bun run build` - Production build
62
- - `bun run check` - TypeScript validation
63
- - `bun test` - Run tests
64
-
65
- ## Features Available
66
-
67
- ✅ Physics (Rapier), rendering (Three.js), input, tweening, player controller, orbital camera, collision detection, respawn, post-processing
68
-
69
- ❌ Audio, multiplayer, save/load, inventory, AI, particles, custom shaders
70
-
71
- ## Documentation Access
72
-
73
- **For detailed information**: Use Context7 to fetch comprehensive docs:
74
- 1. `mcp__context7__resolve-library-id` with "/dylanebert/vibegame"
75
- 2. `mcp__context7__get-library-docs` with resolved ID
76
-
77
- **Quick Reference**: Shapes (`box`, `sphere`, `cylinder`, `capsule`), Physics (`static`, `dynamic`, `kinematic`), Easing functions, Loop modes (`once`, `loop`, `ping-pong`)
78
-
79
- ## Best Practices
80
-
81
- 1. Always include ground platforms
82
- 2. Use recipes over raw entities
83
- 3. Leverage auto-creation defaults
84
- 4. Set positions on physics bodies, not transforms
85
- 5. Query Context7 for detailed API references
86
- 6. Test incrementally
87
-
88
- This provides foundational VibeGame knowledge. Use Context7 for comprehensive documentation and examples.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bun.lock CHANGED
@@ -11,13 +11,12 @@
11
  "@langchain/mcp-adapters": "^0.6.0",
12
  "@modelcontextprotocol/sdk": "^0.6.0",
13
  "@modelcontextprotocol/server-filesystem": "^0.6.2",
14
- "@types/marked": "^6.0.0",
15
  "@types/node": "^24.3.3",
16
  "gsap": "^3.13.0",
17
- "marked": "^16.2.1",
18
  "monaco-editor": "^0.50.0",
19
  "svelte-splitpanes": "^8.0.5",
20
- "vibegame": "^0.1.7",
21
  "zod": "^4.1.8",
22
  },
23
  "devDependencies": {
@@ -117,7 +116,7 @@
117
 
118
  "@huggingface/jinja": ["@huggingface/jinja@0.5.1", "", {}, "sha512-yUZLld4lrM9iFxHCwFQ7D1HW2MWMwSbeB7WzWqFYDWK+rEb+WldkLdAJxUPOmgICMHZLzZGVcVjFh3w/YGubng=="],
119
 
120
- "@huggingface/tasks": ["@huggingface/tasks@0.19.45", "", {}, "sha512-lM3QOgbfkGZ5gAZOYWOmzMM6BbKcXOIHjgnUAoymTdZEcEcGSr0vy/LWGEiK+vBXC4vU+sCT+WNoA/JZ8TEWdA=="],
121
 
122
  "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
123
 
@@ -217,8 +216,6 @@
217
 
218
  "@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="],
219
 
220
- "@types/marked": ["@types/marked@6.0.0", "", { "dependencies": { "marked": "*" } }, "sha512-jmjpa4BwUsmhxcfsgUit/7A9KbrC48Q0q8KvnY107ogcjGgTFDlIL3RpihNpx2Mu1hM4mdFQjoVc4O6JoGKHsA=="],
221
-
222
  "@types/node": ["@types/node@24.5.0", "", { "dependencies": { "undici-types": "~7.12.0" } }, "sha512-y1dMvuvJspJiPSDZUQ+WMBvF7dpnEqN4x9DDC9ie5Fs/HUZJA3wFp7EhHoVaKX/iI0cRoECV8X2jL8zi0xrHCg=="],
223
 
224
  "@types/pug": ["@types/pug@2.0.10", "", {}, "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA=="],
@@ -373,6 +370,14 @@
373
 
374
  "doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="],
375
 
 
 
 
 
 
 
 
 
376
  "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
377
 
378
  "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
@@ -383,6 +388,8 @@
383
 
384
  "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
385
 
 
 
386
  "es-abstract": ["es-abstract@1.24.0", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg=="],
387
 
388
  "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
@@ -529,6 +536,8 @@
529
 
530
  "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
531
 
 
 
532
  "http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="],
533
 
534
  "iconv-lite": ["iconv-lite@0.7.0", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ=="],
@@ -641,8 +650,6 @@
641
 
642
  "magic-string": ["magic-string@0.30.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="],
643
 
644
- "marked": ["marked@16.3.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-K3UxuKu6l6bmA5FUwYho8CfJBlsUWAooKtdGgMcERSpF7gcBUrCGsLH7wDaaNOzwq18JzSUDyoEb/YsrqMac3w=="],
645
-
646
  "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
647
 
648
  "mdn-data": ["mdn-data@2.0.30", "", {}, "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA=="],
@@ -915,7 +922,7 @@
915
 
916
  "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
917
 
918
- "vibegame": ["vibegame@0.1.7", "", { "dependencies": { "@dimforge/rapier3d-compat": "^0.18.2", "gsap": "^3.13.0", "postprocessing": "^6.37.8", "zod": "^4.1.5" }, "peerDependencies": { "bitecs": ">=0.3.40", "three": ">=0.170.0" } }, "sha512-KFzNGi+EnlEWt4R3QKPN5jVJhkgBgMg443Qq3muzSjfrp+GJ1fsHIB8ovYylRFHg8+9P3kSlWmiQ6PlDiZ8msQ=="],
919
 
920
  "vite": ["vite@5.4.20", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g=="],
921
 
@@ -979,6 +986,8 @@
979
 
980
  "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
981
 
 
 
982
  "eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
983
 
984
  "eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
 
11
  "@langchain/mcp-adapters": "^0.6.0",
12
  "@modelcontextprotocol/sdk": "^0.6.0",
13
  "@modelcontextprotocol/server-filesystem": "^0.6.2",
 
14
  "@types/node": "^24.3.3",
15
  "gsap": "^3.13.0",
16
+ "htmlparser2": "^10.0.0",
17
  "monaco-editor": "^0.50.0",
18
  "svelte-splitpanes": "^8.0.5",
19
+ "vibegame": "^0.1.9",
20
  "zod": "^4.1.8",
21
  },
22
  "devDependencies": {
 
116
 
117
  "@huggingface/jinja": ["@huggingface/jinja@0.5.1", "", {}, "sha512-yUZLld4lrM9iFxHCwFQ7D1HW2MWMwSbeB7WzWqFYDWK+rEb+WldkLdAJxUPOmgICMHZLzZGVcVjFh3w/YGubng=="],
118
 
119
+ "@huggingface/tasks": ["@huggingface/tasks@0.19.46", "", {}, "sha512-c6F/r7zRQjmyo6Ji8c2TUbdeeu6WAdZxYLRd+G7Xxvfbadi6iDwk2szt/oinC5v5Ljyc2sjzesaqGB6hLWy/DA=="],
120
 
121
  "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
122
 
 
216
 
217
  "@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="],
218
 
 
 
219
  "@types/node": ["@types/node@24.5.0", "", { "dependencies": { "undici-types": "~7.12.0" } }, "sha512-y1dMvuvJspJiPSDZUQ+WMBvF7dpnEqN4x9DDC9ie5Fs/HUZJA3wFp7EhHoVaKX/iI0cRoECV8X2jL8zi0xrHCg=="],
220
 
221
  "@types/pug": ["@types/pug@2.0.10", "", {}, "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA=="],
 
370
 
371
  "doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="],
372
 
373
+ "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
374
+
375
+ "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="],
376
+
377
+ "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="],
378
+
379
+ "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="],
380
+
381
  "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
382
 
383
  "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
 
388
 
389
  "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
390
 
391
+ "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
392
+
393
  "es-abstract": ["es-abstract@1.24.0", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg=="],
394
 
395
  "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
 
536
 
537
  "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
538
 
539
+ "htmlparser2": ["htmlparser2@10.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.1", "entities": "^6.0.0" } }, "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g=="],
540
+
541
  "http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="],
542
 
543
  "iconv-lite": ["iconv-lite@0.7.0", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ=="],
 
650
 
651
  "magic-string": ["magic-string@0.30.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="],
652
 
 
 
653
  "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
654
 
655
  "mdn-data": ["mdn-data@2.0.30", "", {}, "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA=="],
 
922
 
923
  "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
924
 
925
+ "vibegame": ["vibegame@0.1.9", "", { "dependencies": { "@dimforge/rapier3d-compat": "^0.18.2", "gsap": "^3.13.0", "postprocessing": "^6.37.8", "zod": "^4.1.5" }, "peerDependencies": { "bitecs": ">=0.3.40", "three": ">=0.170.0" } }, "sha512-f83vO9BSuG6IjwI0n6xsPYwTg9ApEBmdQBTebf5UT/Si6lgLdX6EElU8CUORb0147IXyyxk5GXzJzLpF9S24NQ=="],
926
 
927
  "vite": ["vite@5.4.20", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g=="],
928
 
 
986
 
987
  "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
988
 
989
+ "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
990
+
991
  "eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
992
 
993
  "eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
layers/structure.md CHANGED
@@ -54,7 +54,7 @@ vibegame/
54
  ├── tsconfig.json # TypeScript configuration
55
  ├── vite.config.ts # Vite + Svelte + VibeGame config
56
  ├── bun.lock # Dependency lock file
57
- ├── agents.md # VibeGame documentation
58
  └── README.md
59
  ```
60
 
 
54
  ├── tsconfig.json # TypeScript configuration
55
  ├── vite.config.ts # Vite + Svelte + VibeGame config
56
  ├── bun.lock # Dependency lock file
57
+ ├── llms.txt # VibeGame documentation
58
  └── README.md
59
  ```
60
 
llms.txt ADDED
@@ -0,0 +1,2430 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # VibeGame Engine
2
+
3
+ A 3D game engine with declarative XML syntax and ECS architecture. Start playing immediately with automatic player, camera, and lighting - just add a ground to prevent falling.
4
+
5
+ ## Core Architecture
6
+
7
+ ### ECS (Entity Component System)
8
+ - **Entities**: Just numbers (IDs), no data or behavior
9
+ - **Components**: Pure data containers (position, health, color)
10
+ - **Systems**: Functions that process entities with specific components
11
+ - **Queries**: Find entities with specific component combinations
12
+
13
+ ### Plugin System
14
+ Bevy-inspired modular architecture:
15
+ - **Components**: Data definitions using bitECS
16
+ - **Systems**: Logic organized by update phases (setup, fixed, simulation, draw)
17
+ - **Recipes**: XML entity templates with preset components
18
+ - **Config**: Defaults, shorthands, enums, validations, parsers
19
+
20
+ ### Update Phases
21
+ 1. **SetupBatch**: Input gathering and frame setup
22
+ 2. **FixedBatch**: Physics simulation at fixed timestep (50Hz)
23
+ 3. **SimulationBatch**: Game logic and state updates
24
+ 4. **DrawBatch**: Rendering and interpolation
25
+
26
+ ## Critical Rules
27
+
28
+ ⚠️ **Physics Position Override**: Physics bodies override transform positions. Always use `pos` attribute on physics entities, not transform position.
29
+
30
+ ⚠️ **Ground Required**: Always include ground/platforms or the player falls infinitely.
31
+
32
+ ⚠️ **Component Declaration**: Bare attributes mean "include with defaults", not "empty".
33
+
34
+ ## Essential Knowledge
35
+
36
+ ## Instant Playable Game
37
+
38
+ ```html
39
+ <script src="https://cdn.jsdelivr.net/npm/vibegame@latest/dist/cdn/vibegame.standalone.iife.js"></script>
40
+
41
+ <world canvas="#game-canvas" sky="#87ceeb">
42
+ <!-- Ground (REQUIRED to prevent player falling) -->
43
+ <static-part pos="0 -0.5 0" shape="box" size="20 1 20" color="#90ee90"></static-part>
44
+ </world>
45
+
46
+ <canvas id="game-canvas"></canvas>
47
+ <script>
48
+ GAME.run();
49
+ </script>
50
+ ```
51
+
52
+ This creates a complete game with:
53
+ - ✅ Player character (auto-created)
54
+ - ✅ Orbital camera (auto-created)
55
+ - ✅ Directional + ambient lighting (auto-created)
56
+ - ✅ Ground platform (you provide this)
57
+ - ✅ WASD movement, mouse camera, space to jump
58
+
59
+ ## Development Setup
60
+
61
+ After installation with `npm create vibegame@latest my-game`:
62
+
63
+ ```bash
64
+ cd my-game
65
+ bun dev # Start dev server with hot reload
66
+ ```
67
+
68
+ ### Project Structure
69
+ - **TypeScript** - Full TypeScript support with strict type checking
70
+ - **src/main.ts** - Entry point for your game
71
+ - **index.html** - HTML template with canvas element
72
+ - **vite.config.ts** - Build configuration
73
+
74
+ ### Commands
75
+ - `bun dev` - Development server with hot reload
76
+ - `bun run build` - Production build
77
+ - `bun run preview` - Preview production build
78
+ - `bun run check` - TypeScript type checking
79
+ - `bun run lint` - Lint code with ESLint
80
+ - `bun run format` - Format code with Prettier
81
+
82
+ ## Physics Objects
83
+
84
+ ```xml
85
+ <world canvas="#game-canvas">
86
+ <!-- 1. Static: Never moves (grounds, walls, platforms) -->
87
+ <static-part pos="0 -0.5 0" shape="box" size="20 1 20" color="#808080"></static-part>
88
+
89
+ <!-- 2. Dynamic: Falls with gravity (balls, crates, debris) -->
90
+ <dynamic-part pos="0 5 0" shape="sphere" size="1" color="#ff0000"></dynamic-part>
91
+
92
+ <!-- 3. Kinematic: Script-controlled movement (moving platforms, doors) -->
93
+ <kinematic-part pos="5 2 0" shape="box" size="3 0.5 3" color="#0000ff">
94
+ <!-- Animate the platform up and down -->
95
+ <tween target="body.pos-y" from="2" to="5" duration="3" loop="ping-pong"></tween>
96
+ </kinematic-part>
97
+ </world>
98
+ ```
99
+
100
+ ## CRITICAL: Physics Position vs Transform Position
101
+
102
+ <warning>
103
+ ⚠️ **Physics bodies override transform positions!**
104
+ Always set position on the body, not the transform, for physics entities.
105
+ </warning>
106
+
107
+ ```xml
108
+ <!-- ✅ BEST: Use recipe with pos shorthand -->
109
+ <dynamic-part pos="0 5 0" shape="sphere" size="1"></dynamic-part>
110
+
111
+ <!-- ❌ WRONG: Transform position ignored if body exists -->
112
+ <entity transform="pos: 0 5 0" body collider></entity> <!-- Falls to 0,0,0! -->
113
+
114
+ <!-- ✅ CORRECT: Set body position explicitly (if using raw entity) -->
115
+ <entity transform body="pos: 0 5 0" collider></entity>
116
+ ```
117
+
118
+ ## ECS Architecture Explained
119
+
120
+ Unlike traditional game engines with GameObjects, VibeGame uses Entity-Component-System:
121
+
122
+ - **Entities**: Just numbers (IDs), no data or behavior
123
+ - **Components**: Pure data containers (position, health, color)
124
+ - **Systems**: Functions that process entities with specific components
125
+
126
+ ```typescript
127
+ // Component = Data only
128
+ const Health = GAME.defineComponent({
129
+ current: GAME.Types.f32,
130
+ max: GAME.Types.f32
131
+ });
132
+
133
+ // System = Logic only
134
+ const healthQuery = GAME.defineQuery([Health]);
135
+ const DamageSystem: GAME.System = {
136
+ update: (state) => {
137
+ const entities = healthQuery(state.world);
138
+ for (const entity of entities) {
139
+ Health.current[entity] -= 1 * state.time.deltaTime;
140
+ if (Health.current[entity] <= 0) {
141
+ state.destroyEntity(entity);
142
+ }
143
+ }
144
+ }
145
+ };
146
+ ```
147
+
148
+ ## What's Auto-Created (Game Engine Defaults)
149
+
150
+ The engine automatically creates these if missing:
151
+ 1. **Player** - Character with physics, controls, and respawn (at 0, 1, 0)
152
+ 2. **Camera** - Orbital camera following the player
153
+ 3. **Lighting** - Ambient + directional light with shadows
154
+
155
+ You only need to provide:
156
+ - **Ground/platforms** - Or the player falls forever
157
+ - **Game objects** - Whatever makes your game unique
158
+
159
+ ### Override Auto-Creation (When Needed)
160
+
161
+ While auto-creation is recommended, you can manually create these for customization:
162
+
163
+ ```xml
164
+ <world canvas="#game-canvas">
165
+ <static-part pos="0 -0.5 0" shape="box" size="20 1 20" color="#90ee90"></static-part>
166
+
167
+ <!-- Custom player spawn position and properties -->
168
+ <player pos="0 10 0" speed="8" jump-height="3"></player>
169
+
170
+ <!-- Custom camera settings -->
171
+ <camera orbit-camera="distance: 10; target-pitch: 0.5"></camera>
172
+
173
+ <!-- Custom lighting (or use <light> for both ambient + directional) -->
174
+ <ambient-light sky-color="#ff6b6b" ground-color="#4ecdc4" intensity="0.8"></ambient-light>
175
+ <directional-light color="#ffffff" intensity="0.5" direction="-1 -2 -1"></directional-light>
176
+ </world>
177
+ ```
178
+
179
+ **Best Practice**: Use auto-creation unless you specifically need custom positions, properties, or multiple instances. The defaults are well-tuned for most games.
180
+
181
+ ## Post-Processing Effects
182
+
183
+ ```xml
184
+ <!-- Bloom effect for glow -->
185
+ <camera bloom="intensity: 2; luminance-threshold: 0.8"></camera>
186
+
187
+ <!-- Retro dithering (reduces color palette) -->
188
+ <camera dithering="color-bits: 3; scale: 2; noise: 1"></camera>
189
+
190
+ <!-- Tonemapping for HDR-like visuals -->
191
+ <camera tonemapping="mode: aces-filmic"></camera>
192
+
193
+ <!-- Combined cinematic style -->
194
+ <camera bloom="intensity: 1.5" tonemapping="mode: aces-filmic"></camera>
195
+ ```
196
+
197
+ ## Common Game Patterns
198
+
199
+ ### Basic Platformer
200
+ ```xml
201
+ <world canvas="#game-canvas">
202
+ <!-- Ground -->
203
+ <static-part pos="0 -0.5 0" shape="box" size="50 1 50" color="#90ee90"></static-part>
204
+
205
+ <!-- Platforms at different heights -->
206
+ <static-part pos="-5 2 0" shape="box" size="3 0.5 3" color="#808080"></static-part>
207
+ <static-part pos="0 4 0" shape="box" size="3 0.5 3" color="#808080"></static-part>
208
+ <static-part pos="5 6 0" shape="box" size="3 0.5 3" color="#808080"></static-part>
209
+
210
+ <!-- Moving platform -->
211
+ <kinematic-part pos="0 3 5" shape="box" size="4 0.5 4" color="#4169e1">
212
+ <tween target="body.pos-x" from="-10" to="10" duration="5" loop="ping-pong"></tween>
213
+ </kinematic-part>
214
+
215
+ <!-- Goal area -->
216
+ <static-part pos="10 8 0" shape="box" size="5 0.5 5" color="#ffd700"></static-part>
217
+ </world>
218
+ ```
219
+
220
+ ### Collectible Coins (Collision-based)
221
+ ```xml
222
+ <world canvas="#game-canvas">
223
+ <static-part pos="0 -0.5 0" shape="box" size="20 1 20" color="#90ee90"></static-part>
224
+
225
+ <!-- Spinning coins -->
226
+ <kinematic-part pos="2 1 0" shape="cylinder" size="0.5 0.1 0.5" color="#ffd700">
227
+ <tween target="body.euler-y" from="0" to="360" duration="2" loop="loop"></tween>
228
+ </kinematic-part>
229
+
230
+ <kinematic-part pos="-2 1 0" shape="cylinder" size="0.5 0.1 0.5" color="#ffd700">
231
+ <tween target="body.euler-y" from="0" to="360" duration="2" loop="loop"></tween>
232
+ </kinematic-part>
233
+ </world>
234
+ ```
235
+
236
+ ### Physics Playground
237
+ ```xml
238
+ <world canvas="#game-canvas">
239
+ <!-- Ground -->
240
+ <static-part pos="0 -0.5 0" shape="box" size="30 1 30" color="#90ee90"></static-part>
241
+
242
+ <!-- Walls -->
243
+ <static-part pos="15 5 0" shape="box" size="1 10 30" color="#808080"></static-part>
244
+ <static-part pos="-15 5 0" shape="box" size="1 10 30" color="#808080"></static-part>
245
+ <static-part pos="0 5 15" shape="box" size="30 10 1" color="#808080"></static-part>
246
+ <static-part pos="0 5 -15" shape="box" size="30 10 1" color="#808080"></static-part>
247
+
248
+ <!-- Spawn balls at different positions -->
249
+ <dynamic-part pos="-5 10 0" shape="sphere" size="1" color="#ff0000"></dynamic-part>
250
+ <dynamic-part pos="0 12 0" shape="sphere" size="1.5" color="#00ff00"></dynamic-part>
251
+ <dynamic-part pos="5 8 0" shape="sphere" size="0.8" color="#0000ff"></dynamic-part>
252
+
253
+ <!-- Bouncy ball (high restitution) -->
254
+ <dynamic-part pos="0 15 5" shape="sphere" size="1" color="#ff00ff"
255
+ collider="restitution: 0.9"></dynamic-part>
256
+ </world>
257
+ ```
258
+
259
+ ## Recipe Reference
260
+
261
+ | Recipe | Purpose | Key Attributes | Common Use |
262
+ |--------|---------|---------------|------------|
263
+ | `<static-part>` | Immovable objects | `pos`, `shape`, `size`, `color` | Grounds, walls, platforms |
264
+ | `<dynamic-part>` | Gravity-affected objects | `pos`, `shape`, `size`, `color`, `mass` | Balls, crates, falling objects |
265
+ | `<kinematic-part>` | Script-controlled physics | `pos`, `shape`, `size`, `color` | Moving platforms, doors |
266
+ | `<player>` | Player character | `pos`, `speed`, `jump-height` | Main character (auto-created) |
267
+ | `<entity>` | Base entity | Any components via attributes | Custom entities |
268
+
269
+ ### Shape Options
270
+ - `box` - Rectangular solid (default)
271
+ - `sphere` - Ball shape
272
+ - `cylinder` - Cylindrical shape
273
+ - `capsule` - Pill shape (good for characters)
274
+
275
+ ### Size Attribute
276
+ - Box: `size="width height depth"` or `size="2 1 2"`
277
+ - Sphere: `size="diameter"` or `size="1"`
278
+ - Cylinder: `size="diameter height"` or `size="1 2"`
279
+ - Broadcast: `size="2"` becomes `size="2 2 2"`
280
+
281
+ ## How Recipes and Shorthands Work
282
+
283
+ ### Everything is an Entity
284
+ Every XML tag creates an entity. Recipes like `<static-part>` are just shortcuts for `<entity>` with preset components.
285
+
286
+ ```xml
287
+ <!-- These are equivalent: -->
288
+ <static-part pos="0 0 0" color="#ff0000"></static-part>
289
+
290
+ <entity
291
+ transform
292
+ body="type: fixed"
293
+ collider
294
+ renderer="color: 0xff0000"
295
+ pos="0 0 0"></entity>
296
+ ```
297
+
298
+ ### Component Attributes
299
+ Components are declared using bare attributes (no value means "use defaults"):
300
+
301
+ ```xml
302
+ <!-- Bare attributes declare components with default values -->
303
+ <entity transform body collider renderer></entity>
304
+
305
+ <!-- Add properties to override defaults -->
306
+ <entity transform="pos-x: 5; pos-y: 2; pos-z: -3; scale: 2"></entity>
307
+
308
+ <!-- Mix bare and valued attributes -->
309
+ <entity transform="pos: 0 5 0" body="type: dynamic; mass: 10" collider renderer></entity>
310
+
311
+ <!-- Property groups -->
312
+ <entity transform="pos: 5 2 -3; scale: 2 2 2"></entity>
313
+ ```
314
+
315
+ **Important**: Bare attributes like `transform` mean "include this component with default values", NOT "empty" or "disabled".
316
+
317
+ ### Automatic Shorthand Expansion
318
+ Shorthands expand to ANY component with matching properties:
319
+
320
+ ```xml
321
+ <!-- "pos" shorthand applies to components with posX, posY, posZ -->
322
+ <entity transform body pos="0 5 0"></entity>
323
+ <!-- Both transform AND body get pos values -->
324
+
325
+ <!-- "color" shorthand applies to renderer.color -->
326
+ <entity renderer color="#ff0000"></entity>
327
+
328
+ <!-- "size" shorthand (broadcasts single value) -->
329
+ <entity collider size="2"></entity>
330
+ <!-- Expands to: sizeX: 2, sizeY: 2, sizeZ: 2 -->
331
+
332
+ <!-- Multiple shorthands together -->
333
+ <entity transform body collider renderer pos="0 5 0" size="1" color="#ff0000"></entity>
334
+ ```
335
+
336
+ ### Recipe Internals
337
+ Recipes are registered component bundles with defaults:
338
+
339
+ ```xml
340
+ <!-- What <dynamic-part> actually is: -->
341
+ <entity
342
+ transform
343
+ body="type: dynamic" <!-- Override -->
344
+ collider
345
+ renderer
346
+ respawn
347
+ ></entity>
348
+
349
+ <!-- So this: -->
350
+ <dynamic-part pos="0 5 0" color="#ff0000"></dynamic-part>
351
+
352
+ <!-- Is really: -->
353
+ <entity
354
+ transform="pos: 0 5 0"
355
+ body="type: dynamic; pos: 0 5 0" <!-- pos applies to body too! -->
356
+ collider
357
+ renderer="color: 0xff0000"
358
+ respawn
359
+ ></entity>
360
+ ```
361
+
362
+ ### Common Pitfall: Component Requirements
363
+ ```xml
364
+ <!-- ❌ BAD: Missing required components -->
365
+ <entity pos="0 5 0"></entity> <!-- No transform component! -->
366
+
367
+ <!-- ✅ GOOD: Explicit components -->
368
+ <entity transform="pos: 0 5 0"></entity>
369
+
370
+ <!-- ✅ BEST: Use recipe with built-in components -->
371
+ <static-part pos="0 5 0"></static-part>
372
+ ```
373
+
374
+ ### Best Practices Summary
375
+ 1. **Use recipes** (`<static-part>`, `<dynamic-part>`, etc.) instead of raw `<entity>` tags
376
+ 2. **Use shorthands** (`pos`, `size`, `color`) for cleaner code
377
+ 3. **Override only what you need** - recipes have good defaults
378
+ 4. **Mix recipes with custom components** - e.g., `<dynamic-part health="max: 100">`
379
+
380
+ ## Currently Supported Features
381
+
382
+ ### ✅ What Works Well
383
+ - **Basic platforming** - Jump puzzles, obstacle courses
384
+ - **Physics interactions** - Balls, dominoes, stacking
385
+ - **Moving platforms** - Via kinematic bodies + tweening
386
+ - **Collectibles** - Using collision detection in systems
387
+ - **Third-person character control** - WASD + mouse camera
388
+ - **Gamepad support** - Xbox/PlayStation controllers
389
+ - **Visual effects** - Tweening colors, positions, rotations
390
+ - **Post-processing** - Bloom, dithering, and tonemapping effects for visual styling
391
+ - **Game UI/HUD** - HTML/CSS overlays with GSAP animations, ECS state integration
392
+
393
+ ### Example Prompts That Work
394
+ - "Create a platformer with moving platforms and collectible coins"
395
+ - "Make bouncing balls that collide with walls"
396
+ - "Build an obstacle course with rotating platforms"
397
+ - "Add falling crates that stack up"
398
+ - "Create a simple parkour level"
399
+ - "Add a score display and upgrade menu with animations"
400
+ - "Create a game with currency system and floating text effects"
401
+
402
+ ### Troubleshooting
403
+
404
+ - **Physics not working?** → Check if ground exists, verify `<world>` tag
405
+ - **Entity not appearing?** → Verify transform component, check position values
406
+ - **Movement feels wrong?** → Physics body position overrides transform position
407
+ - **Player falling forever?** → Add a ground/platform with `<static-part>`
408
+
409
+ ## Plugin Development Pattern
410
+
411
+ ```typescript
412
+ // Component Definition
413
+ export const MyComponent = GAME.defineComponent({
414
+ value: GAME.Types.f32,
415
+ enabled: GAME.Types.ui8
416
+ });
417
+
418
+ // System Definition
419
+ const myQuery = GAME.defineQuery([MyComponent]);
420
+ export const MySystem: GAME.System = {
421
+ group: 'simulation', // or 'setup', 'fixed', 'draw'
422
+ update: (state) => {
423
+ const entities = myQuery(state.world);
424
+ for (const entity of entities) {
425
+ // System logic
426
+ MyComponent.value[entity] += state.time.deltaTime;
427
+ }
428
+ }
429
+ };
430
+
431
+ // Plugin Bundle
432
+ export const MyPlugin: GAME.Plugin = {
433
+ components: { MyComponent },
434
+ systems: [MySystem],
435
+ config: {
436
+ defaults: { "my-component": { value: 1, enabled: 1 } },
437
+ shorthands: { "my-val": "my-component.value" }
438
+ }
439
+ };
440
+
441
+ // Registration
442
+ GAME.withPlugin(MyPlugin).run();
443
+ ```
444
+
445
+ ## State Management Patterns
446
+
447
+ ### Singleton Entities (Game State)
448
+ ```typescript
449
+ function getOrCreateGameState(state: GAME.State): number {
450
+ const query = GAME.defineQuery([GameState]);
451
+ const entities = query(state.world);
452
+ if (entities.length > 0) return entities[0];
453
+
454
+ const entity = state.createEntity();
455
+ state.addComponent(entity, GameState, { score: 0, level: 1 });
456
+ return entity;
457
+ }
458
+ ```
459
+
460
+ ### Component Access (bitECS style)
461
+ ```typescript
462
+ // Direct property access
463
+ GAME.Transform.posX[entity] = 10;
464
+ GAME.Transform.posY[entity] = 5;
465
+
466
+ // Read values
467
+ const x = GAME.Transform.posX[entity];
468
+ const health = GAME.Health.current[entity];
469
+ ```
470
+
471
+ ## Features Not Yet Built-In
472
+
473
+ ### ❌ Engine Features Not Available
474
+ - **Multiplayer/Networking** - No server sync
475
+ - **Sound/Audio** - No audio system yet
476
+ - **Save/Load** - No persistence system
477
+ - **Inventory** - No item management system built-in (but easily implementable with UI)
478
+ - **Dialog/NPCs** - No conversation system built-in (but easily implementable with UI)
479
+ - **AI/Pathfinding** - No enemy AI
480
+ - **Particles** - No particle effects (though UI can create particle-like effects)
481
+ - **Custom shaders** - Fixed rendering pipeline
482
+ - **Terrain** - Use box platforms instead
483
+
484
+ ### Recommended Approaches
485
+ - **Complex UI** → HTML/CSS overlays (this is actually superior to most game engines)
486
+ - **Animations** → GSAP for smooth transitions and effects
487
+ - **Level progression** → Reload with different XML or hide/show worlds
488
+ - **Enemy behavior** → Tweened movement patterns
489
+ - **Interactions** → Collision detection in custom systems
490
+
491
+ ## Common Mistakes to Avoid
492
+
493
+ ### ❌ Forgetting the Ground
494
+ ```xml
495
+ <!-- BAD: No ground, player falls forever -->
496
+ <world canvas="#game-canvas">
497
+ <dynamic-part pos="0 5 0" shape="sphere"></dynamic-part>
498
+ </world>
499
+ ```
500
+
501
+ ### ❌ Setting Transform Position on Physics Objects
502
+ ```xml
503
+ <!-- BAD: Transform position ignored -->
504
+ <entity transform="pos: 0 5 0" body collider></entity>
505
+
506
+ <!-- GOOD: Set body position (raw entity) -->
507
+ <entity transform body="pos: 0 5 0" collider></entity>
508
+
509
+ <!-- BEST: Use recipes with pos shorthand -->
510
+ <dynamic-part pos="0 5 0" shape="sphere"></dynamic-part>
511
+ ```
512
+
513
+ ### ❌ Missing World Tag
514
+ ```xml
515
+ <!-- BAD: Entities outside world tag -->
516
+ <static-part pos="0 0 0" shape="box"></static-part>
517
+
518
+ <!-- GOOD: Everything inside world -->
519
+ <world canvas="#game-canvas">
520
+ <static-part pos="0 0 0" shape="box"></static-part>
521
+ </world>
522
+ ```
523
+
524
+ ### ❌ Wrong Physics Type
525
+ ```xml
526
+ <!-- BAD: Dynamic platform (falls with gravity) -->
527
+ <dynamic-part pos="0 3 0" shape="box">
528
+ <tween target="body.pos-x" from="-5" to="5"></tween>
529
+ </dynamic-part>
530
+
531
+ <!-- GOOD: Kinematic for controlled movement -->
532
+ <kinematic-part pos="0 3 0" shape="box">
533
+ <tween target="body.pos-x" from="-5" to="5"></tween>
534
+ </kinematic-part>
535
+ ```
536
+
537
+ ## Custom Components and Systems
538
+
539
+ ### Creating a Health System
540
+ ```typescript
541
+ import * as GAME from 'vibegame';
542
+
543
+ // Define the component
544
+ const Health = GAME.defineComponent({
545
+ current: GAME.Types.f32,
546
+ max: GAME.Types.f32
547
+ });
548
+
549
+ // Create the system
550
+ const HealthSystem: GAME.System = {
551
+ update: (state) => {
552
+ const entities = GAME.defineQuery([Health])(state.world);
553
+ for (const entity of entities) {
554
+ // Regenerate health over time
555
+ if (Health.current[entity] < Health.max[entity]) {
556
+ Health.current[entity] += 5 * state.time.deltaTime;
557
+ }
558
+ }
559
+ }
560
+ };
561
+
562
+ // Bundle as plugin
563
+ const HealthPlugin: GAME.Plugin = {
564
+ components: { Health },
565
+ systems: [HealthSystem],
566
+ config: {
567
+ defaults: {
568
+ "health": { current: 100, max: 100 }
569
+ }
570
+ }
571
+ };
572
+
573
+ // Use in game
574
+ GAME.withPlugin(HealthPlugin).run();
575
+ ```
576
+
577
+ ### Using in XML
578
+ ```xml
579
+ <world canvas="#game-canvas">
580
+ <!-- Add health to a dynamic entity (best practice: use recipes) -->
581
+ <dynamic-part pos="0 2 0" shape="sphere" color="#ff0000"
582
+ health="current: 50; max: 100"></dynamic-part>
583
+ </world>
584
+ ```
585
+
586
+ ## State API Reference
587
+
588
+ Available in all systems via the `state` parameter:
589
+
590
+ ### Entity Management
591
+ - `createEntity(): number` - Create new entity
592
+ - `destroyEntity(entity: number)` - Remove entity
593
+ - `query(...Components): number[]` - Find entities with components
594
+
595
+ ### Component Operations
596
+ - `addComponent(entity, Component, data?)` - Add component
597
+ - `removeComponent(entity, Component)` - Remove component
598
+ - `hasComponent(entity, Component): boolean` - Check component
599
+ - `getComponent(name: string): Component | null` - Get by name
600
+
601
+ ### Time
602
+ - `time.delta: number` - Frame time in seconds
603
+ - `time.elapsed: number` - Total time in seconds
604
+ - `time.fixed: number` - Fixed timestep (1/50)
605
+
606
+ ### Physics Helpers
607
+ - `addComponent(entity, ApplyImpulse, {x, y, z})` - One-time push
608
+ - `addComponent(entity, ApplyForce, {x, y, z})` - Continuous force
609
+ - `addComponent(entity, KinematicMove, {x, y, z})` - Move kinematic
610
+
611
+ ## Plugin System
612
+
613
+ ### Using Specific Plugins
614
+ ```typescript
615
+ import * as GAME from 'vibegame';
616
+
617
+ // Start with no plugins
618
+ GAME
619
+ .withoutDefaultPlugins()
620
+ .withPlugin(TransformsPlugin) // Just transforms
621
+ .withPlugin(RenderingPlugin) // Add rendering
622
+ .withPlugin(PhysicsPlugin) // Add physics
623
+ .run();
624
+ ```
625
+
626
+ ### Default Plugin Bundle
627
+ - **RecipesPlugin** - XML parsing and entity creation
628
+ - **TransformsPlugin** - Position, rotation, scale, hierarchy
629
+ - **RenderingPlugin** - Three.js meshes, lights, camera
630
+ - **PhysicsPlugin** - Rapier physics simulation
631
+ - **InputPlugin** - Keyboard, mouse, gamepad input
632
+ - **OrbitCameraPlugin** - Third-person camera
633
+ - **PlayerPlugin** - Character controller
634
+ - **TweenPlugin** - Animation system
635
+ - **RespawnPlugin** - Fall detection and reset
636
+ - **StartupPlugin** - Auto-create player/camera/lights
637
+
638
+ ## Plugin Reference
639
+
640
+ ### Core
641
+
642
+ Math utilities for interpolation and 3D transformations.
643
+
644
+ ### Functions
645
+
646
+ #### lerp(a, b, t): number
647
+ Linear interpolation
648
+
649
+ #### slerp(fromX, fromY, fromZ, fromW, toX, toY, toZ, toW, t): Quaternion
650
+ Quaternion spherical interpolation
651
+
652
+ #### Examples
653
+
654
+ ## Usage Note
655
+
656
+ Math utilities are used internally by systems like tweening and transforms. For animating properties, use the Tween system instead of directly calling interpolation functions. For transformations, use the Transform component's euler angles which are automatically converted to quaternions by the system.
657
+
658
+ ### Animation
659
+
660
+ Procedural character animation with body parts that respond to movement states.
661
+
662
+ ### Components
663
+
664
+ #### AnimatedCharacter
665
+ - headEntity: eid
666
+ - torsoEntity: eid
667
+ - leftArmEntity: eid
668
+ - rightArmEntity: eid
669
+ - leftLegEntity: eid
670
+ - rightLegEntity: eid
671
+ - phase: f32 - Walk cycle phase (0-1)
672
+ - jumpTime: f32
673
+ - fallTime: f32
674
+ - animationState: ui8 - 0=IDLE, 1=WALKING, 2=JUMPING, 3=FALLING, 4=LANDING
675
+ - stateTransition: f32
676
+
677
+ #### HasAnimator
678
+ Tag component (no properties)
679
+
680
+ ### Systems
681
+
682
+ #### AnimatedCharacterInitializationSystem
683
+ - Group: setup
684
+ - Creates body part entities for AnimatedCharacter components
685
+
686
+ #### AnimatedCharacterUpdateSystem
687
+ - Group: simulation
688
+ - Updates character animation based on movement and physics state
689
+
690
+ #### Examples
691
+
692
+ ## Examples
693
+
694
+ ### Basic Usage
695
+
696
+ ```typescript
697
+ import * as GAME from 'vibegame';
698
+
699
+ // Add animated character to a player entity
700
+ const player = state.createEntity();
701
+ state.addComponent(player, GAME.AnimatedCharacter);
702
+ state.addComponent(player, GAME.CharacterController);
703
+ state.addComponent(player, GAME.Transform);
704
+
705
+ // The AnimatedCharacterInitializationSystem will automatically
706
+ // create body parts in the next setup phase
707
+ ```
708
+
709
+ ### Accessing Animation State
710
+
711
+ ```typescript
712
+ import * as GAME from 'vibegame';
713
+
714
+ const characterQuery = GAME.defineQuery([GAME.AnimatedCharacter]);
715
+ const MySystem: GAME.System = {
716
+ update: (state) => {
717
+ const characters = characterQuery(state.world);
718
+ for (const entity of characters) {
719
+ const animState = GAME.AnimatedCharacter.animationState[entity];
720
+ if (animState === 2) { // JUMPING
721
+ console.log('Character is jumping!');
722
+ }
723
+ }
724
+ }
725
+ };
726
+ ```
727
+
728
+ ### XML Declaration
729
+
730
+ ```xml
731
+ <!-- Player entity with animated character -->
732
+ <entity
733
+ animated-character
734
+ character-controller
735
+ transform="pos: 0 2 0"
736
+ />
737
+ ```
738
+
739
+ ### Input
740
+
741
+ Focus-aware input handling for mouse, keyboard, and gamepad with buffered actions. Keyboard input only responds when canvas has focus.
742
+
743
+ ### Components
744
+
745
+ #### InputState
746
+ - moveX: f32 - Horizontal axis (-1 left, 1 right)
747
+ - moveY: f32 - Forward/backward (-1 back, 1 forward)
748
+ - moveZ: f32 - Vertical axis (-1 down, 1 up)
749
+ - lookX: f32 - Mouse delta X
750
+ - lookY: f32 - Mouse delta Y
751
+ - scrollDelta: f32 - Mouse wheel delta
752
+ - jump: ui8 - Jump available (0/1)
753
+ - primaryAction: ui8 - Primary action (0/1)
754
+ - secondaryAction: ui8 - Secondary action (0/1)
755
+ - leftMouse: ui8 - Left button (0/1)
756
+ - rightMouse: ui8 - Right button (0/1)
757
+ - middleMouse: ui8 - Middle button (0/1)
758
+ - jumpBufferTime: f32
759
+ - primaryBufferTime: f32
760
+ - secondaryBufferTime: f32
761
+
762
+ ### Systems
763
+
764
+ #### InputSystem
765
+ - Group: simulation
766
+ - Updates InputState components with current input data
767
+
768
+ ### Functions
769
+
770
+ #### setTargetCanvas(canvas: HTMLCanvasElement | null): void
771
+ Registers canvas for focus-based keyboard input
772
+
773
+ #### consumeJump(): boolean
774
+ Consumes buffered jump input
775
+
776
+ #### consumePrimary(): boolean
777
+ Consumes buffered primary action
778
+
779
+ #### consumeSecondary(): boolean
780
+ Consumes buffered secondary action
781
+
782
+ #### handleMouseMove(event: MouseEvent): void
783
+ Processes mouse movement
784
+
785
+ #### handleMouseDown(event: MouseEvent): void
786
+ Processes mouse button press
787
+
788
+ #### handleMouseUp(event: MouseEvent): void
789
+ Processes mouse button release
790
+
791
+ #### handleWheel(event: WheelEvent): void
792
+ Processes mouse wheel
793
+
794
+ ### Constants
795
+
796
+ #### INPUT_CONFIG
797
+ Default input mappings and sensitivity settings
798
+
799
+ #### Examples
800
+
801
+ ## Examples
802
+
803
+ ### Basic Plugin Registration
804
+
805
+ ```typescript
806
+ import * as GAME from 'vibegame';
807
+
808
+ GAME
809
+ .withPlugin(GAME.InputPlugin)
810
+ .run();
811
+ ```
812
+
813
+ ### Reading Input in a Custom System
814
+
815
+ ```typescript
816
+ import * as GAME from 'vibegame';
817
+
818
+ const playerQuery = GAME.defineQuery([GAME.Player, GAME.InputState]);
819
+ const PlayerControlSystem: GAME.System = {
820
+ update: (state) => {
821
+ const players = playerQuery(state.world);
822
+
823
+ for (const player of players) {
824
+ // Read movement axes
825
+ const moveX = GAME.InputState.moveX[player];
826
+ const moveY = GAME.InputState.moveY[player];
827
+
828
+ // Check for jump
829
+ if (GAME.InputState.jump[player]) {
830
+ // Jump is available this frame
831
+ }
832
+
833
+ // Check mouse buttons
834
+ if (GAME.InputState.leftMouse[player]) {
835
+ // Left mouse is held
836
+ }
837
+ }
838
+ }
839
+ };
840
+ ```
841
+
842
+ ### Consuming Buffered Actions
843
+
844
+ ```typescript
845
+ import * as GAME from 'vibegame';
846
+
847
+ const CombatSystem: GAME.System = {
848
+ update: (state) => {
849
+ // Consume jump if available (prevents double consumption)
850
+ if (GAME.consumeJump()) {
851
+ // Perform jump
852
+ velocity.y = JUMP_FORCE;
853
+ }
854
+
855
+ // Consume primary action
856
+ if (GAME.consumePrimary()) {
857
+ // Fire weapon
858
+ spawnProjectile();
859
+ }
860
+ }
861
+ };
862
+ ```
863
+
864
+ ### Custom Input Mappings
865
+
866
+ ```typescript
867
+ import * as GAME from 'vibegame';
868
+
869
+ // Modify before starting the game
870
+ GAME.INPUT_CONFIG.mappings.jump = ['Space', 'KeyX'];
871
+ GAME.INPUT_CONFIG.mappings.moveForward = ['KeyW', 'KeyZ', 'ArrowUp'];
872
+ GAME.INPUT_CONFIG.mouseSensitivity.look = 0.3;
873
+
874
+ GAME.run();
875
+ ```
876
+
877
+ ### Manual Event Handling
878
+
879
+ ```typescript
880
+ import * as GAME from 'vibegame';
881
+
882
+ // Use the exported handlers directly if needed
883
+ canvas.addEventListener('mousedown', GAME.handleMouseDown);
884
+ canvas.addEventListener('mouseup', GAME.handleMouseUp);
885
+ ```
886
+
887
+ ### Orbit Camera
888
+
889
+ Orbital camera controller for third-person views and smooth target following.
890
+
891
+ ### Components
892
+
893
+ #### OrbitCamera
894
+ - target: eid (0) - Target entity ID
895
+ - current-yaw: f32 (π) - Current horizontal angle
896
+ - current-pitch: f32 (π/6) - Current vertical angle
897
+ - current-distance: f32 (4) - Current distance
898
+ - target-yaw: f32 (π) - Target horizontal angle
899
+ - target-pitch: f32 (π/6) - Target vertical angle
900
+ - target-distance: f32 (4) - Target distance
901
+ - min-distance: f32 (1)
902
+ - max-distance: f32 (25)
903
+ - min-pitch: f32 (0)
904
+ - max-pitch: f32 (π/2)
905
+ - smoothness: f32 (0.5) - Interpolation speed
906
+ - offset-x: f32 (0)
907
+ - offset-y: f32 (1.25)
908
+ - offset-z: f32 (0)
909
+
910
+ ### Systems
911
+
912
+ #### OrbitCameraSystem
913
+ - Group: draw
914
+ - Updates camera position and rotation around target
915
+
916
+ ### Recipes
917
+
918
+ #### camera
919
+ - Creates orbital camera with default settings
920
+ - Components: orbit-camera, transform, world-transform, main-camera
921
+
922
+ #### Examples
923
+
924
+ ## Examples
925
+
926
+ ### Basic Camera
927
+
928
+ ```xml
929
+ <!-- Create default orbital camera -->
930
+ <camera />
931
+ ```
932
+
933
+ ### Camera Following Player
934
+
935
+ ```xml
936
+ <world>
937
+ <!-- Player entity -->
938
+ <player id="player" pos="0 0 0" />
939
+
940
+ <!-- Camera following player -->
941
+ <camera
942
+ target="#player"
943
+ target-distance="10"
944
+ min-distance="5"
945
+ max-distance="20"
946
+ offset-y="2"
947
+ />
948
+ </world>
949
+ ```
950
+
951
+ ### Custom Orbit Settings
952
+
953
+ ```xml
954
+ <entity
955
+ orbit-camera="
956
+ target: #boss;
957
+ target-distance: 15;
958
+ target-yaw: 0;
959
+ target-pitch: 0.5;
960
+ smoothness: 0.2;
961
+ offset-y: 3
962
+ "
963
+ transform
964
+ main-camera
965
+ />
966
+ ```
967
+
968
+ ### Programmatic Usage
969
+
970
+ ```typescript
971
+ import * as GAME from 'vibegame';
972
+
973
+ const cameraQuery = GAME.defineQuery([GAME.OrbitCamera]);
974
+ const CameraControlSystem = {
975
+ update: (state) => {
976
+ const cameras = cameraQuery(state.world);
977
+
978
+ for (const camera of cameras) {
979
+ // Rotate camera on input
980
+ if (state.input.mouse.deltaX) {
981
+ GAME.OrbitCamera.targetYaw[camera] += state.input.mouse.deltaX * 0.01;
982
+ }
983
+
984
+ // Zoom on scroll
985
+ if (state.input.mouse.wheel) {
986
+ GAME.OrbitCamera.targetDistance[camera] = Math.max(
987
+ GAME.OrbitCamera.minDistance[camera],
988
+ Math.min(
989
+ GAME.OrbitCamera.maxDistance[camera],
990
+ GAME.OrbitCamera.targetDistance[camera] - state.input.mouse.wheel * 0.5
991
+ )
992
+ );
993
+ }
994
+ }
995
+ }
996
+ };
997
+ ```
998
+
999
+ ### Dynamic Target Switching
1000
+
1001
+ ```typescript
1002
+ import * as GAME from 'vibegame';
1003
+
1004
+ // Switch camera target
1005
+ const switchTarget = (state, cameraEntity, newTargetEntity) => {
1006
+ GAME.OrbitCamera.target[cameraEntity] = newTargetEntity;
1007
+ };
1008
+ ```
1009
+
1010
+ ### Physics
1011
+
1012
+ 3D physics simulation with Rapier including rigid bodies, collisions, and character controllers.
1013
+
1014
+ ### Constants
1015
+
1016
+ - DEFAULT_GRAVITY: -60
1017
+
1018
+ ### Enums
1019
+
1020
+ #### BodyType
1021
+ - Dynamic = 0 - Affected by forces
1022
+ - Fixed = 1 - Immovable static
1023
+ - KinematicPositionBased = 2 - Script position
1024
+ - KinematicVelocityBased = 3 - Script velocity
1025
+
1026
+ #### ColliderShape
1027
+ - Box = 0
1028
+ - Sphere = 1
1029
+ - Capsule = 2
1030
+
1031
+ ### Components
1032
+
1033
+ #### PhysicsWorld
1034
+ - gravityX: f32 (0)
1035
+ - gravityY: f32 (-60)
1036
+ - gravityZ: f32 (0)
1037
+
1038
+ #### Body
1039
+ - type: ui8 - BodyType enum (Fixed)
1040
+ - mass: f32 (1)
1041
+ - linearDamping: f32 (0)
1042
+ - angularDamping: f32 (0)
1043
+ - gravityScale: f32 (1)
1044
+ - ccd: ui8 (0)
1045
+ - lockRotX: ui8 (0)
1046
+ - lockRotY: ui8 (0)
1047
+ - lockRotZ: ui8 (0)
1048
+ - posX, posY, posZ: f32
1049
+ - rotX, rotY, rotZ, rotW: f32 (rotW=1)
1050
+ - eulerX, eulerY, eulerZ: f32
1051
+ - velX, velY, velZ: f32
1052
+ - rotVelX, rotVelY, rotVelZ: f32
1053
+ - lastPosX, lastPosY, lastPosZ: f32
1054
+
1055
+ #### Collider
1056
+ - shape: ui8 - ColliderShape enum (Box)
1057
+ - sizeX, sizeY, sizeZ: f32 (1)
1058
+ - radius: f32 (0.5)
1059
+ - height: f32 (1)
1060
+ - friction: f32 (0.5)
1061
+ - restitution: f32 (0)
1062
+ - density: f32 (1)
1063
+ - isSensor: ui8 (0)
1064
+ - membershipGroups: ui16 (0xffff)
1065
+ - filterGroups: ui16 (0xffff)
1066
+ - posOffsetX, posOffsetY, posOffsetZ: f32
1067
+ - rotOffsetX, rotOffsetY, rotOffsetZ, rotOffsetW: f32 (rotOffsetW=1)
1068
+
1069
+ #### CharacterController
1070
+ - offset: f32 (0.08)
1071
+ - maxSlope: f32 (45°)
1072
+ - maxSlide: f32 (30°)
1073
+ - snapDist: f32 (0.5)
1074
+ - autoStep: ui8 (1)
1075
+ - maxStepHeight: f32 (0.3)
1076
+ - minStepWidth: f32 (0.05)
1077
+ - upX, upY, upZ: f32 (upY=1)
1078
+ - moveX, moveY, moveZ: f32
1079
+ - grounded: ui8
1080
+ - platform: eid - Entity the character is standing on
1081
+ - platformVelX, platformVelY, platformVelZ: f32
1082
+ - platformDeltaX, platformDeltaY, platformDeltaZ: f32
1083
+
1084
+ #### CharacterMovement
1085
+ - desiredVelX, desiredVelY, desiredVelZ: f32
1086
+ - velocityY: f32
1087
+ - actualMoveX, actualMoveY, actualMoveZ: f32
1088
+
1089
+ #### InterpolatedTransform
1090
+ - prevPosX, prevPosY, prevPosZ: f32
1091
+ - prevRotX, prevRotY, prevRotZ, prevRotW: f32
1092
+ - posX, posY, posZ: f32
1093
+ - rotX, rotY, rotZ, rotW: f32
1094
+
1095
+ #### Force/Impulse Components
1096
+ - ApplyForce: x, y, z (f32)
1097
+ - ApplyTorque: x, y, z (f32)
1098
+ - ApplyImpulse: x, y, z (f32)
1099
+ - ApplyAngularImpulse: x, y, z (f32)
1100
+ - SetLinearVelocity: x, y, z (f32)
1101
+ - SetAngularVelocity: x, y, z (f32)
1102
+ - KinematicMove: x, y, z (f32)
1103
+ - KinematicRotate: x, y, z, w (f32)
1104
+
1105
+ #### Collision Events
1106
+ - CollisionEvents: activeEvents (ui8)
1107
+ - TouchedEvent: other, handle1, handle2 (ui32)
1108
+ - TouchEndedEvent: other, handle1, handle2 (ui32)
1109
+
1110
+ ### Systems
1111
+
1112
+ - PhysicsWorldSystem - Initializes physics world
1113
+ - PhysicsInitializationSystem - Creates bodies and colliders
1114
+ - PhysicsCleanupSystem - Removes physics on entity destroy
1115
+ - PlatformDeltaSystem - Tracks platform position changes
1116
+ - CharacterMovementSystem - Character controller movement with platform sticking
1117
+ - CollisionEventCleanupSystem - Clears collision events
1118
+ - ApplyForcesSystem - Applies forces
1119
+ - ApplyTorquesSystem - Applies torques
1120
+ - ApplyImpulsesSystem - Applies impulses
1121
+ - ApplyAngularImpulsesSystem - Applies angular impulses
1122
+ - SetVelocitySystem - Sets velocities
1123
+ - TeleportationSystem - Instant position changes
1124
+ - KinematicMovementSystem - Kinematic movement
1125
+ - PhysicsStepSystem - Steps simulation
1126
+ - PhysicsRapierSyncSystem - Syncs Rapier to ECS
1127
+ - PhysicsInterpolationSystem - Interpolates for rendering
1128
+
1129
+ ### Functions
1130
+
1131
+ #### initializePhysics(): Promise<void>
1132
+ Initializes Rapier WASM physics engine
1133
+
1134
+ ### Recipes
1135
+
1136
+ - static-part - Immovable physics objects
1137
+ - dynamic-part - Gravity-affected objects
1138
+ - kinematic-part - Script-controlled objects
1139
+
1140
+ #### Examples
1141
+
1142
+ ## Examples
1143
+
1144
+ ### Basic Usage
1145
+
1146
+ #### XML Recipes
1147
+
1148
+ ##### Static Floor
1149
+ ```xml
1150
+ <static-part
1151
+ pos="0 -0.5 0"
1152
+ shape="box"
1153
+ size="20 1 20"
1154
+ color="#90ee90"
1155
+ />
1156
+ ```
1157
+
1158
+ ##### Dynamic Ball
1159
+ ```xml
1160
+ <dynamic-part
1161
+ pos="0 5 0"
1162
+ shape="sphere"
1163
+ radius="0.5"
1164
+ color="#ff0000"
1165
+ mass="2"
1166
+ restitution="0.8"
1167
+ />
1168
+ ```
1169
+
1170
+ ##### Moving Platform
1171
+ ```xml
1172
+ <kinematic-part
1173
+ pos="0 2 0"
1174
+ shape="box"
1175
+ size="3 0.2 3"
1176
+ color="#4169e1"
1177
+ >
1178
+ <!-- Add movement with tweening -->
1179
+ <tween
1180
+ target="body.pos-y"
1181
+ from="2"
1182
+ to="5"
1183
+ duration="3"
1184
+ ease="sine-in-out"
1185
+ loop="ping-pong"
1186
+ />
1187
+ </kinematic-part>
1188
+ ```
1189
+
1190
+ ##### Character with Controller
1191
+ ```xml
1192
+ <entity
1193
+ pos="0 1 0"
1194
+ body="type: kinematic-position"
1195
+ collider="shape: capsule; height: 1.8; radius: 0.4"
1196
+ character-controller
1197
+ character-movement
1198
+ transform
1199
+ renderer
1200
+ />
1201
+ ```
1202
+
1203
+ #### JavaScript API
1204
+
1205
+ ##### Create Physics Entity
1206
+ ```typescript
1207
+ import * as GAME from 'vibegame';
1208
+
1209
+ // Create a dynamic physics box
1210
+ const entity = state.createEntity();
1211
+
1212
+ state.addComponent(entity, GAME.Body, {
1213
+ type: GAME.BodyType.Dynamic,
1214
+ mass: 5,
1215
+ posX: 0, posY: 10, posZ: 0
1216
+ });
1217
+
1218
+ state.addComponent(entity, GAME.Collider, {
1219
+ shape: GAME.ColliderShape.Box,
1220
+ sizeX: 1, sizeY: 1, sizeZ: 1,
1221
+ friction: 0.7,
1222
+ restitution: 0.3
1223
+ });
1224
+
1225
+ // Note: Physics body won't exist until next fixed update
1226
+ // Transform will be overwritten by Body position after initialization
1227
+ ```
1228
+
1229
+ ##### Moving Physics Bodies
1230
+
1231
+ ```typescript
1232
+ import * as GAME from 'vibegame';
1233
+
1234
+ // Dynamic bodies - Use forces/impulses for movement
1235
+ if (GAME.Body.type[entity] === GAME.BodyType.Dynamic) {
1236
+ // Apply force for gradual acceleration
1237
+ state.addComponent(entity, GAME.ApplyForce, { x: 10, y: 0, z: 0 });
1238
+
1239
+ // Apply impulse for instant velocity change
1240
+ state.addComponent(entity, GAME.ApplyImpulse, { x: 0, y: 50, z: 0 });
1241
+
1242
+ // Direct position setting only for teleportation
1243
+ GAME.Body.posX[entity] = 10; // Teleport - use sparingly
1244
+ }
1245
+
1246
+ // Kinematic bodies - Direct control via movement components
1247
+ if (GAME.Body.type[entity] === GAME.BodyType.KinematicPositionBased) {
1248
+ state.addComponent(entity, GAME.KinematicMove, { x: 5, y: 2, z: 0 });
1249
+ }
1250
+
1251
+ if (GAME.Body.type[entity] === GAME.BodyType.KinematicVelocityBased) {
1252
+ state.addComponent(entity, GAME.SetLinearVelocity, { x: 3, y: 0, z: 0 });
1253
+ }
1254
+
1255
+ // Never modify Transform directly for physics entities
1256
+ // GAME.Transform.posX[entity] = 10; // ❌ Will be overwritten by Body
1257
+ ```
1258
+
1259
+ ##### Apply Forces
1260
+ ```typescript
1261
+ import * as GAME from 'vibegame';
1262
+
1263
+ // Apply upward impulse (jump)
1264
+ state.addComponent(entity, GAME.ApplyImpulse, {
1265
+ x: 0, y: 50, z: 0
1266
+ });
1267
+
1268
+ // Apply continuous force
1269
+ state.addComponent(entity, GAME.ApplyForce, {
1270
+ x: 10, y: 0, z: 0
1271
+ });
1272
+
1273
+ // Set velocity directly
1274
+ state.addComponent(entity, GAME.SetLinearVelocity, {
1275
+ x: 0, y: 5, z: 0
1276
+ });
1277
+ ```
1278
+
1279
+ ##### Handle Collisions
1280
+ ```typescript
1281
+ import * as GAME from 'vibegame';
1282
+
1283
+ const touchedQuery = GAME.defineQuery([GAME.TouchedEvent]);
1284
+ const CollisionSystem: GAME.System = {
1285
+ update: (state) => {
1286
+ // Query entities with collision events
1287
+ for (const entity of touchedQuery(state.world)) {
1288
+ const otherEntity = GAME.TouchedEvent.other[entity];
1289
+ console.log(`Entity ${entity} collided with ${otherEntity}`);
1290
+
1291
+ // React to collision
1292
+ state.addComponent(entity, GAME.ApplyImpulse, {
1293
+ x: 0, y: 10, z: 0
1294
+ });
1295
+ }
1296
+ }
1297
+ };
1298
+ ```
1299
+
1300
+ ##### Character Movement
1301
+ ```typescript
1302
+ import * as GAME from 'vibegame';
1303
+
1304
+ const PlayerMovementSystem: GAME.System = {
1305
+ update: (state) => {
1306
+ const movementQuery = GAME.defineQuery([GAME.CharacterMovement, GAME.CharacterController]);
1307
+ for (const entity of movementQuery(state.world)) {
1308
+ // Set desired movement based on input
1309
+ GAME.CharacterMovement.desiredVelX[entity] = input.x * 5;
1310
+ GAME.CharacterMovement.desiredVelZ[entity] = input.z * 5;
1311
+
1312
+ // Jump if grounded
1313
+ if (GAME.CharacterController.grounded[entity] && input.jump) {
1314
+ GAME.CharacterMovement.velocityY[entity] = 10;
1315
+ }
1316
+ }
1317
+ }
1318
+ };
1319
+ ```
1320
+
1321
+ ##### Custom Plugin Integration
1322
+ ```typescript
1323
+ import * as GAME from 'vibegame';
1324
+
1325
+ // Initialize physics before running
1326
+ await GAME.initializePhysics();
1327
+
1328
+ // Use with builder
1329
+ GAME
1330
+ .withPlugin(GAME.PhysicsPlugin)
1331
+ .run();
1332
+ ```
1333
+
1334
+ ### Player
1335
+
1336
+ Complete player character controller with physics movement, jumping, and platform momentum preservation.
1337
+
1338
+ ### Components
1339
+
1340
+ #### Player
1341
+ - speed: f32 (5.3)
1342
+ - jumpHeight: f32 (2.3)
1343
+ - rotationSpeed: f32 (10)
1344
+ - canJump: ui8 (1)
1345
+ - isJumping: ui8 (0)
1346
+ - jumpCooldown: f32 (0)
1347
+ - lastGroundedTime: f32 (0)
1348
+ - jumpBufferTime: f32 (-10000)
1349
+ - cameraSensitivity: f32 (0.007)
1350
+ - cameraZoomSensitivity: f32 (1.5)
1351
+ - cameraEntity: eid (0)
1352
+ - inheritedVelX: f32 (0) - Horizontal momentum inherited from platform
1353
+ - inheritedVelZ: f32 (0) - Horizontal momentum inherited from platform
1354
+
1355
+ ### Systems
1356
+
1357
+ #### PlayerMovementSystem
1358
+ - Group: fixed
1359
+ - Handles movement, rotation, jumping with platform momentum preservation
1360
+
1361
+ #### PlayerGroundedSystem
1362
+ - Group: fixed
1363
+ - Tracks grounded state, jump availability, and clears inherited momentum on landing
1364
+
1365
+ #### PlayerCameraLinkingSystem
1366
+ - Group: simulation
1367
+ - Links player to orbit camera
1368
+
1369
+ #### PlayerCameraControlSystem
1370
+ - Group: simulation
1371
+ - Camera control via mouse input
1372
+
1373
+ ### Recipes
1374
+
1375
+ #### player
1376
+ - Complete player setup with physics
1377
+ - Components: player, character-movement, transform, world-transform, body, collider, character-controller, input-state, respawn
1378
+
1379
+ ### Functions
1380
+
1381
+ #### processInput(moveForward, moveRight, cameraYaw): Vector3
1382
+ Converts input to world-space movement
1383
+
1384
+ #### handleJump(entity, jumpPressed, currentTime): number
1385
+ Processes jump with buffering
1386
+
1387
+ #### updateRotation(entity, inputVector, deltaTime, rotationData): Quaternion
1388
+ Smooth rotation towards movement
1389
+
1390
+ #### Examples
1391
+
1392
+ ## Examples
1393
+
1394
+ ### Basic Player Usage (XML)
1395
+
1396
+ ```xml
1397
+ <world>
1398
+ <!-- Player auto-created if not specified -->
1399
+ <player pos="0 2 0" speed="6" jump-height="3" />
1400
+ </world>
1401
+ ```
1402
+
1403
+ ### Custom Player Configuration (XML)
1404
+
1405
+ ```xml
1406
+ <world>
1407
+ <player
1408
+ pos="5 1 -10"
1409
+ speed="8"
1410
+ jump-height="4"
1411
+ rotation-speed="15"
1412
+ camera-sensitivity="0.005"
1413
+ />
1414
+ </world>
1415
+ ```
1416
+
1417
+ ### Accessing Player Component (JavaScript)
1418
+
1419
+ ```typescript
1420
+ import * as GAME from 'vibegame';
1421
+
1422
+ const playerQuery = GAME.defineQuery([GAME.Player]);
1423
+ const MySystem: GAME.System = {
1424
+ update: (state) => {
1425
+ const players = playerQuery(state.world);
1426
+ for (const entity of players) {
1427
+ // Check if player is jumping
1428
+ if (GAME.Player.isJumping[entity]) {
1429
+ console.log('Player is airborne!');
1430
+ }
1431
+
1432
+ // Modify player speed
1433
+ GAME.Player.speed[entity] = 10;
1434
+ }
1435
+ }
1436
+ };
1437
+ ```
1438
+
1439
+ ### Creating Player Programmatically
1440
+
1441
+ ```typescript
1442
+ import * as GAME from 'vibegame';
1443
+
1444
+ const PlayerSpawnSystem: GAME.System = {
1445
+ setup: (state) => {
1446
+ // Create player entity
1447
+ const player = state.createEntity();
1448
+
1449
+ // Add player recipe components
1450
+ state.addComponent(player, GAME.Player, {
1451
+ speed: 7,
1452
+ jumpHeight: 3.5,
1453
+ cameraSensitivity: 0.01
1454
+ });
1455
+
1456
+ // Add required components
1457
+ state.addComponent(player, GAME.Transform, { posY: 5 });
1458
+ state.addComponent(player, GAME.Body, { type: GAME.BodyType.KinematicPositionBased });
1459
+ state.addComponent(player, GAME.CharacterController);
1460
+ state.addComponent(player, GAME.InputState);
1461
+ }
1462
+ };
1463
+ ```
1464
+
1465
+ ### Movement Controls
1466
+
1467
+ **Keyboard:**
1468
+ - W/S or Arrow Up/Down - Move forward/backward
1469
+ - A/D or Arrow Left/Right - Move left/right
1470
+ - Space - Jump
1471
+
1472
+ **Mouse:**
1473
+ - Right-click + drag - Rotate camera
1474
+ - Scroll wheel - Zoom in/out
1475
+
1476
+ ### Plugin Registration
1477
+
1478
+ ```typescript
1479
+ import * as GAME from 'vibegame';
1480
+
1481
+ GAME
1482
+ .withPlugin(GAME.PlayerPlugin) // Included in defaults
1483
+ .run();
1484
+ ```
1485
+
1486
+ ### Rendering
1487
+
1488
+ Three.js rendering pipeline with meshes, lights, cameras, and post-processing effects.
1489
+
1490
+ ### Components
1491
+
1492
+ #### Renderer
1493
+ - shape: ui8 - 0=box, 1=sphere, 2=cylinder, 3=plane
1494
+ - sizeX, sizeY, sizeZ: f32 (1)
1495
+ - color: ui32 (0xffffff)
1496
+ - visible: ui8 (1)
1497
+
1498
+ #### RenderContext
1499
+ - clearColor: ui32 (0x000000)
1500
+ - hasCanvas: ui8
1501
+
1502
+ #### MainCamera
1503
+ Tag component (no properties)
1504
+
1505
+ #### Ambient
1506
+ - skyColor: ui32 (0x87ceeb)
1507
+ - groundColor: ui32 (0x4a4a4a)
1508
+ - intensity: f32 (0.6)
1509
+
1510
+ #### Directional
1511
+ - color: ui32 (0xffffff)
1512
+ - intensity: f32 (1)
1513
+ - castShadow: ui8 (1)
1514
+ - shadowMapSize: ui32 (4096)
1515
+ - directionX: f32 (-1)
1516
+ - directionY: f32 (2)
1517
+ - directionZ: f32 (-1)
1518
+ - distance: f32 (30)
1519
+
1520
+ #### Bloom
1521
+ - intensity: f32 (1.0) - Bloom intensity
1522
+ - luminanceThreshold: f32 (1.0) - Luminance threshold for bloom
1523
+ - luminanceSmoothing: f32 (0.03) - Smoothness of luminance threshold
1524
+ - mipmapBlur: ui8 (1) - Enable mipmap blur
1525
+ - radius: f32 (0.85) - Blur radius for mipmap blur
1526
+ - levels: ui8 (8) - Number of MIP levels for mipmap blur
1527
+
1528
+ #### Dithering
1529
+ - colorBits: ui8 (4) - Bits per color channel (1-8)
1530
+ - intensity: f32 (1.0) - Effect intensity (0-1)
1531
+ - grayscale: ui8 (0) - Enable grayscale mode (0/1)
1532
+ - scale: f32 (1.0) - Pattern scale (higher = coarser dithering)
1533
+ - noise: f32 (1.0) - Noise threshold intensity
1534
+
1535
+ ### Systems
1536
+
1537
+ #### MeshInstanceSystem
1538
+ - Group: draw
1539
+ - Synchronizes transforms with Three.js meshes
1540
+
1541
+ #### LightSyncSystem
1542
+ - Group: draw
1543
+ - Updates Three.js lights
1544
+
1545
+ #### CameraSyncSystem
1546
+ - Group: draw
1547
+ - Updates Three.js camera and manages post-processing effects
1548
+
1549
+ #### WebGLRenderSystem
1550
+ - Group: draw (last)
1551
+ - Renders scene through EffectComposer
1552
+
1553
+ ### Functions
1554
+
1555
+ #### setCanvasElement(entity, canvas): void
1556
+ Associates canvas with RenderContext
1557
+
1558
+ ### Recipes
1559
+
1560
+ - ambient-light - Ambient hemisphere lighting
1561
+ - directional-light - Directional light with shadows
1562
+ - light - Both ambient and directional
1563
+
1564
+ #### Examples
1565
+
1566
+ ## Examples
1567
+
1568
+ ### Basic Rendering Setup
1569
+
1570
+ ```xml
1571
+ <!-- Declarative scene with lighting and rendered objects -->
1572
+ <world canvas="#game-canvas" sky="#87ceeb">
1573
+ <!-- Default lighting -->
1574
+ <light></light>
1575
+
1576
+ <!-- Rendered box -->
1577
+ <entity
1578
+ transform
1579
+ renderer="shape: box; color: 0xff0000; size-x: 2"
1580
+ pos="0 1 0"
1581
+ />
1582
+
1583
+ <!-- Rendered sphere -->
1584
+ <entity
1585
+ transform
1586
+ renderer="shape: sphere; color: 0x00ff00"
1587
+ pos="3 1 0"
1588
+ />
1589
+ </world>
1590
+ ```
1591
+
1592
+ ### Custom Lighting
1593
+
1594
+ ```xml
1595
+ <!-- Separate ambient and directional lights -->
1596
+ <ambient-light
1597
+ sky-color="#ffd4a3"
1598
+ ground-color="#808080"
1599
+ intensity="0.4"
1600
+ />
1601
+
1602
+ <directional-light
1603
+ color="#ffffff"
1604
+ intensity="1.5"
1605
+ direction-x="-1"
1606
+ direction-y="3"
1607
+ direction-z="-0.5"
1608
+ cast-shadow="1"
1609
+ shadow-map-size="2048"
1610
+ />
1611
+ ```
1612
+
1613
+ ### Imperative Usage
1614
+
1615
+ ```typescript
1616
+ import * as GAME from 'vibegame';
1617
+
1618
+ // Create rendered entity programmatically
1619
+ const entity = state.createEntity();
1620
+
1621
+ // Add transform for positioning
1622
+ state.addComponent(entity, GAME.Transform, {
1623
+ posX: 0, posY: 5, posZ: 0
1624
+ });
1625
+
1626
+ // Add renderer component
1627
+ state.addComponent(entity, GAME.Renderer, {
1628
+ shape: 1, // sphere
1629
+ sizeX: 2,
1630
+ sizeY: 2,
1631
+ sizeZ: 2,
1632
+ color: 0xff00ff,
1633
+ visible: 1
1634
+ });
1635
+
1636
+ // Set canvas for rendering context
1637
+ const contextQuery = GAME.defineQuery([GAME.RenderContext]);
1638
+ const contextEntity = contextQuery(state.world)[0];
1639
+ const canvas = document.getElementById('game-canvas');
1640
+ GAME.setCanvasElement(contextEntity, canvas);
1641
+ ```
1642
+
1643
+ ### Shape Types
1644
+
1645
+ ```typescript
1646
+ import * as GAME from 'vibegame';
1647
+
1648
+ // Available shape enums
1649
+ const shapes = {
1650
+ box: 0,
1651
+ sphere: 1,
1652
+ cylinder: 2,
1653
+ plane: 3
1654
+ };
1655
+
1656
+ // Use in XML
1657
+ <entity renderer="shape: sphere"></entity>
1658
+
1659
+ // Or with enum names
1660
+ <entity renderer="shape: 1"></entity>
1661
+ ```
1662
+
1663
+ ### Visibility Control
1664
+
1665
+ ```typescript
1666
+ import * as GAME from 'vibegame';
1667
+
1668
+ // Hide/show entities
1669
+ GAME.Renderer.visible[entity] = 0; // Hide
1670
+ GAME.Renderer.visible[entity] = 1; // Show
1671
+
1672
+ // In XML
1673
+ <entity renderer="visible: 0"></entity> <!-- Initially hidden -->
1674
+ ```
1675
+
1676
+ ### Post-Processing Effects
1677
+
1678
+ ```xml
1679
+ <!-- Camera with bloom effect (using defaults) -->
1680
+ <camera bloom></camera>
1681
+
1682
+ <!-- Camera with custom bloom settings -->
1683
+ <camera bloom="intensity: 2; luminance-threshold: 0.8; luminance-smoothing: 0.05"></camera>
1684
+
1685
+ <!-- Camera with mipmap blur settings -->
1686
+ <camera bloom="mipmap-blur: 1; radius: 0.9; levels: 10"></camera>
1687
+
1688
+ <!-- Camera without effects (still uses composer internally) -->
1689
+ <camera></camera>
1690
+
1691
+ <!-- Camera with retro dithering effect -->
1692
+ <camera dithering="color-bits: 3; intensity: 0.8; scale: 2"></camera>
1693
+
1694
+ <!-- Combined bloom and dithering for retro aesthetic -->
1695
+ <camera bloom="intensity: 1.5" dithering="color-bits: 2; grayscale: 1; scale: 3"></camera>
1696
+
1697
+ <!-- Subtle dithering for vintage look -->
1698
+ <camera dithering="color-bits: 5; intensity: 0.5; scale: 1"></camera>
1699
+
1700
+ <!-- Coarse pixel-art style dithering -->
1701
+ <camera dithering="color-bits: 2; scale: 4; intensity: 1"></camera>
1702
+ ```
1703
+
1704
+ ### Respawn
1705
+
1706
+ Automatic respawn system that resets entities when falling below Y=-100.
1707
+
1708
+ ### Components
1709
+
1710
+ #### Respawn
1711
+ - posX, posY, posZ: f32 - Spawn position
1712
+ - eulerX, eulerY, eulerZ: f32 - Spawn rotation (degrees)
1713
+
1714
+ ### Systems
1715
+
1716
+ #### RespawnSystem
1717
+ - Group: simulation
1718
+ - Resets entities when Y < -100
1719
+
1720
+ #### Examples
1721
+
1722
+ ## Examples
1723
+
1724
+ ### Player with Respawn (Automatic)
1725
+
1726
+ The `<player>` recipe automatically includes respawn:
1727
+
1728
+ ```xml
1729
+ <world>
1730
+ <!-- Player spawns at 0,5,0 and respawns there if falling -->
1731
+ <player pos="0 5 0"></player>
1732
+ </world>
1733
+ ```
1734
+
1735
+ ### Manual Respawn Component
1736
+
1737
+ ```xml
1738
+ <entity transform body collider respawn="pos: 0 10 -5">
1739
+ <!-- Entity respawns at 0,10,-5 when falling below Y=-100 -->
1740
+ </entity>
1741
+ ```
1742
+
1743
+ ### Imperative Usage
1744
+
1745
+ ```typescript
1746
+ import * as GAME from 'vibegame';
1747
+
1748
+ // Add respawn to an entity
1749
+ const entity = state.createEntity();
1750
+
1751
+ // Set spawn point from current transform
1752
+ state.addComponent(entity, GAME.Transform, {
1753
+ posX: 0, posY: 10, posZ: 0,
1754
+ eulerX: 0, eulerY: 0, eulerZ: 0
1755
+ });
1756
+
1757
+ state.addComponent(entity, GAME.Respawn, {
1758
+ posX: 0, posY: 10, posZ: 0,
1759
+ eulerX: 0, eulerY: 0, eulerZ: 0
1760
+ });
1761
+
1762
+ // Entity will respawn at (0,10,0) when falling
1763
+ ```
1764
+
1765
+ ### Update Spawn Point
1766
+
1767
+ ```typescript
1768
+ import * as GAME from 'vibegame';
1769
+
1770
+ // Change respawn position dynamically
1771
+ GAME.Respawn.posX[entity] = 20;
1772
+ GAME.Respawn.posY[entity] = 5;
1773
+ GAME.Respawn.posZ[entity] = -10;
1774
+ ```
1775
+
1776
+ ### XML with Transform Sync
1777
+
1778
+ Position attributes automatically populate the respawn component:
1779
+
1780
+ ```xml
1781
+ <!-- Position sets both transform and respawn -->
1782
+ <player pos="5 3 -2" euler="0 90 0"></player>
1783
+ ```
1784
+
1785
+ ### Startup
1786
+
1787
+ Auto-creates player, camera, and lighting entities at startup if missing.
1788
+
1789
+ ### Systems
1790
+
1791
+ #### LightingStartupSystem
1792
+ - Group: setup
1793
+ - Creates default lighting if none exists
1794
+
1795
+ #### CameraStartupSystem
1796
+ - Group: setup
1797
+ - Creates main camera if none exists
1798
+
1799
+ #### PlayerStartupSystem
1800
+ - Group: setup
1801
+ - Creates player entity if none exists
1802
+
1803
+ #### PlayerCharacterSystem
1804
+ - Group: setup
1805
+ - Adds animated character to players
1806
+
1807
+ #### Examples
1808
+
1809
+ ## Examples
1810
+
1811
+ ### Basic Usage (Auto-Creation)
1812
+
1813
+ ```typescript
1814
+ // The plugin automatically creates defaults when included
1815
+ import * as GAME from 'vibegame';
1816
+
1817
+ // This will create player, camera, and lighting automatically
1818
+ GAME.run(); // Uses DefaultPlugins which includes StartupPlugin
1819
+ ```
1820
+
1821
+ ### Preventing Auto-Creation with XML
1822
+
1823
+ ```xml
1824
+ <world>
1825
+ <!-- Creating your own player prevents auto-creation -->
1826
+ <player pos="10 2 -5" speed="12" />
1827
+
1828
+ <!-- Creating custom lighting prevents default lights -->
1829
+ <entity ambient="sky-color: 0xff0000" directional />
1830
+ </world>
1831
+ ```
1832
+
1833
+ ### Manual Plugin Registration
1834
+
1835
+ ```typescript
1836
+ import * as GAME from 'vibegame';
1837
+
1838
+ // Use startup plugin without other defaults
1839
+ GAME.withoutDefaultPlugins()
1840
+ .withPlugin(GAME.TransformsPlugin)
1841
+ .withPlugin(GAME.RenderingPlugin)
1842
+ .withPlugin(GAME.StartupPlugin)
1843
+ .run();
1844
+ ```
1845
+
1846
+ ### System Behavior
1847
+
1848
+ The startup systems are idempotent - they check for existing entities before creating:
1849
+
1850
+ ```typescript
1851
+ import * as GAME from 'vibegame';
1852
+
1853
+ // First run: Creates player, camera, lights
1854
+ const playerQuery = GAME.defineQuery([GAME.Player]);
1855
+ playerQuery(state.world).length // 0 -> creates player
1856
+
1857
+ // Subsequent runs: Skips creation
1858
+ playerQuery(state.world).length // 1 -> skips creation
1859
+ ```
1860
+
1861
+ ### Transforms
1862
+
1863
+ 3D transforms with position, rotation, scale, and parent-child hierarchies.
1864
+
1865
+ ### Components
1866
+
1867
+ #### Transform
1868
+ - posX, posY, posZ: f32 (0)
1869
+ - rotX, rotY, rotZ, rotW: f32 (rotW=1) - Quaternion
1870
+ - eulerX, eulerY, eulerZ: f32 (0) - Degrees
1871
+ - scaleX, scaleY, scaleZ: f32 (1)
1872
+
1873
+ #### WorldTransform
1874
+ - Same properties as Transform
1875
+ - Auto-computed from hierarchy (read-only)
1876
+
1877
+ ### Systems
1878
+
1879
+ #### TransformHierarchySystem
1880
+ - Group: simulation (last)
1881
+ - Syncs euler/quaternion and computes world transforms
1882
+
1883
+ #### Examples
1884
+
1885
+ ## Examples
1886
+
1887
+ ### Basic Usage
1888
+
1889
+ #### XML Position and Rotation
1890
+ ```xml
1891
+ <!-- Position only -->
1892
+ <entity transform="pos: 0 5 -3"></entity>
1893
+
1894
+ <!-- Euler rotation (degrees) -->
1895
+ <entity transform="euler: 0 45 0"></entity>
1896
+
1897
+ <!-- Scale (single value applies to all axes) -->
1898
+ <entity transform="scale: 2"></entity>
1899
+
1900
+ <!-- Combined transform -->
1901
+ <entity transform="pos: 0 5 0; euler: 0 45 0; scale: 1.5"></entity>
1902
+ ```
1903
+
1904
+ #### JavaScript API
1905
+ ```typescript
1906
+ import * as GAME from 'vibegame';
1907
+
1908
+ // In a system
1909
+ const MySystem = {
1910
+ update: (state) => {
1911
+ const entity = state.createEntity();
1912
+
1913
+ // Add transform component with initial values
1914
+ state.addComponent(entity, GAME.Transform, {
1915
+ posX: 10, posY: 5, posZ: -3,
1916
+ eulerX: 0, eulerY: 45, eulerZ: 0,
1917
+ scaleX: 2, scaleY: 2, scaleZ: 2
1918
+ });
1919
+
1920
+ // Transform system automatically syncs euler to quaternion
1921
+ }
1922
+ };
1923
+ ```
1924
+
1925
+ ### Transform Hierarchy
1926
+
1927
+ #### Parent-Child Relationships
1928
+ ```xml
1929
+ <!-- Parent at origin -->
1930
+ <entity transform="pos: 0 0 0">
1931
+ <!-- Children positioned relative to parent -->
1932
+ <entity transform="pos: 2 0 0"></entity>
1933
+ <entity transform="pos: -2 0 0"></entity>
1934
+ </entity>
1935
+
1936
+ <!-- Rotating parent affects all children -->
1937
+ <entity transform="euler: 0 45 0">
1938
+ <entity id="arm" transform="pos: 0 2 0">
1939
+ <entity id="hand" transform="pos: 0 2 0"></entity>
1940
+ </entity>
1941
+ </entity>
1942
+ ```
1943
+
1944
+ #### Accessing World Transform
1945
+ ```typescript
1946
+ import * as GAME from 'vibegame';
1947
+
1948
+ const transformQuery = GAME.defineQuery([GAME.Transform, GAME.WorldTransform]);
1949
+ const WorldTransformSystem = {
1950
+ update: (state) => {
1951
+ // Query entities with both transforms
1952
+ const entities = transformQuery(state.world);
1953
+
1954
+ for (const entity of entities) {
1955
+ // Local position
1956
+ const localX = GAME.Transform.posX[entity];
1957
+
1958
+ // World position (after parent transforms)
1959
+ const worldX = GAME.WorldTransform.posX[entity];
1960
+
1961
+ console.log(`Local: ${localX}, World: ${worldX}`);
1962
+ }
1963
+ }
1964
+ };
1965
+ ```
1966
+
1967
+ ### Common Patterns
1968
+
1969
+ #### Setting Transform Values
1970
+ ```typescript
1971
+ import * as GAME from 'vibegame';
1972
+
1973
+ // Direct property access (bitECS style)
1974
+ GAME.Transform.posX[entity] = 10;
1975
+ GAME.Transform.posY[entity] = 5;
1976
+ GAME.Transform.posZ[entity] = -3;
1977
+
1978
+ // Using euler angles for rotation
1979
+ GAME.Transform.eulerX[entity] = 0;
1980
+ GAME.Transform.eulerY[entity] = 45;
1981
+ GAME.Transform.eulerZ[entity] = 0;
1982
+ // Quaternion will be auto-synced by TransformHierarchySystem
1983
+
1984
+ // Uniform scale
1985
+ GAME.Transform.scaleX[entity] = 2;
1986
+ GAME.Transform.scaleY[entity] = 2;
1987
+ GAME.Transform.scaleZ[entity] = 2;
1988
+ ```
1989
+
1990
+ #### Transform Interpolation
1991
+ ```typescript
1992
+ import * as GAME from 'vibegame';
1993
+
1994
+ // Interpolate between two positions
1995
+ const t = 0.5; // 50% between start and end
1996
+ GAME.Transform.posX[entity] = startX + (endX - startX) * t;
1997
+ GAME.Transform.posY[entity] = startY + (endY - startY) * t;
1998
+ GAME.Transform.posZ[entity] = startZ + (endZ - startZ) * t;
1999
+ ```
2000
+
2001
+ ### Tweening
2002
+
2003
+ Animates component properties with easing functions and loop modes. Kinematic velocity bodies use velocity-based tweening for smooth physics-correct movement.
2004
+
2005
+ ### Components
2006
+
2007
+ #### Tween
2008
+ - duration: f32 (1) - Seconds
2009
+ - elapsed: f32
2010
+ - easingIndex: ui8
2011
+ - loopMode: ui8 - 0=Once, 1=Loop, 2=PingPong
2012
+
2013
+ #### TweenValue
2014
+ - source: ui32 - Tween entity
2015
+ - target: ui32 - Target entity
2016
+ - componentId: ui32
2017
+ - fieldIndex: ui32
2018
+ - from: f32
2019
+ - to: f32
2020
+ - value: f32 - Current value
2021
+
2022
+ #### KinematicTween
2023
+ - tweenEntity: ui32 - Associated tween entity
2024
+ - targetEntity: ui32 - Kinematic body entity
2025
+ - axis: ui8 - 0=X, 1=Y, 2=Z
2026
+ - from: f32 - Start position
2027
+ - to: f32 - End position
2028
+ - lastPosition: f32
2029
+ - targetPosition: f32
2030
+
2031
+ ### Systems
2032
+
2033
+ #### KinematicTweenSystem
2034
+ - Group: fixed
2035
+ - Converts position tweens to velocity for kinematic bodies
2036
+
2037
+ #### TweenSystem
2038
+ - Group: simulation
2039
+ - Interpolates values with easing and auto-cleanup
2040
+
2041
+ ### Functions
2042
+
2043
+ #### createTween(state, entity, target, options): number | null
2044
+ Animates component property
2045
+
2046
+ ### Easing Functions
2047
+
2048
+ - linear
2049
+ - sine-in, sine-out, sine-in-out
2050
+ - quad-in, quad-out, quad-in-out
2051
+ - cubic-in, cubic-out, cubic-in-out
2052
+ - quart-in, quart-out, quart-in-out
2053
+ - expo-in, expo-out, expo-in-out
2054
+ - circ-in, circ-out, circ-in-out
2055
+ - back-in, back-out, back-in-out
2056
+ - elastic-in, elastic-out, elastic-in-out
2057
+ - bounce-in, bounce-out, bounce-in-out
2058
+
2059
+ ### Loop Modes
2060
+
2061
+ - once - Play once and destroy
2062
+ - loop - Repeat indefinitely
2063
+ - ping-pong - Alternate directions
2064
+
2065
+ ### Shorthand Targets
2066
+
2067
+ - rotation - body.eulerX/Y/Z
2068
+ - at - body.posX/Y/Z
2069
+ - scale - transform.scaleX/Y/Z
2070
+
2071
+ #### Examples
2072
+
2073
+ ## Examples
2074
+
2075
+ ### Basic XML Tween
2076
+
2077
+ ```xml
2078
+ <!-- Animate Y position from 5 to 10 over 2 seconds -->
2079
+ <kinematic-part pos="0 5 -5">
2080
+ <tween
2081
+ target="body.pos-y"
2082
+ from="5"
2083
+ to="10"
2084
+ duration="2"
2085
+ ease="sine-in-out"
2086
+ loop="ping-pong"
2087
+ />
2088
+ </kinematic-part>
2089
+ ```
2090
+
2091
+ ### Multiple Properties
2092
+
2093
+ ```xml
2094
+ <!-- Animate rotation on all axes -->
2095
+ <entity transform renderer>
2096
+ <tween
2097
+ target="rotation"
2098
+ to="0 360 0"
2099
+ duration="4"
2100
+ loop="loop"
2101
+ />
2102
+ </entity>
2103
+ ```
2104
+
2105
+ ### Body Physics Properties
2106
+
2107
+ ```xml
2108
+ <!-- Animate physics body velocity -->
2109
+ <dynamic-part pos="0 10 0">
2110
+ <tween
2111
+ target="body.velocity-x"
2112
+ from="0"
2113
+ to="10"
2114
+ duration="1"
2115
+ ease="quad-out"
2116
+ />
2117
+ </tween>
2118
+ ```
2119
+
2120
+ ### Programmatic Usage
2121
+
2122
+ ```typescript
2123
+ import * as GAME from 'vibegame';
2124
+
2125
+ // In a system
2126
+ const MySystem = {
2127
+ setup: (state) => {
2128
+ const entity = state.createEntity();
2129
+ state.addComponent(entity, GAME.Transform);
2130
+
2131
+ // Create tween programmatically
2132
+ GAME.createTween(state, entity, 'body.pos-y', {
2133
+ from: 0,
2134
+ to: 10,
2135
+ duration: 2,
2136
+ easing: 'bounce-out',
2137
+ loop: 'once'
2138
+ });
2139
+ }
2140
+ };
2141
+ ```
2142
+
2143
+ ### Complex Animation Sequence
2144
+
2145
+ ```xml
2146
+ <!-- Platform with multiple animated properties -->
2147
+ <kinematic-part pos="0 0 0" color="#ff0000">
2148
+ <!-- Position animation -->
2149
+ <tween target="body.pos-x" from="-5" to="5" duration="3" loop="ping-pong"></tween>
2150
+ <!-- Rotation animation -->
2151
+ <tween target="body.euler-y" to="360" duration="10" loop="loop"></tween>
2152
+ <!-- Scale pulse -->
2153
+ <tween target="transform.scale-x" from="1" to="1.5" duration="1" ease="sine-in-out" loop="ping-pong"></tween>
2154
+ <tween target="transform.scale-z" from="1" to="1.5" duration="1" ease="sine-in-out" loop="ping-pong"></tween>
2155
+ </kinematic-part>
2156
+ ```
2157
+
2158
+ ### Ui
2159
+
2160
+ Web-native UI system using HTML/CSS overlays positioned over the 3D canvas. Includes GSAP for animations, ECS state synchronization, and external library support. This provides capabilities superior to most game engines' built-in UI systems.
2161
+
2162
+ ### Components
2163
+
2164
+ #### UIManager
2165
+ - element: HTMLElement - Root UI container
2166
+ - state: State - ECS state reference for updates
2167
+ - visible: ui8 (1) - UI visibility toggle
2168
+
2169
+ ### Systems
2170
+
2171
+ #### UIUpdateSystem
2172
+ - Group: simulation
2173
+ - Updates UI elements from ECS component state
2174
+
2175
+ #### UIEventSystem
2176
+ - Group: setup
2177
+ - Handles UI event binding and canvas focus management
2178
+
2179
+ ### Functions
2180
+
2181
+ #### createUIOverlay(canvas: HTMLCanvasElement): HTMLElement
2182
+ Creates positioned UI overlay container
2183
+
2184
+ #### bindUIToState(uiManager: UIManager, state: State): void
2185
+ Connects UI updates to ECS state changes
2186
+
2187
+ #### showFloatingText(x: number, y: number, text: string): void
2188
+ Creates animated floating text at screen coordinates
2189
+
2190
+ ### Patterns
2191
+
2192
+ #### Basic UI Setup
2193
+ ```html
2194
+ <div id="game-ui">
2195
+ <div class="hud">
2196
+ <span id="score">0</span>
2197
+ <span id="health">100</span>
2198
+ </div>
2199
+ </div>
2200
+ ```
2201
+
2202
+ #### ECS Integration
2203
+ ```typescript
2204
+ const UISystem = {
2205
+ update: (state) => {
2206
+ // Update UI from game state components
2207
+ const scoreEl = document.getElementById('score');
2208
+ if (scoreEl) scoreEl.textContent = getScore(state);
2209
+ }
2210
+ };
2211
+ ```
2212
+
2213
+ #### GSAP Animations
2214
+ ```typescript
2215
+ gsap.to("#currency", {
2216
+ scale: 1.2,
2217
+ duration: 0.2,
2218
+ yoyo: true,
2219
+ repeat: 1
2220
+ });
2221
+ ```
2222
+
2223
+ #### Examples
2224
+
2225
+ ## Examples
2226
+
2227
+ ### Basic Game HUD
2228
+
2229
+ ```html
2230
+ <!doctype html>
2231
+ <html>
2232
+ <head>
2233
+ <style>
2234
+ body { margin: 0; font-family: Arial; }
2235
+ #game-ui {
2236
+ position: fixed;
2237
+ top: 0;
2238
+ left: 0;
2239
+ right: 0;
2240
+ bottom: 0;
2241
+ pointer-events: none;
2242
+ z-index: 1000;
2243
+ }
2244
+ .hud {
2245
+ position: absolute;
2246
+ top: 20px;
2247
+ left: 20px;
2248
+ background: rgba(0,0,0,0.7);
2249
+ padding: 15px;
2250
+ border-radius: 8px;
2251
+ color: white;
2252
+ pointer-events: auto;
2253
+ }
2254
+ </style>
2255
+ </head>
2256
+ <body>
2257
+ <world canvas="#game-canvas">
2258
+ <static-part pos="0 -0.5 0" shape="box" size="20 1 20" color="#90ee90"></static-part>
2259
+ </world>
2260
+
2261
+ <canvas id="game-canvas"></canvas>
2262
+
2263
+ <div id="game-ui">
2264
+ <div class="hud">
2265
+ <div>Score: <span id="score">0</span></div>
2266
+ <div>Coins: <span id="coins">0</span></div>
2267
+ </div>
2268
+ </div>
2269
+
2270
+ <script type="module">
2271
+ import * as GAME from 'vibegame';
2272
+
2273
+ const GameState = GAME.defineComponent({
2274
+ score: GAME.Types.ui32,
2275
+ coins: GAME.Types.ui32
2276
+ });
2277
+
2278
+ const UISystem = {
2279
+ update: (state) => {
2280
+ const query = GAME.defineQuery([GameState]);
2281
+ const entities = query(state.world);
2282
+
2283
+ if (entities.length > 0) {
2284
+ const entity = entities[0];
2285
+ document.getElementById('score').textContent = GameState.score[entity];
2286
+ document.getElementById('coins').textContent = GameState.coins[entity];
2287
+ }
2288
+ }
2289
+ };
2290
+
2291
+ GAME.withPlugin({
2292
+ components: { GameState },
2293
+ systems: [UISystem]
2294
+ }).run();
2295
+ </script>
2296
+ </body>
2297
+ </html>
2298
+ ```
2299
+
2300
+ ### Animated Currency with GSAP
2301
+
2302
+ ```javascript
2303
+ import gsap from 'gsap';
2304
+
2305
+ class AnimatedCounter {
2306
+ constructor(elementId) {
2307
+ this.element = document.getElementById(elementId);
2308
+ this.currentValue = 0;
2309
+ this.displayValue = 0;
2310
+ }
2311
+
2312
+ setValue(newValue) {
2313
+ this.currentValue = newValue;
2314
+
2315
+ gsap.to(this, {
2316
+ displayValue: newValue,
2317
+ duration: 0.8,
2318
+ ease: "power2.out",
2319
+ onUpdate: () => {
2320
+ this.element.textContent = Math.floor(this.displayValue);
2321
+ }
2322
+ });
2323
+ }
2324
+ }
2325
+
2326
+ // Usage in ECS system
2327
+ const coinCounter = new AnimatedCounter('coins');
2328
+
2329
+ const UISystem = {
2330
+ update: (state) => {
2331
+ const coins = getCoinsFromState(state);
2332
+ coinCounter.setValue(coins);
2333
+ }
2334
+ };
2335
+ ```
2336
+
2337
+ ### Floating Damage Text
2338
+
2339
+ ```javascript
2340
+ function showDamageText(worldX, worldY, worldZ, damage) {
2341
+ // Convert 3D world position to screen coordinates
2342
+ const camera = getMainCamera(state);
2343
+ const screenPos = worldToScreen(worldX, worldY, worldZ, camera);
2344
+
2345
+ const element = document.createElement('div');
2346
+ element.textContent = `-${damage}`;
2347
+ element.style.cssText = `
2348
+ position: fixed;
2349
+ left: ${screenPos.x}px;
2350
+ top: ${screenPos.y}px;
2351
+ color: #ff4444;
2352
+ font-weight: bold;
2353
+ font-size: 24px;
2354
+ pointer-events: none;
2355
+ z-index: 10000;
2356
+ `;
2357
+
2358
+ document.body.appendChild(element);
2359
+
2360
+ gsap.timeline()
2361
+ .to(element, {
2362
+ y: screenPos.y - 100,
2363
+ opacity: 0,
2364
+ duration: 1.5,
2365
+ ease: "power2.out"
2366
+ })
2367
+ .call(() => element.remove());
2368
+ }
2369
+
2370
+ // Usage in collision system
2371
+ const DamageSystem = {
2372
+ update: (state) => {
2373
+ // When player takes damage
2374
+ const playerPos = getPlayerPosition(state);
2375
+ showDamageText(playerPos.x, playerPos.y + 2, playerPos.z, 25);
2376
+ }
2377
+ };
2378
+ ```
2379
+
2380
+ ### Menu System with State
2381
+
2382
+ ```javascript
2383
+ class GameMenu {
2384
+ constructor() {
2385
+ this.isOpen = false;
2386
+ this.element = document.getElementById('main-menu');
2387
+ }
2388
+
2389
+ toggle() {
2390
+ this.isOpen = !this.isOpen;
2391
+
2392
+ if (this.isOpen) {
2393
+ this.show();
2394
+ } else {
2395
+ this.hide();
2396
+ }
2397
+ }
2398
+
2399
+ show() {
2400
+ this.element.style.display = 'block';
2401
+ gsap.fromTo(this.element,
2402
+ { opacity: 0, scale: 0.8 },
2403
+ { opacity: 1, scale: 1, duration: 0.3, ease: "back.out(1.7)" }
2404
+ );
2405
+ }
2406
+
2407
+ hide() {
2408
+ gsap.to(this.element, {
2409
+ opacity: 0,
2410
+ scale: 0.8,
2411
+ duration: 0.2,
2412
+ onComplete: () => {
2413
+ this.element.style.display = 'none';
2414
+ }
2415
+ });
2416
+ }
2417
+ }
2418
+
2419
+ // Integration with input system
2420
+ const menu = new GameMenu();
2421
+
2422
+ const MenuSystem = {
2423
+ update: (state) => {
2424
+ // Check for escape key press
2425
+ if (GAME.consumeInput(state, 'escape')) {
2426
+ menu.toggle();
2427
+ }
2428
+ }
2429
+ };
2430
+ ```
package.json CHANGED
@@ -9,6 +9,8 @@
9
  "check": "tsc --noEmit && svelte-check",
10
  "check:ts": "tsc --noEmit",
11
  "check:svelte": "svelte-check",
 
 
12
  "test": "bun test",
13
  "test:watch": "bun test --watch",
14
  "format": "prettier --write .",
@@ -43,13 +45,12 @@
43
  "@langchain/mcp-adapters": "^0.6.0",
44
  "@modelcontextprotocol/sdk": "^0.6.0",
45
  "@modelcontextprotocol/server-filesystem": "^0.6.2",
46
- "@types/marked": "^6.0.0",
47
  "@types/node": "^24.3.3",
48
  "gsap": "^3.13.0",
49
- "marked": "^16.2.1",
50
  "monaco-editor": "^0.50.0",
51
  "svelte-splitpanes": "^8.0.5",
52
- "vibegame": "^0.1.7",
53
  "zod": "^4.1.8"
54
  }
55
  }
 
9
  "check": "tsc --noEmit && svelte-check",
10
  "check:ts": "tsc --noEmit",
11
  "check:svelte": "svelte-check",
12
+ "update": "cp node_modules/vibegame/llms.txt ./llms.txt && echo '✓ Updated llms.txt from vibegame'",
13
+ "postinstall": "test -f node_modules/vibegame/llms.txt && cp node_modules/vibegame/llms.txt ./llms.txt || true",
14
  "test": "bun test",
15
  "test:watch": "bun test --watch",
16
  "format": "prettier --write .",
 
45
  "@langchain/mcp-adapters": "^0.6.0",
46
  "@modelcontextprotocol/sdk": "^0.6.0",
47
  "@modelcontextprotocol/server-filesystem": "^0.6.2",
 
48
  "@types/node": "^24.3.3",
49
  "gsap": "^3.13.0",
50
+ "htmlparser2": "^10.0.0",
51
  "monaco-editor": "^0.50.0",
52
  "svelte-splitpanes": "^8.0.5",
53
+ "vibegame": "^0.1.9",
54
  "zod": "^4.1.8"
55
  }
56
  }
src/App.svelte CHANGED
@@ -2,10 +2,12 @@
2
  import { onMount, onDestroy } from 'svelte';
3
  import { registerShortcuts, shortcuts } from './lib/config/shortcuts';
4
  import { loadingStore } from './lib/stores/loading';
 
5
  import { contentManager } from './lib/services/content-manager';
6
  import AppHeader from './lib/components/layout/AppHeader.svelte';
7
  import SplitView from './lib/components/layout/SplitView.svelte';
8
  import LoadingScreen from './lib/components/layout/LoadingScreen.svelte';
 
9
 
10
  let unregisterShortcuts: () => void;
11
 
@@ -39,7 +41,11 @@
39
 
40
  <main>
41
  <AppHeader />
42
- <SplitView />
 
 
 
 
43
  </main>
44
 
45
  <style>
 
2
  import { onMount, onDestroy } from 'svelte';
3
  import { registerShortcuts, shortcuts } from './lib/config/shortcuts';
4
  import { loadingStore } from './lib/stores/loading';
5
+ import { uiStore } from './lib/stores/ui';
6
  import { contentManager } from './lib/services/content-manager';
7
  import AppHeader from './lib/components/layout/AppHeader.svelte';
8
  import SplitView from './lib/components/layout/SplitView.svelte';
9
  import LoadingScreen from './lib/components/layout/LoadingScreen.svelte';
10
+ import About from './lib/components/about/About.svelte';
11
 
12
  let unregisterShortcuts: () => void;
13
 
 
41
 
42
  <main>
43
  <AppHeader />
44
+ {#if $uiStore.viewMode === 'about'}
45
+ <About />
46
+ {:else}
47
+ <SplitView />
48
+ {/if}
49
  </main>
50
 
51
  <style>
src/lib/components/about/About.svelte ADDED
@@ -0,0 +1,310 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+ import { uiStore } from '../../stores/ui';
4
+ import gsap from 'gsap';
5
+
6
+ onMount(() => {
7
+ gsap.fromTo('.about-container',
8
+ { opacity: 0, scale: 0.95 },
9
+ { opacity: 1, scale: 1, duration: 0.3, ease: 'power2.out' }
10
+ );
11
+ });
12
+
13
+ function handleClose() {
14
+ uiStore.hideAbout();
15
+ }
16
+ </script>
17
+
18
+ <div class="about-container">
19
+ <div class="about-content">
20
+ <button class="close-button" on:click={handleClose} aria-label="Close">
21
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
22
+ <path d="M8 8.707l3.646 3.647.708-.708L8.707 8l3.647-3.646-.708-.708L8 7.293 4.354 3.646l-.708.708L7.293 8l-3.647 3.646.708.708L8 8.707z"/>
23
+ </svg>
24
+ </button>
25
+
26
+ <div class="about-header">
27
+ <h1>
28
+ <span class="app-icon">🥕</span>
29
+ VibeGame
30
+ </h1>
31
+ <p class="tagline">A 3D game engine designed for vibe coding</p>
32
+ </div>
33
+
34
+ <div class="about-body">
35
+ <p class="description">
36
+ AI-assisted game development with declarative HTML-like syntax, ECS architecture, and built-in physics/rendering.
37
+ Create 3D games with real-time feedback and scale from prototypes to complex projects.
38
+ </p>
39
+
40
+ <div class="features-grid">
41
+ <div class="feature">
42
+ <strong>Declarative</strong>
43
+ <span>HTML-like tags</span>
44
+ </div>
45
+ <div class="feature">
46
+ <strong>ECS</strong>
47
+ <span>Clean architecture</span>
48
+ </div>
49
+ <div class="feature">
50
+ <strong>Physics</strong>
51
+ <span>Rapier built-in</span>
52
+ </div>
53
+ <div class="feature">
54
+ <strong>Rendering</strong>
55
+ <span>Three.js powered</span>
56
+ </div>
57
+ <div class="feature">
58
+ <strong>Live Edit</strong>
59
+ <span>Real-time feedback</span>
60
+ </div>
61
+ <div class="feature">
62
+ <strong>AI-Ready</strong>
63
+ <span>LangGraph.js</span>
64
+ </div>
65
+ </div>
66
+
67
+ <div class="quick-start">
68
+ <h3>Quick Start</h3>
69
+ <pre><code>&lt;world canvas="#game-canvas" sky="#87ceeb"&gt;
70
+ &lt;!-- Ground --&gt;
71
+ &lt;static-part pos="0 -0.5 0" shape="box" size="20 1 20" color="#90ee90"&gt;&lt;/static-part&gt;
72
+
73
+ &lt;!-- Ball --&gt;
74
+ &lt;dynamic-part pos="-2 4 -3" shape="sphere" size="1" color="#ff4500"&gt;&lt;/dynamic-part&gt;
75
+ &lt;/world&gt;
76
+
77
+ &lt;canvas id="game-canvas"&gt;&lt;/canvas&gt;
78
+
79
+ &lt;script type="module"&gt;
80
+ import * as GAME from 'vibegame';
81
+ GAME.run();
82
+ &lt;/script&gt;</code></pre>
83
+ </div>
84
+
85
+ <div class="links">
86
+ <a href="https://github.com/dylanebert/VibeGame" target="_blank" rel="noopener noreferrer" class="link">
87
+ <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
88
+ <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>
89
+ </svg>
90
+ GitHub
91
+ </a>
92
+ <a href="https://www.npmjs.com/package/vibegame" target="_blank" rel="noopener noreferrer" class="link">
93
+ NPM
94
+ </a>
95
+ <a href="https://huggingface.co/spaces/dylanebert/VibeGame" target="_blank" rel="noopener noreferrer" class="link">
96
+ Demo
97
+ </a>
98
+ <a href="https://jsfiddle.net/zhLtd6e2/6/" target="_blank" rel="noopener noreferrer" class="link">
99
+ JSFiddle
100
+ </a>
101
+ </div>
102
+ </div>
103
+ </div>
104
+ </div>
105
+
106
+ <style>
107
+ .about-container {
108
+ width: 100%;
109
+ height: calc(100vh - 40px);
110
+ display: flex;
111
+ justify-content: center;
112
+ align-items: center;
113
+ background: linear-gradient(135deg, #0B0A09 0%, #0F0E0C 100%);
114
+ padding: 1rem;
115
+ }
116
+
117
+ .about-content {
118
+ position: relative;
119
+ max-width: 860px;
120
+ width: 100%;
121
+ background: rgba(15, 14, 12, 0.95);
122
+ border: 1px solid rgba(139, 115, 85, 0.2);
123
+ border-radius: 0.75rem;
124
+ padding: 2.5rem;
125
+ max-height: calc(100vh - 60px);
126
+ overflow-y: auto;
127
+ }
128
+
129
+ .close-button {
130
+ position: absolute;
131
+ top: 1rem;
132
+ right: 1rem;
133
+ background: transparent;
134
+ border: none;
135
+ color: rgba(251, 248, 244, 0.5);
136
+ cursor: pointer;
137
+ padding: 0.25rem;
138
+ border-radius: 0.25rem;
139
+ transition: all 0.2s;
140
+ display: flex;
141
+ align-items: center;
142
+ justify-content: center;
143
+ }
144
+
145
+ .close-button:hover {
146
+ color: rgba(251, 248, 244, 0.9);
147
+ background: rgba(139, 115, 85, 0.1);
148
+ }
149
+
150
+ .about-header {
151
+ text-align: center;
152
+ margin-bottom: 1.5rem;
153
+ padding-bottom: 1rem;
154
+ border-bottom: 1px solid rgba(139, 115, 85, 0.15);
155
+ }
156
+
157
+ .about-header h1 {
158
+ display: flex;
159
+ align-items: center;
160
+ justify-content: center;
161
+ gap: 0.5rem;
162
+ font-size: 1.75rem;
163
+ color: rgba(251, 248, 244, 0.95);
164
+ margin: 0 0 0.25rem 0;
165
+ }
166
+
167
+ .app-icon {
168
+ font-size: 1.75rem;
169
+ }
170
+
171
+ .tagline {
172
+ font-size: 1rem;
173
+ color: rgba(251, 248, 244, 0.7);
174
+ margin: 0;
175
+ }
176
+
177
+ .about-body {
178
+ display: flex;
179
+ flex-direction: column;
180
+ gap: 1.5rem;
181
+ }
182
+
183
+ .description {
184
+ font-size: 0.9375rem;
185
+ line-height: 1.6;
186
+ color: rgba(251, 248, 244, 0.8);
187
+ margin: 0;
188
+ }
189
+
190
+ .features-grid {
191
+ display: grid;
192
+ grid-template-columns: repeat(3, 1fr);
193
+ gap: 0.75rem;
194
+ }
195
+
196
+ .feature {
197
+ background: rgba(139, 115, 85, 0.06);
198
+ border: 1px solid rgba(139, 115, 85, 0.12);
199
+ border-radius: 0.375rem;
200
+ padding: 0.75rem;
201
+ display: flex;
202
+ flex-direction: column;
203
+ gap: 0.25rem;
204
+ transition: all 0.2s;
205
+ }
206
+
207
+ .feature:hover {
208
+ background: rgba(139, 115, 85, 0.1);
209
+ border-color: rgba(124, 152, 133, 0.2);
210
+ }
211
+
212
+ .feature strong {
213
+ font-size: 0.875rem;
214
+ color: rgba(251, 248, 244, 0.9);
215
+ font-weight: 600;
216
+ }
217
+
218
+ .feature span {
219
+ font-size: 0.75rem;
220
+ color: rgba(251, 248, 244, 0.65);
221
+ }
222
+
223
+ .quick-start {
224
+ background: rgba(15, 14, 12, 0.6);
225
+ border: 1px solid rgba(139, 115, 85, 0.15);
226
+ border-radius: 0.375rem;
227
+ padding: 1rem;
228
+ }
229
+
230
+ .quick-start h3 {
231
+ font-size: 1rem;
232
+ color: rgba(251, 248, 244, 0.9);
233
+ margin: 0 0 0.75rem 0;
234
+ font-weight: 600;
235
+ }
236
+
237
+ .quick-start pre {
238
+ margin: 0;
239
+ background: rgba(0, 0, 0, 0.3);
240
+ padding: 0.875rem;
241
+ border-radius: 0.375rem;
242
+ overflow-x: auto;
243
+ }
244
+
245
+ .quick-start code {
246
+ font-family: 'Monaco', 'Courier New', monospace;
247
+ font-size: 0.8125rem;
248
+ color: #7C9885;
249
+ line-height: 1.5;
250
+ white-space: pre;
251
+ }
252
+
253
+ .links {
254
+ display: flex;
255
+ gap: 0.5rem;
256
+ flex-wrap: wrap;
257
+ }
258
+
259
+ .link {
260
+ display: inline-flex;
261
+ align-items: center;
262
+ gap: 0.375rem;
263
+ padding: 0.625rem 1rem;
264
+ background: rgba(139, 115, 85, 0.08);
265
+ border: 1px solid rgba(139, 115, 85, 0.2);
266
+ border-radius: 0.375rem;
267
+ color: rgba(251, 248, 244, 0.85);
268
+ text-decoration: none;
269
+ font-size: 0.875rem;
270
+ font-weight: 500;
271
+ transition: all 0.2s;
272
+ }
273
+
274
+ .link:hover {
275
+ background: rgba(124, 152, 133, 0.15);
276
+ border-color: rgba(124, 152, 133, 0.3);
277
+ color: rgba(251, 248, 244, 0.95);
278
+ transform: translateY(-1px);
279
+ }
280
+
281
+ .link svg {
282
+ width: 14px;
283
+ height: 14px;
284
+ }
285
+
286
+ /* Scrollbar */
287
+ .about-content::-webkit-scrollbar {
288
+ width: 6px;
289
+ }
290
+
291
+ .about-content::-webkit-scrollbar-track {
292
+ background: rgba(139, 115, 85, 0.05);
293
+ border-radius: 3px;
294
+ }
295
+
296
+ .about-content::-webkit-scrollbar-thumb {
297
+ background: rgba(139, 115, 85, 0.2);
298
+ border-radius: 3px;
299
+ }
300
+
301
+ .about-content::-webkit-scrollbar-thumb:hover {
302
+ background: rgba(139, 115, 85, 0.3);
303
+ }
304
+
305
+ @media (max-width: 600px) {
306
+ .features-grid {
307
+ grid-template-columns: repeat(2, 1fr);
308
+ }
309
+ }
310
+ </style>
src/lib/components/about/context.md ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # About Component
2
+
3
+ Project information display
4
+
5
+ ## Purpose
6
+
7
+ - Display project overview and features
8
+ - Provide quick start example
9
+ - Link to resources (GitHub, NPM, demos)
10
+ - Modal-style overlay presentation
11
+
12
+ ## Structure
13
+
14
+ ```
15
+ about/
16
+ ├── context.md # This file
17
+ └── About.svelte # Project info modal
18
+ ```
19
+
20
+ ## Features
21
+
22
+ - Compact card layout with 860px max width
23
+ - Feature grid (3 columns)
24
+ - Full Quick Start code example
25
+ - Resource links to GitHub, NPM, Demo, JSFiddle
26
+ - Close via X button, About toggle, or title click
27
+
28
+ ## Dependencies
29
+
30
+ - uiStore for view mode control
31
+ - GSAP for fade animation
src/lib/components/chat/ChatPanel.svelte CHANGED
@@ -4,6 +4,7 @@
4
  import gsap from "gsap";
5
  import MessageList from "./MessageList.svelte";
6
  import MessageInput from "./MessageInput.svelte";
 
7
  import { isConnected, isProcessing, messages, error } from "../../stores/chat-store";
8
  import { chatController } from "../../controllers/chat-controller";
9
  import { authStore } from "../../services/auth";
@@ -118,6 +119,11 @@
118
  {/if}
119
  </div>
120
 
 
 
 
 
 
121
  <MessageInput
122
  bind:value={inputValue}
123
  placeholder={!$authStore.isAuthenticated
@@ -140,8 +146,8 @@
140
  flex-direction: column;
141
  height: 100%;
142
  width: 100%;
143
- background: rgba(8, 7, 6, 0.4);
144
- border-top: 1px solid rgba(139, 115, 85, 0.06);
145
  position: relative;
146
  }
147
 
@@ -150,8 +156,8 @@
150
  justify-content: space-between;
151
  align-items: center;
152
  padding: 10px 16px;
153
- background: rgba(15, 14, 12, 0.3);
154
- border-bottom: 1px solid rgba(139, 115, 85, 0.06);
155
  flex-shrink: 0;
156
  }
157
 
@@ -159,7 +165,7 @@
159
  margin: 0;
160
  font-size: 11px;
161
  font-weight: 500;
162
- color: rgba(251, 248, 244, 0.5);
163
  text-transform: uppercase;
164
  letter-spacing: 0.5px;
165
  }
@@ -169,9 +175,9 @@
169
 
170
  .clear-button {
171
  padding: 4px 10px;
172
- background: rgba(139, 115, 85, 0.03);
173
- color: rgba(251, 248, 244, 0.4);
174
- border: 1px solid rgba(139, 115, 85, 0.08);
175
  border-radius: 4px;
176
  font-size: 10px;
177
  font-weight: 500;
@@ -185,9 +191,9 @@
185
  }
186
 
187
  .clear-button:hover {
188
- background: rgba(139, 115, 85, 0.06);
189
- color: rgba(251, 248, 244, 0.6);
190
- border-color: rgba(139, 115, 85, 0.12);
191
  }
192
 
193
  .clear-button:active {
 
4
  import gsap from "gsap";
5
  import MessageList from "./MessageList.svelte";
6
  import MessageInput from "./MessageInput.svelte";
7
+ import ExampleRow from "./ExampleRow.svelte";
8
  import { isConnected, isProcessing, messages, error } from "../../stores/chat-store";
9
  import { chatController } from "../../controllers/chat-controller";
10
  import { authStore } from "../../services/auth";
 
119
  {/if}
120
  </div>
121
 
122
+ <ExampleRow
123
+ onSendMessage={handleExampleMessage}
124
+ visible={$messages.length === 0 && $authStore.isAuthenticated && $isConnected}
125
+ />
126
+
127
  <MessageInput
128
  bind:value={inputValue}
129
  placeholder={!$authStore.isAuthenticated
 
146
  flex-direction: column;
147
  height: 100%;
148
  width: 100%;
149
+ background: rgba(12, 11, 10, 0.85);
150
+ border-top: 1px solid rgba(139, 115, 85, 0.12);
151
  position: relative;
152
  }
153
 
 
156
  justify-content: space-between;
157
  align-items: center;
158
  padding: 10px 16px;
159
+ background: rgba(18, 17, 16, 0.6);
160
+ border-bottom: 1px solid rgba(139, 115, 85, 0.15);
161
  flex-shrink: 0;
162
  }
163
 
 
165
  margin: 0;
166
  font-size: 11px;
167
  font-weight: 500;
168
+ color: rgba(251, 248, 244, 0.7);
169
  text-transform: uppercase;
170
  letter-spacing: 0.5px;
171
  }
 
175
 
176
  .clear-button {
177
  padding: 4px 10px;
178
+ background: rgba(139, 115, 85, 0.08);
179
+ color: rgba(251, 248, 244, 0.55);
180
+ border: 1px solid rgba(139, 115, 85, 0.15);
181
  border-radius: 4px;
182
  font-size: 10px;
183
  font-weight: 500;
 
191
  }
192
 
193
  .clear-button:hover {
194
+ background: rgba(139, 115, 85, 0.12);
195
+ color: rgba(251, 248, 244, 0.75);
196
+ border-color: rgba(139, 115, 85, 0.2);
197
  }
198
 
199
  .clear-button:active {
src/lib/components/chat/ExampleMessages.svelte CHANGED
@@ -1,183 +1,152 @@
1
  <script lang="ts">
2
- import { onMount, onDestroy } from "svelte";
3
  import { fade } from "svelte/transition";
4
- import gsap from "gsap";
5
-
6
- export let onSendMessage: (message: string) => void;
7
-
8
- const examples = [
9
- { icon: "🏀", text: "add another ball" },
10
- { icon: "📝", text: "explain what the code does" },
11
- { icon: "🎮", text: "make an obby" },
12
- { icon: "⬆️", text: "increase the player jump height" }
13
- ];
14
-
15
- let exampleCards: HTMLButtonElement[] = [];
16
-
17
- onMount(() => {
18
- exampleCards.forEach((card, index) => {
19
- if (!card) return;
20
-
21
- gsap.fromTo(card, {
22
- opacity: 0,
23
- y: 15,
24
- scale: 0.97
25
- }, {
26
- opacity: 1,
27
- y: 0,
28
- scale: 1,
29
- duration: 0.2,
30
- delay: index * 0.02,
31
- ease: "power3.out"
32
- });
33
- });
34
- });
35
 
36
- onDestroy(() => {
37
- exampleCards.forEach((card) => {
38
- if (card) {
39
- gsap.killTweensOf(card);
40
- }
41
- });
42
- });
 
 
 
 
 
 
 
 
 
 
 
 
43
 
44
- function handleClick(text: string) {
45
- onSendMessage(text);
 
 
 
 
 
46
  }
47
 
48
- function handleMouseEnter(event: MouseEvent) {
49
- const card = event.currentTarget as HTMLButtonElement;
50
- gsap.to(card, {
51
- scale: 1.03,
52
- boxShadow: "0 6px 24px rgba(65, 105, 225, 0.25)",
53
- borderColor: "rgba(65, 105, 225, 0.5)",
54
- duration: 0.08,
55
- ease: "power2.out"
56
- });
 
 
 
57
  }
58
 
59
- function handleMouseLeave(event: MouseEvent) {
60
- const card = event.currentTarget as HTMLButtonElement;
61
- gsap.to(card, {
62
- scale: 1,
63
- boxShadow: "0 2px 10px rgba(0, 0, 0, 0.2)",
64
- borderColor: "rgba(255, 255, 255, 0.05)",
65
- duration: 0.1,
66
- ease: "power2.out"
67
- });
 
 
 
68
  }
69
 
70
- function handleMouseDown(event: MouseEvent) {
71
- const card = event.currentTarget as HTMLButtonElement;
72
- gsap.to(card, {
73
- scale: 0.97,
74
- duration: 0.03,
75
- ease: "power2.in"
76
- });
 
 
77
  }
78
 
79
- function handleMouseUp(event: MouseEvent) {
80
- const card = event.currentTarget as HTMLButtonElement;
81
- gsap.to(card, {
82
- scale: 1.03,
83
- duration: 0.05,
84
- ease: "back.out(1.5)"
85
- });
 
 
86
  }
87
- </script>
88
 
89
- <div class="examples-container" transition:fade={{ duration: 200 }}>
90
- <div class="examples-header">
91
- <span class="header-text">Try an example</span>
92
- </div>
93
- <div class="examples-grid">
94
- {#each examples as example, i}
95
- <button
96
- bind:this={exampleCards[i]}
97
- class="example-card"
98
- on:click={() => handleClick(example.text)}
99
- on:mouseenter={handleMouseEnter}
100
- on:mouseleave={handleMouseLeave}
101
- on:mousedown={handleMouseDown}
102
- on:mouseup={handleMouseUp}
103
- >
104
- <span class="example-icon">{example.icon}</span>
105
- <span class="example-text">{example.text}</span>
106
- </button>
107
- {/each}
108
- </div>
109
- </div>
110
-
111
- <style>
112
- .examples-container {
113
- display: flex;
114
- flex-direction: column;
115
- align-items: center;
116
- justify-content: center;
117
- padding: 2rem 1rem;
118
- height: 100%;
119
- min-height: 300px;
120
- gap: 1.5rem;
121
  }
122
 
123
- .examples-header {
124
- color: rgba(255, 255, 255, 0.3);
125
- font-size: 0.75rem;
126
  text-transform: uppercase;
127
- letter-spacing: 0.1em;
128
- font-weight: 500;
 
 
129
  }
130
 
131
- .header-text {
132
- animation: gentle-pulse 3s ease-in-out infinite;
 
 
 
 
 
 
133
  }
134
 
135
- @keyframes gentle-pulse {
136
- 0%, 100% {
137
- opacity: 0.5;
138
- }
139
- 50% {
140
- opacity: 0.8;
141
- }
142
  }
143
 
144
- .examples-grid {
145
- display: grid;
146
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
147
- gap: 0.75rem;
148
- width: 100%;
149
- max-width: 600px;
150
  }
151
 
152
- .example-card {
153
- display: flex;
154
  align-items: center;
155
- gap: 0.5rem;
156
  padding: 0.5rem 1rem;
157
- background: rgba(255, 255, 255, 0.02);
158
- border: 1px solid rgba(255, 255, 255, 0.05);
159
- border-radius: 6px;
160
- cursor: pointer;
161
- transition: none;
 
 
 
162
  font-family: "Monaco", "Menlo", monospace;
163
- font-size: 0.875rem;
164
- color: rgba(255, 255, 255, 0.85);
165
- will-change: transform, box-shadow;
166
- box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
167
  }
168
 
169
- .example-icon {
170
- font-size: 1rem;
171
- opacity: 0.8;
 
 
 
172
  }
173
 
174
- .example-text {
175
- flex: 1;
176
  }
177
 
178
- @media (max-width: 600px) {
179
- .examples-grid {
180
- grid-template-columns: 1fr;
181
- }
182
  }
183
- </style>
 
1
  <script lang="ts">
 
2
  import { fade } from "svelte/transition";
3
+ </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
 
5
+ <div class="warning-container" transition:fade={{ duration: 200 }}>
6
+ <div class="model-warning">
7
+ <div class="warning-badge">
8
+ <span class="warning-icon">⚠️</span>
9
+ <span class="warning-label">Model Limitations</span>
10
+ </div>
11
+ <p class="warning-text">
12
+ This demo uses <a href="https://huggingface.co/Qwen/Qwen3-Next-80B-A3B-Instruct" target="_blank" rel="noopener">Qwen3-Next-80B-A3B-Instruct</a>.
13
+ <br />
14
+ For complex tasks, run locally with frontier code agents like Claude Code.
15
+ </p>
16
+ <a href="https://github.com/dylanebert/VibeGame" target="_blank" rel="noopener" class="warning-link">
17
+ <span>Learn More</span>
18
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
19
+ <path d="M7 17L17 7M17 7H7M17 7V17" />
20
+ </svg>
21
+ </a>
22
+ </div>
23
+ </div>
24
 
25
+ <style>
26
+ .warning-container {
27
+ display: flex;
28
+ align-items: center;
29
+ justify-content: center;
30
+ height: 100%;
31
+ padding: 2rem 1rem;
32
  }
33
 
34
+ .model-warning {
35
+ display: flex;
36
+ flex-direction: column;
37
+ align-items: center;
38
+ gap: 1rem;
39
+ padding: 1.5rem;
40
+ background: rgba(255, 255, 255, 0.01);
41
+ border: 1px solid rgba(255, 255, 255, 0.06);
42
+ border-radius: 12px;
43
+ max-width: 500px;
44
+ animation: warning-fade-in 0.4s ease-out;
45
+ position: relative;
46
  }
47
 
48
+ .model-warning::before {
49
+ content: '';
50
+ position: absolute;
51
+ top: 0;
52
+ left: 0;
53
+ right: 0;
54
+ bottom: 0;
55
+ background: linear-gradient(135deg,
56
+ rgba(255, 165, 0, 0.02) 0%,
57
+ rgba(255, 165, 0, 0.04) 100%);
58
+ border-radius: 12px;
59
+ pointer-events: none;
60
  }
61
 
62
+ @keyframes warning-fade-in {
63
+ from {
64
+ opacity: 0;
65
+ transform: translateY(10px);
66
+ }
67
+ to {
68
+ opacity: 1;
69
+ transform: translateY(0);
70
+ }
71
  }
72
 
73
+ .warning-badge {
74
+ display: inline-flex;
75
+ align-items: center;
76
+ gap: 0.5rem;
77
+ padding: 0.25rem 0.75rem;
78
+ background: rgba(255, 165, 0, 0.08);
79
+ border: 1px solid rgba(255, 165, 0, 0.15);
80
+ border-radius: 20px;
81
+ z-index: 1;
82
  }
 
83
 
84
+ .warning-icon {
85
+ font-size: 0.875rem;
86
+ line-height: 1;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  }
88
 
89
+ .warning-label {
90
+ font-size: 0.625rem;
 
91
  text-transform: uppercase;
92
+ letter-spacing: 0.08em;
93
+ color: rgba(255, 165, 0, 0.9);
94
+ font-weight: 600;
95
+ font-family: "Monaco", "Menlo", monospace;
96
  }
97
 
98
+ .warning-text {
99
+ margin: 0;
100
+ font-size: 0.75rem;
101
+ line-height: 1.6;
102
+ color: rgba(255, 255, 255, 0.5);
103
+ font-family: "Monaco", "Menlo", monospace;
104
+ text-align: center;
105
+ z-index: 1;
106
  }
107
 
108
+ .warning-text a {
109
+ color: rgba(255, 165, 0, 0.85);
110
+ text-decoration: none;
111
+ transition: all 0.15s ease;
112
+ border-bottom: 1px dotted rgba(255, 165, 0, 0.3);
 
 
113
  }
114
 
115
+ .warning-text a:hover {
116
+ color: rgba(255, 190, 0, 1);
117
+ border-bottom-color: rgba(255, 190, 0, 0.5);
 
 
 
118
  }
119
 
120
+ .warning-link {
121
+ display: inline-flex;
122
  align-items: center;
123
+ gap: 0.375rem;
124
  padding: 0.5rem 1rem;
125
+ background: transparent;
126
+ border: 1px solid rgba(255, 255, 255, 0.08);
127
+ border-radius: 8px;
128
+ color: rgba(255, 255, 255, 0.7);
129
+ font-size: 0.75rem;
130
+ font-weight: 500;
131
+ text-decoration: none;
132
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
133
  font-family: "Monaco", "Menlo", monospace;
134
+ z-index: 1;
 
 
 
135
  }
136
 
137
+ .warning-link:hover {
138
+ background: rgba(255, 255, 255, 0.03);
139
+ border-color: rgba(255, 255, 255, 0.15);
140
+ color: rgba(255, 255, 255, 0.9);
141
+ transform: translateY(-2px);
142
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
143
  }
144
 
145
+ .warning-link svg {
146
+ transition: transform 0.2s ease;
147
  }
148
 
149
+ .warning-link:hover svg {
150
+ transform: translate(2px, -2px);
 
 
151
  }
152
+ </style>
src/lib/components/chat/ExampleRow.svelte ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { onMount, onDestroy } from "svelte";
3
+ import { fade } from "svelte/transition";
4
+ import gsap from "gsap";
5
+
6
+ export let onSendMessage: (message: string) => void;
7
+ export let visible: boolean = true;
8
+
9
+ const examples = [
10
+ { icon: "🏀", text: "add another ball" },
11
+ { icon: "📝", text: "explain what the code does" },
12
+ { icon: "🎮", text: "make a moving platform" },
13
+ { icon: "⬆️", text: "increase the player jump height" }
14
+ ];
15
+
16
+ let exampleCards: HTMLButtonElement[] = [];
17
+
18
+ onMount(() => {
19
+ if (visible) {
20
+ exampleCards.forEach((card, index) => {
21
+ if (!card) return;
22
+
23
+ gsap.fromTo(card, {
24
+ opacity: 0,
25
+ x: -10,
26
+ scale: 0.95
27
+ }, {
28
+ opacity: 1,
29
+ x: 0,
30
+ scale: 1,
31
+ duration: 0.15,
32
+ delay: index * 0.03,
33
+ ease: "power2.out"
34
+ });
35
+ });
36
+ }
37
+ });
38
+
39
+ onDestroy(() => {
40
+ exampleCards.forEach((card) => {
41
+ if (card) {
42
+ gsap.killTweensOf(card);
43
+ }
44
+ });
45
+ });
46
+
47
+ function handleClick(text: string) {
48
+ onSendMessage(text);
49
+ }
50
+
51
+ function handleMouseEnter(event: MouseEvent) {
52
+ const card = event.currentTarget as HTMLButtonElement;
53
+ gsap.to(card, {
54
+ scale: 1.05,
55
+ duration: 0.08,
56
+ ease: "power2.out"
57
+ });
58
+ }
59
+
60
+ function handleMouseLeave(event: MouseEvent) {
61
+ const card = event.currentTarget as HTMLButtonElement;
62
+ gsap.to(card, {
63
+ scale: 1,
64
+ duration: 0.1,
65
+ ease: "power2.out"
66
+ });
67
+ }
68
+
69
+ function handleMouseDown(event: MouseEvent) {
70
+ const card = event.currentTarget as HTMLButtonElement;
71
+ gsap.to(card, {
72
+ scale: 0.97,
73
+ duration: 0.03,
74
+ ease: "power2.in"
75
+ });
76
+ }
77
+
78
+ function handleMouseUp(event: MouseEvent) {
79
+ const card = event.currentTarget as HTMLButtonElement;
80
+ gsap.to(card, {
81
+ scale: 1.05,
82
+ duration: 0.05,
83
+ ease: "back.out(1.5)"
84
+ });
85
+ }
86
+ </script>
87
+
88
+ {#if visible}
89
+ <div class="examples-row" transition:fade={{ duration: 200 }}>
90
+ <span class="examples-label">Try an example:</span>
91
+ <div class="examples-list">
92
+ {#each examples as example, i}
93
+ <button
94
+ bind:this={exampleCards[i]}
95
+ class="example-pill"
96
+ on:click={() => handleClick(example.text)}
97
+ on:mouseenter={handleMouseEnter}
98
+ on:mouseleave={handleMouseLeave}
99
+ on:mousedown={handleMouseDown}
100
+ on:mouseup={handleMouseUp}
101
+ >
102
+ <span class="example-icon">{example.icon}</span>
103
+ <span class="example-text">{example.text}</span>
104
+ </button>
105
+ {/each}
106
+ </div>
107
+ </div>
108
+ {/if}
109
+
110
+ <style>
111
+ .examples-row {
112
+ display: flex;
113
+ align-items: center;
114
+ gap: 1rem;
115
+ padding: 0.75rem 1rem;
116
+ background: rgba(0, 0, 0, 0.3);
117
+ border-top: 1px solid rgba(255, 255, 255, 0.03);
118
+ }
119
+
120
+ .examples-label {
121
+ font-size: 0.7rem;
122
+ color: rgba(255, 255, 255, 0.35);
123
+ text-transform: uppercase;
124
+ letter-spacing: 0.08em;
125
+ font-weight: 500;
126
+ font-family: "Monaco", "Menlo", monospace;
127
+ flex-shrink: 0;
128
+ }
129
+
130
+ .examples-list {
131
+ display: flex;
132
+ gap: 0.5rem;
133
+ flex-wrap: wrap;
134
+ flex: 1;
135
+ }
136
+
137
+ .example-pill {
138
+ display: inline-flex;
139
+ align-items: center;
140
+ gap: 0.375rem;
141
+ padding: 0.3rem 0.625rem;
142
+ background: rgba(255, 255, 255, 0.02);
143
+ border: 1px solid rgba(255, 255, 255, 0.05);
144
+ border-radius: 16px;
145
+ cursor: pointer;
146
+ transition: none;
147
+ font-family: "Monaco", "Menlo", monospace;
148
+ font-size: 0.7rem;
149
+ color: rgba(255, 255, 255, 0.65);
150
+ will-change: transform, box-shadow;
151
+ white-space: nowrap;
152
+ }
153
+
154
+ .example-pill:hover {
155
+ background: rgba(255, 255, 255, 0.04);
156
+ border-color: rgba(255, 255, 255, 0.08);
157
+ color: rgba(255, 255, 255, 0.85);
158
+ }
159
+
160
+ .example-icon {
161
+ font-size: 0.75rem;
162
+ opacity: 0.8;
163
+ }
164
+
165
+ .example-text {
166
+ font-size: 0.7rem;
167
+ }
168
+
169
+ @media (max-width: 600px) {
170
+ .examples-row {
171
+ flex-direction: column;
172
+ align-items: stretch;
173
+ gap: 0.5rem;
174
+ padding: 0.75rem;
175
+ }
176
+ .examples-label {
177
+ text-align: center;
178
+ }
179
+ .examples-list {
180
+ justify-content: center;
181
+ }
182
+ }
183
+ </style>
src/lib/components/chat/Message.svelte CHANGED
@@ -1,5 +1,5 @@
1
  <script lang="ts">
2
- import { onMount } from "svelte";
3
  import gsap from "gsap";
4
  import type { ChatMessage } from "../../models/chat-data";
5
  import MessageContent from "./MessageContent.svelte";
@@ -7,16 +7,19 @@
7
  export let message: ChatMessage;
8
 
9
  let messageRef: HTMLDivElement;
 
10
 
11
  onMount(() => {
12
- if (messageRef) {
13
  const isUser = message.role === 'user';
14
 
15
- gsap.fromTo(messageRef,
 
 
16
  {
17
  opacity: 0,
18
- y: 8,
19
- x: isUser ? 10 : -10,
20
  scale: 0.98
21
  },
22
  {
@@ -24,18 +27,47 @@
24
  y: 0,
25
  x: 0,
26
  scale: 1,
27
- duration: 0.25,
28
- ease: "power2.out",
29
- delay: 0.05
30
  }
31
  );
32
 
33
  if (!isUser) {
34
- gsap.fromTo(messageRef.querySelector('.message-content'),
35
- { opacity: 0 },
36
- { opacity: 1, duration: 0.3, delay: 0.1, ease: "power1.out" }
 
 
 
 
 
 
 
 
 
37
  );
38
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  }
40
  });
41
  </script>
 
1
  <script lang="ts">
2
+ import { onMount, afterUpdate } from "svelte";
3
  import gsap from "gsap";
4
  import type { ChatMessage } from "../../models/chat-data";
5
  import MessageContent from "./MessageContent.svelte";
 
7
  export let message: ChatMessage;
8
 
9
  let messageRef: HTMLDivElement;
10
+ let hasAnimated = false;
11
 
12
  onMount(() => {
13
+ if (messageRef && !hasAnimated) {
14
  const isUser = message.role === 'user';
15
 
16
+ const tl = gsap.timeline();
17
+
18
+ tl.fromTo(messageRef,
19
  {
20
  opacity: 0,
21
+ y: isUser ? 5 : 10,
22
+ x: isUser ? 8 : -8,
23
  scale: 0.98
24
  },
25
  {
 
27
  y: 0,
28
  x: 0,
29
  scale: 1,
30
+ duration: 0.35,
31
+ ease: "power2.out"
 
32
  }
33
  );
34
 
35
  if (!isUser) {
36
+ tl.fromTo(messageRef.querySelector('.message-content'),
37
+ {
38
+ opacity: 0,
39
+ borderLeftWidth: "0px"
40
+ },
41
+ {
42
+ opacity: 1,
43
+ borderLeftWidth: "2px",
44
+ duration: 0.3,
45
+ ease: "power2.out"
46
+ },
47
+ "-=0.2"
48
  );
49
  }
50
+
51
+ hasAnimated = true;
52
+ }
53
+ });
54
+
55
+ afterUpdate(() => {
56
+ if (messageRef && !message.streaming && message.role === 'assistant' && hasAnimated) {
57
+ const borderEl = messageRef.querySelector('.message-content') as HTMLElement;
58
+ if (borderEl && !borderEl.dataset.completed) {
59
+ borderEl.dataset.completed = "true";
60
+ gsap.to(borderEl, {
61
+ borderLeftColor: "rgba(76, 175, 80, 0.4)",
62
+ duration: 0.3,
63
+ yoyo: true,
64
+ repeat: 1,
65
+ ease: "power2.inOut",
66
+ onComplete: () => {
67
+ gsap.set(borderEl, { borderLeftColor: "rgba(139, 115, 85, 0.08)" });
68
+ }
69
+ });
70
+ }
71
  }
72
  });
73
  </script>
src/lib/components/chat/MessageContent.svelte CHANGED
@@ -3,14 +3,15 @@
3
  import TextRenderer from "./TextRenderer.svelte";
4
  import ToolBlock from "./segments/ToolBlock.svelte";
5
  import TodoSegment from "./segments/TodoSegment.svelte";
 
6
 
7
  export let message: ChatMessage;
8
 
9
  $: hasSegments = message.segments && message.segments.length > 0;
10
  $: isStreaming = message.streaming || (message.segments?.some(s => s.streaming));
11
 
12
- // Normalize content for consistent rendering
13
- $: normalizedContent = hasSegments ? null : (message.content || "");
14
 
15
  // Filter segments for rendering
16
  $: segmentsToRender = hasSegments ? message.segments!.filter((segment) => {
@@ -34,7 +35,7 @@
34
  <!-- Segmented content rendering -->
35
  {#each segmentsToRender as segment (segment.id)}
36
  {#if segment.type === "text"}
37
- <TextRenderer content={segment.content} streaming={segment.streaming || false} />
38
  {:else if segment.type === "tool-invocation"}
39
  <ToolBlock
40
  invocation={segment}
 
3
  import TextRenderer from "./TextRenderer.svelte";
4
  import ToolBlock from "./segments/ToolBlock.svelte";
5
  import TodoSegment from "./segments/TodoSegment.svelte";
6
+ import { filterToolCalls } from "../../utils/tool-call-parser";
7
 
8
  export let message: ChatMessage;
9
 
10
  $: hasSegments = message.segments && message.segments.length > 0;
11
  $: isStreaming = message.streaming || (message.segments?.some(s => s.streaming));
12
 
13
+ // Normalize content for consistent rendering with tool call filtering
14
+ $: normalizedContent = hasSegments ? null : filterToolCalls(message.content || "");
15
 
16
  // Filter segments for rendering
17
  $: segmentsToRender = hasSegments ? message.segments!.filter((segment) => {
 
35
  <!-- Segmented content rendering -->
36
  {#each segmentsToRender as segment (segment.id)}
37
  {#if segment.type === "text"}
38
+ <TextRenderer content={filterToolCalls(segment.content)} streaming={segment.streaming || false} />
39
  {:else if segment.type === "tool-invocation"}
40
  <ToolBlock
41
  invocation={segment}
src/lib/components/chat/MessageInput.svelte CHANGED
@@ -16,32 +16,48 @@
16
 
17
  function handleSubmit() {
18
  if (value.trim() && !disabled && !processing) {
 
 
19
  if (sendButtonRef) {
20
- gsap.to(sendButtonRef, {
21
- scale: 0.9,
22
  duration: 0.1,
23
- yoyo: true,
24
- repeat: 1,
25
- ease: "power2.inOut",
26
- onComplete: () => {
27
- gsap.to(sendButtonRef, {
28
- rotationY: 360,
29
- duration: 0.6,
30
- ease: "back.out(1.7)"
31
- });
32
- }
33
- });
 
 
34
  }
35
 
36
- dispatch("send", value.trim());
37
- value = "";
38
-
39
  if (textareaRef) {
40
- gsap.fromTo(textareaRef,
41
- { scale: 1.02 },
42
- { scale: 1, duration: 0.3, ease: "power2.out" }
 
 
 
 
 
 
 
 
 
 
 
 
43
  );
44
  }
 
 
 
45
  }
46
  }
47
 
@@ -60,6 +76,8 @@
60
  }
61
  }
62
 
 
 
63
  onMount(() => {
64
  if (inputAreaRef) {
65
  gsap.fromTo(inputAreaRef,
@@ -84,6 +102,30 @@
84
  gsap.set(stopButtonRef, { scale: 1 });
85
  }
86
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  function handleKeydown(event: KeyboardEvent) {
88
  if (event.key === "Enter" && !event.shiftKey) {
89
  event.preventDefault();
@@ -135,8 +177,8 @@
135
  display: flex;
136
  gap: 0.5rem;
137
  padding: 12px 16px;
138
- background: transparent;
139
- border-top: 1px solid rgba(139, 115, 85, 0.06);
140
  }
141
 
142
  .input-wrapper {
@@ -161,9 +203,9 @@
161
 
162
  textarea {
163
  width: 100%;
164
- background: rgba(139, 115, 85, 0.03);
165
- color: rgba(251, 248, 244, 0.9);
166
- border: 1px solid rgba(139, 115, 85, 0.08);
167
  border-radius: 4px;
168
  padding: 0.4rem 0.6rem;
169
  resize: none;
@@ -177,9 +219,9 @@
177
 
178
  textarea:focus {
179
  outline: none;
180
- border-color: rgba(124, 152, 133, 0.2);
181
- background: rgba(139, 115, 85, 0.06);
182
- box-shadow: 0 0 0 1px rgba(124, 152, 133, 0.1);
183
  }
184
 
185
  textarea:focus + .input-glow {
@@ -197,9 +239,9 @@
197
  display: flex;
198
  align-items: center;
199
  justify-content: center;
200
- background: rgba(124, 152, 133, 0.08);
201
- color: rgba(124, 152, 133, 0.8);
202
- border: 1px solid rgba(124, 152, 133, 0.15);
203
  border-radius: 4px;
204
  cursor: pointer;
205
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
@@ -219,10 +261,10 @@
219
  }
220
 
221
  .send-btn:hover:not(:disabled) {
222
- background: rgba(124, 152, 133, 0.15);
223
- border-color: rgba(124, 152, 133, 0.3);
224
  transform: translateY(-1px);
225
- box-shadow: 0 4px 12px rgba(124, 152, 133, 0.2);
226
  color: rgba(124, 152, 133, 1);
227
  }
228
 
@@ -246,9 +288,9 @@
246
  display: flex;
247
  align-items: center;
248
  justify-content: center;
249
- background: rgba(184, 84, 80, 0.08);
250
- color: rgba(184, 84, 80, 0.8);
251
- border: 1px solid rgba(184, 84, 80, 0.15);
252
  border-radius: 4px;
253
  cursor: pointer;
254
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
@@ -281,10 +323,10 @@
281
  }
282
 
283
  .stop-btn:hover {
284
- background: rgba(184, 84, 80, 0.15);
285
- border-color: rgba(184, 84, 80, 0.3);
286
  transform: translateY(-1px);
287
- box-shadow: 0 4px 12px rgba(184, 84, 80, 0.2);
288
  color: rgba(184, 84, 80, 1);
289
  }
290
 
 
16
 
17
  function handleSubmit() {
18
  if (value.trim() && !disabled && !processing) {
19
+ const tl = gsap.timeline();
20
+
21
  if (sendButtonRef) {
22
+ tl.to(sendButtonRef, {
23
+ scale: 0.85,
24
  duration: 0.1,
25
+ ease: "power2.in"
26
+ })
27
+ .to(sendButtonRef, {
28
+ scale: 1.1,
29
+ rotation: 360,
30
+ duration: 0.4,
31
+ ease: "back.out(2)"
32
+ })
33
+ .to(sendButtonRef, {
34
+ scale: 1,
35
+ duration: 0.2,
36
+ ease: "power2.out"
37
+ }, "-=0.1");
38
  }
39
 
 
 
 
40
  if (textareaRef) {
41
+ tl.to(textareaRef,
42
+ {
43
+ borderColor: "rgba(76, 175, 80, 0.3)",
44
+ backgroundColor: "rgba(76, 175, 80, 0.02)",
45
+ duration: 0.2
46
+ },
47
+ 0
48
+ )
49
+ .to(textareaRef,
50
+ {
51
+ borderColor: "rgba(139, 115, 85, 0.08)",
52
+ backgroundColor: "rgba(139, 115, 85, 0.03)",
53
+ duration: 0.3,
54
+ ease: "power2.out"
55
+ }
56
  );
57
  }
58
+
59
+ dispatch("send", value.trim());
60
+ value = "";
61
  }
62
  }
63
 
 
76
  }
77
  }
78
 
79
+ let isTyping = false;
80
+
81
  onMount(() => {
82
  if (inputAreaRef) {
83
  gsap.fromTo(inputAreaRef,
 
102
  gsap.set(stopButtonRef, { scale: 1 });
103
  }
104
 
105
+ $: if (value.length > 0 && !isTyping) {
106
+ isTyping = true;
107
+ if (sendButtonRef) {
108
+ gsap.to(sendButtonRef, {
109
+ scale: 1.05,
110
+ backgroundColor: "rgba(124, 152, 133, 0.12)",
111
+ duration: 0.3,
112
+ ease: "power2.out"
113
+ });
114
+ }
115
+ }
116
+
117
+ $: if (value.length === 0 && isTyping) {
118
+ isTyping = false;
119
+ if (sendButtonRef) {
120
+ gsap.to(sendButtonRef, {
121
+ scale: 1,
122
+ backgroundColor: "rgba(124, 152, 133, 0.08)",
123
+ duration: 0.3,
124
+ ease: "power2.out"
125
+ });
126
+ }
127
+ }
128
+
129
  function handleKeydown(event: KeyboardEvent) {
130
  if (event.key === "Enter" && !event.shiftKey) {
131
  event.preventDefault();
 
177
  display: flex;
178
  gap: 0.5rem;
179
  padding: 12px 16px;
180
+ background: rgba(20, 19, 17, 0.5);
181
+ border-top: 1px solid rgba(139, 115, 85, 0.18);
182
  }
183
 
184
  .input-wrapper {
 
203
 
204
  textarea {
205
  width: 100%;
206
+ background: rgba(30, 28, 26, 0.5);
207
+ color: rgba(251, 248, 244, 0.95);
208
+ border: 1px solid rgba(139, 115, 85, 0.15);
209
  border-radius: 4px;
210
  padding: 0.4rem 0.6rem;
211
  resize: none;
 
219
 
220
  textarea:focus {
221
  outline: none;
222
+ border-color: rgba(124, 152, 133, 0.3);
223
+ background: rgba(35, 33, 30, 0.6);
224
+ box-shadow: 0 0 0 1px rgba(124, 152, 133, 0.15);
225
  }
226
 
227
  textarea:focus + .input-glow {
 
239
  display: flex;
240
  align-items: center;
241
  justify-content: center;
242
+ background: rgba(124, 152, 133, 0.12);
243
+ color: rgba(124, 152, 133, 0.9);
244
+ border: 1px solid rgba(124, 152, 133, 0.2);
245
  border-radius: 4px;
246
  cursor: pointer;
247
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
 
261
  }
262
 
263
  .send-btn:hover:not(:disabled) {
264
+ background: rgba(124, 152, 133, 0.2);
265
+ border-color: rgba(124, 152, 133, 0.35);
266
  transform: translateY(-1px);
267
+ box-shadow: 0 4px 12px rgba(124, 152, 133, 0.25);
268
  color: rgba(124, 152, 133, 1);
269
  }
270
 
 
288
  display: flex;
289
  align-items: center;
290
  justify-content: center;
291
+ background: rgba(184, 84, 80, 0.12);
292
+ color: rgba(184, 84, 80, 0.9);
293
+ border: 1px solid rgba(184, 84, 80, 0.2);
294
  border-radius: 4px;
295
  cursor: pointer;
296
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
 
323
  }
324
 
325
  .stop-btn:hover {
326
+ background: rgba(184, 84, 80, 0.2);
327
+ border-color: rgba(184, 84, 80, 0.35);
328
  transform: translateY(-1px);
329
+ box-shadow: 0 4px 12px rgba(184, 84, 80, 0.25);
330
  color: rgba(184, 84, 80, 1);
331
  }
332
 
src/lib/components/chat/MessageList.svelte CHANGED
@@ -35,15 +35,27 @@
35
 
36
  $: if ($typingIndicator && typingIndicatorRef) {
37
  gsap.fromTo(typingIndicatorRef,
38
- { opacity: 0, y: 10 },
39
- { opacity: 1, y: 0, duration: 0.3, ease: "power2.out" }
40
  );
 
 
 
 
 
 
 
 
 
 
41
  }
42
 
43
  $: if (!$typingIndicator && typingIndicatorRef) {
 
44
  gsap.to(typingIndicatorRef, {
45
  opacity: 0,
46
  y: -5,
 
47
  duration: 0.2,
48
  ease: "power2.in"
49
  });
@@ -53,7 +65,7 @@
53
  <div class="messages" bind:this={messagesDiv} on:scroll={handleScroll}>
54
  {#if messages.length === 0 && isConnected && onSendMessage}
55
  <div class="welcome-section">
56
- <ExampleMessages {onSendMessage} />
57
  </div>
58
  {/if}
59
 
@@ -101,44 +113,63 @@
101
  display: flex;
102
  align-items: center;
103
  gap: 0.5rem;
104
- padding: 0.375rem 0.5rem;
105
- margin: 0.125rem 0;
106
- border-left: 2px solid rgba(124, 152, 133, 0.2);
107
- padding-left: 0.5rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  }
109
 
110
  .typing-dots {
111
  display: flex;
112
- gap: 0.25rem;
 
113
  }
114
 
115
  .dot {
116
- width: 4px;
117
- height: 4px;
118
  border-radius: 50%;
119
- background: rgba(124, 152, 133, 0.6);
120
- animation: typing-bounce 1.4s ease-in-out infinite both;
121
- }
122
-
123
- .dot:nth-child(1) { animation-delay: -0.32s; }
124
- .dot:nth-child(2) { animation-delay: -0.16s; }
125
- .dot:nth-child(3) { animation-delay: 0s; }
126
-
127
- @keyframes typing-bounce {
128
- 0%, 80%, 100% {
129
- transform: scale(0);
130
- opacity: 0.5;
131
- }
132
- 40% {
133
- transform: scale(1);
134
- opacity: 1;
135
- }
136
  }
137
 
138
  .typing-text {
139
  font-size: 11px;
140
- color: rgba(251, 248, 244, 0.3);
141
- font-style: italic;
 
 
 
 
 
 
 
142
  }
143
 
144
  .error-message {
 
35
 
36
  $: if ($typingIndicator && typingIndicatorRef) {
37
  gsap.fromTo(typingIndicatorRef,
38
+ { opacity: 0, y: 10, scale: 0.95 },
39
+ { opacity: 1, y: 0, scale: 1, duration: 0.3, ease: "power2.out" }
40
  );
41
+
42
+ const dots = typingIndicatorRef.querySelectorAll('.dot');
43
+ gsap.to(dots, {
44
+ y: -3,
45
+ duration: 0.6,
46
+ stagger: 0.15,
47
+ repeat: -1,
48
+ yoyo: true,
49
+ ease: "power1.inOut"
50
+ });
51
  }
52
 
53
  $: if (!$typingIndicator && typingIndicatorRef) {
54
+ gsap.killTweensOf(typingIndicatorRef.querySelectorAll('.dot'));
55
  gsap.to(typingIndicatorRef, {
56
  opacity: 0,
57
  y: -5,
58
+ scale: 0.95,
59
  duration: 0.2,
60
  ease: "power2.in"
61
  });
 
65
  <div class="messages" bind:this={messagesDiv} on:scroll={handleScroll}>
66
  {#if messages.length === 0 && isConnected && onSendMessage}
67
  <div class="welcome-section">
68
+ <ExampleMessages />
69
  </div>
70
  {/if}
71
 
 
113
  display: flex;
114
  align-items: center;
115
  gap: 0.5rem;
116
+ padding: 0.5rem;
117
+ margin: 0.25rem 0;
118
+ border-left: 2px solid rgba(33, 150, 243, 0.3);
119
+ background: linear-gradient(90deg,
120
+ rgba(33, 150, 243, 0.05),
121
+ transparent);
122
+ border-radius: 0 4px 4px 0;
123
+ position: relative;
124
+ overflow: hidden;
125
+ }
126
+
127
+ .typing-indicator::before {
128
+ content: '';
129
+ position: absolute;
130
+ top: 0;
131
+ left: -100%;
132
+ width: 100%;
133
+ height: 100%;
134
+ background: linear-gradient(90deg,
135
+ transparent,
136
+ rgba(33, 150, 243, 0.1),
137
+ transparent);
138
+ animation: shimmer 2s ease-in-out infinite;
139
+ }
140
+
141
+ @keyframes shimmer {
142
+ to { left: 100%; }
143
  }
144
 
145
  .typing-dots {
146
  display: flex;
147
+ gap: 0.3rem;
148
+ align-items: center;
149
  }
150
 
151
  .dot {
152
+ width: 6px;
153
+ height: 6px;
154
  border-radius: 50%;
155
+ background: linear-gradient(135deg,
156
+ rgba(33, 150, 243, 0.8),
157
+ rgba(100, 181, 246, 0.6));
158
+ display: inline-block;
159
+ position: relative;
 
 
 
 
 
 
 
 
 
 
 
 
160
  }
161
 
162
  .typing-text {
163
  font-size: 11px;
164
+ color: rgba(251, 248, 244, 0.4);
165
+ font-style: normal;
166
+ letter-spacing: 0.3px;
167
+ animation: fade-pulse 2s ease-in-out infinite;
168
+ }
169
+
170
+ @keyframes fade-pulse {
171
+ 0%, 100% { opacity: 0.4; }
172
+ 50% { opacity: 0.7; }
173
  }
174
 
175
  .error-message {
src/lib/components/chat/TextRenderer.svelte CHANGED
@@ -1,22 +1,66 @@
1
  <script lang="ts">
2
- import { onMount } from "svelte";
3
  import gsap from "gsap";
 
4
 
5
  export let content: string;
6
  export let streaming: boolean = false;
7
 
8
  let textEl: HTMLDivElement;
9
  let cursorEl: HTMLSpanElement;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
  onMount(() => {
12
  if (textEl && !streaming) {
13
  gsap.fromTo(textEl,
14
  { opacity: 0, y: 3, scale: 0.99 },
15
- { opacity: 1, y: 0, scale: 1, duration: 0.2, ease: "power2.out" }
16
  );
17
  }
18
  });
19
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  $: if (streaming && cursorEl) {
21
  gsap.to(cursorEl, {
22
  opacity: 0.3,
@@ -32,14 +76,32 @@
32
  gsap.to(cursorEl, {
33
  opacity: 0,
34
  duration: 0.3,
35
- ease: "power2.out"
 
 
 
 
 
 
 
 
 
 
 
 
36
  });
37
  }
38
  </script>
39
 
40
  <div class="text-renderer" bind:this={textEl}>
41
  <span class="content">
42
- {content}{#if streaming}<span class="cursor" bind:this={cursorEl}>▊</span>{/if}
 
 
 
 
 
 
43
  </span>
44
  </div>
45
 
 
1
  <script lang="ts">
2
+ import { onMount, afterUpdate } from "svelte";
3
  import gsap from "gsap";
4
+ import { StreamingToolCallParser, filterToolCalls } from "../../utils/tool-call-parser";
5
 
6
  export let content: string;
7
  export let streaming: boolean = false;
8
 
9
  let textEl: HTMLDivElement;
10
  let cursorEl: HTMLSpanElement;
11
+ let lastContentLength = 0;
12
+ let newContentEl: HTMLSpanElement;
13
+ let parser: StreamingToolCallParser | null = null;
14
+
15
+ // Initialize parser for streaming mode
16
+ $: {
17
+ if (streaming && !parser) {
18
+ parser = new StreamingToolCallParser();
19
+ } else if (!streaming && parser) {
20
+ parser = null;
21
+ }
22
+ }
23
+
24
+ // Filter content based on streaming state
25
+ $: filteredContent = (() => {
26
+ if (streaming && parser) {
27
+ // Use streaming parser to incrementally process content
28
+ parser.reset();
29
+ const result = parser.process(content);
30
+ return result.safeText;
31
+ } else {
32
+ // Use static filter for non-streaming content
33
+ return filterToolCalls(content);
34
+ }
35
+ })();
36
 
37
  onMount(() => {
38
  if (textEl && !streaming) {
39
  gsap.fromTo(textEl,
40
  { opacity: 0, y: 3, scale: 0.99 },
41
+ { opacity: 1, y: 0, scale: 1, duration: 0.3, ease: "power2.out" }
42
  );
43
  }
44
  });
45
 
46
+ // Animate new characters appearing during streaming
47
+ afterUpdate(() => {
48
+ if (streaming && filteredContent.length > lastContentLength) {
49
+ const newChars = filteredContent.slice(lastContentLength);
50
+ if (newContentEl && newChars.length > 0) {
51
+ gsap.fromTo(newContentEl,
52
+ { opacity: 0, scale: 0.95 },
53
+ { opacity: 1, scale: 1, duration: 0.15, ease: "power2.out" }
54
+ );
55
+ }
56
+ lastContentLength = filteredContent.length;
57
+ }
58
+
59
+ if (!streaming) {
60
+ lastContentLength = 0;
61
+ }
62
+ });
63
+
64
  $: if (streaming && cursorEl) {
65
  gsap.to(cursorEl, {
66
  opacity: 0.3,
 
76
  gsap.to(cursorEl, {
77
  opacity: 0,
78
  duration: 0.3,
79
+ ease: "power2.out",
80
+ onComplete: () => {
81
+ // Small celebration pulse on complete
82
+ if (textEl) {
83
+ gsap.to(textEl, {
84
+ borderLeftColor: "rgba(76, 175, 80, 0.3)",
85
+ duration: 0.3,
86
+ yoyo: true,
87
+ repeat: 1,
88
+ ease: "power2.inOut"
89
+ });
90
+ }
91
+ }
92
  });
93
  }
94
  </script>
95
 
96
  <div class="text-renderer" bind:this={textEl}>
97
  <span class="content">
98
+ {#if streaming && lastContentLength > 0}
99
+ {filteredContent.slice(0, lastContentLength)}
100
+ <span bind:this={newContentEl}>{filteredContent.slice(lastContentLength)}</span>
101
+ {:else}
102
+ {filteredContent}
103
+ {/if}
104
+ {#if streaming}<span class="cursor" bind:this={cursorEl}>▊</span>{/if}
105
  </span>
106
  </div>
107
 
src/lib/components/chat/context.md CHANGED
@@ -1,30 +1,31 @@
1
  # Chat Context
2
 
3
- AI chat interface with enhanced feedback and micro-interactions.
4
 
5
  ## Components
6
 
7
  - `ChatPanel.svelte` - Main container with auth handling
8
- - `MessageList.svelte` - Message container with typing indicators
9
- - `Message.svelte` - Message wrapper with entrance animations
10
- - `MessageContent.svelte` - Content normalizer and router
11
- - `TextRenderer.svelte` - Text display with streaming cursors
12
- - `MessageInput.svelte` - Input with enhanced button feedback
13
- - `ExampleMessages.svelte` - Initial prompt suggestions
14
- - `segments/ToolBlock.svelte` - Collapsible tool invocation/result
15
- - `segments/TodoSegment.svelte` - Task list display
 
16
 
17
  ## Architecture
18
 
19
  Clean MVC separation:
20
 
21
- - **Model**: Enhanced chat store with feedback states
22
  - **View**: Component hierarchy with GSAP animations
23
- - **Controller**: WebSocket handling with state management
24
 
25
  ## Design
26
 
27
- - Consistent styling matches console/header color scheme
28
- - GSAP micro-interactions provide constant user feedback
29
- - Typing indicators and status management for engagement
30
- - Single text renderer with streaming state animations
 
1
  # Chat Context
2
 
3
+ AI chat interface with enhanced contrast dark theme and dopamine-driven micro-interactions.
4
 
5
  ## Components
6
 
7
  - `ChatPanel.svelte` - Main container with auth handling
8
+ - `MessageList.svelte` - Message container with animated typing indicators
9
+ - `Message.svelte` - Message wrapper with coordinated entrance animations
10
+ - `MessageContent.svelte` - Content normalizer and router with segment-level filtering
11
+ - `TextRenderer.svelte` - Progressive text reveal with streaming-aware tool call filtering
12
+ - `MessageInput.svelte` - Input with charge-up animations and success feedback
13
+ - `ExampleMessages.svelte` - Model limitations warning when chat is empty
14
+ - `ExampleRow.svelte` - Horizontal example prompts above input
15
+ - `segments/ToolBlock.svelte` - Tool execution with icons, colors, and progress bars
16
+ - `segments/TodoSegment.svelte` - Task tracker with progress bar and milestone celebrations
17
 
18
  ## Architecture
19
 
20
  Clean MVC separation:
21
 
22
+ - **Model**: Chat store with state management
23
  - **View**: Component hierarchy with GSAP animations
24
+ - **Controller**: WebSocket handling with agent communication
25
 
26
  ## Design
27
 
28
+ - Tool-specific icons and color coding for visual hierarchy
29
+ - Progress indicators with anticipation curves
30
+ - Success celebrations through subtle animations
31
+ - Consistent feedback loops for all interactions
src/lib/components/chat/segments/TodoSegment.svelte CHANGED
@@ -1,5 +1,5 @@
1
  <script lang="ts">
2
- import { onMount } from "svelte";
3
  import gsap from "gsap";
4
  import type { MessageSegment } from "../../../models/chat-data";
5
  import type { TodoListView } from "../../../models/segment-view";
@@ -9,20 +9,66 @@
9
 
10
  let todoList: TodoListView | null = null;
11
  let containerEl: HTMLDivElement;
 
 
 
12
 
13
  $: {
14
  const content = segment.toolOutput || segment.content;
15
  todoList = parseTodoList(content);
16
  }
17
 
 
 
 
 
18
  onMount(() => {
19
  if (containerEl) {
20
  gsap.from(containerEl, {
21
  opacity: 0,
22
- y: 3,
 
 
 
 
 
 
 
23
  duration: 0.2,
 
 
 
 
 
 
 
 
 
 
 
24
  ease: "power2.out",
25
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  }
27
  });
28
  </script>
@@ -36,10 +82,25 @@
36
  </span>
37
  </div>
38
 
 
 
 
 
39
  <div class="todo-list">
40
- {#each todoList.tasks as task}
41
- <div class="todo-task {task.status}">
42
- <span class="task-emoji">{task.emoji}</span>
 
 
 
 
 
 
 
 
 
 
 
43
  <span class="task-text">{task.description}</span>
44
  </div>
45
  {/each}
@@ -81,6 +142,21 @@
81
  border-radius: 10px;
82
  }
83
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  .todo-list {
85
  padding: 0.375rem 0.5rem;
86
  }
@@ -89,16 +165,48 @@
89
  display: flex;
90
  align-items: center;
91
  gap: 0.375rem;
92
- padding: 0.25rem 0;
93
  font-size: 0.75rem;
94
  color: rgba(255, 255, 255, 0.7);
 
95
  }
96
 
97
- .task-emoji {
98
- font-size: 0.9rem;
 
 
 
 
99
  flex-shrink: 0;
100
  }
101
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  .task-text {
103
  flex: 1;
104
  }
@@ -110,6 +218,10 @@
110
 
111
  .todo-task.in_progress {
112
  color: rgba(255, 255, 255, 0.9);
 
 
 
 
113
  }
114
 
115
  .todo-task.in_progress .task-text {
 
1
  <script lang="ts">
2
+ import { onMount, afterUpdate } from "svelte";
3
  import gsap from "gsap";
4
  import type { MessageSegment } from "../../../models/chat-data";
5
  import type { TodoListView } from "../../../models/segment-view";
 
9
 
10
  let todoList: TodoListView | null = null;
11
  let containerEl: HTMLDivElement;
12
+ let progressBarEl: HTMLDivElement;
13
+ let lastCompletedCount = 0;
14
+ let taskElements: HTMLDivElement[] = [];
15
 
16
  $: {
17
  const content = segment.toolOutput || segment.content;
18
  todoList = parseTodoList(content);
19
  }
20
 
21
+ $: progressPercentage = todoList
22
+ ? (todoList.completedCount / todoList.totalCount) * 100
23
+ : 0;
24
+
25
  onMount(() => {
26
  if (containerEl) {
27
  gsap.from(containerEl, {
28
  opacity: 0,
29
+ y: 5,
30
+ duration: 0.3,
31
+ ease: "power2.out",
32
+ });
33
+
34
+ gsap.from(".todo-task", {
35
+ opacity: 0,
36
+ x: -10,
37
  duration: 0.2,
38
+ stagger: 0.05,
39
+ ease: "power2.out",
40
+ });
41
+ }
42
+ });
43
+
44
+ afterUpdate(() => {
45
+ if (progressBarEl && todoList) {
46
+ gsap.to(progressBarEl, {
47
+ width: `${progressPercentage}%`,
48
+ duration: 0.6,
49
  ease: "power2.out",
50
  });
51
+
52
+ if (todoList.completedCount > lastCompletedCount) {
53
+ if (progressPercentage === 100) {
54
+ gsap.to(containerEl, {
55
+ borderColor: "rgba(76, 175, 80, 0.5)",
56
+ duration: 0.3,
57
+ yoyo: true,
58
+ repeat: 2,
59
+ ease: "power2.inOut",
60
+ });
61
+ } else if (progressPercentage === 50 || progressPercentage === 75) {
62
+ gsap.to(progressBarEl, {
63
+ backgroundColor: "rgba(76, 175, 80, 0.6)",
64
+ duration: 0.3,
65
+ yoyo: true,
66
+ repeat: 1,
67
+ ease: "power2.inOut",
68
+ });
69
+ }
70
+ lastCompletedCount = todoList.completedCount;
71
+ }
72
  }
73
  });
74
  </script>
 
82
  </span>
83
  </div>
84
 
85
+ <div class="progress-track">
86
+ <div class="progress-bar" bind:this={progressBarEl} style="width: {progressPercentage}%"></div>
87
+ </div>
88
+
89
  <div class="todo-list">
90
+ {#each todoList.tasks as task, i}
91
+ <div
92
+ class="todo-task {task.status}"
93
+ bind:this={taskElements[i]}
94
+ >
95
+ <span class="task-indicator">
96
+ {#if task.status === 'completed'}
97
+ <span class="checkmark">✓</span>
98
+ {:else if task.status === 'in_progress'}
99
+ <span class="progress-dot">●</span>
100
+ {:else}
101
+ <span class="pending-circle">○</span>
102
+ {/if}
103
+ </span>
104
  <span class="task-text">{task.description}</span>
105
  </div>
106
  {/each}
 
142
  border-radius: 10px;
143
  }
144
 
145
+ .progress-track {
146
+ height: 3px;
147
+ background: rgba(255, 255, 255, 0.05);
148
+ position: relative;
149
+ overflow: hidden;
150
+ }
151
+
152
+ .progress-bar {
153
+ height: 100%;
154
+ background: linear-gradient(90deg,
155
+ rgba(76, 175, 80, 0.3),
156
+ rgba(76, 175, 80, 0.5));
157
+ transition: width 0.6s ease-out;
158
+ }
159
+
160
  .todo-list {
161
  padding: 0.375rem 0.5rem;
162
  }
 
165
  display: flex;
166
  align-items: center;
167
  gap: 0.375rem;
168
+ padding: 0.3rem 0;
169
  font-size: 0.75rem;
170
  color: rgba(255, 255, 255, 0.7);
171
+ transition: all 0.2s ease;
172
  }
173
 
174
+ .task-indicator {
175
+ width: 16px;
176
+ height: 16px;
177
+ display: flex;
178
+ align-items: center;
179
+ justify-content: center;
180
  flex-shrink: 0;
181
  }
182
 
183
+ .checkmark {
184
+ color: rgba(76, 175, 80, 0.9);
185
+ font-weight: bold;
186
+ animation: checkPop 0.3s ease-out;
187
+ }
188
+
189
+ @keyframes checkPop {
190
+ 0% { transform: scale(0); }
191
+ 50% { transform: scale(1.2); }
192
+ 100% { transform: scale(1); }
193
+ }
194
+
195
+ .progress-dot {
196
+ color: rgba(33, 150, 243, 0.9);
197
+ animation: progressPulse 1.5s ease-in-out infinite;
198
+ }
199
+
200
+ @keyframes progressPulse {
201
+ 0%, 100% { opacity: 0.4; transform: scale(0.8); }
202
+ 50% { opacity: 1; transform: scale(1.2); }
203
+ }
204
+
205
+ .pending-circle {
206
+ color: rgba(255, 255, 255, 0.3);
207
+ font-size: 10px;
208
+ }
209
+
210
  .task-text {
211
  flex: 1;
212
  }
 
218
 
219
  .todo-task.in_progress {
220
  color: rgba(255, 255, 255, 0.9);
221
+ background: rgba(33, 150, 243, 0.05);
222
+ border-left: 2px solid rgba(33, 150, 243, 0.3);
223
+ padding-left: 0.5rem;
224
+ margin-left: -0.5rem;
225
  }
226
 
227
  .todo-task.in_progress .task-text {
src/lib/components/chat/segments/ToolBlock.svelte CHANGED
@@ -1,24 +1,119 @@
1
  <script lang="ts">
2
  import { slide } from "svelte/transition";
 
 
3
  import type { MessageSegment } from "../../../models/chat-data";
4
 
5
  export let invocation: MessageSegment | null = null;
6
  export let result: MessageSegment | null = null;
7
 
8
  let isExpanded = false;
 
 
 
9
 
10
  $: segment = result || invocation;
11
  $: isRunning = segment?.streaming || segment?.toolStatus === "running";
12
  $: isError = segment?.toolError || segment?.toolStatus === "error";
13
  $: hasOutput = result?.toolOutput || result?.content;
 
14
 
15
- // Auto-expand on error
16
  $: if (isError && !isExpanded) {
17
  isExpanded = true;
18
  }
19
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  function toggleExpanded() {
21
  isExpanded = !isExpanded;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  }
23
 
24
  function getToolName(): string {
@@ -43,12 +138,17 @@
43
  }
44
  </script>
45
 
46
- <div class="tool-block" class:error={isError}>
 
 
 
 
47
  <button
48
  class="tool-header"
49
  on:click={toggleExpanded}
50
  class:expanded={isExpanded}
51
  >
 
52
  <span class="tool-name">{getToolName()}</span>
53
 
54
  {#if isRunning}
@@ -59,7 +159,7 @@
59
  {:else if isError}
60
  <span class="status error">❌ {getStatusText()}</span>
61
  {:else if hasOutput}
62
- <span class="status completed">{getStatusText()}</span>
63
  {/if}
64
 
65
  <span class="expand-icon" class:expanded={isExpanded}>▶</span>
@@ -101,10 +201,42 @@
101
  border-radius: 4px;
102
  background: rgba(255, 255, 255, 0.01);
103
  overflow: hidden;
 
 
104
  }
105
 
106
  .tool-block.error {
107
  border-color: rgba(244, 67, 54, 0.2);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  }
109
 
110
  .tool-header {
@@ -126,6 +258,12 @@
126
  background: rgba(255, 255, 255, 0.02);
127
  }
128
 
 
 
 
 
 
 
129
  .tool-name {
130
  flex: 1;
131
  font-size: 0.8rem;
@@ -152,7 +290,8 @@
152
  }
153
 
154
  .status.completed {
155
- color: rgba(255, 255, 255, 0.5);
 
156
  }
157
 
158
  .pulse-dot {
 
1
  <script lang="ts">
2
  import { slide } from "svelte/transition";
3
+ import { onMount } from "svelte";
4
+ import gsap from "gsap";
5
  import type { MessageSegment } from "../../../models/chat-data";
6
 
7
  export let invocation: MessageSegment | null = null;
8
  export let result: MessageSegment | null = null;
9
 
10
  let isExpanded = false;
11
+ let blockRef: HTMLDivElement;
12
+ let progressRef: HTMLDivElement;
13
+ let iconRef: HTMLSpanElement;
14
 
15
  $: segment = result || invocation;
16
  $: isRunning = segment?.streaming || segment?.toolStatus === "running";
17
  $: isError = segment?.toolError || segment?.toolStatus === "error";
18
  $: hasOutput = result?.toolOutput || result?.content;
19
+ $: isSuccess = !isRunning && !isError && hasOutput;
20
 
 
21
  $: if (isError && !isExpanded) {
22
  isExpanded = true;
23
  }
24
 
25
+ $: if (isSuccess && iconRef) {
26
+ gsap.fromTo(iconRef,
27
+ { scale: 1, rotation: 0 },
28
+ {
29
+ scale: 1.2,
30
+ rotation: 360,
31
+ duration: 0.6,
32
+ ease: "elastic.out(1, 0.5)",
33
+ onComplete: () => {
34
+ gsap.to(iconRef, { scale: 1, duration: 0.2 });
35
+ }
36
+ }
37
+ );
38
+ }
39
+
40
+ onMount(() => {
41
+ if (blockRef) {
42
+ gsap.fromTo(blockRef,
43
+ { opacity: 0, x: -10 },
44
+ { opacity: 1, x: 0, duration: 0.3, ease: "power2.out" }
45
+ );
46
+ }
47
+
48
+ if (isRunning && progressRef) {
49
+ gsap.fromTo(progressRef,
50
+ { scaleX: 0 },
51
+ { scaleX: 1, duration: 3, ease: "power1.inOut" }
52
+ );
53
+ }
54
+ });
55
+
56
  function toggleExpanded() {
57
  isExpanded = !isExpanded;
58
+ if (blockRef) {
59
+ gsap.to(blockRef, {
60
+ backgroundColor: isExpanded ? "rgba(255, 255, 255, 0.02)" : "rgba(255, 255, 255, 0.01)",
61
+ duration: 0.2
62
+ });
63
+ }
64
+ }
65
+
66
+ function getToolIcon(): string {
67
+ const name = invocation?.toolName || result?.toolName || "";
68
+ const lowerName = name.toLowerCase();
69
+
70
+ // Editor tools
71
+ if (lowerName === "read_editor" || lowerName === "read_editor_lines") return "📄";
72
+ if (lowerName === "write_editor") return "💾";
73
+ if (lowerName === "edit_editor") return "✏️";
74
+ if (lowerName === "search_editor") return "🔍";
75
+
76
+ // Task management
77
+ if (lowerName === "plan_tasks") return "📋";
78
+ if (lowerName === "update_task") return "✅";
79
+ if (lowerName === "view_tasks") return "👁️";
80
+
81
+ // Documentation/library tools
82
+ if (lowerName === "resolve_library_id") return "📚";
83
+ if (lowerName === "get_library_docs") return "📖";
84
+
85
+ // Console observation
86
+ if (lowerName === "observe_console") return "🖥️";
87
+
88
+ // Generic fallbacks
89
+ if (lowerName.includes("read")) return "📄";
90
+ if (lowerName.includes("write") || lowerName.includes("edit")) return "✏️";
91
+ if (lowerName.includes("search")) return "🔍";
92
+ if (lowerName.includes("task")) return "✅";
93
+
94
+ return "🔧";
95
+ }
96
+
97
+ function getToolColor(): string {
98
+ const name = invocation?.toolName || result?.toolName || "";
99
+ const lowerName = name.toLowerCase();
100
+
101
+ // Editor tools
102
+ if (lowerName === "read_editor" || lowerName === "read_editor_lines") return "rgba(100, 149, 237, 0.08)"; // Cornflower blue
103
+ if (lowerName === "write_editor") return "rgba(255, 165, 0, 0.08)"; // Orange
104
+ if (lowerName === "edit_editor") return "rgba(255, 215, 0, 0.08)"; // Gold
105
+ if (lowerName === "search_editor") return "rgba(147, 112, 219, 0.08)"; // Medium purple
106
+
107
+ // Task management
108
+ if (lowerName.includes("task")) return "rgba(76, 175, 80, 0.08)"; // Green
109
+
110
+ // Documentation
111
+ if (lowerName.includes("library") || lowerName.includes("docs")) return "rgba(70, 130, 180, 0.08)"; // Steel blue
112
+
113
+ // Console
114
+ if (lowerName === "observe_console") return "rgba(96, 125, 139, 0.08)"; // Blue grey
115
+
116
+ return "rgba(255, 255, 255, 0.03)";
117
  }
118
 
119
  function getToolName(): string {
 
138
  }
139
  </script>
140
 
141
+ <div class="tool-block" class:error={isError} class:success={isSuccess} bind:this={blockRef} style="background: {getToolColor()}">
142
+ {#if isRunning}
143
+ <div class="progress-bar" bind:this={progressRef}></div>
144
+ {/if}
145
+
146
  <button
147
  class="tool-header"
148
  on:click={toggleExpanded}
149
  class:expanded={isExpanded}
150
  >
151
+ <span class="tool-icon" bind:this={iconRef}>{getToolIcon()}</span>
152
  <span class="tool-name">{getToolName()}</span>
153
 
154
  {#if isRunning}
 
159
  {:else if isError}
160
  <span class="status error">❌ {getStatusText()}</span>
161
  {:else if hasOutput}
162
+ <span class="status completed">✓ {getStatusText()}</span>
163
  {/if}
164
 
165
  <span class="expand-icon" class:expanded={isExpanded}>▶</span>
 
201
  border-radius: 4px;
202
  background: rgba(255, 255, 255, 0.01);
203
  overflow: hidden;
204
+ position: relative;
205
+ transition: all 0.3s ease;
206
  }
207
 
208
  .tool-block.error {
209
  border-color: rgba(244, 67, 54, 0.2);
210
+ animation: errorShake 0.3s ease-in-out;
211
+ }
212
+
213
+ .tool-block.success {
214
+ border-color: rgba(76, 175, 80, 0.15);
215
+ }
216
+
217
+ @keyframes errorShake {
218
+ 0%, 100% { transform: translateX(0); }
219
+ 25% { transform: translateX(-2px); }
220
+ 75% { transform: translateX(2px); }
221
+ }
222
+
223
+ .progress-bar {
224
+ position: absolute;
225
+ top: 0;
226
+ left: 0;
227
+ height: 2px;
228
+ width: 100%;
229
+ background: linear-gradient(90deg,
230
+ rgba(33, 150, 243, 0.8),
231
+ rgba(100, 181, 246, 1),
232
+ rgba(33, 150, 243, 0.8));
233
+ transform-origin: left;
234
+ animation: shimmer 2s ease-in-out infinite;
235
+ }
236
+
237
+ @keyframes shimmer {
238
+ 0%, 100% { opacity: 0.6; }
239
+ 50% { opacity: 1; }
240
  }
241
 
242
  .tool-header {
 
258
  background: rgba(255, 255, 255, 0.02);
259
  }
260
 
261
+ .tool-icon {
262
+ font-size: 1rem;
263
+ margin-right: 0.25rem;
264
+ display: inline-block;
265
+ }
266
+
267
  .tool-name {
268
  flex: 1;
269
  font-size: 0.8rem;
 
290
  }
291
 
292
  .status.completed {
293
+ background: rgba(76, 175, 80, 0.1);
294
+ color: rgba(76, 175, 80, 0.9);
295
  }
296
 
297
  .pulse-dot {
src/lib/components/game/GameCanvas.svelte CHANGED
@@ -10,21 +10,29 @@
10
  let previousContent = '';
11
  let isInitialized = false;
12
  let lastRestartTime = 0;
 
13
 
14
  $: if ($contentManager.content !== previousContent && $gameStore.isAutoRunning && isInitialized) {
15
  previousContent = $contentManager.content;
16
  clearTimeout(reloadTimer);
17
 
18
  const now = Date.now();
19
- if (now - lastRestartTime >= 2000) {
 
20
  reloadTimer = setTimeout(async () => {
21
  const currentTime = Date.now();
22
- if (currentTime - lastRestartTime < 2000 || $gameStore.isStarting) {
23
  return;
24
  }
25
  lastRestartTime = currentTime;
 
26
 
27
- await gameEngine.startFromDocument($contentManager.content);
 
 
 
 
 
28
  }, 1500);
29
  }
30
  }
 
10
  let previousContent = '';
11
  let isInitialized = false;
12
  let lastRestartTime = 0;
13
+ let reloadInProgress = false;
14
 
15
  $: if ($contentManager.content !== previousContent && $gameStore.isAutoRunning && isInitialized) {
16
  previousContent = $contentManager.content;
17
  clearTimeout(reloadTimer);
18
 
19
  const now = Date.now();
20
+ // Ensure minimum time between reloads and no overlapping reloads
21
+ if (now - lastRestartTime >= 2000 && !reloadInProgress) {
22
  reloadTimer = setTimeout(async () => {
23
  const currentTime = Date.now();
24
+ if (currentTime - lastRestartTime < 2000 || $gameStore.isStarting || reloadInProgress) {
25
  return;
26
  }
27
  lastRestartTime = currentTime;
28
+ reloadInProgress = true;
29
 
30
+ try {
31
+ await gameEngine.startFromDocument($contentManager.content);
32
+ } finally {
33
+ // Ensure we always reset the flag
34
+ reloadInProgress = false;
35
+ }
36
  }, 1500);
37
  }
38
  }
src/lib/components/game/context.md CHANGED
@@ -6,20 +6,20 @@ Game rendering and error display
6
 
7
  - Render game canvas
8
  - Display game errors
9
- - Manage game lifecycle
10
 
11
  ## Layout
12
 
13
  ```
14
  game/
15
  ├── context.md # This file
16
- ├── GameCanvas.svelte # Game rendering canvas
17
  └── GameError.svelte # Error message display
18
  ```
19
 
20
  ## Scope
21
 
22
- - In-scope: Game display, error UI
23
  - Out-of-scope: Game logic, physics
24
 
25
  ## Entrypoints
 
6
 
7
  - Render game canvas
8
  - Display game errors
9
+ - Manage game lifecycle with reload protection
10
 
11
  ## Layout
12
 
13
  ```
14
  game/
15
  ├── context.md # This file
16
+ ├── GameCanvas.svelte # Game rendering with automatic reload
17
  └── GameError.svelte # Error message display
18
  ```
19
 
20
  ## Scope
21
 
22
+ - In-scope: Game display, error UI, reload management
23
  - Out-of-scope: Game logic, physics
24
 
25
  ## Entrypoints
src/lib/components/layout/AppHeader.svelte CHANGED
@@ -12,10 +12,24 @@
12
  await gameEngine.startFromDocument($contentManager.content);
13
  }
14
  }
15
-
16
  function handleViewModeChange(mode: 'code' | 'preview') {
17
  uiStore.setViewMode(mode);
18
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
  onMount(() => {
21
  gsap.fromTo(".app-header",
@@ -62,10 +76,24 @@
62
 
63
  <div class="app-header">
64
  <div class="header-left">
65
- <div class="app-title">
 
 
 
 
66
  <span class="app-icon">🥕</span>
67
  <span class="app-name">VibeGame</span>
68
- </div>
 
 
 
 
 
 
 
 
 
 
69
  <a href="https://github.com/dylanebert/VibeGame"
70
  target="_blank"
71
  rel="noopener noreferrer"
@@ -78,22 +106,24 @@
78
  </div>
79
 
80
  <div class="header-center">
81
- <div class="view-toggle">
82
- <button
83
- class="toggle-btn"
84
- class:active={$uiStore.viewMode === 'code'}
85
- on:click={() => handleViewModeChange('code')}
86
- >
87
- Code
88
- </button>
89
- <button
90
- class="toggle-btn"
91
- class:active={$uiStore.viewMode === 'preview'}
92
- on:click={() => handleViewModeChange('preview')}
93
- >
94
- Preview
95
- </button>
96
- </div>
 
 
97
  </div>
98
 
99
  <div class="header-right">
@@ -146,14 +176,28 @@
146
 
147
  .header-left {
148
  justify-content: flex-start;
149
- gap: 0.75rem;
 
150
  }
151
 
152
- .app-title {
153
  display: flex;
154
  align-items: center;
155
  gap: 0.5rem;
156
  user-select: none;
 
 
 
 
 
 
 
 
 
 
 
 
 
157
  }
158
 
159
  .app-icon {
@@ -168,6 +212,59 @@
168
  letter-spacing: 0.025em;
169
  }
170
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
  .github-link {
172
  display: flex;
173
  align-items: center;
 
12
  await gameEngine.startFromDocument($contentManager.content);
13
  }
14
  }
15
+
16
  function handleViewModeChange(mode: 'code' | 'preview') {
17
  uiStore.setViewMode(mode);
18
  }
19
+
20
+ function handleAboutClick() {
21
+ if ($uiStore.viewMode === 'about') {
22
+ uiStore.hideAbout();
23
+ } else {
24
+ uiStore.showAbout();
25
+ }
26
+ }
27
+
28
+ function handleTitleClick() {
29
+ if ($uiStore.viewMode === 'about') {
30
+ uiStore.hideAbout();
31
+ }
32
+ }
33
 
34
  onMount(() => {
35
  gsap.fromTo(".app-header",
 
76
 
77
  <div class="app-header">
78
  <div class="header-left">
79
+ <button
80
+ class="app-title-button"
81
+ class:clickable={$uiStore.viewMode === 'about'}
82
+ on:click={handleTitleClick}
83
+ >
84
  <span class="app-icon">🥕</span>
85
  <span class="app-name">VibeGame</span>
86
+ </button>
87
+ <span class="header-separator">|</span>
88
+ <button
89
+ class="nav-button"
90
+ class:active={$uiStore.viewMode === 'about'}
91
+ on:click={handleAboutClick}
92
+ aria-label="About"
93
+ >
94
+ About
95
+ </button>
96
+ <span class="header-separator">|</span>
97
  <a href="https://github.com/dylanebert/VibeGame"
98
  target="_blank"
99
  rel="noopener noreferrer"
 
106
  </div>
107
 
108
  <div class="header-center">
109
+ {#if $uiStore.viewMode !== 'about'}
110
+ <div class="view-toggle">
111
+ <button
112
+ class="toggle-btn"
113
+ class:active={$uiStore.viewMode === 'code'}
114
+ on:click={() => handleViewModeChange('code')}
115
+ >
116
+ Code
117
+ </button>
118
+ <button
119
+ class="toggle-btn"
120
+ class:active={$uiStore.viewMode === 'preview'}
121
+ on:click={() => handleViewModeChange('preview')}
122
+ >
123
+ Preview
124
+ </button>
125
+ </div>
126
+ {/if}
127
  </div>
128
 
129
  <div class="header-right">
 
176
 
177
  .header-left {
178
  justify-content: flex-start;
179
+ gap: 0.5rem;
180
+ align-items: center;
181
  }
182
 
183
+ .app-title-button {
184
  display: flex;
185
  align-items: center;
186
  gap: 0.5rem;
187
  user-select: none;
188
+ background: transparent;
189
+ border: none;
190
+ padding: 0;
191
+ cursor: default;
192
+ transition: all 0.2s;
193
+ }
194
+
195
+ .app-title-button.clickable {
196
+ cursor: pointer;
197
+ }
198
+
199
+ .app-title-button.clickable:hover .app-name {
200
+ color: rgba(251, 248, 244, 0.75);
201
  }
202
 
203
  .app-icon {
 
212
  letter-spacing: 0.025em;
213
  }
214
 
215
+ .header-separator {
216
+ color: rgba(251, 248, 244, 0.2);
217
+ font-size: 0.875rem;
218
+ user-select: none;
219
+ padding: 0 0.25rem;
220
+ }
221
+
222
+ .nav-button {
223
+ background: transparent;
224
+ border: none;
225
+ color: rgba(251, 248, 244, 0.5);
226
+ font-size: 0.875rem;
227
+ font-weight: 500;
228
+ padding: 0.375rem 0.625rem;
229
+ border-radius: 0.25rem;
230
+ cursor: pointer;
231
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
232
+ position: relative;
233
+ }
234
+
235
+ .nav-button::before {
236
+ content: '';
237
+ position: absolute;
238
+ inset: 0;
239
+ border-radius: 0.25rem;
240
+ background: rgba(139, 115, 85, 0.08);
241
+ opacity: 0;
242
+ transition: opacity 0.2s ease-out;
243
+ }
244
+
245
+ .nav-button:hover {
246
+ color: rgba(251, 248, 244, 0.9);
247
+ }
248
+
249
+ .nav-button:hover::before {
250
+ opacity: 1;
251
+ }
252
+
253
+ .nav-button:active {
254
+ transform: scale(0.98);
255
+ }
256
+
257
+ .nav-button.active {
258
+ background: rgba(124, 152, 133, 0.12);
259
+ color: rgba(251, 248, 244, 0.95);
260
+ border-radius: 0.25rem;
261
+ }
262
+
263
+ .nav-button.active::before {
264
+ opacity: 1;
265
+ background: rgba(124, 152, 133, 0.1);
266
+ }
267
+
268
  .github-link {
269
  display: flex;
270
  align-items: center;
src/lib/components/layout/SplitView.svelte CHANGED
@@ -37,7 +37,7 @@
37
  };
38
  });
39
 
40
- $: if (viewMode) {
41
  handleViewModeAnimation(viewMode);
42
  }
43
 
 
37
  };
38
  });
39
 
40
+ $: if (viewMode && viewMode !== 'about') {
41
  handleViewModeAnimation(viewMode);
42
  }
43
 
src/lib/components/layout/context.md CHANGED
@@ -5,9 +5,9 @@ Application layout structure
5
  ## Purpose
6
 
7
  - Loading state management
8
- - Header navigation
9
  - Two-pane layout with nested splits
10
- - View mode transitions
11
 
12
  ## Structure
13
 
@@ -15,7 +15,7 @@ Application layout structure
15
  layout/
16
  ├── context.md # This file
17
  ├── LoadingScreen.svelte # Initial loading with spinner
18
- ├── AppHeader.svelte # Top navigation bar
19
  └── SplitView.svelte # Two-pane layout with nested splits
20
  ```
21
 
@@ -24,10 +24,11 @@ layout/
24
  - Main split: Editor pane (left) | Preview pane (right)
25
  - Editor pane: Code editor (top) | Chat panel (bottom)
26
  - Preview pane: Game canvas (top) | Console (bottom)
 
27
 
28
  ## Dependencies
29
 
30
  - loadingStore for loading state
31
- - uiStore for view mode
32
  - svelte-splitpanes for resizable panes
33
  - GSAP for animations
 
5
  ## Purpose
6
 
7
  - Loading state management
8
+ - Header navigation with view switching
9
  - Two-pane layout with nested splits
10
+ - View mode transitions (code/preview/about)
11
 
12
  ## Structure
13
 
 
15
  layout/
16
  ├── context.md # This file
17
  ├── LoadingScreen.svelte # Initial loading with spinner
18
+ ├── AppHeader.svelte # Top navigation bar with Title | About | Repo
19
  └── SplitView.svelte # Two-pane layout with nested splits
20
  ```
21
 
 
24
  - Main split: Editor pane (left) | Preview pane (right)
25
  - Editor pane: Code editor (top) | Chat panel (bottom)
26
  - Preview pane: Game canvas (top) | Console (bottom)
27
+ - View modes: code, preview, or about page overlay
28
 
29
  ## Dependencies
30
 
31
  - loadingStore for loading state
32
+ - uiStore for view mode management
33
  - svelte-splitpanes for resizable panes
34
  - GSAP for animations
src/lib/config/animations.ts DELETED
@@ -1,43 +0,0 @@
1
- export const animationConfig = {
2
- header: {
3
- initial: { opacity: 0, y: -20 },
4
- animate: { opacity: 1, y: 0, duration: 0.6, ease: "power3.out" },
5
- },
6
-
7
- editor: {
8
- initial: { opacity: 0, x: -20 },
9
- animate: {
10
- opacity: 1,
11
- x: 0,
12
- duration: 0.6,
13
- delay: 0.1,
14
- ease: "power3.out",
15
- },
16
- },
17
-
18
- preview: {
19
- initial: { opacity: 0, x: 20 },
20
- animate: {
21
- opacity: 1,
22
- x: 0,
23
- duration: 0.6,
24
- delay: 0.2,
25
- ease: "power3.out",
26
- },
27
- },
28
-
29
- consoleMessage: {
30
- initial: { opacity: 0, y: -5 },
31
- animate: { opacity: 1, y: 0, duration: 0.3, ease: "power2.out" },
32
- },
33
-
34
- error: {
35
- initial: { opacity: 0, transform: "translateY(-10px)" },
36
- animate: { opacity: 1, transform: "translateY(0)", duration: 0.3 },
37
- },
38
-
39
- viewTransition: {
40
- duration: 0.2,
41
- ease: "power2.inOut",
42
- },
43
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/config/context.md CHANGED
@@ -5,16 +5,14 @@ Application configuration and constants
5
  ## Purpose
6
 
7
  - Centralize configuration values
8
- - Define animation settings
9
  - Keyboard shortcuts registry
10
 
11
  ## Layout
12
 
13
  ```
14
  config/
15
- ├── context.md # This file
16
- ├── animations.ts # GSAP animation configs
17
- └── shortcuts.ts # Keyboard shortcut definitions
18
  ```
19
 
20
  ## Scope
@@ -24,7 +22,6 @@ config/
24
 
25
  ## Entrypoints
26
 
27
- - `animationConfig` - Animation presets
28
  - `shortcuts` - Keyboard shortcut definitions
29
  - `registerShortcuts()` - Setup keyboard handlers
30
 
 
5
  ## Purpose
6
 
7
  - Centralize configuration values
 
8
  - Keyboard shortcuts registry
9
 
10
  ## Layout
11
 
12
  ```
13
  config/
14
+ ├── context.md # This file
15
+ └── shortcuts.ts # Keyboard shortcut definitions
 
16
  ```
17
 
18
  ## Scope
 
22
 
23
  ## Entrypoints
24
 
 
25
  - `shortcuts` - Keyboard shortcut definitions
26
  - `registerShortcuts()` - Setup keyboard handlers
27
 
src/lib/controllers/animation-controller.ts DELETED
@@ -1,190 +0,0 @@
1
- import gsap from "gsap";
2
-
3
- type AnimationTarget = HTMLElement | null;
4
- type AnimationId = string;
5
-
6
- interface AnimationConfig {
7
- target: AnimationTarget;
8
- animation: gsap.core.Tween | gsap.core.Timeline;
9
- }
10
-
11
- export class AnimationController {
12
- private animations = new Map<AnimationId, AnimationConfig>();
13
-
14
- register(id: AnimationId, target: AnimationTarget): void {
15
- if (!target) return;
16
- this.cleanup(id);
17
- this.animations.set(id, { target, animation: null! });
18
- }
19
-
20
- cleanup(id: AnimationId): void {
21
- const config = this.animations.get(id);
22
- if (config?.animation) {
23
- config.animation.kill();
24
- }
25
- if (config?.target) {
26
- gsap.killTweensOf(config.target);
27
- }
28
- this.animations.delete(id);
29
- }
30
-
31
- cleanupAll(): void {
32
- this.animations.forEach((_, id) => this.cleanup(id));
33
- }
34
-
35
- buttonPress(element: AnimationTarget): void {
36
- if (!element) return;
37
- gsap.to(element, {
38
- scale: 0.9,
39
- duration: 0.1,
40
- ease: "power2.in",
41
- onComplete: () => {
42
- gsap.to(element, {
43
- scale: 1,
44
- duration: 0.2,
45
- ease: "elastic.out(1, 0.5)",
46
- });
47
- },
48
- });
49
- }
50
-
51
- buttonHover(element: AnimationTarget, entering: boolean): void {
52
- if (!element) return;
53
- gsap.to(element, {
54
- scale: entering ? 1.05 : 1,
55
- duration: 0.2,
56
- ease: "power2.out",
57
- });
58
- }
59
-
60
- fadeIn(element: AnimationTarget, duration = 0.3): void {
61
- if (!element) return;
62
- gsap.fromTo(
63
- element,
64
- { opacity: 0, y: 5 },
65
- { opacity: 1, y: 0, duration, ease: "power2.out" },
66
- );
67
- }
68
-
69
- fadeOut(element: AnimationTarget, duration = 0.2): Promise<void> {
70
- return new Promise((resolve) => {
71
- if (!element) {
72
- resolve();
73
- return;
74
- }
75
- gsap.to(element, {
76
- opacity: 0,
77
- duration,
78
- ease: "power2.in",
79
- onComplete: resolve,
80
- });
81
- });
82
- }
83
-
84
- pulseGlow(id: AnimationId, element: AnimationTarget, color: string): void {
85
- if (!element) return;
86
- const animation = gsap.to(element, {
87
- boxShadow: `0 0 25px ${color}`,
88
- duration: 2.5,
89
- repeat: -1,
90
- yoyo: true,
91
- ease: "sine.inOut",
92
- });
93
- this.animations.set(id, { target: element, animation });
94
- }
95
-
96
- borderPulse(id: AnimationId, element: AnimationTarget, color: string): void {
97
- if (!element) return;
98
- const animation = gsap.to(element, {
99
- borderColor: color,
100
- duration: 2,
101
- repeat: -1,
102
- yoyo: true,
103
- ease: "sine.inOut",
104
- });
105
- this.animations.set(id, { target: element, animation });
106
- }
107
-
108
- smoothScroll(
109
- element: AnimationTarget,
110
- targetScroll: number,
111
- duration?: number,
112
- ): Promise<void> {
113
- return new Promise((resolve) => {
114
- if (!element) {
115
- resolve();
116
- return;
117
- }
118
-
119
- const currentScroll = element.scrollTop;
120
- const distance = Math.abs(targetScroll - currentScroll);
121
- const calculatedDuration = duration ?? Math.min(0.5, distance / 1000);
122
-
123
- gsap.to(element, {
124
- scrollTop: targetScroll,
125
- duration: calculatedDuration,
126
- ease: "power2.out",
127
- onComplete: resolve,
128
- });
129
- });
130
- }
131
-
132
- cursorBlink(id: AnimationId, element: AnimationTarget): void {
133
- if (!element) return;
134
- gsap.set(element, { display: "inline-block", opacity: 1 });
135
- const animation = gsap.to(element, {
136
- opacity: 0,
137
- duration: 0.5,
138
- repeat: -1,
139
- yoyo: true,
140
- ease: "steps(1)",
141
- });
142
- this.animations.set(id, { target: element, animation });
143
- }
144
-
145
- hideCursor(element: AnimationTarget): Promise<void> {
146
- return new Promise((resolve) => {
147
- if (!element) {
148
- resolve();
149
- return;
150
- }
151
- gsap.killTweensOf(element);
152
- gsap.to(element, {
153
- opacity: 0,
154
- duration: 0.2,
155
- onComplete: () => {
156
- gsap.set(element, { display: "none" });
157
- resolve();
158
- },
159
- });
160
- });
161
- }
162
-
163
- slideDown(element: AnimationTarget, duration = 0.3): void {
164
- if (!element) return;
165
- gsap.from(element, {
166
- height: 0,
167
- opacity: 0,
168
- duration,
169
- ease: "power2.out",
170
- });
171
- }
172
-
173
- slideUp(element: AnimationTarget, duration = 0.3): Promise<void> {
174
- return new Promise((resolve) => {
175
- if (!element) {
176
- resolve();
177
- return;
178
- }
179
- gsap.to(element, {
180
- height: 0,
181
- opacity: 0,
182
- duration,
183
- ease: "power2.in",
184
- onComplete: resolve,
185
- });
186
- });
187
- }
188
- }
189
-
190
- export const animationController = new AnimationController();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/controllers/context.md CHANGED
@@ -12,23 +12,20 @@ Business logic and orchestration layer
12
 
13
  ```
14
  controllers/
15
- ├── context.md # This file
16
- ├── chat-controller.ts # Chat operations and state management
17
- └── animation-controller.ts # Centralized GSAP animations
18
  ```
19
 
20
  ## Scope
21
 
22
- - In-scope: Business logic, state orchestration, animations
23
  - Out-of-scope: UI rendering, direct store access from components
24
 
25
  ## Entrypoints
26
 
27
  - `chatController` - All chat operations
28
- - `animationController` - UI animations
29
 
30
  ## Dependencies
31
 
32
  - Stores for state updates
33
  - Services for external operations
34
- - GSAP for animations
 
12
 
13
  ```
14
  controllers/
15
+ ├── context.md # This file
16
+ └── chat-controller.ts # Chat operations and state management
 
17
  ```
18
 
19
  ## Scope
20
 
21
+ - In-scope: Business logic, state orchestration
22
  - Out-of-scope: UI rendering, direct store access from components
23
 
24
  ## Entrypoints
25
 
26
  - `chatController` - All chat operations
 
27
 
28
  ## Dependencies
29
 
30
  - Stores for state updates
31
  - Services for external operations
 
src/lib/models/context.md CHANGED
@@ -10,8 +10,9 @@ Define data shapes, types, and factory functions for type safety
10
 
11
  ```
12
  models/
13
- ├── context.md # This file
14
- └── chat-data.ts # Chat types and factories
 
15
  ```
16
 
17
  ## Scope
 
10
 
11
  ```
12
  models/
13
+ ├── context.md # This file
14
+ ├── chat-data.ts # Chat types and factories
15
+ └── segment-view.ts # Segment visualization and todo parsing
16
  ```
17
 
18
  ## Scope
src/lib/models/segment-view.ts CHANGED
@@ -77,9 +77,6 @@ function getSegmentTitle(segment: MessageSegment): string {
77
  update_task: "✏️ Update Task",
78
  view_tasks: "👀 View Tasks",
79
  observe_console: "📺 Console Output",
80
- read_file: "📖 Read File",
81
- write_file: "✍️ Write File",
82
- edit_file: "✏️ Edit File",
83
  };
84
  return toolNames[segment.toolName] || `🔧 ${segment.toolName}`;
85
  }
 
77
  update_task: "✏️ Update Task",
78
  view_tasks: "👀 View Tasks",
79
  observe_console: "📺 Console Output",
 
 
 
80
  };
81
  return toolNames[segment.toolName] || `🔧 ${segment.toolName}`;
82
  }
src/lib/server/console-buffer.ts CHANGED
@@ -3,13 +3,16 @@ export interface ConsoleBufferMessage {
3
  type: "log" | "warn" | "error" | "info";
4
  message: string;
5
  timestamp: number;
 
6
  }
7
 
8
  export class ConsoleBuffer {
9
  private static instance: ConsoleBuffer | null = null;
10
  private messages: ConsoleBufferMessage[] = [];
11
- private maxMessages = 100;
12
  private lastReadTimestamp = 0;
 
 
13
 
14
  private constructor() {}
15
 
@@ -21,19 +24,39 @@ export class ConsoleBuffer {
21
  }
22
 
23
  addMessage(message: ConsoleBufferMessage): void {
 
 
 
 
24
  this.messages.push(message);
 
25
 
26
  if (this.messages.length > this.maxMessages) {
27
  this.messages = this.messages.slice(-this.maxMessages);
28
  }
 
 
 
 
29
  }
30
 
31
- getRecentMessages(since?: number): ConsoleBufferMessage[] {
32
  const sinceTimestamp = since || this.lastReadTimestamp;
33
- return this.messages.filter((msg) => msg.timestamp > sinceTimestamp);
 
 
 
 
 
 
 
 
34
  }
35
 
36
- getAllMessages(): ConsoleBufferMessage[] {
 
 
 
37
  return [...this.messages];
38
  }
39
 
@@ -45,8 +68,40 @@ export class ConsoleBuffer {
45
  }
46
 
47
  clear(): void {
48
- this.messages = [];
49
  this.lastReadTimestamp = Date.now();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  }
51
 
52
  getGameStateFromMessages(): {
@@ -54,8 +109,21 @@ export class ConsoleBuffer {
54
  hasError: boolean;
55
  lastError?: string;
56
  isReady: boolean;
 
 
57
  } {
58
- const recentMessages = this.getRecentMessages(Date.now() - 5000);
 
 
 
 
 
 
 
 
 
 
 
59
 
60
  const hasStartMessage = recentMessages.some(
61
  (msg) =>
@@ -70,6 +138,7 @@ export class ConsoleBuffer {
70
  msg.message.includes("✅ Game started") ||
71
  msg.message.includes("Game started!") ||
72
  msg.message.includes("Game script loaded!") ||
 
73
  msg.message.includes("successfully"),
74
  );
75
 
@@ -87,12 +156,18 @@ export class ConsoleBuffer {
87
  ? errorMessages[errorMessages.length - 1].message
88
  : undefined;
89
 
 
 
90
  return {
91
  isLoading:
92
- hasStartMessage && !hasSuccessMessage && errorMessages.length === 0,
 
 
93
  hasError: errorMessages.length > 0,
94
  lastError,
95
- isReady: hasSuccessMessage && errorMessages.length === 0,
 
 
96
  };
97
  }
98
  }
 
3
  type: "log" | "warn" | "error" | "info";
4
  message: string;
5
  timestamp: number;
6
+ context?: string;
7
  }
8
 
9
  export class ConsoleBuffer {
10
  private static instance: ConsoleBuffer | null = null;
11
  private messages: ConsoleBufferMessage[] = [];
12
+ private maxMessages = 200;
13
  private lastReadTimestamp = 0;
14
+ private executionContext: string | null = null;
15
+ private messagesSinceLastTool: ConsoleBufferMessage[] = [];
16
 
17
  private constructor() {}
18
 
 
24
  }
25
 
26
  addMessage(message: ConsoleBufferMessage): void {
27
+ if (this.executionContext) {
28
+ message.context = this.executionContext;
29
+ }
30
+
31
  this.messages.push(message);
32
+ this.messagesSinceLastTool.push(message);
33
 
34
  if (this.messages.length > this.maxMessages) {
35
  this.messages = this.messages.slice(-this.maxMessages);
36
  }
37
+
38
+ if (this.messagesSinceLastTool.length > 50) {
39
+ this.messagesSinceLastTool = this.messagesSinceLastTool.slice(-50);
40
+ }
41
  }
42
 
43
+ getRecentMessages(since?: number, limit?: number): ConsoleBufferMessage[] {
44
  const sinceTimestamp = since || this.lastReadTimestamp;
45
+ const filtered = this.messages.filter(
46
+ (msg) => msg.timestamp > sinceTimestamp,
47
+ );
48
+
49
+ if (limit && limit > 0 && filtered.length > limit) {
50
+ return filtered.slice(-limit);
51
+ }
52
+
53
+ return filtered;
54
  }
55
 
56
+ getAllMessages(limit?: number): ConsoleBufferMessage[] {
57
+ if (limit && limit > 0 && this.messages.length > limit) {
58
+ return this.messages.slice(-limit);
59
+ }
60
  return [...this.messages];
61
  }
62
 
 
68
  }
69
 
70
  clear(): void {
 
71
  this.lastReadTimestamp = Date.now();
72
+ this.messagesSinceLastTool = [];
73
+ }
74
+
75
+ onGameReloadStart(): void {
76
+ this.messagesSinceLastTool = [];
77
+ this.lastReadTimestamp = Date.now();
78
+ this.addMessage({
79
+ id: `reload-${Date.now()}`,
80
+ type: "info",
81
+ message: "🔄 Game reloading...",
82
+ timestamp: Date.now(),
83
+ });
84
+ }
85
+
86
+ onGameReloadComplete(): void {
87
+ this.addMessage({
88
+ id: `reload-complete-${Date.now()}`,
89
+ type: "info",
90
+ message: "✅ Game reload complete",
91
+ timestamp: Date.now(),
92
+ });
93
+ }
94
+
95
+ setExecutionContext(context: string | null): void {
96
+ this.executionContext = context;
97
+ }
98
+
99
+ getMessagesSinceLastTool(): ConsoleBufferMessage[] {
100
+ return [...this.messagesSinceLastTool];
101
+ }
102
+
103
+ clearToolMessages(): void {
104
+ this.messagesSinceLastTool = [];
105
  }
106
 
107
  getGameStateFromMessages(): {
 
109
  hasError: boolean;
110
  lastError?: string;
111
  isReady: boolean;
112
+ isReloading: boolean;
113
+ messageCount: number;
114
  } {
115
+ const recentMessages =
116
+ this.messagesSinceLastTool.length > 0
117
+ ? this.messagesSinceLastTool
118
+ : this.getRecentMessages(Date.now() - 5000);
119
+
120
+ const hasReloadStartMessage = recentMessages.some((msg) =>
121
+ msg.message.includes("🔄 Game reloading"),
122
+ );
123
+
124
+ const hasReloadCompleteMessage = recentMessages.some((msg) =>
125
+ msg.message.includes("✅ Game reload complete"),
126
+ );
127
 
128
  const hasStartMessage = recentMessages.some(
129
  (msg) =>
 
138
  msg.message.includes("✅ Game started") ||
139
  msg.message.includes("Game started!") ||
140
  msg.message.includes("Game script loaded!") ||
141
+ msg.message.includes("✅ Game reload complete") ||
142
  msg.message.includes("successfully"),
143
  );
144
 
 
156
  ? errorMessages[errorMessages.length - 1].message
157
  : undefined;
158
 
159
+ const isReloading = hasReloadStartMessage && !hasReloadCompleteMessage;
160
+
161
  return {
162
  isLoading:
163
+ (hasStartMessage || isReloading) &&
164
+ !hasSuccessMessage &&
165
+ errorMessages.length === 0,
166
  hasError: errorMessages.length > 0,
167
  lastError,
168
+ isReady: hasSuccessMessage && errorMessages.length === 0 && !isReloading,
169
+ isReloading,
170
+ messageCount: recentMessages.length,
171
  };
172
  }
173
  }
src/lib/server/context.md CHANGED
@@ -4,19 +4,21 @@ WebSocket server with LangGraph agent for AI-assisted game development.
4
 
5
  ## Key Components
6
 
7
- - **api.ts** - WebSocket message routing with virtual file sync
8
- - **langgraph-agent.ts** - LangGraph agent with MCP tools
9
- - **mcp-client.ts** - MCP tools for virtual file operations and Context7 docs
10
- - **tools.ts** - Console observation and task tracking
11
- - **console-buffer.ts** - Console message buffering with game state analysis
 
12
 
13
  ## Architecture
14
 
15
- LangGraph agent with bidirectional content synchronization:
16
 
17
- - Virtual file system treats editor as single HTML file
18
- - MCP tools operate directly on virtual file system
19
- - Real-time bidirectional sync with ContentManager
 
20
  - Context7 integration for external library documentation
21
  - Buffered streaming with segment handling
22
  - AbortController for canceling conversations
 
4
 
5
  ## Key Components
6
 
7
+ - **api.ts** - WebSocket message routing and connection management
8
+ - **langgraph-agent.ts** - LangGraph agent with tool deduplication and conflict detection
9
+ - **mcp-client.ts** - Editor tools with pre-validation and standardized responses
10
+ - **tools.ts** - Console observation with tail limiting
11
+ - **task-tracker.ts** - Task planning and management tools
12
+ - **console-buffer.ts** - Console buffering with lifecycle events and tail limiting
13
 
14
  ## Architecture
15
 
16
+ LangGraph agent with robust tool execution:
17
 
18
+ - Virtual file system with version tracking and edit history
19
+ - Tool call deduplication to prevent redundant operations
20
+ - Sequential tool execution with state tracking
21
+ - Pre-validation for edit operations to detect conflicts
22
  - Context7 integration for external library documentation
23
  - Buffered streaming with segment handling
24
  - AbortController for canceling conversations
src/lib/server/documentation.ts CHANGED
@@ -5,7 +5,7 @@ export class DocumentationService {
5
  private cache: string | null = null;
6
  private readonly docsPath: string;
7
 
8
- constructor(filename: string = "agents.md") {
9
  this.docsPath = join(process.cwd(), filename);
10
  }
11
 
 
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
 
src/lib/server/langgraph-agent.ts CHANGED
@@ -10,6 +10,11 @@ import { observeConsoleTool } from "./tools";
10
  import { mcpClientManager, setMCPWebSocketConnection } from "./mcp-client";
11
  import { planTasksTool, updateTaskTool, viewTasksTool } from "./task-tracker";
12
  import { documentationService } from "./documentation";
 
 
 
 
 
13
  import type { WebSocket } from "ws";
14
 
15
  const AgentState = Annotation.Root({
@@ -58,146 +63,86 @@ export class LangGraphAgent {
58
  const messages = this.formatMessages(state.messages, systemPrompt);
59
 
60
  let fullResponse = "";
 
61
  let currentSegmentId: string | null = null;
62
- let currentSegmentContent = "";
63
- let buffer = "";
64
  const messageId = config?.metadata?.messageId;
65
  const abortSignal = config?.metadata?.abortSignal as
66
  | AbortSignal
67
  | undefined;
68
 
69
- const toolRegex = /TOOL:\s*(\w+)\s+ARGS:\s*({[^}]*})/g;
70
-
71
  for await (const token of this.streamModelResponse(
72
  messages,
73
  abortSignal,
74
  )) {
75
  fullResponse += token;
76
  config?.writer?.({ type: "token", content: token });
77
- buffer += token;
78
-
79
- let processedUpTo = 0;
80
- let match;
81
- toolRegex.lastIndex = 0;
82
-
83
- while ((match = toolRegex.exec(buffer)) !== null) {
84
- const textBefore = buffer.substring(processedUpTo, match.index);
85
-
86
- if (textBefore.trim() && this.ws) {
87
- if (!currentSegmentId) {
88
- currentSegmentId = `seg_${Date.now()}_${Math.random()}`;
89
- currentSegmentContent = "";
90
- this.ws.send(
91
- JSON.stringify({
92
- type: "segment_start",
93
- payload: {
94
- segmentId: currentSegmentId,
95
- segmentType: "text",
96
- messageId,
97
- },
98
- timestamp: Date.now(),
99
- }),
100
- );
101
- }
102
 
103
- currentSegmentContent += textBefore;
104
- this.ws.send(
105
- JSON.stringify({
106
- type: "segment_token",
107
- payload: {
108
- segmentId: currentSegmentId,
109
- token: textBefore,
110
- messageId,
111
- },
112
- timestamp: Date.now(),
113
- }),
114
- );
115
- }
116
 
117
- if (currentSegmentId && currentSegmentContent.trim() && this.ws) {
 
 
 
 
118
  this.ws.send(
119
  JSON.stringify({
120
- type: "segment_end",
121
  payload: {
122
  segmentId: currentSegmentId,
123
- content: currentSegmentContent,
124
  messageId,
125
  },
126
  timestamp: Date.now(),
127
  }),
128
  );
129
- currentSegmentId = null;
130
- currentSegmentContent = "";
131
  }
132
 
133
- processedUpTo = match.index + match[0].length;
134
- }
135
-
136
- if (processedUpTo > 0) {
137
- buffer = buffer.substring(processedUpTo);
138
- } else if (buffer.length > 100 && !buffer.includes("TOOL:")) {
139
- if (!currentSegmentId && buffer.trim() && this.ws) {
140
- currentSegmentId = `seg_${Date.now()}_${Math.random()}`;
141
- currentSegmentContent = "";
142
  this.ws.send(
143
  JSON.stringify({
144
- type: "segment_start",
145
  payload: {
146
  segmentId: currentSegmentId,
147
- segmentType: "text",
148
  messageId,
149
  },
150
  timestamp: Date.now(),
151
  }),
152
  );
153
  }
154
-
155
- if (currentSegmentId) {
156
- currentSegmentContent += buffer;
157
- if (this.ws) {
158
- this.ws.send(
159
- JSON.stringify({
160
- type: "segment_token",
161
- payload: {
162
- segmentId: currentSegmentId,
163
- token: buffer,
164
- messageId,
165
- },
166
- timestamp: Date.now(),
167
- }),
168
- );
169
- }
170
- }
171
- buffer = "";
172
  }
173
- }
174
 
175
- if (buffer.trim() && !buffer.match(/TOOL:\s*(\w+)\s+ARGS:\s*({[^}]*})/)) {
176
- if (!currentSegmentId && this.ws) {
177
- currentSegmentId = `seg_${Date.now()}_${Math.random()}`;
178
- currentSegmentContent = "";
179
  this.ws.send(
180
  JSON.stringify({
181
- type: "segment_start",
182
  payload: {
183
  segmentId: currentSegmentId,
184
- segmentType: "text",
185
  messageId,
186
  },
187
  timestamp: Date.now(),
188
  }),
189
  );
 
190
  }
 
191
 
192
- if (currentSegmentId) {
193
- currentSegmentContent += buffer;
194
- if (this.ws) {
 
 
 
 
195
  this.ws.send(
196
  JSON.stringify({
197
  type: "segment_token",
198
  payload: {
199
  segmentId: currentSegmentId,
200
- token: buffer,
201
  messageId,
202
  },
203
  timestamp: Date.now(),
@@ -205,15 +150,12 @@ export class LangGraphAgent {
205
  );
206
  }
207
  }
208
- }
209
 
210
- if (currentSegmentId && currentSegmentContent.trim() && this.ws) {
211
  this.ws.send(
212
  JSON.stringify({
213
  type: "segment_end",
214
  payload: {
215
  segmentId: currentSegmentId,
216
- content: currentSegmentContent,
217
  messageId,
218
  },
219
  timestamp: Date.now(),
@@ -221,7 +163,7 @@ export class LangGraphAgent {
221
  );
222
  }
223
 
224
- const toolCalls = this.parseToolCalls(fullResponse);
225
 
226
  if (toolCalls.length > 0) {
227
  const toolResults = await this.executeToolsWithSegments(
@@ -294,85 +236,156 @@ export class LangGraphAgent {
294
  }
295
 
296
  private buildSystemPrompt(): string {
297
- return `You are an expert VibeGame developer assistant with MCP (Model Context Protocol) tools for direct code manipulation.
298
-
299
- ## Core Principles
300
- - ALWAYS use tools to complete tasks - never provide instructions without execution
301
- - Use the EXACT format: TOOL: tool_name ARGS: {"param": "value"}
302
- - Wait for tool results before proceeding to next step
303
- - The game auto-reloads after each editor change
304
-
305
- ## Tool Usage Strategy
306
-
307
- ### 1. SEARCH FIRST
308
- Always search before reading or editing to locate relevant code:
309
- - search_editor: Find functions, components, or patterns by text/regex
310
- - read_editor_lines: Examine specific sections after search
311
- - read_editor: Only when you need the complete file context
312
-
313
- ### 2. EDIT INCREMENTALLY
314
- Keep edits small and focused:
315
- - edit_editor: For targeted changes (max ~20 lines)
316
- - write_editor: Only for complete file rewrites or new files
317
- - Break large changes into multiple edit_editor calls
318
-
319
- ### 3. TASK PLANNING
320
- For complex work (3+ steps), use task management:
321
- - plan_tasks: Decompose work into clear steps
322
- - update_task: Track progress (in_progress → completed)
323
- - view_tasks: Review current task list
324
-
325
- ### 4. RESEARCH LIBRARIES
326
- For external libraries/frameworks beyond VibeGame:
327
- - resolve_library_id: Find Context7-compatible library ID first
328
- - get_library_docs: Fetch current documentation with targeted search
329
-
330
- ### 5. VERIFY CHANGES
331
- - observe_console: Check for errors after edits
332
- - Monitor game reload status in tool responses
333
-
334
- ## MCP Tools Reference
335
-
336
- ### Code Analysis
337
- - search_editor: {"query": "text", "mode": "text|regex", "contextLines": 2}
338
- - read_editor: {}
339
- - read_editor_lines: {"startLine": 1, "endLine": 10}
340
-
341
- ### Code Modification
342
- - edit_editor: {"oldText": "exact text", "newText": "replacement"}
343
- - write_editor: {"content": "complete file content"}
344
-
345
- ### Task Management
346
- - plan_tasks: {"tasks": ["step 1", "step 2", ...]}
347
- - update_task: {"taskId": 1, "status": "in_progress|completed"}
348
- - view_tasks: {}
349
-
350
-
351
- ### Documentation & Research
352
- - resolve_library_id: {"libraryName": "gsap"} - Find Context7-compatible library ID
353
- - get_library_docs: {"context7CompatibleLibraryID": "/greensock/gsap", "tokens": 5000, "topic": "animations"} - Fetch up-to-date docs
354
-
355
- ### Debugging
356
- - observe_console: {}
357
-
358
- ## VibeGame Context
359
  ${this.documentation}
360
 
361
- ## Example Workflows
362
-
363
- ### VibeGame Edit:
364
- User: "Change the ball color to blue"
365
- 1. TOOL: search_editor ARGS: {"query": "ball"}
366
- 2. TOOL: read_editor_lines ARGS: {"startLine": 10, "endLine": 15}
367
- 3. TOOL: edit_editor ARGS: {"oldText": "color=\\"#ff4500\\"", "newText": "color=\\"#0000ff\\""}
368
- 4. TOOL: observe_console ARGS: {}
369
-
370
- ### External Library Research:
371
- User: "How do I create GSAP animations?"
372
- 1. TOOL: resolve_library_id ARGS: {"libraryName": "gsap"}
373
- 2. TOOL: get_library_docs ARGS: {"context7CompatibleLibraryID": "/greensock/gsap", "topic": "animations"}
374
-
375
- Remember: Execute immediately. Don't explain - just do.`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
376
  }
377
 
378
  private async *streamModelResponse(
@@ -407,37 +420,6 @@ Remember: Execute immediately. Don't explain - just do.`;
407
  return fullContent;
408
  }
409
 
410
- private parseToolCalls(
411
- response: string,
412
- ): Array<{ name: string; args: Record<string, unknown> }> {
413
- const toolCalls = [];
414
- const toolRegex = /TOOL:\s*(\w+)\s+ARGS:\s*({[^}]*})/gs;
415
- let match;
416
-
417
- while ((match = toolRegex.exec(response)) !== null) {
418
- const toolName = match[1];
419
- let params = {};
420
-
421
- if (match[2]) {
422
- try {
423
- params = JSON.parse(match[2]);
424
- } catch {
425
- console.error(
426
- `Failed to parse tool parameters for ${toolName}: ${match[2]}`,
427
- );
428
- params = {};
429
- }
430
- }
431
-
432
- toolCalls.push({
433
- name: toolName,
434
- args: params,
435
- });
436
- }
437
-
438
- return toolCalls;
439
- }
440
-
441
  private shouldUseTools(content: string): boolean {
442
  const lowerContent = content.toLowerCase();
443
 
@@ -469,6 +451,39 @@ Remember: Execute immediately. Don't explain - just do.`;
469
  return actionKeywords.some((keyword) => lowerContent.includes(keyword));
470
  }
471
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
472
  private formatMessages(
473
  messages: BaseMessage[],
474
  systemPrompt: string,
@@ -504,9 +519,47 @@ Remember: Execute immediately. Don't explain - just do.`;
504
  ): Promise<BaseMessage[]> {
505
  const results = [];
506
 
507
- for (const call of toolCalls) {
 
 
 
 
 
 
508
  const segmentId = `seg_tool_${Date.now()}_${Math.random()}`;
509
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
510
  try {
511
  const argString = JSON.stringify(call.args);
512
  const estimatedTokens = argString.length / 4;
@@ -571,6 +624,12 @@ Remember: Execute immediately. Don't explain - just do.`;
571
  if (tool) {
572
  result = await tool.func(call.args);
573
 
 
 
 
 
 
 
574
  const consoleMatch = result.match(/Console output:\n([\s\S]*?)$/);
575
  if (consoleMatch) {
576
  consoleOutput = consoleMatch[1]
@@ -674,6 +733,91 @@ Remember: Execute immediately. Don't explain - just do.`;
674
  return results;
675
  }
676
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
677
  async processMessage(
678
  message: string,
679
  messageHistory: BaseMessage[] = [],
 
10
  import { mcpClientManager, setMCPWebSocketConnection } from "./mcp-client";
11
  import { planTasksTool, updateTaskTool, viewTasksTool } from "./task-tracker";
12
  import { documentationService } from "./documentation";
13
+ import {
14
+ StreamingToolCallParser,
15
+ extractToolCalls,
16
+ } from "../utils/tool-call-parser";
17
+ import { virtualFileSystem } from "../services/virtual-fs";
18
  import type { WebSocket } from "ws";
19
 
20
  const AgentState = Annotation.Root({
 
63
  const messages = this.formatMessages(state.messages, systemPrompt);
64
 
65
  let fullResponse = "";
66
+ const parser = new StreamingToolCallParser();
67
  let currentSegmentId: string | null = null;
 
 
68
  const messageId = config?.metadata?.messageId;
69
  const abortSignal = config?.metadata?.abortSignal as
70
  | AbortSignal
71
  | undefined;
72
 
 
 
73
  for await (const token of this.streamModelResponse(
74
  messages,
75
  abortSignal,
76
  )) {
77
  fullResponse += token;
78
  config?.writer?.({ type: "token", content: token });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
 
80
+ // Process token through the parser
81
+ const parseResult = parser.process(token);
 
 
 
 
 
 
 
 
 
 
 
82
 
83
+ // Only stream safe text (without tool calls)
84
+ if (parseResult.safeText) {
85
+ // Start a text segment if needed
86
+ if (!currentSegmentId && parseResult.safeText.trim() && this.ws) {
87
+ currentSegmentId = `seg_${Date.now()}_${Math.random()}`;
88
  this.ws.send(
89
  JSON.stringify({
90
+ type: "segment_start",
91
  payload: {
92
  segmentId: currentSegmentId,
93
+ segmentType: "text",
94
  messageId,
95
  },
96
  timestamp: Date.now(),
97
  }),
98
  );
 
 
99
  }
100
 
101
+ // Stream the safe text
102
+ if (currentSegmentId && this.ws) {
 
 
 
 
 
 
 
103
  this.ws.send(
104
  JSON.stringify({
105
+ type: "segment_token",
106
  payload: {
107
  segmentId: currentSegmentId,
108
+ token: parseResult.safeText,
109
  messageId,
110
  },
111
  timestamp: Date.now(),
112
  }),
113
  );
114
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
  }
 
116
 
117
+ // If we detected a tool call start, end the current text segment
118
+ if (parseResult.pendingToolCall && currentSegmentId && this.ws) {
 
 
119
  this.ws.send(
120
  JSON.stringify({
121
+ type: "segment_end",
122
  payload: {
123
  segmentId: currentSegmentId,
 
124
  messageId,
125
  },
126
  timestamp: Date.now(),
127
  }),
128
  );
129
+ currentSegmentId = null;
130
  }
131
+ }
132
 
133
+ // Finalize any remaining text segment
134
+ if (currentSegmentId && this.ws) {
135
+ // Process any remaining buffered content
136
+ const remainingBuffer = parser.getBuffer();
137
+ if (remainingBuffer && !parser.isInToolCall()) {
138
+ const finalResult = parser.process("");
139
+ if (finalResult.safeText) {
140
  this.ws.send(
141
  JSON.stringify({
142
  type: "segment_token",
143
  payload: {
144
  segmentId: currentSegmentId,
145
+ token: finalResult.safeText,
146
  messageId,
147
  },
148
  timestamp: Date.now(),
 
150
  );
151
  }
152
  }
 
153
 
 
154
  this.ws.send(
155
  JSON.stringify({
156
  type: "segment_end",
157
  payload: {
158
  segmentId: currentSegmentId,
 
159
  messageId,
160
  },
161
  timestamp: Date.now(),
 
163
  );
164
  }
165
 
166
+ const toolCalls = extractToolCalls(fullResponse);
167
 
168
  if (toolCalls.length > 0) {
169
  const toolResults = await this.executeToolsWithSegments(
 
236
  }
237
 
238
  private buildSystemPrompt(): string {
239
+ return `## Role & Primary Guidance
240
+ You are a VibeGame Engine Specialist operating in a single-file editor environment. You work with ONE game file that users can see and edit in real-time, similar to JSFiddle or CodePen.
241
+
242
+ ## Live Coding Environment Context
243
+ - **SINGLE EDITOR**: There is exactly ONE editor file containing the game's XML/HTML code
244
+ - **"The code" ALWAYS refers to**: The content currently in the editor
245
+ - **Live Preview**: Changes to the editor automatically reload the game
246
+ - **User's View**: Users see the same editor you're modifying
247
+ - **GAME is PRE-IMPORTED**: The GAME object is automatically available - NEVER write \`import * as GAME from 'vibegame'\`
248
+ - **Auto-run enabled**: GAME.run() is called automatically - use \`GAME.withPlugin(MyPlugin)\` NOT \`GAME.withPlugin(MyPlugin).run()\`
249
+
250
+ ## VibeGame Expert Knowledge
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
251
  ${this.documentation}
252
 
253
+ ## MCP Tool Execution Protocol
254
+
255
+ ### Format Requirements
256
+ - MANDATORY format: <tool name="tool_name">{"param": "value"}</tool>
257
+ - JSON arguments must be valid JSON (use double quotes for strings)
258
+ - **CRITICAL**: Execute ONE tool at a time and wait for the result
259
+ - The parser will handle unclosed tags gracefully for recovery
260
+ - **NEVER** generate multiple tool calls in a single response
261
+ - After each tool execution, analyze the result before deciding next action
262
+ - ALWAYS check console after making changes
263
+ - The game auto-reloads after editor changes
264
+
265
+ ### Tool Execution Rules
266
+ 1. Execute a single tool
267
+ 2. Read and analyze the tool's response
268
+ 3. Only then decide if another tool is needed
269
+ 4. If multiple edits are needed, use plan_tasks first to organize them
270
+
271
+ ### Available MCP Tools (All operate on the SINGLE editor file)
272
+
273
+ #### Understanding the Editor Content
274
+ - search_editor: Find text/patterns in the current game code
275
+ Parameters:
276
+ - query: string - Text or regex pattern to search for
277
+ - mode: "text" | "regex" - Search mode (optional, default: "text")
278
+ - contextLines: number - Lines of context (optional, default: 2, max: 5)
279
+ Example: <tool name="search_editor">{"query": "dynamic-part", "mode": "text"}</tool>
280
+
281
+ - read_editor_lines: Read specific lines from the current game code
282
+ Parameters:
283
+ - startLine: number - Starting line (1-indexed)
284
+ - endLine: number - Ending line (optional, defaults to startLine)
285
+ Example: <tool name="read_editor_lines">{"startLine": 10, "endLine": 20}</tool>
286
+
287
+ - read_editor: Read the complete game code currently in the editor
288
+ Parameters: none
289
+ Example: <tool name="read_editor">{}</tool>
290
+ Note: Use when user asks to "explain the code", "show the code", or needs full context
291
+
292
+ #### Modifying the Game
293
+ - edit_editor: Make targeted changes to the game code
294
+ Parameters:
295
+ - oldText: string - Exact text to find and replace (max ~20 lines). MUST include actual content, not just whitespace
296
+ - newText: string - Replacement text
297
+ Example: <tool name="edit_editor">{"oldText": "color='#ff0000'", "newText": "color='#00ff00'"}</tool>
298
+ IMPORTANT: Always include meaningful content (tags, comments, or code) in oldText, never just spaces/newlines
299
+ Note: Returns standardized response with game status and console output
300
+
301
+ - write_editor: Replace all game code with new content
302
+ Parameters:
303
+ - content: string - Complete new game code
304
+ Example: <tool name="write_editor">{"content": "<world>...</world>"}</tool>
305
+ Note: Use ONLY for starting fresh or complete rewrites
306
+
307
+ #### Task Management
308
+ - plan_tasks: Create task list for complex operations
309
+ Parameters:
310
+ - tasks: string[] - Array of task descriptions
311
+ Example: <tool name="plan_tasks">{"tasks": ["Add physics component", "Create gravity system", "Test gravity effect"]}</tool>
312
+
313
+ - update_task: Update task status
314
+ Parameters:
315
+ - taskId: number - Task ID to update
316
+ - status: "pending" | "in_progress" | "completed"
317
+ Example: <tool name="update_task">{"taskId": 1, "status": "completed"}</tool>
318
+
319
+ - view_tasks: View current task list
320
+ Parameters: none
321
+ Example: <tool name="view_tasks">{}</tool>
322
+
323
+ #### Runtime Monitoring
324
+ - observe_console: Check console messages and game state
325
+ Parameters: none
326
+ Example: <tool name="observe_console">{}</tool>
327
+ Note: Returns recent console messages and game status
328
+
329
+ ## Tool Response Format
330
+ All editor modification tools return:
331
+ - Success indicator (✅ or ❌)
332
+ - Action description
333
+ - Game status (Running/Error/Loading)
334
+ - Console output from the game
335
+
336
+ ## Common User Requests (Context Clarification)
337
+ When users say:
338
+ - "explain the code" → Read and explain the CURRENT editor content
339
+ - "what's in the game?" → Describe the entities/elements in the editor
340
+ - "fix the error" → Check console, then modify the editor content
341
+ - "add a..." → Add new elements to the existing editor content
342
+ - "change the..." → Modify existing elements in the editor
343
+
344
+ ## Execution Patterns
345
+
346
+ ### Code Writing Rules (Live Environment)
347
+ - **NO IMPORTS**: GAME is pre-imported globally - NEVER write \`import * as GAME from 'vibegame'\`
348
+ - **NO .run()**: Auto-run is enabled - write \`GAME.withPlugin(MyPlugin)\` not \`GAME.withPlugin(MyPlugin).run()\`
349
+ - **Direct access**: Use GAME.defineComponent, GAME.Types, etc. directly
350
+ - **Script tags**: When adding JavaScript, use \`<script>\` tags in the XML/HTML
351
+
352
+ ### Standard Workflow (ONE TOOL AT A TIME)
353
+ 1. Execute: <tool name="read_editor">{}</tool> (if needed) → WAIT for result
354
+ 2. Execute: <tool name="search_editor">{"query": "target_element"}</tool> → WAIT for result
355
+ 3. Execute: <tool name="read_editor_lines">{"startLine": X, "endLine": Y}</tool> → WAIT for result
356
+ 4. Execute: <tool name="edit_editor">{"oldText": "...", "newText": "..."}</tool> → WAIT for result
357
+ 5. Execute: <tool name="observe_console">{}</tool> → WAIT for result
358
+ 6. Based on console output, decide if another edit is needed
359
+
360
+ ### IMPORTANT: Sequential Tool Execution
361
+ - NEVER chain multiple TOOL: commands in one response
362
+ - Each tool call must be in its own separate message
363
+ - Always analyze the tool result before proceeding
364
+ - If you need to make multiple edits, use plan_tasks to organize them first
365
+
366
+ ### Error Recovery
367
+ When you see an error in tool results:
368
+ 1. Read the error message carefully
369
+ 2. Search for the problematic code
370
+ 3. Make corrections with edit_editor
371
+ 4. Verify fix with observe_console
372
+
373
+ ### Complex Changes
374
+ For changes requiring multiple edits:
375
+ 1. <tool name="plan_tasks">{"tasks": [...]}</tool>
376
+ 2. Update task status as you progress
377
+ 3. Break edits into small chunks (<20 lines each)
378
+ 4. Test after each significant change
379
+
380
+ ## Critical Rules
381
+ - REMEMBER: You work with ONE editor file - all references to "the code" mean this single file
382
+ - ALWAYS check console output after edits
383
+ - NEVER skip tool execution - implement directly
384
+ - Use exact text matches for edit_editor
385
+ - Keep individual edits small and focused
386
+ - When unsure what user refers to, assume they mean the editor content
387
+
388
+ Remember: You're in a live coding environment. Users see the same editor you're modifying. Tool responses include console output - read them carefully to understand game state.`;
389
  }
390
 
391
  private async *streamModelResponse(
 
420
  return fullContent;
421
  }
422
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
423
  private shouldUseTools(content: string): boolean {
424
  const lowerContent = content.toLowerCase();
425
 
 
451
  return actionKeywords.some((keyword) => lowerContent.includes(keyword));
452
  }
453
 
454
+ /**
455
+ * Deduplicate tool calls to prevent redundant operations
456
+ */
457
+ private deduplicateToolCalls(
458
+ toolCalls: Array<{ name: string; args: Record<string, unknown> }>,
459
+ ): Array<{ name: string; args: Record<string, unknown> }> {
460
+ const deduplicated: Array<{ name: string; args: Record<string, unknown> }> =
461
+ [];
462
+ const seenOperations = new Set<string>();
463
+
464
+ for (const call of toolCalls) {
465
+ let signature = `${call.name}:`;
466
+
467
+ if (call.name === "edit_editor") {
468
+ const oldText = call.args.oldText as string;
469
+ signature += oldText?.substring(0, 100);
470
+ } else if (call.name === "write_editor") {
471
+ signature += "FULL_REPLACE";
472
+ } else {
473
+ signature += JSON.stringify(call.args);
474
+ }
475
+
476
+ if (!seenOperations.has(signature)) {
477
+ seenOperations.add(signature);
478
+ deduplicated.push(call);
479
+ } else {
480
+ console.warn(`Skipping duplicate tool call: ${call.name}`);
481
+ }
482
+ }
483
+
484
+ return deduplicated;
485
+ }
486
+
487
  private formatMessages(
488
  messages: BaseMessage[],
489
  systemPrompt: string,
 
519
  ): Promise<BaseMessage[]> {
520
  const results = [];
521
 
522
+ const processedCalls = this.deduplicateToolCalls(toolCalls);
523
+ const stateTracker = {
524
+ editorModified: false,
525
+ lastEditContent: "",
526
+ };
527
+
528
+ for (const call of processedCalls) {
529
  const segmentId = `seg_tool_${Date.now()}_${Math.random()}`;
530
 
531
+ const validation = this.validateToolCall(call);
532
+ if (!validation.valid) {
533
+ console.error(`Invalid tool call: ${call.name}`, validation.error);
534
+ results.push(
535
+ new ToolMessage({
536
+ content: `Error: ${validation.error}`,
537
+ tool_call_id: segmentId,
538
+ name: call.name,
539
+ }),
540
+ );
541
+ continue;
542
+ }
543
+
544
+ if (call.name === "edit_editor" && stateTracker.editorModified) {
545
+ const currentContent = virtualFileSystem.getGameFile().content;
546
+ const oldText = call.args.oldText as string;
547
+
548
+ if (!currentContent.includes(oldText)) {
549
+ console.warn(
550
+ `Skipping edit_editor: Text no longer exists after previous edit`,
551
+ );
552
+ results.push(
553
+ new ToolMessage({
554
+ content: `Skipped: The text to replace was already modified by a previous edit. The current operation is no longer needed.`,
555
+ tool_call_id: segmentId,
556
+ name: call.name,
557
+ }),
558
+ );
559
+ continue;
560
+ }
561
+ }
562
+
563
  try {
564
  const argString = JSON.stringify(call.args);
565
  const estimatedTokens = argString.length / 4;
 
624
  if (tool) {
625
  result = await tool.func(call.args);
626
 
627
+ if (call.name === "edit_editor" || call.name === "write_editor") {
628
+ stateTracker.editorModified = true;
629
+ stateTracker.lastEditContent =
630
+ virtualFileSystem.getGameFile().content;
631
+ }
632
+
633
  const consoleMatch = result.match(/Console output:\n([\s\S]*?)$/);
634
  if (consoleMatch) {
635
  consoleOutput = consoleMatch[1]
 
733
  return results;
734
  }
735
 
736
+ private validateToolCall(call: {
737
+ name: string;
738
+ args: Record<string, unknown>;
739
+ }): { valid: boolean; error?: string } {
740
+ // Basic validation for known tools
741
+ const toolValidations: Record<
742
+ string,
743
+ (args: Record<string, unknown>) => string | null
744
+ > = {
745
+ edit_editor: (args) => {
746
+ if (!args.oldText || typeof args.oldText !== "string") {
747
+ return "Missing or invalid 'oldText' parameter";
748
+ }
749
+ if (!args.newText || typeof args.newText !== "string") {
750
+ return "Missing or invalid 'newText' parameter";
751
+ }
752
+ const lines = (args.oldText as string).split("\n").length;
753
+ if (lines > 30) {
754
+ return `Edit too large (${lines} lines). Break into smaller edits.`;
755
+ }
756
+ return null;
757
+ },
758
+ write_editor: (args) => {
759
+ if (!args.content || typeof args.content !== "string") {
760
+ return "Missing or invalid 'content' parameter";
761
+ }
762
+ return null;
763
+ },
764
+ search_editor: (args) => {
765
+ if (!args.query || typeof args.query !== "string") {
766
+ return "Missing or invalid 'query' parameter";
767
+ }
768
+ if (args.mode && !["text", "regex"].includes(args.mode as string)) {
769
+ return "Invalid 'mode' parameter. Must be 'text' or 'regex'";
770
+ }
771
+ return null;
772
+ },
773
+ read_editor_lines: (args) => {
774
+ if (!args.startLine || typeof args.startLine !== "number") {
775
+ return "Missing or invalid 'startLine' parameter";
776
+ }
777
+ if (args.startLine < 1) {
778
+ return "'startLine' must be >= 1";
779
+ }
780
+ if (args.endLine && typeof args.endLine !== "number") {
781
+ return "Invalid 'endLine' parameter";
782
+ }
783
+ return null;
784
+ },
785
+ plan_tasks: (args) => {
786
+ if (!args.tasks || !Array.isArray(args.tasks)) {
787
+ return "Missing or invalid 'tasks' parameter. Must be an array.";
788
+ }
789
+ if (args.tasks.length === 0) {
790
+ return "'tasks' array cannot be empty";
791
+ }
792
+ return null;
793
+ },
794
+ update_task: (args) => {
795
+ if (typeof args.taskId !== "number") {
796
+ return "Missing or invalid 'taskId' parameter";
797
+ }
798
+ if (
799
+ !args.status ||
800
+ !["pending", "in_progress", "completed"].includes(
801
+ args.status as string,
802
+ )
803
+ ) {
804
+ return "Invalid 'status' parameter. Must be 'pending', 'in_progress', or 'completed'";
805
+ }
806
+ return null;
807
+ },
808
+ };
809
+
810
+ const validator = toolValidations[call.name];
811
+ if (validator) {
812
+ const error = validator(call.args);
813
+ if (error) {
814
+ return { valid: false, error };
815
+ }
816
+ }
817
+
818
+ return { valid: true };
819
+ }
820
+
821
  async processMessage(
822
  message: string,
823
  messageHistory: BaseMessage[] = [],
src/lib/server/mcp-client.ts CHANGED
@@ -2,7 +2,7 @@ import { DynamicStructuredTool } from "@langchain/core/tools";
2
  import { z } from "zod";
3
  import type { WebSocket } from "ws";
4
  import { consoleBuffer } from "./console-buffer";
5
- import { virtualFileSystem, VirtualFileSystem } from "../services/virtual-fs";
6
 
7
  interface EditorWebSocketConnection {
8
  send: (message: {
@@ -28,6 +28,116 @@ export function setMCPWebSocketConnection(ws: WebSocket) {
28
  };
29
  }
30
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  /**
32
  * MCPClientManager provides MCP-style tools for editor operations
33
  * Currently uses local implementation, can be extended to use actual MCP servers
@@ -48,30 +158,6 @@ export class MCPClientManager {
48
  private createEditorTools(): DynamicStructuredTool[] {
49
  const tools: DynamicStructuredTool[] = [];
50
 
51
- tools.push(
52
- new DynamicStructuredTool({
53
- name: "read_file",
54
- description:
55
- "Read a file from the virtual file system - defaults to /game.html",
56
- schema: z.object({
57
- path: z
58
- .string()
59
- .optional()
60
- .describe("File path to read (default: /game.html)"),
61
- }),
62
- func: async (input: { path?: string }) => {
63
- const filePath = input.path || VirtualFileSystem.GAME_FILE_PATH;
64
- const file = virtualFileSystem.readFile(filePath);
65
-
66
- if (!file) {
67
- return `Error: File not found: ${filePath}`;
68
- }
69
-
70
- return `Current content of ${filePath}:\n${file.content}`;
71
- },
72
- }),
73
- );
74
-
75
  tools.push(
76
  new DynamicStructuredTool({
77
  name: "read_editor",
@@ -85,47 +171,6 @@ export class MCPClientManager {
85
  }),
86
  );
87
 
88
- tools.push(
89
- new DynamicStructuredTool({
90
- name: "read_lines",
91
- description:
92
- "Read specific lines from a file - use AFTER search_file to examine found code sections in detail",
93
- schema: z.object({
94
- startLine: z
95
- .number()
96
- .min(1)
97
- .describe("The starting line number (1-indexed)"),
98
- endLine: z
99
- .number()
100
- .min(1)
101
- .optional()
102
- .describe(
103
- "The ending line number (inclusive). If not provided, only the start line is returned",
104
- ),
105
- path: z
106
- .string()
107
- .optional()
108
- .describe("File path to read from (default: /game.html)"),
109
- }),
110
- func: async (input: {
111
- startLine: number;
112
- endLine?: number;
113
- path?: string;
114
- }) => {
115
- const result = virtualFileSystem.getLines(
116
- input.startLine,
117
- input.endLine,
118
- );
119
-
120
- if (result.error) {
121
- return `Error: ${result.error}`;
122
- }
123
-
124
- return result.content;
125
- },
126
- }),
127
- );
128
-
129
  tools.push(
130
  new DynamicStructuredTool({
131
  name: "read_editor_lines",
@@ -159,56 +204,6 @@ export class MCPClientManager {
159
  }),
160
  );
161
 
162
- tools.push(
163
- new DynamicStructuredTool({
164
- name: "search_file",
165
- description:
166
- "Search for content in a file and get line numbers - use FIRST to locate specific functions, classes, or components before reading or editing",
167
- schema: z.object({
168
- query: z.string().describe("Text or regex pattern to search for"),
169
- mode: z
170
- .enum(["text", "regex"])
171
- .optional()
172
- .describe(
173
- "Search mode: 'text' for literal text search, 'regex' for pattern matching (default: text)",
174
- ),
175
- path: z
176
- .string()
177
- .optional()
178
- .describe("File path to search in (default: /game.html)"),
179
- }),
180
- func: async (input: {
181
- query: string;
182
- mode?: "text" | "regex";
183
- path?: string;
184
- }) => {
185
- const mode = input.mode || "text";
186
- const results = virtualFileSystem.searchContent(input.query, mode);
187
-
188
- if (results.length === 0) {
189
- return `No matches found for "${input.query}" in virtual file system`;
190
- }
191
-
192
- const totalMatches = results.length;
193
- const displayMatches = results.slice(0, 10);
194
-
195
- let output = `Found ${totalMatches} match${totalMatches > 1 ? "es" : ""} for "${input.query}":\n\n`;
196
-
197
- displayMatches.forEach((match, index) => {
198
- if (index > 0) output += "\n---\n\n";
199
- output += `${match.path}:\n`;
200
- output += match.context.join("\n");
201
- });
202
-
203
- if (totalMatches > 10) {
204
- output += `\n\n(Showing first 10 of ${totalMatches} matches. Use more specific search terms to narrow results)`;
205
- }
206
-
207
- return output;
208
- },
209
- }),
210
- );
211
-
212
  tools.push(
213
  new DynamicStructuredTool({
214
  name: "search_editor",
@@ -262,78 +257,6 @@ export class MCPClientManager {
262
  }),
263
  );
264
 
265
- tools.push(
266
- new DynamicStructuredTool({
267
- name: "edit_file",
268
- description:
269
- "Replace specific text in a file - use for SMALL, targeted changes (max ~20 lines). For large changes, use multiple edit_file calls with plan_tasks",
270
- schema: z.object({
271
- oldText: z
272
- .string()
273
- .describe(
274
- "The exact text to find and replace (keep small - max ~20 lines)",
275
- ),
276
- newText: z.string().describe("The text to replace it with"),
277
- path: z
278
- .string()
279
- .optional()
280
- .describe("File path to edit (default: /game.html)"),
281
- }),
282
- func: async (input: {
283
- oldText: string;
284
- newText: string;
285
- path?: string;
286
- }) => {
287
- const result = virtualFileSystem.editContent(
288
- input.oldText,
289
- input.newText,
290
- );
291
-
292
- if (!result.success) {
293
- return `Error: ${result.error}`;
294
- }
295
-
296
- const file = virtualFileSystem.getGameFile();
297
- this.syncEditorContent(file.content);
298
-
299
- consoleBuffer.clear();
300
-
301
- const startTime = Date.now();
302
- const maxWaitTime = 3000;
303
-
304
- await new Promise((resolve) => setTimeout(resolve, 1000));
305
-
306
- while (Date.now() - startTime < maxWaitTime) {
307
- const gameState = consoleBuffer.getGameStateFromMessages();
308
-
309
- if (gameState.isReady) {
310
- const messages = consoleBuffer.getRecentMessages();
311
- consoleBuffer.markAsRead();
312
- return `Text replaced successfully. Game reloaded without errors.\nRecent console output:\n${messages
313
- .map((m) => `[${m.type}] ${m.message}`)
314
- .join("\n")}`;
315
- }
316
-
317
- if (gameState.hasError) {
318
- const messages = consoleBuffer.getRecentMessages();
319
- consoleBuffer.markAsRead();
320
- return `Text replaced but game failed to start.\nError: ${gameState.lastError}\nFull console output:\n${messages
321
- .map((m) => `[${m.type}] ${m.message}`)
322
- .join("\n")}`;
323
- }
324
-
325
- await new Promise((resolve) => setTimeout(resolve, 100));
326
- }
327
-
328
- const messages = consoleBuffer.getRecentMessages();
329
- consoleBuffer.markAsRead();
330
- return `Text replaced. Game reload status uncertain (timeout).\nConsole output:\n${messages
331
- .map((m) => `[${m.type}] ${m.message}`)
332
- .join("\n")}`;
333
- },
334
- }),
335
- );
336
-
337
  tools.push(
338
  new DynamicStructuredTool({
339
  name: "edit_editor",
@@ -348,112 +271,45 @@ export class MCPClientManager {
348
  newText: z.string().describe("The text to replace it with"),
349
  }),
350
  func: async (input: { oldText: string; newText: string }) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
351
  const result = virtualFileSystem.editContent(
352
  input.oldText,
353
  input.newText,
354
  );
355
 
356
  if (!result.success) {
357
- return `Error: ${result.error}`;
 
 
 
 
358
  }
359
 
360
  const file = virtualFileSystem.getGameFile();
361
  this.syncEditorContent(file.content);
362
 
363
- consoleBuffer.clear();
364
-
365
- const startTime = Date.now();
366
- const maxWaitTime = 3000;
367
-
368
- await new Promise((resolve) => setTimeout(resolve, 1000));
369
-
370
- while (Date.now() - startTime < maxWaitTime) {
371
- const gameState = consoleBuffer.getGameStateFromMessages();
372
-
373
- if (gameState.isReady) {
374
- const messages = consoleBuffer.getRecentMessages();
375
- consoleBuffer.markAsRead();
376
- return `Text replaced successfully. Game reloaded without errors.\nRecent console output:\n${messages
377
- .map((m) => `[${m.type}] ${m.message}`)
378
- .join("\n")}`;
379
- }
380
-
381
- if (gameState.hasError) {
382
- const messages = consoleBuffer.getRecentMessages();
383
- consoleBuffer.markAsRead();
384
- return `Text replaced but game failed to start.\nError: ${gameState.lastError}\nFull console output:\n${messages
385
- .map((m) => `[${m.type}] ${m.message}`)
386
- .join("\n")}`;
387
- }
388
-
389
- await new Promise((resolve) => setTimeout(resolve, 100));
390
- }
391
-
392
- const messages = consoleBuffer.getRecentMessages();
393
- consoleBuffer.markAsRead();
394
- return `Text replaced. Game reload status uncertain (timeout).\nConsole output:\n${messages
395
- .map((m) => `[${m.type}] ${m.message}`)
396
- .join("\n")}`;
397
- },
398
- }),
399
- );
400
 
401
- tools.push(
402
- new DynamicStructuredTool({
403
- name: "write_file",
404
- description:
405
- "Replace entire file content - use ONLY for creating new files or complete rewrites. For modifications, use edit_file with plan_tasks instead",
406
- schema: z.object({
407
- content: z.string().describe("The complete file content to write"),
408
- path: z
409
- .string()
410
- .optional()
411
- .describe("File path to write to (default: /game.html)"),
412
- }),
413
- func: async (input: { content: string; path?: string }) => {
414
- const filePath = input.path || VirtualFileSystem.GAME_FILE_PATH;
415
-
416
- if (filePath !== VirtualFileSystem.GAME_FILE_PATH) {
417
- return `Error: Only ${VirtualFileSystem.GAME_FILE_PATH} can be written to`;
418
- }
419
-
420
- virtualFileSystem.updateGameContent(input.content);
421
- this.syncEditorContent(input.content);
422
-
423
- consoleBuffer.clear();
424
-
425
- const startTime = Date.now();
426
- const maxWaitTime = 3000;
427
-
428
- await new Promise((resolve) => setTimeout(resolve, 1000));
429
-
430
- while (Date.now() - startTime < maxWaitTime) {
431
- const gameState = consoleBuffer.getGameStateFromMessages();
432
-
433
- if (gameState.isReady) {
434
- const messages = consoleBuffer.getRecentMessages();
435
- consoleBuffer.markAsRead();
436
- return `File updated successfully. Game reloaded without errors.\nRecent console output:\n${messages
437
- .map((m) => `[${m.type}] ${m.message}`)
438
- .join("\n")}`;
439
- }
440
-
441
- if (gameState.hasError) {
442
- const messages = consoleBuffer.getRecentMessages();
443
- consoleBuffer.markAsRead();
444
- return `File updated but game failed to start.\nError: ${gameState.lastError}\nFull console output:\n${messages
445
- .map((m) => `[${m.type}] ${m.message}`)
446
- .join("\n")}`;
447
- }
448
-
449
- await new Promise((resolve) => setTimeout(resolve, 100));
450
- }
451
-
452
- const messages = consoleBuffer.getRecentMessages();
453
- consoleBuffer.markAsRead();
454
- return `File updated. Game reload status uncertain (timeout).\nConsole output:\n${messages
455
- .map((m) => `[${m.type}] ${m.message}`)
456
- .join("\n")}`;
457
  },
458
  }),
459
  );
@@ -472,40 +328,16 @@ export class MCPClientManager {
472
  virtualFileSystem.updateGameContent(input.content);
473
  this.syncEditorContent(input.content);
474
 
475
- consoleBuffer.clear();
476
-
477
- const startTime = Date.now();
478
- const maxWaitTime = 3000;
479
-
480
- await new Promise((resolve) => setTimeout(resolve, 1000));
481
-
482
- while (Date.now() - startTime < maxWaitTime) {
483
- const gameState = consoleBuffer.getGameStateFromMessages();
484
-
485
- if (gameState.isReady) {
486
- const messages = consoleBuffer.getRecentMessages();
487
- consoleBuffer.markAsRead();
488
- return `Code updated successfully. Game reloaded without errors.\nRecent console output:\n${messages
489
- .map((m) => `[${m.type}] ${m.message}`)
490
- .join("\n")}`;
491
- }
492
-
493
- if (gameState.hasError) {
494
- const messages = consoleBuffer.getRecentMessages();
495
- consoleBuffer.markAsRead();
496
- return `Code updated but game failed to start.\nError: ${gameState.lastError}\nFull console output:\n${messages
497
- .map((m) => `[${m.type}] ${m.message}`)
498
- .join("\n")}`;
499
- }
500
 
501
- await new Promise((resolve) => setTimeout(resolve, 100));
502
- }
503
-
504
- const messages = consoleBuffer.getRecentMessages();
505
- consoleBuffer.markAsRead();
506
- return `Code updated. Game reload status uncertain (timeout).\nConsole output:\n${messages
507
- .map((m) => `[${m.type}] ${m.message}`)
508
- .join("\n")}`;
509
  },
510
  }),
511
  );
 
2
  import { z } from "zod";
3
  import type { WebSocket } from "ws";
4
  import { consoleBuffer } from "./console-buffer";
5
+ import { virtualFileSystem } from "../services/virtual-fs";
6
 
7
  interface EditorWebSocketConnection {
8
  send: (message: {
 
28
  };
29
  }
30
 
31
+ /**
32
+ * Standardized tool response builder
33
+ */
34
+ function buildToolResponse({
35
+ success,
36
+ action,
37
+ error,
38
+ gameState,
39
+ consoleOutput,
40
+ }: {
41
+ success: boolean;
42
+ action: string;
43
+ error?: string;
44
+ gameState?: ReturnType<typeof consoleBuffer.getGameStateFromMessages>;
45
+ consoleOutput?: string[];
46
+ }): string {
47
+ const sections: string[] = [];
48
+
49
+ // Status section
50
+ if (success) {
51
+ sections.push(`✅ ${action} completed successfully.`);
52
+ } else {
53
+ sections.push(`❌ ${action} failed.`);
54
+ }
55
+
56
+ // Error section
57
+ if (error) {
58
+ sections.push(`\nError: ${error}`);
59
+ }
60
+
61
+ // Game state section
62
+ if (gameState) {
63
+ if (gameState.isReady) {
64
+ sections.push("\n🎮 Game Status: Running");
65
+ } else if (gameState.hasError) {
66
+ sections.push(`\n🎮 Game Status: Error\n ${gameState.lastError}`);
67
+ } else if (gameState.isLoading) {
68
+ sections.push("\n🎮 Game Status: Loading...");
69
+ } else {
70
+ sections.push("\n🎮 Game Status: Unknown");
71
+ }
72
+ }
73
+
74
+ // Console output section
75
+ if (consoleOutput && consoleOutput.length > 0) {
76
+ sections.push("\n📝 Console Output:");
77
+ sections.push(consoleOutput.join("\n"));
78
+ }
79
+
80
+ return sections.join("\n");
81
+ }
82
+
83
+ /**
84
+ * Wait for game state with improved console capture
85
+ */
86
+ async function waitForGameState(
87
+ toolName: string,
88
+ maxWaitTime: number = 5000,
89
+ ): Promise<{
90
+ gameState: ReturnType<typeof consoleBuffer.getGameStateFromMessages>;
91
+ consoleOutput: string[];
92
+ }> {
93
+ // Set execution context for console messages
94
+ consoleBuffer.setExecutionContext(toolName);
95
+ consoleBuffer.clearToolMessages();
96
+
97
+ const startTime = Date.now();
98
+
99
+ // Initial wait for game to process changes (increased to match GameCanvas reload delay)
100
+ await new Promise((resolve) => setTimeout(resolve, 2000));
101
+
102
+ while (Date.now() - startTime < maxWaitTime) {
103
+ const gameState = consoleBuffer.getGameStateFromMessages();
104
+
105
+ // Check if we have a definitive state (not reloading, and either ready or error)
106
+ if (!gameState.isReloading && (gameState.isReady || gameState.hasError)) {
107
+ const messages = consoleBuffer.getMessagesSinceLastTool();
108
+ const consoleOutput = messages
109
+ .slice(-30)
110
+ .map((m) => `[${m.type}] ${m.message}`);
111
+
112
+ // Clear context
113
+ consoleBuffer.setExecutionContext(null);
114
+ consoleBuffer.markAsRead();
115
+
116
+ return { gameState, consoleOutput };
117
+ }
118
+
119
+ // If still reloading, wait longer
120
+ if (gameState.isReloading) {
121
+ await new Promise((resolve) => setTimeout(resolve, 200));
122
+ } else {
123
+ await new Promise((resolve) => setTimeout(resolve, 100));
124
+ }
125
+ }
126
+
127
+ // Timeout - return current state
128
+ const gameState = consoleBuffer.getGameStateFromMessages();
129
+ const messages = consoleBuffer.getMessagesSinceLastTool();
130
+ const consoleOutput = messages
131
+ .slice(-30)
132
+ .map((m) => `[${m.type}] ${m.message}`);
133
+
134
+ // Clear context
135
+ consoleBuffer.setExecutionContext(null);
136
+ consoleBuffer.markAsRead();
137
+
138
+ return { gameState, consoleOutput };
139
+ }
140
+
141
  /**
142
  * MCPClientManager provides MCP-style tools for editor operations
143
  * Currently uses local implementation, can be extended to use actual MCP servers
 
158
  private createEditorTools(): DynamicStructuredTool[] {
159
  const tools: DynamicStructuredTool[] = [];
160
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
  tools.push(
162
  new DynamicStructuredTool({
163
  name: "read_editor",
 
171
  }),
172
  );
173
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
  tools.push(
175
  new DynamicStructuredTool({
176
  name: "read_editor_lines",
 
204
  }),
205
  );
206
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
  tools.push(
208
  new DynamicStructuredTool({
209
  name: "search_editor",
 
257
  }),
258
  );
259
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
  tools.push(
261
  new DynamicStructuredTool({
262
  name: "edit_editor",
 
271
  newText: z.string().describe("The text to replace it with"),
272
  }),
273
  func: async (input: { oldText: string; newText: string }) => {
274
+ const currentContent = virtualFileSystem.getGameFile().content;
275
+ if (!currentContent.includes(input.oldText)) {
276
+ const shortPreview =
277
+ input.oldText.substring(0, 50) +
278
+ (input.oldText.length > 50 ? "..." : "");
279
+
280
+ return buildToolResponse({
281
+ success: false,
282
+ action: "Text replacement",
283
+ error: `Text not found: "${shortPreview}". This might be due to a previous edit already modifying this text. Please verify the current content with read_editor or search_editor.`,
284
+ });
285
+ }
286
+
287
  const result = virtualFileSystem.editContent(
288
  input.oldText,
289
  input.newText,
290
  );
291
 
292
  if (!result.success) {
293
+ return buildToolResponse({
294
+ success: false,
295
+ action: "Text replacement",
296
+ error: result.error,
297
+ });
298
  }
299
 
300
  const file = virtualFileSystem.getGameFile();
301
  this.syncEditorContent(file.content);
302
 
303
+ // Wait for game to reload and capture console
304
+ const { gameState, consoleOutput } =
305
+ await waitForGameState("edit_editor");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
306
 
307
+ return buildToolResponse({
308
+ success: true,
309
+ action: "Text replacement",
310
+ gameState,
311
+ consoleOutput,
312
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
313
  },
314
  }),
315
  );
 
328
  virtualFileSystem.updateGameContent(input.content);
329
  this.syncEditorContent(input.content);
330
 
331
+ // Wait for game to reload and capture console
332
+ const { gameState, consoleOutput } =
333
+ await waitForGameState("write_editor");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
334
 
335
+ return buildToolResponse({
336
+ success: true,
337
+ action: "Editor content update",
338
+ gameState,
339
+ consoleOutput,
340
+ });
 
 
341
  },
342
  }),
343
  );
src/lib/server/tools.ts CHANGED
@@ -1,12 +1,20 @@
1
  import { DynamicTool } from "@langchain/core/tools";
2
  import { consoleBuffer } from "./console-buffer";
3
 
 
 
 
 
4
  export const observeConsoleTool = new DynamicTool({
5
  name: "observe_console",
6
  description:
7
  "Read console messages and game state - use to check for errors after making changes",
8
- func: async () => {
9
- const messages = consoleBuffer.getRecentMessages();
 
 
 
 
10
  const allMessages = consoleBuffer.getAllMessages();
11
  const gameState = consoleBuffer.getGameStateFromMessages();
12
 
@@ -17,6 +25,7 @@ export const observeConsoleTool = new DynamicTool({
17
  Total messages in buffer: ${allMessages.length}
18
  Game State Analysis:
19
  - Loading: ${gameState.isLoading}
 
20
  - Ready: ${gameState.isReady}
21
  - Has Error: ${gameState.hasError}
22
  ${gameState.lastError ? `- Last Error: ${gameState.lastError}` : ""}
@@ -32,7 +41,14 @@ ${allMessages
32
  return debugInfo;
33
  }
34
 
35
- let output = `Console Messages (${messages.length} new):\n`;
 
 
 
 
 
 
 
36
  output += messages
37
  .map(
38
  (msg) =>
@@ -42,6 +58,7 @@ ${allMessages
42
 
43
  output += "\n\nGame State:";
44
  output += `\n- Loading: ${gameState.isLoading}`;
 
45
  output += `\n- Ready: ${gameState.isReady}`;
46
  output += `\n- Has Error: ${gameState.hasError}`;
47
  if (gameState.lastError) {
@@ -52,7 +69,3 @@ ${allMessages
52
  return output;
53
  },
54
  });
55
-
56
- import { taskTrackerTools } from "./task-tracker";
57
-
58
- export const tools = [observeConsoleTool, ...taskTrackerTools];
 
1
  import { DynamicTool } from "@langchain/core/tools";
2
  import { consoleBuffer } from "./console-buffer";
3
 
4
+ interface ObserveConsoleOptions {
5
+ tailSize?: number;
6
+ }
7
+
8
  export const observeConsoleTool = new DynamicTool({
9
  name: "observe_console",
10
  description:
11
  "Read console messages and game state - use to check for errors after making changes",
12
+ func: async (input: string | ObserveConsoleOptions = {}) => {
13
+ const options: ObserveConsoleOptions =
14
+ typeof input === "string" ? {} : input;
15
+ const tailSize = options.tailSize || 10;
16
+
17
+ const messages = consoleBuffer.getRecentMessages(undefined, tailSize);
18
  const allMessages = consoleBuffer.getAllMessages();
19
  const gameState = consoleBuffer.getGameStateFromMessages();
20
 
 
25
  Total messages in buffer: ${allMessages.length}
26
  Game State Analysis:
27
  - Loading: ${gameState.isLoading}
28
+ - Reloading: ${gameState.isReloading}
29
  - Ready: ${gameState.isReady}
30
  - Has Error: ${gameState.hasError}
31
  ${gameState.lastError ? `- Last Error: ${gameState.lastError}` : ""}
 
41
  return debugInfo;
42
  }
43
 
44
+ const totalNew = consoleBuffer.getRecentMessages().length;
45
+ const displayedCount = messages.length;
46
+ const headerInfo =
47
+ totalNew > displayedCount
48
+ ? `Console Messages (showing last ${displayedCount} of ${totalNew} new):`
49
+ : `Console Messages (${displayedCount} new):`;
50
+
51
+ let output = `${headerInfo}\n`;
52
  output += messages
53
  .map(
54
  (msg) =>
 
58
 
59
  output += "\n\nGame State:";
60
  output += `\n- Loading: ${gameState.isLoading}`;
61
+ output += `\n- Reloading: ${gameState.isReloading}`;
62
  output += `\n- Ready: ${gameState.isReady}`;
63
  output += `\n- Has Error: ${gameState.hasError}`;
64
  if (gameState.lastError) {
 
69
  return output;
70
  },
71
  });
 
 
 
 
src/lib/services/content-manager.ts CHANGED
@@ -10,6 +10,8 @@ export interface ContentState {
10
  version: number;
11
  isUISynced: boolean;
12
  isAgentSynced: boolean;
 
 
13
  }
14
 
15
  export interface ContentChange {
@@ -46,11 +48,15 @@ class ContentManager {
46
  version: 1,
47
  isUISynced: true,
48
  isAgentSynced: true,
 
 
49
  });
50
 
51
  private syncTimeout: number | null = null;
52
  private readonly DEBOUNCE_MS = 300;
53
  private isUpdating = false;
 
 
54
 
55
  private constructor() {
56
  this.setupSyncSubscription();
@@ -84,7 +90,16 @@ class ContentManager {
84
  * Debounced for smooth typing experience
85
  */
86
  updateFromUI(content: string): void {
87
- if (this.isUpdating) return;
 
 
 
 
 
 
 
 
 
88
 
89
  this.updateContent(content, "ui");
90
  this.debouncedAgentSync();
@@ -92,14 +107,42 @@ class ContentManager {
92
 
93
  /**
94
  * Update content from agent (MCP tools)
95
- * Always overwrites UI content - agent wins conflicts
96
  */
97
  updateFromAgent(content: string): void {
98
  if (this.isUpdating) return;
99
 
100
  this.isUpdating = true;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  this.updateContent(content, "agent");
 
102
  this.clearSyncTimeout();
 
 
 
 
 
 
 
 
 
 
103
  this.isUpdating = false;
104
  }
105
 
@@ -116,7 +159,11 @@ class ContentManager {
116
  this.contentStore.update((state) => ({
117
  ...state,
118
  isAgentSynced: true,
 
119
  }));
 
 
 
120
  }
121
 
122
  /**
@@ -133,6 +180,13 @@ class ContentManager {
133
  return get(this.contentStore);
134
  }
135
 
 
 
 
 
 
 
 
136
  /**
137
  * Force full sync (for reconnection scenarios)
138
  */
@@ -185,6 +239,8 @@ class ContentManager {
185
  version: state.version + 1,
186
  isUISynced: source === "ui" || source === "init",
187
  isAgentSynced: source === "agent" || source === "init",
 
 
188
  };
189
  });
190
  }
 
10
  version: number;
11
  isUISynced: boolean;
12
  isAgentSynced: boolean;
13
+ lastSource: "ui" | "agent" | "init";
14
+ isConflicted: boolean;
15
  }
16
 
17
  export interface ContentChange {
 
48
  version: 1,
49
  isUISynced: true,
50
  isAgentSynced: true,
51
+ lastSource: "init",
52
+ isConflicted: false,
53
  });
54
 
55
  private syncTimeout: number | null = null;
56
  private readonly DEBOUNCE_MS = 300;
57
  private isUpdating = false;
58
+ private agentEditInProgress = false;
59
+ private lastAgentVersion = 0;
60
 
61
  private constructor() {
62
  this.setupSyncSubscription();
 
90
  * Debounced for smooth typing experience
91
  */
92
  updateFromUI(content: string): void {
93
+ if (this.isUpdating || this.agentEditInProgress) {
94
+ // Agent is editing, mark as conflicted
95
+ if (this.agentEditInProgress) {
96
+ this.contentStore.update((state) => ({
97
+ ...state,
98
+ isConflicted: true,
99
+ }));
100
+ }
101
+ return;
102
+ }
103
 
104
  this.updateContent(content, "ui");
105
  this.debouncedAgentSync();
 
107
 
108
  /**
109
  * Update content from agent (MCP tools)
110
+ * Now handles version conflicts more gracefully
111
  */
112
  updateFromAgent(content: string): void {
113
  if (this.isUpdating) return;
114
 
115
  this.isUpdating = true;
116
+ this.agentEditInProgress = true;
117
+
118
+ const currentState = get(this.contentStore);
119
+
120
+ // Check for version conflict
121
+ if (
122
+ currentState.version > this.lastAgentVersion &&
123
+ currentState.lastSource === "ui"
124
+ ) {
125
+ // UI has made changes since agent started editing
126
+ console.warn("Agent edit conflicted with UI changes - agent wins");
127
+ this.contentStore.update((state) => ({
128
+ ...state,
129
+ isConflicted: true,
130
+ }));
131
+ }
132
+
133
  this.updateContent(content, "agent");
134
+ this.lastAgentVersion = get(this.contentStore).version;
135
  this.clearSyncTimeout();
136
+
137
+ // Allow UI to resume editing after a short delay
138
+ setTimeout(() => {
139
+ this.agentEditInProgress = false;
140
+ this.contentStore.update((state) => ({
141
+ ...state,
142
+ isConflicted: false,
143
+ }));
144
+ }, 500);
145
+
146
  this.isUpdating = false;
147
  }
148
 
 
159
  this.contentStore.update((state) => ({
160
  ...state,
161
  isAgentSynced: true,
162
+ isConflicted: false,
163
  }));
164
+
165
+ // Initialize version tracking
166
+ this.lastAgentVersion = 1;
167
  }
168
 
169
  /**
 
180
  return get(this.contentStore);
181
  }
182
 
183
+ /**
184
+ * Check if agent is currently editing
185
+ */
186
+ isAgentEditing(): boolean {
187
+ return this.agentEditInProgress;
188
+ }
189
+
190
  /**
191
  * Force full sync (for reconnection scenarios)
192
  */
 
239
  version: state.version + 1,
240
  isUISynced: source === "ui" || source === "init",
241
  isAgentSynced: source === "agent" || source === "init",
242
+ lastSource: source,
243
+ isConflicted: false,
244
  };
245
  });
246
  }
src/lib/services/context.md CHANGED
@@ -11,13 +11,13 @@ Single source of truth content management, game lifecycle, and virtual file oper
11
  ```
12
  services/
13
  ├── context.md # This file
14
- ├── content-manager.ts # Single source of truth for editor content
15
  ├── auth.ts # Hugging Face OAuth
16
  ├── websocket.ts # WebSocket connection
17
  ├── message-handler.ts # Message routing with segment processing
18
- ├── game-engine.ts # VibeGame lifecycle with DOM-based rendering
19
  ├── html-document-parser.ts # HTML parsing using DOMParser
20
- ├── virtual-fs.ts # Virtual file system for editor content
21
  └── console-sync.ts # Console interception
22
  ```
23
 
 
11
  ```
12
  services/
13
  ├── context.md # This file
14
+ ├── content-manager.ts # Editor content with conflict detection
15
  ├── auth.ts # Hugging Face OAuth
16
  ├── websocket.ts # WebSocket connection
17
  ├── message-handler.ts # Message routing with segment processing
18
+ ├── game-engine.ts # VibeGame lifecycle with reload events
19
  ├── html-document-parser.ts # HTML parsing using DOMParser
20
+ ├── virtual-fs.ts # Virtual file system with version tracking
21
  └── console-sync.ts # Console interception
22
  ```
23
 
src/lib/services/game-engine.ts CHANGED
@@ -2,6 +2,7 @@ import * as GAME from "vibegame";
2
  import type { System, Plugin, Component, BuilderOptions } from "vibegame";
3
  import { gameStore } from "../stores/game";
4
  import { uiStore } from "../stores/ui";
 
5
  import {
6
  HTMLDocumentParser,
7
  type ParsedDocument,
@@ -24,6 +25,7 @@ export class GameEngine {
24
 
25
  async startFromDocument(htmlContent: string): Promise<void> {
26
  if (this.gameInstance) {
 
27
  this.stop();
28
  await new Promise((resolve) => setTimeout(resolve, 100));
29
  }
@@ -37,6 +39,7 @@ export class GameEngine {
37
  this.renderDocument(parsed);
38
  await this.initializeGame(parsed.scripts);
39
  console.info("✅ Game started!");
 
40
  } catch (error: unknown) {
41
  const errorMsg = error instanceof Error ? error.message : String(error);
42
  uiStore.setError(errorMsg);
@@ -59,6 +62,7 @@ export class GameEngine {
59
 
60
  private async startFromParsed(parsed: ParsedDocument): Promise<void> {
61
  if (this.gameInstance) {
 
62
  this.stop();
63
  await new Promise((resolve) => setTimeout(resolve, 100));
64
  }
@@ -71,6 +75,7 @@ export class GameEngine {
71
  this.renderDocument(parsed);
72
  await this.initializeGame(parsed.scripts);
73
  console.info("✅ Game started!");
 
74
  } catch (error: unknown) {
75
  const errorMsg = error instanceof Error ? error.message : String(error);
76
  uiStore.setError(errorMsg);
 
2
  import type { System, Plugin, Component, BuilderOptions } from "vibegame";
3
  import { gameStore } from "../stores/game";
4
  import { uiStore } from "../stores/ui";
5
+ import { consoleBuffer } from "../server/console-buffer";
6
  import {
7
  HTMLDocumentParser,
8
  type ParsedDocument,
 
25
 
26
  async startFromDocument(htmlContent: string): Promise<void> {
27
  if (this.gameInstance) {
28
+ consoleBuffer.onGameReloadStart();
29
  this.stop();
30
  await new Promise((resolve) => setTimeout(resolve, 100));
31
  }
 
39
  this.renderDocument(parsed);
40
  await this.initializeGame(parsed.scripts);
41
  console.info("✅ Game started!");
42
+ consoleBuffer.onGameReloadComplete();
43
  } catch (error: unknown) {
44
  const errorMsg = error instanceof Error ? error.message : String(error);
45
  uiStore.setError(errorMsg);
 
62
 
63
  private async startFromParsed(parsed: ParsedDocument): Promise<void> {
64
  if (this.gameInstance) {
65
+ consoleBuffer.onGameReloadStart();
66
  this.stop();
67
  await new Promise((resolve) => setTimeout(resolve, 100));
68
  }
 
75
  this.renderDocument(parsed);
76
  await this.initializeGame(parsed.scripts);
77
  console.info("✅ Game started!");
78
+ consoleBuffer.onGameReloadComplete();
79
  } catch (error: unknown) {
80
  const errorMsg = error instanceof Error ? error.message : String(error);
81
  uiStore.setError(errorMsg);
src/lib/services/message-handler.ts CHANGED
@@ -8,6 +8,7 @@ export class MessageHandler {
8
  private currentSegments: Map<string, MessageSegment> = new Map();
9
  private completedSegments: MessageSegment[] = [];
10
  private latestTodoSegmentId: string | null = null;
 
11
 
12
  handleMessage(message: WebSocketMessage): void {
13
  switch (message.type) {
@@ -143,7 +144,15 @@ export class MessageHandler {
143
  content: segment.content + (token as string),
144
  };
145
  this.currentSegments.set(segmentId as string, updatedSegment);
146
- this.updateMessageSegments();
 
 
 
 
 
 
 
 
147
  }
148
  }
149
 
@@ -158,6 +167,12 @@ export class MessageHandler {
158
  } = message.payload;
159
  if (!segmentId) return;
160
 
 
 
 
 
 
 
161
  const segment = this.currentSegments.get(segmentId as string);
162
  if (segment) {
163
  const completedSegment: MessageSegment = {
@@ -219,16 +234,19 @@ export class MessageHandler {
219
 
220
  private buildContentFromSegments(segments: MessageSegment[]): string {
221
  let content = "";
 
222
 
223
  for (const segment of segments) {
224
  if (segment.type === "text" && segment.content) {
 
 
 
 
225
  content += segment.content;
226
- } else if (segment.type === "tool-invocation") {
227
- // Tool invocations are now handled in the UI
228
- continue;
229
- } else if (segment.type === "tool-result") {
230
- // Tool results are now handled in the UI
231
- continue;
232
  }
233
  }
234
 
 
8
  private currentSegments: Map<string, MessageSegment> = new Map();
9
  private completedSegments: MessageSegment[] = [];
10
  private latestTodoSegmentId: string | null = null;
11
+ private updateTimer: ReturnType<typeof setTimeout> | null = null;
12
 
13
  handleMessage(message: WebSocketMessage): void {
14
  switch (message.type) {
 
144
  content: segment.content + (token as string),
145
  };
146
  this.currentSegments.set(segmentId as string, updatedSegment);
147
+
148
+ // Throttle updates during streaming to avoid excessive re-renders
149
+ if (this.updateTimer) {
150
+ clearTimeout(this.updateTimer);
151
+ }
152
+ this.updateTimer = setTimeout(() => {
153
+ this.updateMessageSegments();
154
+ this.updateTimer = null;
155
+ }, 50);
156
  }
157
  }
158
 
 
167
  } = message.payload;
168
  if (!segmentId) return;
169
 
170
+ // Clear any pending update timer for immediate final update
171
+ if (this.updateTimer) {
172
+ clearTimeout(this.updateTimer);
173
+ this.updateTimer = null;
174
+ }
175
+
176
  const segment = this.currentSegments.get(segmentId as string);
177
  if (segment) {
178
  const completedSegment: MessageSegment = {
 
234
 
235
  private buildContentFromSegments(segments: MessageSegment[]): string {
236
  let content = "";
237
+ let lastWasText = false;
238
 
239
  for (const segment of segments) {
240
  if (segment.type === "text" && segment.content) {
241
+ // Add spacing between text segments if needed
242
+ if (lastWasText && content && !content.endsWith("\n")) {
243
+ content += " ";
244
+ }
245
  content += segment.content;
246
+ lastWasText = true;
247
+ } else {
248
+ // Tool segments are handled in UI, but reset text flag
249
+ lastWasText = false;
 
 
250
  }
251
  }
252
 
src/lib/services/segment-formatter.ts CHANGED
@@ -144,9 +144,6 @@ export class SegmentFormatter {
144
  update_task: "✏️",
145
  view_tasks: "👀",
146
  observe_console: "📺",
147
- read_file: "📖",
148
- write_file: "✍️",
149
- edit_file: "✏️",
150
  };
151
  return toolIcons[segment.toolName] || iconMap[segment.type] || "📄";
152
  }
 
144
  update_task: "✏️",
145
  view_tasks: "👀",
146
  observe_console: "📺",
 
 
 
147
  };
148
  return toolIcons[segment.toolName] || iconMap[segment.type] || "📄";
149
  }
src/lib/services/virtual-fs.ts CHANGED
@@ -2,6 +2,7 @@ export interface VirtualFile {
2
  path: string;
3
  content: string;
4
  lastModified: Date;
 
5
  }
6
 
7
  /**
@@ -11,6 +12,12 @@ export interface VirtualFile {
11
  export class VirtualFileSystem {
12
  private static instance: VirtualFileSystem | null = null;
13
  private gameFile: VirtualFile;
 
 
 
 
 
 
14
 
15
  public static readonly GAME_FILE_PATH = "/game.html";
16
 
@@ -19,6 +26,7 @@ export class VirtualFileSystem {
19
  path: VirtualFileSystem.GAME_FILE_PATH,
20
  content: "",
21
  lastModified: new Date(),
 
22
  };
23
  }
24
 
@@ -43,10 +51,12 @@ export class VirtualFileSystem {
43
  );
44
  }
45
 
 
46
  this.gameFile = {
47
  path,
48
  content,
49
  lastModified: new Date(),
 
50
  };
51
  }
52
 
@@ -114,29 +124,199 @@ export class VirtualFileSystem {
114
  return results;
115
  }
116
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  editContent(
118
  oldText: string,
119
  newText: string,
120
- ): { success: boolean; error?: string } {
121
- if (!this.gameFile.content.includes(oldText)) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  return {
123
  success: false,
124
- error: "Could not find the specified text to replace.",
125
  };
126
  }
127
 
128
- const occurrences = this.gameFile.content.split(oldText).length - 1;
129
- if (occurrences > 1) {
 
 
 
 
 
 
 
 
130
  return {
131
  success: false,
132
- error: `Found ${occurrences} occurrences of the text. Be more specific.`,
133
  };
134
  }
135
 
136
- const newContent = this.gameFile.content.replace(oldText, newText);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
  this.updateGameContent(newContent);
 
 
138
 
139
- return { success: true };
 
 
 
 
 
 
 
 
 
140
  }
141
 
142
  getLines(
 
2
  path: string;
3
  content: string;
4
  lastModified: Date;
5
+ version?: number;
6
  }
7
 
8
  /**
 
12
  export class VirtualFileSystem {
13
  private static instance: VirtualFileSystem | null = null;
14
  private gameFile: VirtualFile;
15
+ private editHistory: Array<{
16
+ timestamp: Date;
17
+ oldText: string;
18
+ newText: string;
19
+ version: number;
20
+ }> = [];
21
 
22
  public static readonly GAME_FILE_PATH = "/game.html";
23
 
 
26
  path: VirtualFileSystem.GAME_FILE_PATH,
27
  content: "",
28
  lastModified: new Date(),
29
+ version: 0,
30
  };
31
  }
32
 
 
51
  );
52
  }
53
 
54
+ const newVersion = (this.gameFile.version || 0) + 1;
55
  this.gameFile = {
56
  path,
57
  content,
58
  lastModified: new Date(),
59
+ version: newVersion,
60
  };
61
  }
62
 
 
124
  return results;
125
  }
126
 
127
+ /**
128
+ * Normalize text for flexible matching while preserving exact replacement
129
+ */
130
+ private normalizeForMatching(text: string): string {
131
+ // Normalize line endings and trim each line
132
+ return text
133
+ .split(/\r?\n/)
134
+ .map((line) => line.trimEnd())
135
+ .join("\n")
136
+ .trim();
137
+ }
138
+
139
+ /**
140
+ * Find all positions where normalized text matches
141
+ */
142
+ private findNormalizedMatches(
143
+ content: string,
144
+ searchText: string,
145
+ ): Array<{ start: number; end: number; text: string }> {
146
+ const matches: Array<{ start: number; end: number; text: string }> = [];
147
+ const normalizedSearch = this.normalizeForMatching(searchText);
148
+ const lines = content.split(/\r?\n/);
149
+
150
+ // Try to find matches line by line with flexible whitespace
151
+ for (let startLine = 0; startLine < lines.length; startLine++) {
152
+ // Build potential match starting from this line
153
+ for (
154
+ let endLine = startLine;
155
+ endLine < lines.length && endLine < startLine + 100;
156
+ endLine++
157
+ ) {
158
+ const candidateLines = lines.slice(startLine, endLine + 1);
159
+ const candidateText = candidateLines.join("\n");
160
+ const normalizedCandidate = this.normalizeForMatching(candidateText);
161
+
162
+ if (normalizedCandidate === normalizedSearch) {
163
+ // Found a match! Calculate actual positions in original content
164
+ let position = 0;
165
+ for (let i = 0; i < startLine; i++) {
166
+ position += lines[i].length + 1; // +1 for newline
167
+ }
168
+ const start = position;
169
+ const end = start + candidateText.length;
170
+
171
+ matches.push({
172
+ start,
173
+ end,
174
+ text: candidateText,
175
+ });
176
+ break; // Don't look for longer matches starting from same line
177
+ }
178
+ }
179
+ }
180
+
181
+ return matches;
182
+ }
183
+
184
+ /**
185
+ * Get context lines around a position in content
186
+ */
187
+ private getContextAtPosition(
188
+ content: string,
189
+ position: number,
190
+ contextLines: number = 3,
191
+ ): string {
192
+ const lines = content.split(/\r?\n/);
193
+ let currentPos = 0;
194
+ let targetLine = 0;
195
+
196
+ // Find which line contains the position
197
+ for (let i = 0; i < lines.length; i++) {
198
+ if (currentPos + lines[i].length >= position) {
199
+ targetLine = i;
200
+ break;
201
+ }
202
+ currentPos += lines[i].length + 1;
203
+ }
204
+
205
+ const startLine = Math.max(0, targetLine - contextLines);
206
+ const endLine = Math.min(lines.length - 1, targetLine + contextLines);
207
+
208
+ const contextParts: string[] = [];
209
+ for (let i = startLine; i <= endLine; i++) {
210
+ const lineNum = i + 1;
211
+ const prefix = i === targetLine ? ">>> " : " ";
212
+ contextParts.push(`${prefix}${lineNum}: ${lines[i]}`);
213
+ }
214
+
215
+ return contextParts.join("\n");
216
+ }
217
+
218
  editContent(
219
  oldText: string,
220
  newText: string,
221
+ ): { success: boolean; error?: string; version?: number } {
222
+ const currentVersion = this.gameFile.version || 0;
223
+ if (this.gameFile.content.includes(oldText)) {
224
+ const occurrences = this.gameFile.content.split(oldText).length - 1;
225
+ if (occurrences === 1) {
226
+ const newContent = this.gameFile.content.replace(oldText, newText);
227
+ this.editHistory.push({
228
+ timestamp: new Date(),
229
+ oldText,
230
+ newText,
231
+ version: currentVersion,
232
+ });
233
+
234
+ if (this.editHistory.length > 20) {
235
+ this.editHistory = this.editHistory.slice(-20);
236
+ }
237
+
238
+ this.updateGameContent(newContent);
239
+ return { success: true, version: this.gameFile.version };
240
+ } else if (occurrences > 1) {
241
+ return {
242
+ success: false,
243
+ error: `Found ${occurrences} exact occurrences of the text. Be more specific.`,
244
+ };
245
+ }
246
+ }
247
+
248
+ const matches = this.findNormalizedMatches(this.gameFile.content, oldText);
249
+
250
+ if (matches.length === 0) {
251
+ // Show context to help understand why match failed
252
+ const shortPreview =
253
+ oldText.substring(0, 50) + (oldText.length > 50 ? "..." : "");
254
+ const searchResults = this.searchContent(
255
+ oldText.split(/\r?\n/)[0].trim(),
256
+ "text",
257
+ );
258
+
259
+ let errorMsg = `Could not find the specified text to replace: "${shortPreview}"`;
260
+ if (searchResults.length > 0) {
261
+ errorMsg += "\n\nDid you mean one of these locations?\n";
262
+ searchResults.slice(0, 3).forEach((result) => {
263
+ errorMsg += "\n" + result.context.join("\n") + "\n";
264
+ });
265
+ }
266
+
267
  return {
268
  success: false,
269
+ error: errorMsg,
270
  };
271
  }
272
 
273
+ if (matches.length > 1) {
274
+ let errorMsg = `Found ${matches.length} matches with normalized whitespace. Please be more specific.\n\nMatches found at:`;
275
+ matches.slice(0, 3).forEach((match, i) => {
276
+ errorMsg += `\n\nMatch ${i + 1}:\n`;
277
+ errorMsg += this.getContextAtPosition(
278
+ this.gameFile.content,
279
+ match.start,
280
+ );
281
+ });
282
+
283
  return {
284
  success: false,
285
+ error: errorMsg,
286
  };
287
  }
288
 
289
+ // Exactly one match found - replace it
290
+ const match = matches[0];
291
+ const newContent =
292
+ this.gameFile.content.substring(0, match.start) +
293
+ newText +
294
+ this.gameFile.content.substring(match.end);
295
+ this.editHistory.push({
296
+ timestamp: new Date(),
297
+ oldText,
298
+ newText,
299
+ version: currentVersion,
300
+ });
301
+
302
+ if (this.editHistory.length > 20) {
303
+ this.editHistory = this.editHistory.slice(-20);
304
+ }
305
+
306
  this.updateGameContent(newContent);
307
+ return { success: true, version: this.gameFile.version };
308
+ }
309
 
310
+ /**
311
+ * Get recent edit history for debugging
312
+ */
313
+ getEditHistory(): Array<{
314
+ timestamp: Date;
315
+ oldText: string;
316
+ newText: string;
317
+ version: number;
318
+ }> {
319
+ return [...this.editHistory];
320
  }
321
 
322
  getLines(
src/lib/stores/context.md CHANGED
@@ -16,7 +16,7 @@ stores/
16
  ├── console.ts # Console messages with unique IDs
17
  ├── editor.ts # Editor content and settings
18
  ├── chat-store.ts # Chat messages and connection state
19
- └── ui.ts # UI state (view mode, errors)
20
  ```
21
 
22
  ## Scope
 
16
  ├── console.ts # Console messages with unique IDs
17
  ├── editor.ts # Editor content and settings
18
  ├── chat-store.ts # Chat messages and connection state
19
+ └── ui.ts # UI state (view modes including about, errors)
20
  ```
21
 
22
  ## Scope
src/lib/stores/ui.ts CHANGED
@@ -1,9 +1,10 @@
1
  import { writable } from "svelte/store";
2
 
3
- export type ViewMode = "code" | "preview";
4
 
5
  export interface UIState {
6
  viewMode: ViewMode;
 
7
  error: string | null;
8
  isAnimating: boolean;
9
  }
@@ -11,6 +12,7 @@ export interface UIState {
11
  function createUIStore() {
12
  const { subscribe, update, set } = writable<UIState>({
13
  viewMode: "code",
 
14
  error: null,
15
  isAnimating: false,
16
  });
@@ -18,7 +20,15 @@ function createUIStore() {
18
  return {
19
  subscribe,
20
  setViewMode: (viewMode: ViewMode) =>
21
- update((state) => ({ ...state, viewMode })),
 
 
 
 
 
 
 
 
22
  setError: (error: string | null) =>
23
  update((state) => ({ ...state, error })),
24
  setAnimating: (isAnimating: boolean) =>
@@ -27,10 +37,26 @@ function createUIStore() {
27
  update((state) => ({
28
  ...state,
29
  viewMode: state.viewMode === "code" ? "preview" : "code",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  })),
31
  reset: () =>
32
  set({
33
  viewMode: "code",
 
34
  error: null,
35
  isAnimating: false,
36
  }),
 
1
  import { writable } from "svelte/store";
2
 
3
+ export type ViewMode = "code" | "preview" | "about";
4
 
5
  export interface UIState {
6
  viewMode: ViewMode;
7
+ previousViewMode: "code" | "preview";
8
  error: string | null;
9
  isAnimating: boolean;
10
  }
 
12
  function createUIStore() {
13
  const { subscribe, update, set } = writable<UIState>({
14
  viewMode: "code",
15
+ previousViewMode: "code",
16
  error: null,
17
  isAnimating: false,
18
  });
 
20
  return {
21
  subscribe,
22
  setViewMode: (viewMode: ViewMode) =>
23
+ update((state) => {
24
+ const previousViewMode =
25
+ state.viewMode === "about"
26
+ ? state.previousViewMode
27
+ : state.viewMode === "code" || state.viewMode === "preview"
28
+ ? state.viewMode
29
+ : state.previousViewMode;
30
+ return { ...state, viewMode, previousViewMode };
31
+ }),
32
  setError: (error: string | null) =>
33
  update((state) => ({ ...state, error })),
34
  setAnimating: (isAnimating: boolean) =>
 
37
  update((state) => ({
38
  ...state,
39
  viewMode: state.viewMode === "code" ? "preview" : "code",
40
+ previousViewMode: state.viewMode === "code" ? "preview" : "code",
41
+ })),
42
+ showAbout: () =>
43
+ update((state) => ({
44
+ ...state,
45
+ previousViewMode:
46
+ state.viewMode === "code" || state.viewMode === "preview"
47
+ ? state.viewMode
48
+ : state.previousViewMode,
49
+ viewMode: "about",
50
+ })),
51
+ hideAbout: () =>
52
+ update((state) => ({
53
+ ...state,
54
+ viewMode: state.previousViewMode,
55
  })),
56
  reset: () =>
57
  set({
58
  viewMode: "code",
59
+ previousViewMode: "code",
60
  error: null,
61
  isAnimating: false,
62
  }),
src/lib/utils/context.md ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Utils Context
2
+
3
+ Shared utility functions and helpers
4
+
5
+ ## Purpose
6
+
7
+ - Reusable utilities for the application
8
+ - Cross-cutting concerns like parsing and validation
9
+
10
+ ## Layout
11
+
12
+ ```
13
+ utils/
14
+ ├── context.md # This file
15
+ ├── tool-call-parser.ts # Tool call extraction wrapper
16
+ └── tool-parser-htmlparser2.ts # Regex-based parser for tool tags
17
+ ```
18
+
19
+ ## Scope
20
+
21
+ - In-scope: Stateless utilities, parsers, formatters
22
+ - Out-of-scope: Business logic, UI components, state management
23
+
24
+ ## Entrypoints
25
+
26
+ - `tool-call-parser.ts` - StreamingToolCallParser class and utility functions
27
+ - `tool-parser-htmlparser2.ts` - ToolParser class for extracting tool tags
28
+
29
+ ## Dependencies
30
+
31
+ - No external dependencies
32
+ - Used by chat components and server-side agent
src/lib/utils/tool-call-parser.ts ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ StreamingToolParser as HTMLStreamingToolParser,
3
+ extractToolCalls as htmlExtractToolCalls,
4
+ filterToolCalls as htmlFilterToolCalls,
5
+ } from "./tool-parser-htmlparser2";
6
+
7
+ export interface ToolCallMatch {
8
+ toolName: string;
9
+ startIndex: number;
10
+ endIndex?: number;
11
+ args?: string;
12
+ complete: boolean;
13
+ }
14
+
15
+ export class StreamingToolCallParser {
16
+ private htmlParser: HTMLStreamingToolParser;
17
+ private buffer = "";
18
+
19
+ constructor() {
20
+ this.htmlParser = new HTMLStreamingToolParser();
21
+ }
22
+
23
+ reset(): void {
24
+ this.buffer = "";
25
+ this.htmlParser.reset();
26
+ }
27
+
28
+ /**
29
+ * Process incoming text and detect tool call boundaries
30
+ * Returns the safe text (without tool calls) and any detected tool calls
31
+ */
32
+ process(text: string): {
33
+ safeText: string;
34
+ toolCalls: ToolCallMatch[];
35
+ pendingToolCall: boolean;
36
+ } {
37
+ this.buffer += text;
38
+ const result = this.htmlParser.write(text);
39
+
40
+ return {
41
+ safeText: result.safeText,
42
+ toolCalls: result.toolCalls.map((tc) => ({
43
+ toolName: tc.name,
44
+ startIndex: tc.startIndex || 0,
45
+ endIndex: tc.endIndex,
46
+ args: tc.rawArgs,
47
+ complete: tc.complete,
48
+ })),
49
+ pendingToolCall: result.pendingToolCall,
50
+ };
51
+ }
52
+
53
+ /**
54
+ * Get any buffered content that hasn't been processed yet
55
+ */
56
+ getBuffer(): string {
57
+ return this.buffer;
58
+ }
59
+
60
+ /**
61
+ * Check if parser is currently inside a tool call
62
+ */
63
+ isInToolCall(): boolean {
64
+ const result = this.htmlParser.write("");
65
+ return result.pendingToolCall;
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Static utility to filter tool calls from text (non-streaming)
71
+ */
72
+ export function filterToolCalls(text: string): string {
73
+ return htmlFilterToolCalls(text);
74
+ }
75
+
76
+ /**
77
+ * Static utility to extract tool calls from text
78
+ */
79
+ export function extractToolCalls(
80
+ text: string,
81
+ ): Array<{ name: string; args: Record<string, unknown> }> {
82
+ const htmlToolCalls = htmlExtractToolCalls(text);
83
+
84
+ return htmlToolCalls.map((tc) => ({
85
+ name: tc.name,
86
+ args: tc.args,
87
+ }));
88
+ }
src/lib/utils/tool-parser-htmlparser2.ts ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export interface ToolCall {
2
+ name: string;
3
+ args: Record<string, unknown>;
4
+ rawArgs: string;
5
+ startIndex?: number;
6
+ endIndex?: number;
7
+ complete: boolean;
8
+ }
9
+
10
+ export interface ParseResult {
11
+ safeText: string;
12
+ toolCalls: ToolCall[];
13
+ pendingToolCall: boolean;
14
+ errors: string[];
15
+ }
16
+
17
+ export class ToolParser {
18
+ private buffer = "";
19
+ private textBuffer = "";
20
+ private lastReturnedTextLength = 0;
21
+ private toolCalls: ToolCall[] = [];
22
+ private errors: string[];
23
+ private processedUpTo = 0;
24
+
25
+ constructor() {
26
+ this.errors = [];
27
+ }
28
+
29
+ reset(): void {
30
+ this.buffer = "";
31
+ this.textBuffer = "";
32
+ this.lastReturnedTextLength = 0;
33
+ this.toolCalls = [];
34
+ this.errors = [];
35
+ this.processedUpTo = 0;
36
+ }
37
+
38
+ process(text: string): ParseResult {
39
+ this.buffer += text;
40
+
41
+ // Use regex to find complete tool tags
42
+ // This avoids htmlparser2 interpreting HTML inside JSON strings
43
+ const toolRegex = /<tool\s+name="([^"]+)">([^]*?)<\/tool>/g;
44
+ toolRegex.lastIndex = 0;
45
+
46
+ let match;
47
+ let lastMatchEnd = this.processedUpTo;
48
+
49
+ while ((match = toolRegex.exec(this.buffer)) !== null) {
50
+ // Only process new matches
51
+ if (match.index < this.processedUpTo) continue;
52
+
53
+ // Add text before the tool tag to textBuffer
54
+ if (match.index > lastMatchEnd) {
55
+ this.textBuffer += this.buffer.slice(lastMatchEnd, match.index);
56
+ }
57
+
58
+ const toolName = match[1];
59
+ const rawContent = match[2];
60
+
61
+ // Parse the JSON content
62
+ let args: Record<string, unknown> = {};
63
+ try {
64
+ const trimmedContent = rawContent.trim();
65
+ if (trimmedContent) {
66
+ args = JSON.parse(trimmedContent);
67
+ }
68
+ } catch (e) {
69
+ this.errors.push(`Failed to parse tool args for ${toolName}: ${e}`);
70
+ args = {};
71
+ }
72
+
73
+ this.toolCalls.push({
74
+ name: toolName,
75
+ args,
76
+ rawArgs: rawContent,
77
+ startIndex: match.index,
78
+ endIndex: match.index + match[0].length,
79
+ complete: true,
80
+ });
81
+
82
+ lastMatchEnd = match.index + match[0].length;
83
+ this.processedUpTo = lastMatchEnd;
84
+ }
85
+
86
+ // Check for incomplete tool tag at the end
87
+ const remainingText = this.buffer.slice(this.processedUpTo);
88
+ const incompleteToolRegex = /<tool\s+name="[^"]*"?\s*>?[^]*$/;
89
+ const incompleteMatch = incompleteToolRegex.test(remainingText);
90
+
91
+ if (!incompleteMatch && remainingText) {
92
+ // No pending tool tag, add remaining text
93
+ this.textBuffer += remainingText;
94
+ this.processedUpTo = this.buffer.length;
95
+ }
96
+
97
+ // Return only new text since last call
98
+ const newText = this.textBuffer.slice(this.lastReturnedTextLength);
99
+ this.lastReturnedTextLength = this.textBuffer.length;
100
+
101
+ return {
102
+ safeText: newText,
103
+ toolCalls: [...this.toolCalls],
104
+ pendingToolCall: incompleteMatch,
105
+ errors: [...this.errors],
106
+ };
107
+ }
108
+
109
+ finalize(): ParseResult {
110
+ // Process any remaining text
111
+ const remainingText = this.buffer.slice(this.processedUpTo);
112
+ if (remainingText && !/<tool\s/.test(remainingText)) {
113
+ this.textBuffer += remainingText;
114
+ }
115
+
116
+ // Return any remaining text
117
+ const newText = this.textBuffer.slice(this.lastReturnedTextLength);
118
+ this.lastReturnedTextLength = this.textBuffer.length;
119
+
120
+ return {
121
+ safeText: newText,
122
+ toolCalls: [...this.toolCalls],
123
+ pendingToolCall: false,
124
+ errors: [...this.errors],
125
+ };
126
+ }
127
+
128
+ getAllText(): string {
129
+ return this.textBuffer;
130
+ }
131
+ }
132
+
133
+ export class StreamingToolParser {
134
+ private parser: ToolParser;
135
+ private lastToolCount = 0;
136
+ private safeTextCallback?: (text: string) => void;
137
+ private toolCallCallback?: (tool: ToolCall) => void;
138
+ private errorCallback?: (error: string) => void;
139
+
140
+ constructor(options?: {
141
+ onSafeText?: (text: string) => void;
142
+ onToolCall?: (tool: ToolCall) => void;
143
+ onError?: (error: string) => void;
144
+ }) {
145
+ this.parser = new ToolParser();
146
+ this.safeTextCallback = options?.onSafeText;
147
+ this.toolCallCallback = options?.onToolCall;
148
+ this.errorCallback = options?.onError;
149
+ }
150
+
151
+ write(chunk: string): ParseResult {
152
+ const result = this.parser.process(chunk);
153
+
154
+ // Handle callbacks
155
+ if (result.safeText && this.safeTextCallback) {
156
+ this.safeTextCallback(result.safeText);
157
+ }
158
+
159
+ if (result.toolCalls.length > this.lastToolCount) {
160
+ const newTools = result.toolCalls.slice(this.lastToolCount);
161
+ for (const tool of newTools) {
162
+ if (tool.complete && this.toolCallCallback) {
163
+ this.toolCallCallback(tool);
164
+ }
165
+ }
166
+ this.lastToolCount = result.toolCalls.length;
167
+ }
168
+
169
+ if (result.errors.length > 0 && this.errorCallback) {
170
+ for (const error of result.errors) {
171
+ this.errorCallback(error);
172
+ }
173
+ }
174
+
175
+ return result;
176
+ }
177
+
178
+ end(): ParseResult {
179
+ return this.parser.finalize();
180
+ }
181
+
182
+ reset(): void {
183
+ this.parser.reset();
184
+ this.lastToolCount = 0;
185
+ }
186
+ }
187
+
188
+ export function extractToolCalls(text: string): ToolCall[] {
189
+ const parser = new ToolParser();
190
+ parser.process(text);
191
+ const result = parser.finalize();
192
+
193
+ if (result.errors.length > 0) {
194
+ console.warn("Tool parsing errors:", result.errors);
195
+ }
196
+
197
+ return result.toolCalls.filter((tc) => tc.complete);
198
+ }
199
+
200
+ export function filterToolCalls(text: string): string {
201
+ const parser = new ToolParser();
202
+ parser.process(text);
203
+ parser.finalize();
204
+ return parser.getAllText();
205
+ }
tests/api.test.ts DELETED
@@ -1,229 +0,0 @@
1
- import { describe, test, expect, beforeEach, mock } from "bun:test";
2
- import { wsManager } from "../src/lib/server/api";
3
- import type { WebSocket } from "ws";
4
- import type { IncomingMessage } from "http";
5
-
6
- describe("WebSocket API", () => {
7
- let manager: typeof wsManager;
8
- let mockWs: Partial<WebSocket>;
9
- let mockRequest: Partial<IncomingMessage>;
10
-
11
- beforeEach(() => {
12
- manager = wsManager;
13
-
14
- mockWs = {
15
- readyState: 1,
16
- OPEN: 1,
17
- send: mock((_data: string) => {}),
18
- on: mock((_event: string, _handler: (...args: unknown[]) => void) => {}),
19
- close: mock(() => {}),
20
- };
21
-
22
- mockRequest = {
23
- headers: {},
24
- url: "/",
25
- };
26
- });
27
-
28
- test("handles new connection", () => {
29
- manager.handleConnection(
30
- mockWs as WebSocket,
31
- mockRequest as IncomingMessage,
32
- );
33
-
34
- expect(mockWs.send).toHaveBeenCalled();
35
- const sentData = (mockWs.send as ReturnType<typeof mock>).mock
36
- .calls[0][0] as string;
37
- const message = JSON.parse(sentData);
38
-
39
- expect(message.type).toBe("status");
40
- expect(message.payload.connected).toBe(true);
41
- });
42
-
43
- test("registers event handlers", () => {
44
- manager.handleConnection(
45
- mockWs as WebSocket,
46
- mockRequest as IncomingMessage,
47
- );
48
-
49
- expect(mockWs.on).toHaveBeenCalledWith("message", expect.any(Function));
50
- expect(mockWs.on).toHaveBeenCalledWith("close", expect.any(Function));
51
- expect(mockWs.on).toHaveBeenCalledWith("error", expect.any(Function));
52
- });
53
-
54
- test("handles authentication message", async () => {
55
- const handlers: Record<string, (...args: unknown[]) => void> = {};
56
- mockWs.on = mock((event: string, handler: (...args: unknown[]) => void) => {
57
- handlers[event] = handler;
58
- });
59
-
60
- manager.handleConnection(
61
- mockWs as WebSocket,
62
- mockRequest as IncomingMessage,
63
- );
64
-
65
- const authMessage = JSON.stringify({
66
- type: "auth",
67
- payload: { token: "test-token" },
68
- timestamp: Date.now(),
69
- });
70
-
71
- await handlers["message"](authMessage);
72
-
73
- expect(mockWs.send).toHaveBeenCalled();
74
- });
75
-
76
- test("handles editor sync message", async () => {
77
- const handlers: Record<string, (...args: unknown[]) => void> = {};
78
- mockWs.on = mock((event: string, handler: (...args: unknown[]) => void) => {
79
- handlers[event] = handler;
80
- });
81
-
82
- manager.handleConnection(
83
- mockWs as WebSocket,
84
- mockRequest as IncomingMessage,
85
- );
86
-
87
- const editorMessage = JSON.stringify({
88
- type: "editor_sync",
89
- payload: { content: "<div>New content</div>" },
90
- timestamp: Date.now(),
91
- });
92
-
93
- await handlers["message"](editorMessage);
94
-
95
- expect(mockWs.send).toHaveBeenCalled();
96
- });
97
-
98
- test("handles console sync message", async () => {
99
- const handlers: Record<string, (...args: unknown[]) => void> = {};
100
- mockWs.on = mock((event: string, handler: (...args: unknown[]) => void) => {
101
- handlers[event] = handler;
102
- });
103
-
104
- manager.handleConnection(
105
- mockWs as WebSocket,
106
- mockRequest as IncomingMessage,
107
- );
108
-
109
- const consoleMessage = JSON.stringify({
110
- type: "console_sync",
111
- payload: {
112
- id: "1",
113
- type: "log",
114
- message: "Test log",
115
- },
116
- timestamp: Date.now(),
117
- });
118
-
119
- await handlers["message"](consoleMessage);
120
- });
121
-
122
- test("handles abort message", async () => {
123
- const handlers: Record<string, (...args: unknown[]) => void> = {};
124
- mockWs.on = mock((event: string, handler: (...args: unknown[]) => void) => {
125
- handlers[event] = handler;
126
- });
127
-
128
- manager.handleConnection(
129
- mockWs as WebSocket,
130
- mockRequest as IncomingMessage,
131
- );
132
-
133
- const abortMessage = JSON.stringify({
134
- type: "abort",
135
- payload: {},
136
- timestamp: Date.now(),
137
- });
138
-
139
- await handlers["message"](abortMessage);
140
-
141
- expect(mockWs.send).toHaveBeenCalled();
142
- });
143
-
144
- test("handles clear conversation message", async () => {
145
- const handlers: Record<string, (...args: unknown[]) => void> = {};
146
- mockWs.on = mock((event: string, handler: (...args: unknown[]) => void) => {
147
- handlers[event] = handler;
148
- });
149
-
150
- manager.handleConnection(
151
- mockWs as WebSocket,
152
- mockRequest as IncomingMessage,
153
- );
154
-
155
- const clearMessage = JSON.stringify({
156
- type: "clear_conversation",
157
- payload: {},
158
- timestamp: Date.now(),
159
- });
160
-
161
- await handlers["message"](clearMessage);
162
-
163
- expect(mockWs.send).toHaveBeenCalled();
164
- });
165
-
166
- test("handles malformed message", async () => {
167
- const handlers: Record<string, (...args: unknown[]) => void> = {};
168
- mockWs.on = mock((event: string, handler: (...args: unknown[]) => void) => {
169
- handlers[event] = handler;
170
- });
171
-
172
- manager.handleConnection(
173
- mockWs as WebSocket,
174
- mockRequest as IncomingMessage,
175
- );
176
-
177
- await handlers["message"]("invalid json");
178
-
179
- expect(mockWs.send).toHaveBeenCalled();
180
- const sentData = (mockWs.send as ReturnType<typeof mock>).mock
181
- .calls[1][0] as string;
182
- const message = JSON.parse(sentData);
183
- expect(message.type).toBe("error");
184
- });
185
-
186
- test("cleans up on close", () => {
187
- const handlers: Record<string, (...args: unknown[]) => void> = {};
188
- mockWs.on = mock((event: string, handler: (...args: unknown[]) => void) => {
189
- handlers[event] = handler;
190
- });
191
-
192
- manager.handleConnection(
193
- mockWs as WebSocket,
194
- mockRequest as IncomingMessage,
195
- );
196
-
197
- handlers["close"]();
198
- });
199
-
200
- test("handles connection error", () => {
201
- const handlers: Record<string, (...args: unknown[]) => void> = {};
202
- mockWs.on = mock((event: string, handler: (...args: unknown[]) => void) => {
203
- handlers[event] = handler;
204
- });
205
-
206
- manager.handleConnection(
207
- mockWs as WebSocket,
208
- mockRequest as IncomingMessage,
209
- );
210
-
211
- const error = new Error("Connection failed");
212
- handlers["error"](error);
213
- });
214
-
215
- test("sends status updates", () => {
216
- manager.handleConnection(
217
- mockWs as WebSocket,
218
- mockRequest as IncomingMessage,
219
- );
220
-
221
- manager["sendMessage"](mockWs as WebSocket, {
222
- type: "status",
223
- payload: { processing: true },
224
- timestamp: Date.now(),
225
- });
226
-
227
- expect(mockWs.send).toHaveBeenCalled();
228
- });
229
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/console-buffer.test.ts DELETED
@@ -1,162 +0,0 @@
1
- import { describe, test, expect, beforeEach } from "bun:test";
2
- import { ConsoleBuffer } from "../src/lib/server/console-buffer";
3
-
4
- describe("Console Buffer", () => {
5
- let buffer: ConsoleBuffer;
6
-
7
- beforeEach(() => {
8
- buffer = ConsoleBuffer.getInstance();
9
- buffer.clear();
10
- });
11
-
12
- test("adds and retrieves messages", () => {
13
- buffer.addMessage({
14
- id: "1",
15
- type: "log",
16
- message: "Test message",
17
- timestamp: Date.now(),
18
- });
19
-
20
- const messages = buffer.getAllMessages();
21
- expect(messages).toHaveLength(1);
22
- expect(messages[0].message).toBe("Test message");
23
- });
24
-
25
- test("retrieves recent messages since timestamp", () => {
26
- const now = Date.now();
27
-
28
- buffer.addMessage({
29
- id: "1",
30
- type: "log",
31
- message: "Old message",
32
- timestamp: now - 1000,
33
- });
34
-
35
- buffer.addMessage({
36
- id: "2",
37
- type: "log",
38
- message: "New message",
39
- timestamp: now + 1000,
40
- });
41
-
42
- const recent = buffer.getRecentMessages(now);
43
- expect(recent).toHaveLength(1);
44
- expect(recent[0].message).toBe("New message");
45
- });
46
-
47
- test("marks messages as read", () => {
48
- buffer.addMessage({
49
- id: "1",
50
- type: "log",
51
- message: "Message 1",
52
- timestamp: Date.now(),
53
- });
54
-
55
- buffer.markAsRead();
56
-
57
- buffer.addMessage({
58
- id: "2",
59
- type: "log",
60
- message: "Message 2",
61
- timestamp: Date.now() + 100,
62
- });
63
-
64
- const recent = buffer.getRecentMessages();
65
- expect(recent).toHaveLength(1);
66
- expect(recent[0].message).toBe("Message 2");
67
- });
68
-
69
- test("limits buffer to max messages", () => {
70
- for (let i = 0; i < 150; i++) {
71
- buffer.addMessage({
72
- id: `msg-${i}`,
73
- type: "log",
74
- message: `Message ${i}`,
75
- timestamp: Date.now() + i,
76
- });
77
- }
78
-
79
- const messages = buffer.getAllMessages();
80
- expect(messages.length).toBeLessThanOrEqual(100);
81
- expect(messages[messages.length - 1].message).toContain("149");
82
- });
83
-
84
- test("clears all messages", () => {
85
- buffer.addMessage({
86
- id: "1",
87
- type: "log",
88
- message: "Test",
89
- timestamp: Date.now(),
90
- });
91
-
92
- buffer.clear();
93
- const messages = buffer.getAllMessages();
94
- expect(messages).toHaveLength(0);
95
- });
96
-
97
- test("detects loading game state", () => {
98
- buffer.addMessage({
99
- id: "1",
100
- type: "log",
101
- message: "🎮 Starting game",
102
- timestamp: Date.now(),
103
- });
104
-
105
- const state = buffer.getGameStateFromMessages();
106
- expect(state.isLoading).toBe(true);
107
- expect(state.hasError).toBe(false);
108
- expect(state.isReady).toBe(false);
109
- });
110
-
111
- test("detects ready game state", () => {
112
- buffer.addMessage({
113
- id: "1",
114
- type: "log",
115
- message: "✅ Game started",
116
- timestamp: Date.now(),
117
- });
118
-
119
- const state = buffer.getGameStateFromMessages();
120
- expect(state.isLoading).toBe(false);
121
- expect(state.hasError).toBe(false);
122
- expect(state.isReady).toBe(true);
123
- });
124
-
125
- test("detects error game state", () => {
126
- buffer.addMessage({
127
- id: "1",
128
- type: "error",
129
- message: "❌ Error: Failed to load",
130
- timestamp: Date.now(),
131
- });
132
-
133
- const state = buffer.getGameStateFromMessages();
134
- expect(state.hasError).toBe(true);
135
- expect(state.lastError).toContain("Failed to load");
136
- expect(state.isReady).toBe(false);
137
- });
138
-
139
- test("handles different message types", () => {
140
- const types = ["log", "warn", "error", "info"] as const;
141
-
142
- types.forEach((type, index) => {
143
- buffer.addMessage({
144
- id: `${index}`,
145
- type,
146
- message: `${type} message`,
147
- timestamp: Date.now() + index,
148
- });
149
- });
150
-
151
- const messages = buffer.getAllMessages();
152
- expect(messages).toHaveLength(4);
153
- expect(messages.map((m) => m.type)).toEqual(types);
154
- });
155
-
156
- test("singleton instance", () => {
157
- const instance1 = ConsoleBuffer.getInstance();
158
- const instance2 = ConsoleBuffer.getInstance();
159
-
160
- expect(instance1).toBe(instance2);
161
- });
162
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/langgraph-agent.test.ts DELETED
@@ -1,133 +0,0 @@
1
- import { describe, test, expect, beforeEach, mock } from "bun:test";
2
- import { LangGraphAgent } from "../src/lib/server/langgraph-agent";
3
- import { HumanMessage, AIMessage } from "@langchain/core/messages";
4
- import type { WebSocket } from "ws";
5
-
6
- describe("LangGraph Agent", () => {
7
- let agent: LangGraphAgent;
8
-
9
- beforeEach(() => {
10
- agent = new LangGraphAgent();
11
- });
12
-
13
- test("initializes with HF token", async () => {
14
- const mockWs = {
15
- readyState: 1,
16
- OPEN: 1,
17
- send: mock(() => {}),
18
- } as unknown as WebSocket;
19
-
20
- await agent.initialize("test-token", mockWs);
21
- expect(agent).toBeDefined();
22
- });
23
-
24
- test("throws error without HF token", async () => {
25
- expect(async () => {
26
- await agent.initialize("");
27
- }).toThrow();
28
- });
29
-
30
- test("processes simple message", async () => {
31
- await agent.initialize("test-token");
32
-
33
- const messages = [new HumanMessage("Hello")];
34
-
35
- const mockWriter = {
36
- writes: [] as Array<{ type: string; content?: string }>,
37
- write: function (data: { type: string; content?: string }) {
38
- this.writes.push(data);
39
- },
40
- };
41
-
42
- const config = {
43
- writer: mockWriter.write.bind(mockWriter),
44
- metadata: {
45
- messageId: "test-123",
46
- },
47
- };
48
-
49
- try {
50
- const result = await agent.processMessage(messages, config);
51
- expect(result).toBeDefined();
52
- expect(result.messages).toBeDefined();
53
- expect(result.messages.length).toBeGreaterThan(0);
54
- } catch (error) {
55
- expect(error).toBeDefined();
56
- }
57
- });
58
-
59
- test("handles abort signal", async () => {
60
- await agent.initialize("test-token");
61
-
62
- const controller = new AbortController();
63
- const messages = [new HumanMessage("Test")];
64
-
65
- const config = {
66
- metadata: {
67
- abortSignal: controller.signal,
68
- messageId: "test-abort",
69
- },
70
- };
71
-
72
- setTimeout(() => controller.abort(), 10);
73
-
74
- try {
75
- await agent.processMessage(messages, config);
76
- } catch (error) {
77
- expect(["AbortError", "TypeError"]).toContain((error as Error).name);
78
- }
79
- });
80
-
81
- test("clears conversation", () => {
82
- if (typeof agent.clearConversation === "function") {
83
- agent.clearConversation();
84
- }
85
- expect(agent).toBeDefined();
86
- });
87
-
88
- test("formats messages with system prompt", async () => {
89
- await agent.initialize("test-token");
90
-
91
- const messages = [
92
- new HumanMessage("Create a game"),
93
- new AIMessage("I'll help you create a game"),
94
- ];
95
-
96
- const formatted = agent["formatMessages"](messages, "System prompt");
97
- expect(formatted).toBeDefined();
98
- expect(formatted.length).toBeGreaterThan(0);
99
- expect(formatted[0].role).toBe("system");
100
- });
101
-
102
- test("builds system prompt with documentation", async () => {
103
- await agent.initialize("test-token");
104
-
105
- const prompt = agent["buildSystemPrompt"]();
106
- expect(prompt).toBeDefined();
107
- expect(prompt).toContain("VibeGame");
108
- expect(prompt).toContain("WebGL");
109
- });
110
-
111
- test("handles multiple messages in conversation", async () => {
112
- await agent.initialize("test-token");
113
-
114
- const messages = [
115
- new HumanMessage("First message"),
116
- new AIMessage("First response"),
117
- new HumanMessage("Second message"),
118
- ];
119
-
120
- const config = {
121
- metadata: {
122
- messageId: "test-multi",
123
- },
124
- };
125
-
126
- try {
127
- const result = await agent.processMessage(messages, config);
128
- expect(result.messages).toBeDefined();
129
- } catch (error) {
130
- expect(error).toBeDefined();
131
- }
132
- });
133
- });