Spaces:
Running
Running
Commit
·
db9635c
1
Parent(s):
712a0e0
improved prompting/UX
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- agents.md +0 -88
- bun.lock +18 -9
- layers/structure.md +1 -1
- llms.txt +2430 -0
- package.json +4 -3
- src/App.svelte +7 -1
- src/lib/components/about/About.svelte +310 -0
- src/lib/components/about/context.md +31 -0
- src/lib/components/chat/ChatPanel.svelte +17 -11
- src/lib/components/chat/ExampleMessages.svelte +117 -148
- src/lib/components/chat/ExampleRow.svelte +183 -0
- src/lib/components/chat/Message.svelte +43 -11
- src/lib/components/chat/MessageContent.svelte +4 -3
- src/lib/components/chat/MessageInput.svelte +81 -39
- src/lib/components/chat/MessageList.svelte +60 -29
- src/lib/components/chat/TextRenderer.svelte +66 -4
- src/lib/components/chat/context.md +16 -15
- src/lib/components/chat/segments/TodoSegment.svelte +120 -8
- src/lib/components/chat/segments/ToolBlock.svelte +143 -4
- src/lib/components/game/GameCanvas.svelte +11 -3
- src/lib/components/game/context.md +3 -3
- src/lib/components/layout/AppHeader.svelte +118 -21
- src/lib/components/layout/SplitView.svelte +1 -1
- src/lib/components/layout/context.md +5 -4
- src/lib/config/animations.ts +0 -43
- src/lib/config/context.md +2 -5
- src/lib/controllers/animation-controller.ts +0 -190
- src/lib/controllers/context.md +3 -6
- src/lib/models/context.md +3 -2
- src/lib/models/segment-view.ts +0 -3
- src/lib/server/console-buffer.ts +83 -8
- src/lib/server/context.md +11 -9
- src/lib/server/documentation.ts +1 -1
- src/lib/server/langgraph-agent.ts +344 -200
- src/lib/server/mcp-client.ts +147 -315
- src/lib/server/tools.ts +20 -7
- src/lib/services/content-manager.ts +58 -2
- src/lib/services/context.md +3 -3
- src/lib/services/game-engine.ts +5 -0
- src/lib/services/message-handler.ts +25 -7
- src/lib/services/segment-formatter.ts +0 -3
- src/lib/services/virtual-fs.ts +188 -8
- src/lib/stores/context.md +1 -1
- src/lib/stores/ui.ts +28 -2
- src/lib/utils/context.md +32 -0
- src/lib/utils/tool-call-parser.ts +88 -0
- src/lib/utils/tool-parser-htmlparser2.ts +205 -0
- tests/api.test.ts +0 -229
- tests/console-buffer.test.ts +0 -162
- 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 |
-
"
|
| 18 |
"monaco-editor": "^0.50.0",
|
| 19 |
"svelte-splitpanes": "^8.0.5",
|
| 20 |
-
"vibegame": "^0.1.
|
| 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.
|
| 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.
|
| 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 |
-
├──
|
| 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 |
-
"
|
| 50 |
"monaco-editor": "^0.50.0",
|
| 51 |
"svelte-splitpanes": "^8.0.5",
|
| 52 |
-
"vibegame": "^0.1.
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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><world canvas="#game-canvas" sky="#87ceeb">
|
| 70 |
+
<!-- Ground -->
|
| 71 |
+
<static-part pos="0 -0.5 0" shape="box" size="20 1 20" color="#90ee90"></static-part>
|
| 72 |
+
|
| 73 |
+
<!-- Ball -->
|
| 74 |
+
<dynamic-part pos="-2 4 -3" shape="sphere" size="1" color="#ff4500"></dynamic-part>
|
| 75 |
+
</world>
|
| 76 |
+
|
| 77 |
+
<canvas id="game-canvas"></canvas>
|
| 78 |
+
|
| 79 |
+
<script type="module">
|
| 80 |
+
import * as GAME from 'vibegame';
|
| 81 |
+
GAME.run();
|
| 82 |
+
</script></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(
|
| 144 |
-
border-top: 1px solid rgba(139, 115, 85, 0.
|
| 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(
|
| 154 |
-
border-bottom: 1px solid rgba(139, 115, 85, 0.
|
| 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.
|
| 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.
|
| 173 |
-
color: rgba(251, 248, 244, 0.
|
| 174 |
-
border: 1px solid rgba(139, 115, 85, 0.
|
| 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.
|
| 189 |
-
color: rgba(251, 248, 244, 0.
|
| 190 |
-
border-color: rgba(139, 115, 85, 0.
|
| 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 |
-
|
| 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 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
-
|
| 45 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
}
|
| 47 |
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
|
|
|
|
|
|
|
|
|
| 57 |
}
|
| 58 |
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
|
|
|
|
|
|
|
|
|
| 68 |
}
|
| 69 |
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
|
|
|
|
|
|
| 77 |
}
|
| 78 |
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
|
|
|
|
|
|
| 86 |
}
|
| 87 |
-
</script>
|
| 88 |
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 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 |
-
.
|
| 124 |
-
|
| 125 |
-
font-size: 0.75rem;
|
| 126 |
text-transform: uppercase;
|
| 127 |
-
letter-spacing: 0.
|
| 128 |
-
|
|
|
|
|
|
|
| 129 |
}
|
| 130 |
|
| 131 |
-
.
|
| 132 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
}
|
| 134 |
|
| 135 |
-
|
| 136 |
-
0
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
opacity: 0.8;
|
| 141 |
-
}
|
| 142 |
}
|
| 143 |
|
| 144 |
-
.
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
gap: 0.75rem;
|
| 148 |
-
width: 100%;
|
| 149 |
-
max-width: 600px;
|
| 150 |
}
|
| 151 |
|
| 152 |
-
.
|
| 153 |
-
display: flex;
|
| 154 |
align-items: center;
|
| 155 |
-
gap: 0.
|
| 156 |
padding: 0.5rem 1rem;
|
| 157 |
-
background:
|
| 158 |
-
border: 1px solid rgba(255, 255, 255, 0.
|
| 159 |
-
border-radius:
|
| 160 |
-
|
| 161 |
-
|
|
|
|
|
|
|
|
|
|
| 162 |
font-family: "Monaco", "Menlo", monospace;
|
| 163 |
-
|
| 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 |
-
.
|
| 170 |
-
|
| 171 |
-
|
|
|
|
|
|
|
|
|
|
| 172 |
}
|
| 173 |
|
| 174 |
-
.
|
| 175 |
-
|
| 176 |
}
|
| 177 |
|
| 178 |
-
|
| 179 |
-
|
| 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.
|
|
|
|
|
|
|
| 16 |
{
|
| 17 |
opacity: 0,
|
| 18 |
-
y:
|
| 19 |
-
x: isUser ?
|
| 20 |
scale: 0.98
|
| 21 |
},
|
| 22 |
{
|
|
@@ -24,18 +27,47 @@
|
|
| 24 |
y: 0,
|
| 25 |
x: 0,
|
| 26 |
scale: 1,
|
| 27 |
-
duration: 0.
|
| 28 |
-
ease: "power2.out"
|
| 29 |
-
delay: 0.05
|
| 30 |
}
|
| 31 |
);
|
| 32 |
|
| 33 |
if (!isUser) {
|
| 34 |
-
|
| 35 |
-
{
|
| 36 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 21 |
-
scale: 0.
|
| 22 |
duration: 0.1,
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
|
|
|
|
|
|
| 34 |
}
|
| 35 |
|
| 36 |
-
dispatch("send", value.trim());
|
| 37 |
-
value = "";
|
| 38 |
-
|
| 39 |
if (textareaRef) {
|
| 40 |
-
|
| 41 |
-
{
|
| 42 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|
| 139 |
-
border-top: 1px solid rgba(139, 115, 85, 0.
|
| 140 |
}
|
| 141 |
|
| 142 |
.input-wrapper {
|
|
@@ -161,9 +203,9 @@
|
|
| 161 |
|
| 162 |
textarea {
|
| 163 |
width: 100%;
|
| 164 |
-
background: rgba(
|
| 165 |
-
color: rgba(251, 248, 244, 0.
|
| 166 |
-
border: 1px solid rgba(139, 115, 85, 0.
|
| 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.
|
| 181 |
-
background: rgba(
|
| 182 |
-
box-shadow: 0 0 0 1px rgba(124, 152, 133, 0.
|
| 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.
|
| 201 |
-
color: rgba(124, 152, 133, 0.
|
| 202 |
-
border: 1px solid rgba(124, 152, 133, 0.
|
| 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.
|
| 223 |
-
border-color: rgba(124, 152, 133, 0.
|
| 224 |
transform: translateY(-1px);
|
| 225 |
-
box-shadow: 0 4px 12px rgba(124, 152, 133, 0.
|
| 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.
|
| 250 |
-
color: rgba(184, 84, 80, 0.
|
| 251 |
-
border: 1px solid rgba(184, 84, 80, 0.
|
| 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.
|
| 285 |
-
border-color: rgba(184, 84, 80, 0.
|
| 286 |
transform: translateY(-1px);
|
| 287 |
-
box-shadow: 0 4px 12px rgba(184, 84, 80, 0.
|
| 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
|
| 57 |
</div>
|
| 58 |
{/if}
|
| 59 |
|
|
@@ -101,44 +113,63 @@
|
|
| 101 |
display: flex;
|
| 102 |
align-items: center;
|
| 103 |
gap: 0.5rem;
|
| 104 |
-
padding: 0.
|
| 105 |
-
margin: 0.
|
| 106 |
-
border-left: 2px solid rgba(
|
| 107 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
}
|
| 109 |
|
| 110 |
.typing-dots {
|
| 111 |
display: flex;
|
| 112 |
-
gap: 0.
|
|
|
|
| 113 |
}
|
| 114 |
|
| 115 |
.dot {
|
| 116 |
-
width:
|
| 117 |
-
height:
|
| 118 |
border-radius: 50%;
|
| 119 |
-
background:
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 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.
|
| 141 |
-
font-style:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 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 |
-
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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` -
|
| 12 |
-
- `MessageInput.svelte` - Input with
|
| 13 |
-
- `ExampleMessages.svelte` -
|
| 14 |
-
- `
|
| 15 |
-
- `segments/
|
|
|
|
| 16 |
|
| 17 |
## Architecture
|
| 18 |
|
| 19 |
Clean MVC separation:
|
| 20 |
|
| 21 |
-
- **Model**:
|
| 22 |
- **View**: Component hierarchy with GSAP animations
|
| 23 |
-
- **Controller**: WebSocket handling with
|
| 24 |
|
| 25 |
## Design
|
| 26 |
|
| 27 |
-
-
|
| 28 |
-
-
|
| 29 |
-
-
|
| 30 |
-
-
|
|
|
|
| 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:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 42 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 93 |
font-size: 0.75rem;
|
| 94 |
color: rgba(255, 255, 255, 0.7);
|
|
|
|
| 95 |
}
|
| 96 |
|
| 97 |
-
.task-
|
| 98 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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"
|
| 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 |
-
|
|
|
|
| 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 |
-
|
|
|
|
| 20 |
reloadTimer = setTimeout(async () => {
|
| 21 |
const currentTime = Date.now();
|
| 22 |
-
if (currentTime - lastRestartTime < 2000 || $gameStore.isStarting) {
|
| 23 |
return;
|
| 24 |
}
|
| 25 |
lastRestartTime = currentTime;
|
|
|
|
| 26 |
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
<span class="app-icon">🥕</span>
|
| 67 |
<span class="app-name">VibeGame</span>
|
| 68 |
-
</
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 82 |
-
<
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
|
|
|
|
|
|
| 97 |
</div>
|
| 98 |
|
| 99 |
<div class="header-right">
|
|
@@ -146,14 +176,28 @@
|
|
| 146 |
|
| 147 |
.header-left {
|
| 148 |
justify-content: flex-start;
|
| 149 |
-
gap: 0.
|
|
|
|
| 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
|
| 16 |
-
|
| 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
|
| 16 |
-
|
| 17 |
-
└── animation-controller.ts # Centralized GSAP animations
|
| 18 |
```
|
| 19 |
|
| 20 |
## Scope
|
| 21 |
|
| 22 |
-
- In-scope: Business logic, state orchestration
|
| 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
|
| 14 |
-
|
|
|
|
| 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 =
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
| 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
|
| 8 |
-
- **langgraph-agent.ts** - LangGraph agent with
|
| 9 |
-
- **mcp-client.ts** -
|
| 10 |
-
- **tools.ts** - Console observation
|
| 11 |
-
- **
|
|
|
|
| 12 |
|
| 13 |
## Architecture
|
| 14 |
|
| 15 |
-
LangGraph agent with
|
| 16 |
|
| 17 |
-
- Virtual file system
|
| 18 |
-
-
|
| 19 |
-
-
|
|
|
|
| 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 = "
|
| 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 |
-
|
| 104 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
this.ws.send(
|
| 119 |
JSON.stringify({
|
| 120 |
-
type: "
|
| 121 |
payload: {
|
| 122 |
segmentId: currentSegmentId,
|
| 123 |
-
|
| 124 |
messageId,
|
| 125 |
},
|
| 126 |
timestamp: Date.now(),
|
| 127 |
}),
|
| 128 |
);
|
| 129 |
-
currentSegmentId = null;
|
| 130 |
-
currentSegmentContent = "";
|
| 131 |
}
|
| 132 |
|
| 133 |
-
|
| 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: "
|
| 145 |
payload: {
|
| 146 |
segmentId: currentSegmentId,
|
| 147 |
-
|
| 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 |
-
|
| 176 |
-
if (
|
| 177 |
-
currentSegmentId = `seg_${Date.now()}_${Math.random()}`;
|
| 178 |
-
currentSegmentContent = "";
|
| 179 |
this.ws.send(
|
| 180 |
JSON.stringify({
|
| 181 |
-
type: "
|
| 182 |
payload: {
|
| 183 |
segmentId: currentSegmentId,
|
| 184 |
-
segmentType: "text",
|
| 185 |
messageId,
|
| 186 |
},
|
| 187 |
timestamp: Date.now(),
|
| 188 |
}),
|
| 189 |
);
|
|
|
|
| 190 |
}
|
|
|
|
| 191 |
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
this.ws.send(
|
| 196 |
JSON.stringify({
|
| 197 |
type: "segment_token",
|
| 198 |
payload: {
|
| 199 |
segmentId: currentSegmentId,
|
| 200 |
-
token:
|
| 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 =
|
| 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
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
-
|
| 302 |
-
-
|
| 303 |
-
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 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 |
-
##
|
| 362 |
-
|
| 363 |
-
###
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
| 358 |
}
|
| 359 |
|
| 360 |
const file = virtualFileSystem.getGameFile();
|
| 361 |
this.syncEditorContent(file.content);
|
| 362 |
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 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 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 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 |
-
|
| 476 |
-
|
| 477 |
-
|
| 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 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
*
|
| 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 #
|
| 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
|
| 19 |
├── html-document-parser.ts # HTML parsing using DOMParser
|
| 20 |
-
├── virtual-fs.ts # Virtual file system
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
return {
|
| 123 |
success: false,
|
| 124 |
-
error:
|
| 125 |
};
|
| 126 |
}
|
| 127 |
|
| 128 |
-
|
| 129 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
return {
|
| 131 |
success: false,
|
| 132 |
-
error:
|
| 133 |
};
|
| 134 |
}
|
| 135 |
|
| 136 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
this.updateGameContent(newContent);
|
|
|
|
|
|
|
| 138 |
|
| 139 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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) =>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|