Spaces:
Running
Running
Commit
·
dd99b77
1
Parent(s):
9434357
improve agent
Browse files- bun.lock +2 -2
- llms.txt +2 -1
- package.json +1 -1
- src/App.svelte +2 -8
- src/lib/components/chat/StreamingText.svelte +7 -5
- src/lib/components/game/GameCanvas.svelte +19 -10
- src/lib/components/layout/AppHeader.svelte +2 -2
- src/lib/server/langgraph-agent.ts +129 -121
- src/lib/server/tools.ts +1 -1
- src/lib/services/console-forward.ts +0 -65
- src/lib/services/{console-capture.ts → console-sync.ts} +46 -45
- src/lib/services/context.md +7 -8
- src/lib/services/game-engine.ts +78 -9
- src/lib/services/html-parser.ts +22 -2
- src/lib/stores/editor.ts +34 -7
- src/main.ts +3 -0
bun.lock
CHANGED
|
@@ -14,7 +14,7 @@
|
|
| 14 |
"marked": "^16.2.1",
|
| 15 |
"monaco-editor": "^0.50.0",
|
| 16 |
"svelte-splitpanes": "^8.0.5",
|
| 17 |
-
"vibegame": "^0.1.
|
| 18 |
"zod": "^4.1.8",
|
| 19 |
},
|
| 20 |
"devDependencies": {
|
|
@@ -780,7 +780,7 @@
|
|
| 780 |
|
| 781 |
"uuid": ["[email protected]", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="],
|
| 782 |
|
| 783 |
-
"vibegame": ["[email protected].
|
| 784 |
|
| 785 |
"vite": ["[email protected]", "", { "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=="],
|
| 786 |
|
|
|
|
| 14 |
"marked": "^16.2.1",
|
| 15 |
"monaco-editor": "^0.50.0",
|
| 16 |
"svelte-splitpanes": "^8.0.5",
|
| 17 |
+
"vibegame": "^0.1.4",
|
| 18 |
"zod": "^4.1.8",
|
| 19 |
},
|
| 20 |
"devDependencies": {
|
|
|
|
| 780 |
|
| 781 |
"uuid": ["[email protected]", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="],
|
| 782 |
|
| 783 |
+
"vibegame": ["[email protected].4", "", { "dependencies": { "@dimforge/rapier3d-compat": "^0.18.2", "gsap": "^3.13.0", "zod": "^4.1.5" }, "peerDependencies": { "bitecs": ">=0.3.40", "three": ">=0.170.0" } }, "sha512-U8iZzedz/egganPKym2Kjc7ZG1YJpnZ2CuB1q+e+1H3tOPrH5rBrwUKK+5jfBtgdUkMVT451GIcXioF+YBsKHg=="],
|
| 784 |
|
| 785 |
"vite": ["[email protected]", "", { "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=="],
|
| 786 |
|
llms.txt
CHANGED
|
@@ -101,9 +101,10 @@ const Health = GAME.defineComponent({
|
|
| 101 |
});
|
| 102 |
|
| 103 |
// System = Logic only
|
|
|
|
| 104 |
const DamageSystem: GAME.System = {
|
| 105 |
update: (state) => {
|
| 106 |
-
const entities =
|
| 107 |
for (const entity of entities) {
|
| 108 |
Health.current[entity] -= 1 * state.time.delta;
|
| 109 |
if (Health.current[entity] <= 0) {
|
|
|
|
| 101 |
});
|
| 102 |
|
| 103 |
// System = Logic only
|
| 104 |
+
const healthQuery = GAME.defineQuery([Health]);
|
| 105 |
const DamageSystem: GAME.System = {
|
| 106 |
update: (state) => {
|
| 107 |
+
const entities = healthQuery(state.world);
|
| 108 |
for (const entity of entities) {
|
| 109 |
Health.current[entity] -= 1 * state.time.delta;
|
| 110 |
if (Health.current[entity] <= 0) {
|
package.json
CHANGED
|
@@ -46,7 +46,7 @@
|
|
| 46 |
"marked": "^16.2.1",
|
| 47 |
"monaco-editor": "^0.50.0",
|
| 48 |
"svelte-splitpanes": "^8.0.5",
|
| 49 |
-
"vibegame": "^0.1.
|
| 50 |
"zod": "^4.1.8"
|
| 51 |
}
|
| 52 |
}
|
|
|
|
| 46 |
"marked": "^16.2.1",
|
| 47 |
"monaco-editor": "^0.50.0",
|
| 48 |
"svelte-splitpanes": "^8.0.5",
|
| 49 |
+
"vibegame": "^0.1.4",
|
| 50 |
"zod": "^4.1.8"
|
| 51 |
}
|
| 52 |
}
|
src/App.svelte
CHANGED
|
@@ -1,20 +1,16 @@
|
|
| 1 |
<script lang="ts">
|
| 2 |
import { onMount, onDestroy } from 'svelte';
|
| 3 |
-
import { consoleCapture } from './lib/services/console-capture';
|
| 4 |
-
import { consoleForwarder } from './lib/services/console-forward';
|
| 5 |
import { registerShortcuts, shortcuts } from './lib/config/shortcuts';
|
| 6 |
import { loadingStore } from './lib/stores/loading';
|
| 7 |
import AppHeader from './lib/components/layout/AppHeader.svelte';
|
| 8 |
import SplitView from './lib/components/layout/SplitView.svelte';
|
| 9 |
import LoadingScreen from './lib/components/layout/LoadingScreen.svelte';
|
| 10 |
-
|
| 11 |
let unregisterShortcuts: () => void;
|
| 12 |
-
|
| 13 |
onMount(() => {
|
| 14 |
loadingStore.startLoading();
|
| 15 |
|
| 16 |
-
consoleCapture.setup();
|
| 17 |
-
consoleForwarder.start();
|
| 18 |
unregisterShortcuts = registerShortcuts(shortcuts);
|
| 19 |
|
| 20 |
setTimeout(() => {
|
|
@@ -29,8 +25,6 @@
|
|
| 29 |
});
|
| 30 |
|
| 31 |
onDestroy(() => {
|
| 32 |
-
consoleCapture.teardown();
|
| 33 |
-
consoleForwarder.stop();
|
| 34 |
if (unregisterShortcuts) unregisterShortcuts();
|
| 35 |
});
|
| 36 |
</script>
|
|
|
|
| 1 |
<script lang="ts">
|
| 2 |
import { onMount, onDestroy } from 'svelte';
|
|
|
|
|
|
|
| 3 |
import { registerShortcuts, shortcuts } from './lib/config/shortcuts';
|
| 4 |
import { loadingStore } from './lib/stores/loading';
|
| 5 |
import AppHeader from './lib/components/layout/AppHeader.svelte';
|
| 6 |
import SplitView from './lib/components/layout/SplitView.svelte';
|
| 7 |
import LoadingScreen from './lib/components/layout/LoadingScreen.svelte';
|
| 8 |
+
|
| 9 |
let unregisterShortcuts: () => void;
|
| 10 |
+
|
| 11 |
onMount(() => {
|
| 12 |
loadingStore.startLoading();
|
| 13 |
|
|
|
|
|
|
|
| 14 |
unregisterShortcuts = registerShortcuts(shortcuts);
|
| 15 |
|
| 16 |
setTimeout(() => {
|
|
|
|
| 25 |
});
|
| 26 |
|
| 27 |
onDestroy(() => {
|
|
|
|
|
|
|
| 28 |
if (unregisterShortcuts) unregisterShortcuts();
|
| 29 |
});
|
| 30 |
</script>
|
src/lib/components/chat/StreamingText.svelte
CHANGED
|
@@ -16,7 +16,6 @@
|
|
| 16 |
let lastProcessedLength = 0;
|
| 17 |
|
| 18 |
$: if (streaming && content) {
|
| 19 |
-
// Only process truly new content
|
| 20 |
if (content.length > lastProcessedLength) {
|
| 21 |
const newChars = content.slice(lastProcessedLength);
|
| 22 |
if (newChars) {
|
|
@@ -39,12 +38,10 @@
|
|
| 39 |
isProcessing = true;
|
| 40 |
|
| 41 |
while (buffer.length > 0) {
|
| 42 |
-
// Process multiple characters at once for better performance
|
| 43 |
const chunkSize = Math.min(3, buffer.length);
|
| 44 |
const chunk = buffer.splice(0, chunkSize).join('');
|
| 45 |
displayedContent += chunk;
|
| 46 |
|
| 47 |
-
// Only delay if there are more characters to process
|
| 48 |
if (buffer.length > 0) {
|
| 49 |
await new Promise(resolve => setTimeout(resolve, 1000 / speed));
|
| 50 |
}
|
|
@@ -89,10 +86,15 @@
|
|
| 89 |
onMount(() => {
|
| 90 |
if (streaming) {
|
| 91 |
showCursor();
|
| 92 |
-
// Reset tracking when component mounts with streaming
|
| 93 |
lastProcessedLength = 0;
|
| 94 |
displayedContent = "";
|
| 95 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
gsap.fromTo(containerElement,
|
| 97 |
{ opacity: 0, y: 5 },
|
| 98 |
{ opacity: 1, y: 0, duration: 0.3, ease: "power2.out" }
|
|
@@ -141,4 +143,4 @@
|
|
| 141 |
animation: none;
|
| 142 |
vertical-align: baseline;
|
| 143 |
}
|
| 144 |
-
</style>
|
|
|
|
| 16 |
let lastProcessedLength = 0;
|
| 17 |
|
| 18 |
$: if (streaming && content) {
|
|
|
|
| 19 |
if (content.length > lastProcessedLength) {
|
| 20 |
const newChars = content.slice(lastProcessedLength);
|
| 21 |
if (newChars) {
|
|
|
|
| 38 |
isProcessing = true;
|
| 39 |
|
| 40 |
while (buffer.length > 0) {
|
|
|
|
| 41 |
const chunkSize = Math.min(3, buffer.length);
|
| 42 |
const chunk = buffer.splice(0, chunkSize).join('');
|
| 43 |
displayedContent += chunk;
|
| 44 |
|
|
|
|
| 45 |
if (buffer.length > 0) {
|
| 46 |
await new Promise(resolve => setTimeout(resolve, 1000 / speed));
|
| 47 |
}
|
|
|
|
| 86 |
onMount(() => {
|
| 87 |
if (streaming) {
|
| 88 |
showCursor();
|
|
|
|
| 89 |
lastProcessedLength = 0;
|
| 90 |
displayedContent = "";
|
| 91 |
|
| 92 |
+
if (content) {
|
| 93 |
+
buffer.push(...content.split(''));
|
| 94 |
+
lastProcessedLength = content.length;
|
| 95 |
+
processBuffer();
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
gsap.fromTo(containerElement,
|
| 99 |
{ opacity: 0, y: 5 },
|
| 100 |
{ opacity: 1, y: 0, duration: 0.3, ease: "power2.out" }
|
|
|
|
| 143 |
animation: none;
|
| 144 |
vertical-align: baseline;
|
| 145 |
}
|
| 146 |
+
</style>
|
src/lib/components/game/GameCanvas.svelte
CHANGED
|
@@ -10,24 +10,33 @@
|
|
| 10 |
let reloadTimer: any;
|
| 11 |
let previousContent = '';
|
| 12 |
let isInitialized = false;
|
| 13 |
-
|
|
|
|
| 14 |
$: if ($editorStore.content !== previousContent && $gameStore.isAutoRunning && isInitialized) {
|
| 15 |
previousContent = $editorStore.content;
|
| 16 |
clearTimeout(reloadTimer);
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
}
|
| 24 |
-
|
| 25 |
onMount(async () => {
|
| 26 |
previousContent = $editorStore.content;
|
| 27 |
setTimeout(async () => {
|
| 28 |
if (!$gameStore.instance && $gameStore.isAutoRunning) {
|
| 29 |
-
const
|
| 30 |
-
await gameEngine.start(
|
| 31 |
}
|
| 32 |
isInitialized = true;
|
| 33 |
}, 400);
|
|
|
|
| 10 |
let reloadTimer: any;
|
| 11 |
let previousContent = '';
|
| 12 |
let isInitialized = false;
|
| 13 |
+
let lastRestartTime = 0;
|
| 14 |
+
|
| 15 |
$: if ($editorStore.content !== previousContent && $gameStore.isAutoRunning && isInitialized) {
|
| 16 |
previousContent = $editorStore.content;
|
| 17 |
clearTimeout(reloadTimer);
|
| 18 |
+
|
| 19 |
+
const now = Date.now();
|
| 20 |
+
if (now - lastRestartTime >= 2000) {
|
| 21 |
+
reloadTimer = setTimeout(async () => {
|
| 22 |
+
const currentTime = Date.now();
|
| 23 |
+
if (currentTime - lastRestartTime < 2000 || $gameStore.isStarting) {
|
| 24 |
+
return;
|
| 25 |
+
}
|
| 26 |
+
lastRestartTime = currentTime;
|
| 27 |
+
|
| 28 |
+
const { world, scripts } = HTMLParser.extractGameContent($editorStore.content);
|
| 29 |
+
await gameEngine.start(world, scripts);
|
| 30 |
+
}, 1500);
|
| 31 |
+
}
|
| 32 |
}
|
| 33 |
+
|
| 34 |
onMount(async () => {
|
| 35 |
previousContent = $editorStore.content;
|
| 36 |
setTimeout(async () => {
|
| 37 |
if (!$gameStore.instance && $gameStore.isAutoRunning) {
|
| 38 |
+
const { world, scripts } = HTMLParser.extractGameContent($editorStore.content);
|
| 39 |
+
await gameEngine.start(world, scripts);
|
| 40 |
}
|
| 41 |
isInitialized = true;
|
| 42 |
}, 400);
|
src/lib/components/layout/AppHeader.svelte
CHANGED
|
@@ -9,8 +9,8 @@
|
|
| 9 |
import gsap from 'gsap';
|
| 10 |
|
| 11 |
async function restartGame() {
|
| 12 |
-
const
|
| 13 |
-
await gameEngine.start(
|
| 14 |
}
|
| 15 |
|
| 16 |
function handleViewModeChange(mode: 'code' | 'preview') {
|
|
|
|
| 9 |
import gsap from 'gsap';
|
| 10 |
|
| 11 |
async function restartGame() {
|
| 12 |
+
const { world, scripts } = HTMLParser.extractGameContent($editorStore.content);
|
| 13 |
+
await gameEngine.start(world, scripts);
|
| 14 |
}
|
| 15 |
|
| 16 |
function handleViewModeChange(mode: 'code' | 'preview') {
|
src/lib/server/langgraph-agent.ts
CHANGED
|
@@ -72,24 +72,23 @@ export class LangGraphAgent {
|
|
| 72 |
let currentSegmentContent = "";
|
| 73 |
let buffer = "";
|
| 74 |
const messageId = config?.metadata?.messageId;
|
| 75 |
-
|
|
|
|
| 76 |
|
| 77 |
for await (const token of this.streamModelResponse(messages)) {
|
| 78 |
fullResponse += token;
|
| 79 |
config?.writer?.({ type: "token", content: token });
|
| 80 |
buffer += token;
|
| 81 |
|
| 82 |
-
// Process buffer to separate text from tool calls
|
| 83 |
let processedUpTo = 0;
|
| 84 |
let match;
|
| 85 |
-
toolRegex.lastIndex = 0;
|
| 86 |
|
| 87 |
while ((match = toolRegex.exec(buffer)) !== null) {
|
| 88 |
-
// Send any text before the tool call
|
| 89 |
const textBefore = buffer.substring(processedUpTo, match.index);
|
| 90 |
|
| 91 |
-
if (textBefore.trim()) {
|
| 92 |
-
if (!currentSegmentId
|
| 93 |
currentSegmentId = `seg_${Date.now()}_${Math.random()}`;
|
| 94 |
currentSegmentContent = "";
|
| 95 |
this.ws.send(
|
|
@@ -105,25 +104,20 @@ export class LangGraphAgent {
|
|
| 105 |
);
|
| 106 |
}
|
| 107 |
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
}),
|
| 121 |
-
);
|
| 122 |
-
}
|
| 123 |
-
}
|
| 124 |
}
|
| 125 |
|
| 126 |
-
// End current text segment before tool
|
| 127 |
if (currentSegmentId && currentSegmentContent.trim() && this.ws) {
|
| 128 |
this.ws.send(
|
| 129 |
JSON.stringify({
|
|
@@ -143,11 +137,9 @@ export class LangGraphAgent {
|
|
| 143 |
processedUpTo = match.index + match[0].length;
|
| 144 |
}
|
| 145 |
|
| 146 |
-
// Keep unprocessed text in buffer or flush if no potential tool start
|
| 147 |
if (processedUpTo > 0) {
|
| 148 |
buffer = buffer.substring(processedUpTo);
|
| 149 |
-
} else if (buffer.length > 100 && !buffer.includes("
|
| 150 |
-
// Flush buffer if it's getting large and has no tool pattern
|
| 151 |
if (!currentSegmentId && buffer.trim() && this.ws) {
|
| 152 |
currentSegmentId = `seg_${Date.now()}_${Math.random()}`;
|
| 153 |
currentSegmentContent = "";
|
|
@@ -184,11 +176,7 @@ export class LangGraphAgent {
|
|
| 184 |
}
|
| 185 |
}
|
| 186 |
|
| 187 |
-
|
| 188 |
-
if (
|
| 189 |
-
buffer.trim() &&
|
| 190 |
-
!buffer.match(/\[TOOL:\s*(\w+)(?:\s+({[^}]+}))?\]/)
|
| 191 |
-
) {
|
| 192 |
if (!currentSegmentId && this.ws) {
|
| 193 |
currentSegmentId = `seg_${Date.now()}_${Math.random()}`;
|
| 194 |
currentSegmentContent = "";
|
|
@@ -256,6 +244,24 @@ export class LangGraphAgent {
|
|
| 256 |
};
|
| 257 |
}
|
| 258 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 259 |
return {
|
| 260 |
messages: [new AIMessage(fullResponse)],
|
| 261 |
hasToolCalls: false,
|
|
@@ -292,102 +298,44 @@ export class LangGraphAgent {
|
|
| 292 |
}
|
| 293 |
|
| 294 |
private buildSystemPrompt(): string {
|
| 295 |
-
return `You are an expert VibeGame developer assistant
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 296 |
|
| 297 |
-
VIBEGAME
|
| 298 |
${this.documentation}
|
| 299 |
|
|
|
|
|
|
|
| 300 |
AVAILABLE TOOLS:
|
|
|
|
|
|
|
| 301 |
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
Parameters:
|
| 305 |
-
- query (string, required): Text or regex pattern to search for
|
| 306 |
-
- mode (string, optional): "text" or "regex" (default: "text")
|
| 307 |
-
- contextLines (number, optional): Context lines before/after match (0-5, default: 2)
|
| 308 |
-
Example: [TOOL: search_editor {"query": "dynamic-part", "mode": "text"}]
|
| 309 |
-
|
| 310 |
-
2. read_editor - Read the entire code in the editor
|
| 311 |
-
Use when: Need complete file overview or search returned no results
|
| 312 |
-
No parameters needed
|
| 313 |
-
Example: [TOOL: read_editor]
|
| 314 |
-
|
| 315 |
-
3. read_editor_lines - Read specific lines from the editor
|
| 316 |
-
Use AFTER search_editor: To read detailed context around found elements
|
| 317 |
-
Parameters:
|
| 318 |
-
- startLine (number, required): Starting line number (1-indexed)
|
| 319 |
-
- endLine (number, optional): Ending line number (inclusive)
|
| 320 |
-
Example: [TOOL: read_editor_lines {"startLine": 5, "endLine": 10}]
|
| 321 |
-
|
| 322 |
-
4. edit_editor - Replace specific text in the editor
|
| 323 |
-
Use when: Making targeted changes to existing code
|
| 324 |
-
Parameters:
|
| 325 |
-
- oldText (string, required): Exact text to find and replace
|
| 326 |
-
- newText (string, required): Replacement text
|
| 327 |
-
Example: [TOOL: edit_editor {"oldText": "color=\"#ff4500\"", "newText": "color=\"#00ff00\""}]
|
| 328 |
-
|
| 329 |
-
5. write_editor - Replace entire editor content
|
| 330 |
-
Use when: Creating new file or complete rewrite
|
| 331 |
-
Parameters:
|
| 332 |
-
- content (string, required): Complete new content
|
| 333 |
-
Example: [TOOL: write_editor {"content": "<world>...</world>"}]
|
| 334 |
-
|
| 335 |
-
6. observe_console - Read recent console messages
|
| 336 |
-
Use when: Checking for errors or game state after changes
|
| 337 |
-
No parameters needed
|
| 338 |
-
Example: [TOOL: observe_console]
|
| 339 |
-
|
| 340 |
-
EDITOR EXPLORATION WORKFLOW:
|
| 341 |
-
1. For understanding code structure:
|
| 342 |
-
- Use search_editor to locate specific elements (classes, functions, components)
|
| 343 |
-
- Use read_editor_lines with found line numbers for detailed context
|
| 344 |
-
|
| 345 |
-
2. For making targeted changes:
|
| 346 |
-
- First search_editor to find exact location
|
| 347 |
-
- Then read_editor_lines to understand surrounding context (if needed)
|
| 348 |
-
- Finally edit_editor with precise text replacement
|
| 349 |
-
|
| 350 |
-
3. For broad understanding:
|
| 351 |
-
- Use read_editor to see complete file structure
|
| 352 |
-
- Then search_editor to navigate to specific sections
|
| 353 |
-
|
| 354 |
-
EXAMPLE WORKFLOW - "Change the ball color to blue":
|
| 355 |
-
1. [TOOL: search_editor {"query": "ball", "mode": "text"}]
|
| 356 |
-
2. [TOOL: search_editor {"query": "dynamic-part", "mode": "text"}]
|
| 357 |
-
3. [TOOL: read_editor_lines {"startLine": 12, "endLine": 12}]
|
| 358 |
-
4. [TOOL: edit_editor {"oldText": "color=\"#ff4500\"", "newText": "color=\"#0000ff\""}]
|
| 359 |
-
|
| 360 |
-
IMPORTANT NOTES:
|
| 361 |
-
- When search_editor returns no matches, try alternative search terms or use read_editor
|
| 362 |
-
- Always continue after receiving tool results - don't stop until task is complete
|
| 363 |
-
- After making changes, check observe_console for any errors
|
| 364 |
-
|
| 365 |
-
Be concise, accurate, and focus on practical solutions.`;
|
| 366 |
-
}
|
| 367 |
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
systemPrompt: string,
|
| 371 |
-
): Array<{ role: string; content: string }> {
|
| 372 |
-
const formatted = [
|
| 373 |
-
{ role: "system", content: systemPrompt },
|
| 374 |
-
...messages.map((msg) => {
|
| 375 |
-
let role = "assistant";
|
| 376 |
-
if (msg instanceof HumanMessage) {
|
| 377 |
-
role = "user";
|
| 378 |
-
} else if (msg instanceof ToolMessage) {
|
| 379 |
-
const content = `Tool result for ${msg.name}: ${msg.content}`;
|
| 380 |
-
return { role: "assistant", content };
|
| 381 |
-
}
|
| 382 |
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 391 |
}
|
| 392 |
|
| 393 |
private async *streamModelResponse(
|
|
@@ -421,7 +369,7 @@ Be concise, accurate, and focus on practical solutions.`;
|
|
| 421 |
response: string,
|
| 422 |
): Array<{ name: string; args: Record<string, unknown> }> {
|
| 423 |
const toolCalls = [];
|
| 424 |
-
const toolRegex =
|
| 425 |
let match;
|
| 426 |
|
| 427 |
while ((match = toolRegex.exec(response)) !== null) {
|
|
@@ -448,6 +396,66 @@ Be concise, accurate, and focus on practical solutions.`;
|
|
| 448 |
return toolCalls;
|
| 449 |
}
|
| 450 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 451 |
private async executeToolsWithSegments(
|
| 452 |
toolCalls: Array<{ name: string; args: Record<string, unknown> }>,
|
| 453 |
messageId?: string,
|
|
|
|
| 72 |
let currentSegmentContent = "";
|
| 73 |
let buffer = "";
|
| 74 |
const messageId = config?.metadata?.messageId;
|
| 75 |
+
|
| 76 |
+
const toolRegex = /TOOL:\s*(\w+)\s+ARGS:\s*({[^}]*})/g;
|
| 77 |
|
| 78 |
for await (const token of this.streamModelResponse(messages)) {
|
| 79 |
fullResponse += token;
|
| 80 |
config?.writer?.({ type: "token", content: token });
|
| 81 |
buffer += token;
|
| 82 |
|
|
|
|
| 83 |
let processedUpTo = 0;
|
| 84 |
let match;
|
| 85 |
+
toolRegex.lastIndex = 0;
|
| 86 |
|
| 87 |
while ((match = toolRegex.exec(buffer)) !== null) {
|
|
|
|
| 88 |
const textBefore = buffer.substring(processedUpTo, match.index);
|
| 89 |
|
| 90 |
+
if (textBefore.trim() && this.ws) {
|
| 91 |
+
if (!currentSegmentId) {
|
| 92 |
currentSegmentId = `seg_${Date.now()}_${Math.random()}`;
|
| 93 |
currentSegmentContent = "";
|
| 94 |
this.ws.send(
|
|
|
|
| 104 |
);
|
| 105 |
}
|
| 106 |
|
| 107 |
+
currentSegmentContent += textBefore;
|
| 108 |
+
this.ws.send(
|
| 109 |
+
JSON.stringify({
|
| 110 |
+
type: "segment_token",
|
| 111 |
+
payload: {
|
| 112 |
+
segmentId: currentSegmentId,
|
| 113 |
+
token: textBefore,
|
| 114 |
+
messageId,
|
| 115 |
+
},
|
| 116 |
+
timestamp: Date.now(),
|
| 117 |
+
}),
|
| 118 |
+
);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
}
|
| 120 |
|
|
|
|
| 121 |
if (currentSegmentId && currentSegmentContent.trim() && this.ws) {
|
| 122 |
this.ws.send(
|
| 123 |
JSON.stringify({
|
|
|
|
| 137 |
processedUpTo = match.index + match[0].length;
|
| 138 |
}
|
| 139 |
|
|
|
|
| 140 |
if (processedUpTo > 0) {
|
| 141 |
buffer = buffer.substring(processedUpTo);
|
| 142 |
+
} else if (buffer.length > 100 && !buffer.includes("TOOL:")) {
|
|
|
|
| 143 |
if (!currentSegmentId && buffer.trim() && this.ws) {
|
| 144 |
currentSegmentId = `seg_${Date.now()}_${Math.random()}`;
|
| 145 |
currentSegmentContent = "";
|
|
|
|
| 176 |
}
|
| 177 |
}
|
| 178 |
|
| 179 |
+
if (buffer.trim() && !buffer.match(/TOOL:\s*(\w+)\s+ARGS:\s*({[^}]*})/)) {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
if (!currentSegmentId && this.ws) {
|
| 181 |
currentSegmentId = `seg_${Date.now()}_${Math.random()}`;
|
| 182 |
currentSegmentContent = "";
|
|
|
|
| 244 |
};
|
| 245 |
}
|
| 246 |
|
| 247 |
+
if (state.messages.length > 0) {
|
| 248 |
+
const lastUserMessage = state.messages[state.messages.length - 1];
|
| 249 |
+
if (lastUserMessage instanceof HumanMessage) {
|
| 250 |
+
const needsTools = this.shouldUseTools(
|
| 251 |
+
lastUserMessage.content as string,
|
| 252 |
+
);
|
| 253 |
+
if (needsTools && !state.hasToolCalls) {
|
| 254 |
+
const reminderMessage = new AIMessage(
|
| 255 |
+
"I need to use tools to complete this task. Let me try again with the appropriate tool.",
|
| 256 |
+
);
|
| 257 |
+
return {
|
| 258 |
+
messages: [reminderMessage],
|
| 259 |
+
hasToolCalls: false,
|
| 260 |
+
};
|
| 261 |
+
}
|
| 262 |
+
}
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
return {
|
| 266 |
messages: [new AIMessage(fullResponse)],
|
| 267 |
hasToolCalls: false,
|
|
|
|
| 298 |
}
|
| 299 |
|
| 300 |
private buildSystemPrompt(): string {
|
| 301 |
+
return `You are an expert VibeGame developer assistant that MUST use tools to complete tasks.
|
| 302 |
+
|
| 303 |
+
CRITICAL INSTRUCTIONS:
|
| 304 |
+
- You MUST use tools for ALL tasks. NEVER provide instructions without executing them.
|
| 305 |
+
- You MUST respond using the EXACT format: TOOL: tool_name ARGS: {"param": "value"}
|
| 306 |
+
- After using a tool, wait for the result before proceeding
|
| 307 |
+
- Chain multiple tool calls to complete complex tasks
|
| 308 |
|
| 309 |
+
VIBEGAME CONTEXT:
|
| 310 |
${this.documentation}
|
| 311 |
|
| 312 |
+
The game auto-reloads on every change. The GAME import is automatically provided by the framework.
|
| 313 |
+
|
| 314 |
AVAILABLE TOOLS:
|
| 315 |
+
1. search_editor - Find text/patterns in code
|
| 316 |
+
Example: TOOL: search_editor ARGS: {"query": "dynamic-part"}
|
| 317 |
|
| 318 |
+
2. read_editor - Read entire editor content
|
| 319 |
+
Example: TOOL: read_editor ARGS: {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 320 |
|
| 321 |
+
3. read_editor_lines - Read specific lines (use after search_editor)
|
| 322 |
+
Example: TOOL: read_editor_lines ARGS: {"startLine": 10, "endLine": 20}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 323 |
|
| 324 |
+
4. edit_editor - Replace specific text
|
| 325 |
+
Example: TOOL: edit_editor ARGS: {"oldText": "color='red'", "newText": "color='blue'"}
|
| 326 |
+
|
| 327 |
+
5. write_editor - Replace entire content
|
| 328 |
+
Example: TOOL: write_editor ARGS: {"content": "<world>...</world>"}
|
| 329 |
+
|
| 330 |
+
6. observe_console - Check console for errors
|
| 331 |
+
Example: TOOL: observe_console ARGS: {}
|
| 332 |
+
|
| 333 |
+
WORKFLOW:
|
| 334 |
+
- To find code: TOOL: search_editor ARGS: {"query": "search_term"}
|
| 335 |
+
- To make changes: TOOL: edit_editor ARGS: {"oldText": "...", "newText": "..."}
|
| 336 |
+
- After changes: TOOL: observe_console ARGS: {}
|
| 337 |
+
|
| 338 |
+
IMPORTANT: You are an executor. Take action immediately using tools, don't explain what you would do.`;
|
| 339 |
}
|
| 340 |
|
| 341 |
private async *streamModelResponse(
|
|
|
|
| 369 |
response: string,
|
| 370 |
): Array<{ name: string; args: Record<string, unknown> }> {
|
| 371 |
const toolCalls = [];
|
| 372 |
+
const toolRegex = /TOOL:\s*(\w+)\s+ARGS:\s*({[^}]*})/gs;
|
| 373 |
let match;
|
| 374 |
|
| 375 |
while ((match = toolRegex.exec(response)) !== null) {
|
|
|
|
| 396 |
return toolCalls;
|
| 397 |
}
|
| 398 |
|
| 399 |
+
private shouldUseTools(content: string): boolean {
|
| 400 |
+
const lowerContent = content.toLowerCase();
|
| 401 |
+
|
| 402 |
+
const actionKeywords = [
|
| 403 |
+
"find",
|
| 404 |
+
"search",
|
| 405 |
+
"look for",
|
| 406 |
+
"where",
|
| 407 |
+
"change",
|
| 408 |
+
"modify",
|
| 409 |
+
"update",
|
| 410 |
+
"edit",
|
| 411 |
+
"replace",
|
| 412 |
+
"show",
|
| 413 |
+
"display",
|
| 414 |
+
"read",
|
| 415 |
+
"what",
|
| 416 |
+
"check",
|
| 417 |
+
"create",
|
| 418 |
+
"write",
|
| 419 |
+
"add",
|
| 420 |
+
"implement",
|
| 421 |
+
"fix",
|
| 422 |
+
"debug",
|
| 423 |
+
"error",
|
| 424 |
+
"console",
|
| 425 |
+
];
|
| 426 |
+
|
| 427 |
+
return actionKeywords.some((keyword) => lowerContent.includes(keyword));
|
| 428 |
+
}
|
| 429 |
+
|
| 430 |
+
private formatMessages(
|
| 431 |
+
messages: BaseMessage[],
|
| 432 |
+
systemPrompt: string,
|
| 433 |
+
): Array<{ role: string; content: string }> {
|
| 434 |
+
const formatted = [
|
| 435 |
+
{ role: "system", content: systemPrompt },
|
| 436 |
+
...messages.map((msg) => {
|
| 437 |
+
let role = "assistant";
|
| 438 |
+
if (msg instanceof HumanMessage) {
|
| 439 |
+
role = "user";
|
| 440 |
+
} else if (msg instanceof ToolMessage) {
|
| 441 |
+
const content = `Tool result for ${msg.name}: ${
|
| 442 |
+
typeof msg.content === "string"
|
| 443 |
+
? msg.content
|
| 444 |
+
: JSON.stringify(msg.content)
|
| 445 |
+
}`;
|
| 446 |
+
return { role: "assistant", content };
|
| 447 |
+
}
|
| 448 |
+
|
| 449 |
+
const content =
|
| 450 |
+
typeof msg.content === "string"
|
| 451 |
+
? msg.content
|
| 452 |
+
: JSON.stringify(msg.content);
|
| 453 |
+
return { role, content };
|
| 454 |
+
}),
|
| 455 |
+
];
|
| 456 |
+
return formatted;
|
| 457 |
+
}
|
| 458 |
+
|
| 459 |
private async executeToolsWithSegments(
|
| 460 |
toolCalls: Array<{ name: string; args: Record<string, unknown> }>,
|
| 461 |
messageId?: string,
|
src/lib/server/tools.ts
CHANGED
|
@@ -245,7 +245,7 @@ export const searchEditorTool = new DynamicStructuredTool({
|
|
| 245 |
try {
|
| 246 |
const regex = new RegExp(input.query);
|
| 247 |
isMatch = regex.test(lines[i]);
|
| 248 |
-
} catch
|
| 249 |
return `Error: Invalid regex pattern "${input.query}"`;
|
| 250 |
}
|
| 251 |
}
|
|
|
|
| 245 |
try {
|
| 246 |
const regex = new RegExp(input.query);
|
| 247 |
isMatch = regex.test(lines[i]);
|
| 248 |
+
} catch {
|
| 249 |
return `Error: Invalid regex pattern "${input.query}"`;
|
| 250 |
}
|
| 251 |
}
|
src/lib/services/console-forward.ts
DELETED
|
@@ -1,65 +0,0 @@
|
|
| 1 |
-
import { consoleStore, type ConsoleMessage } from "../stores/console";
|
| 2 |
-
import { agentService } from "./agent";
|
| 3 |
-
|
| 4 |
-
export class ConsoleForwarder {
|
| 5 |
-
private static instance: ConsoleForwarder | null = null;
|
| 6 |
-
private unsubscribe: (() => void) | null = null;
|
| 7 |
-
private lastForwardedId: string | null = null;
|
| 8 |
-
|
| 9 |
-
private constructor() {}
|
| 10 |
-
|
| 11 |
-
static getInstance(): ConsoleForwarder {
|
| 12 |
-
if (!ConsoleForwarder.instance) {
|
| 13 |
-
ConsoleForwarder.instance = new ConsoleForwarder();
|
| 14 |
-
}
|
| 15 |
-
return ConsoleForwarder.instance;
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
start(): void {
|
| 19 |
-
if (this.unsubscribe) return;
|
| 20 |
-
|
| 21 |
-
this.unsubscribe = consoleStore.subscribe((state) => {
|
| 22 |
-
if (state.messages.length === 0) return;
|
| 23 |
-
|
| 24 |
-
const messagesToForward = this.lastForwardedId
|
| 25 |
-
? state.messages.filter(
|
| 26 |
-
(msg) =>
|
| 27 |
-
msg.timestamp >
|
| 28 |
-
(state.messages.find((m) => m.id === this.lastForwardedId)
|
| 29 |
-
?.timestamp || 0),
|
| 30 |
-
)
|
| 31 |
-
: state.messages;
|
| 32 |
-
|
| 33 |
-
if (messagesToForward.length > 0) {
|
| 34 |
-
this.forwardMessages(messagesToForward);
|
| 35 |
-
this.lastForwardedId =
|
| 36 |
-
messagesToForward[messagesToForward.length - 1].id;
|
| 37 |
-
}
|
| 38 |
-
});
|
| 39 |
-
}
|
| 40 |
-
|
| 41 |
-
private forwardMessages(messages: ConsoleMessage[]): void {
|
| 42 |
-
messages.forEach((message) => {
|
| 43 |
-
agentService.sendRawMessage({
|
| 44 |
-
type: "console_sync",
|
| 45 |
-
payload: {
|
| 46 |
-
id: message.id,
|
| 47 |
-
type: message.type,
|
| 48 |
-
message: message.message,
|
| 49 |
-
timestamp: message.timestamp,
|
| 50 |
-
},
|
| 51 |
-
timestamp: Date.now(),
|
| 52 |
-
});
|
| 53 |
-
});
|
| 54 |
-
}
|
| 55 |
-
|
| 56 |
-
stop(): void {
|
| 57 |
-
if (this.unsubscribe) {
|
| 58 |
-
this.unsubscribe();
|
| 59 |
-
this.unsubscribe = null;
|
| 60 |
-
}
|
| 61 |
-
this.lastForwardedId = null;
|
| 62 |
-
}
|
| 63 |
-
}
|
| 64 |
-
|
| 65 |
-
export const consoleForwarder = ConsoleForwarder.getInstance();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/lib/services/{console-capture.ts → console-sync.ts}
RENAMED
|
@@ -1,50 +1,35 @@
|
|
| 1 |
import { consoleStore } from "../stores/console";
|
|
|
|
| 2 |
|
| 3 |
type ConsoleMethod = "log" | "warn" | "error" | "info";
|
| 4 |
-
type ConsoleMethodFunc = (...args: unknown[]) => void;
|
| 5 |
-
type OriginalConsole = Record<ConsoleMethod, ConsoleMethodFunc>;
|
| 6 |
|
| 7 |
-
export class
|
| 8 |
-
private
|
| 9 |
-
private originalConsole:
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
private constructor() {}
|
| 13 |
-
|
| 14 |
-
static getInstance(): ConsoleCapture {
|
| 15 |
-
if (!ConsoleCapture.instance) {
|
| 16 |
-
ConsoleCapture.instance = new ConsoleCapture();
|
| 17 |
-
}
|
| 18 |
-
return ConsoleCapture.instance;
|
| 19 |
-
}
|
| 20 |
|
| 21 |
setup(): void {
|
| 22 |
-
if (this.
|
| 23 |
|
| 24 |
this.originalConsole = {
|
| 25 |
-
log: console.log,
|
| 26 |
-
warn: console.warn,
|
| 27 |
-
error: console.error,
|
| 28 |
-
info: console.info,
|
| 29 |
};
|
| 30 |
|
| 31 |
-
const
|
| 32 |
-
|
| 33 |
-
"warn",
|
| 34 |
-
"error",
|
| 35 |
-
"info",
|
| 36 |
-
];
|
| 37 |
-
|
| 38 |
-
methods.forEach((method) => {
|
| 39 |
console[method] = (...args: unknown[]) => {
|
| 40 |
-
|
| 41 |
|
| 42 |
const firstArg = args[0];
|
| 43 |
if (typeof firstArg === "string") {
|
| 44 |
if (
|
| 45 |
firstArg.includes("[vite]") ||
|
| 46 |
firstArg.includes("[VibeGame] Console forwarding") ||
|
| 47 |
-
firstArg.includes("[DEBUG]")
|
|
|
|
| 48 |
) {
|
| 49 |
return;
|
| 50 |
}
|
|
@@ -65,26 +50,42 @@ export class ConsoleCapture {
|
|
| 65 |
})
|
| 66 |
.join(" ");
|
| 67 |
|
| 68 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
};
|
| 70 |
-
}
|
| 71 |
|
| 72 |
-
|
|
|
|
|
|
|
|
|
|
| 73 |
}
|
| 74 |
|
| 75 |
teardown(): void {
|
| 76 |
-
if (!this.
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
}
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
|
|
|
| 87 |
}
|
| 88 |
}
|
| 89 |
|
| 90 |
-
export const
|
|
|
|
| 1 |
import { consoleStore } from "../stores/console";
|
| 2 |
+
import { agentService } from "./agent";
|
| 3 |
|
| 4 |
type ConsoleMethod = "log" | "warn" | "error" | "info";
|
|
|
|
|
|
|
| 5 |
|
| 6 |
+
export class ConsoleSyncService {
|
| 7 |
+
private isSetup = false;
|
| 8 |
+
private originalConsole: Record<ConsoleMethod, (...args: unknown[]) => void> =
|
| 9 |
+
{} as Record<ConsoleMethod, (...args: unknown[]) => void>;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
setup(): void {
|
| 12 |
+
if (this.isSetup) return;
|
| 13 |
|
| 14 |
this.originalConsole = {
|
| 15 |
+
log: console.log.bind(console),
|
| 16 |
+
warn: console.warn.bind(console),
|
| 17 |
+
error: console.error.bind(console),
|
| 18 |
+
info: console.info.bind(console),
|
| 19 |
};
|
| 20 |
|
| 21 |
+
const interceptConsole = (method: ConsoleMethod) => {
|
| 22 |
+
const original = this.originalConsole[method];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
console[method] = (...args: unknown[]) => {
|
| 24 |
+
original(...args);
|
| 25 |
|
| 26 |
const firstArg = args[0];
|
| 27 |
if (typeof firstArg === "string") {
|
| 28 |
if (
|
| 29 |
firstArg.includes("[vite]") ||
|
| 30 |
firstArg.includes("[VibeGame] Console forwarding") ||
|
| 31 |
+
firstArg.includes("[DEBUG]") ||
|
| 32 |
+
firstArg.includes("hot updated:")
|
| 33 |
) {
|
| 34 |
return;
|
| 35 |
}
|
|
|
|
| 50 |
})
|
| 51 |
.join(" ");
|
| 52 |
|
| 53 |
+
const messageId = `${Date.now()}-${Math.random()}`;
|
| 54 |
+
consoleStore.addMessage(method === "log" ? "info" : method, message);
|
| 55 |
+
|
| 56 |
+
agentService.sendRawMessage({
|
| 57 |
+
type: "console_sync",
|
| 58 |
+
payload: {
|
| 59 |
+
id: messageId,
|
| 60 |
+
type: method === "log" ? "info" : method,
|
| 61 |
+
message: message,
|
| 62 |
+
timestamp: Date.now(),
|
| 63 |
+
},
|
| 64 |
+
timestamp: Date.now(),
|
| 65 |
+
});
|
| 66 |
};
|
| 67 |
+
};
|
| 68 |
|
| 69 |
+
(["log", "warn", "error", "info"] as ConsoleMethod[]).forEach(
|
| 70 |
+
interceptConsole,
|
| 71 |
+
);
|
| 72 |
+
this.isSetup = true;
|
| 73 |
}
|
| 74 |
|
| 75 |
teardown(): void {
|
| 76 |
+
if (!this.isSetup) return;
|
| 77 |
+
|
| 78 |
+
console.log = this.originalConsole.log;
|
| 79 |
+
console.warn = this.originalConsole.warn;
|
| 80 |
+
console.error = this.originalConsole.error;
|
| 81 |
+
console.info = this.originalConsole.info;
|
| 82 |
+
|
| 83 |
+
this.originalConsole = {} as Record<
|
| 84 |
+
ConsoleMethod,
|
| 85 |
+
(...args: unknown[]) => void
|
| 86 |
+
>;
|
| 87 |
+
this.isSetup = false;
|
| 88 |
}
|
| 89 |
}
|
| 90 |
|
| 91 |
+
export const consoleSyncService = new ConsoleSyncService();
|
src/lib/services/context.md
CHANGED
|
@@ -18,8 +18,7 @@ services/
|
|
| 18 |
├── websocket.ts # WebSocket connection management
|
| 19 |
├── message-handler.ts # WebSocket message processing
|
| 20 |
├── game-engine.ts # Game lifecycle management
|
| 21 |
-
├── console-
|
| 22 |
-
├── console-forward.ts # WebSocket console forwarding
|
| 23 |
└── html-parser.ts # Game HTML parsing
|
| 24 |
```
|
| 25 |
|
|
@@ -38,12 +37,12 @@ services/
|
|
| 38 |
- `agentService.disconnect()` - Disconnect from server
|
| 39 |
- `agentService.sendMessage()` - Send chat message
|
| 40 |
- `agentService.sendRawMessage()` - Send raw WebSocket message
|
| 41 |
-
- `gameEngine.start()` - Start game with world
|
| 42 |
-
- `gameEngine.stop()` -
|
| 43 |
-
- `
|
| 44 |
-
- `
|
| 45 |
-
- `
|
| 46 |
-
- `HTMLParser.extractGameContent()` - Parse world from HTML
|
| 47 |
|
| 48 |
## Dependencies
|
| 49 |
|
|
|
|
| 18 |
├── websocket.ts # WebSocket connection management
|
| 19 |
├── message-handler.ts # WebSocket message processing
|
| 20 |
├── game-engine.ts # Game lifecycle management
|
| 21 |
+
├── console-sync.ts # VibeGame console synchronization
|
|
|
|
| 22 |
└── html-parser.ts # Game HTML parsing
|
| 23 |
```
|
| 24 |
|
|
|
|
| 37 |
- `agentService.disconnect()` - Disconnect from server
|
| 38 |
- `agentService.sendMessage()` - Send chat message
|
| 39 |
- `agentService.sendRawMessage()` - Send raw WebSocket message
|
| 40 |
+
- `gameEngine.start(worldContent, scripts)` - Start game with parsed world and scripts
|
| 41 |
+
- `gameEngine.stop()` - Destroy game instance and clean up
|
| 42 |
+
- `gameEngine.isRunning()` - Check if game is active
|
| 43 |
+
- `consoleSyncService.setup()` - Intercept console methods
|
| 44 |
+
- `consoleSyncService.teardown()` - Restore original console
|
| 45 |
+
- `HTMLParser.extractGameContent(html)` - Parse world and scripts from HTML
|
| 46 |
|
| 47 |
## Dependencies
|
| 48 |
|
src/lib/services/game-engine.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
import * as GAME from "vibegame";
|
|
|
|
| 2 |
import { gameStore } from "../stores/game";
|
| 3 |
import { consoleStore } from "../stores/console";
|
| 4 |
import { uiStore } from "../stores/ui";
|
|
@@ -18,11 +19,15 @@ export class GameEngine {
|
|
| 18 |
return GameEngine.instance;
|
| 19 |
}
|
| 20 |
|
| 21 |
-
async start(worldContent: string): Promise<void> {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
gameStore.setStarting(true);
|
| 23 |
consoleStore.addMessage("info", "🎮 Starting game...");
|
| 24 |
-
|
| 25 |
-
this.stop();
|
| 26 |
uiStore.setError(null);
|
| 27 |
|
| 28 |
try {
|
|
@@ -33,6 +38,69 @@ export class GameEngine {
|
|
| 33 |
|
| 34 |
container.innerHTML = worldContent;
|
| 35 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
this.gameInstance = await GAME.run();
|
| 37 |
gameStore.setInstance(this.gameInstance);
|
| 38 |
consoleStore.addMessage("info", "✅ Game started!");
|
|
@@ -41,6 +109,7 @@ export class GameEngine {
|
|
| 41 |
uiStore.setError(errorMsg);
|
| 42 |
consoleStore.addMessage("error", `❌ Error: ${errorMsg}`);
|
| 43 |
gameStore.setInstance(null);
|
|
|
|
| 44 |
} finally {
|
| 45 |
gameStore.setStarting(false);
|
| 46 |
}
|
|
@@ -49,13 +118,11 @@ export class GameEngine {
|
|
| 49 |
stop(): void {
|
| 50 |
if (this.gameInstance) {
|
| 51 |
try {
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
} else if (typeof this.gameInstance.stop === "function") {
|
| 55 |
-
this.gameInstance.stop();
|
| 56 |
-
}
|
| 57 |
} catch (error) {
|
| 58 |
-
console.error("Error
|
|
|
|
| 59 |
}
|
| 60 |
this.gameInstance = null;
|
| 61 |
gameStore.setInstance(null);
|
|
@@ -67,6 +134,8 @@ export class GameEngine {
|
|
| 67 |
container.removeChild(container.firstChild);
|
| 68 |
}
|
| 69 |
}
|
|
|
|
|
|
|
| 70 |
}
|
| 71 |
|
| 72 |
isRunning(): boolean {
|
|
|
|
| 1 |
import * as GAME from "vibegame";
|
| 2 |
+
import type { System, Plugin, Component, BuilderOptions } from "vibegame";
|
| 3 |
import { gameStore } from "../stores/game";
|
| 4 |
import { consoleStore } from "../stores/console";
|
| 5 |
import { uiStore } from "../stores/ui";
|
|
|
|
| 19 |
return GameEngine.instance;
|
| 20 |
}
|
| 21 |
|
| 22 |
+
async start(worldContent: string, scripts: string[] = []): Promise<void> {
|
| 23 |
+
if (this.gameInstance) {
|
| 24 |
+
consoleStore.addMessage("info", "Stopping previous game instance...");
|
| 25 |
+
this.stop();
|
| 26 |
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
gameStore.setStarting(true);
|
| 30 |
consoleStore.addMessage("info", "🎮 Starting game...");
|
|
|
|
|
|
|
| 31 |
uiStore.setError(null);
|
| 32 |
|
| 33 |
try {
|
|
|
|
| 38 |
|
| 39 |
container.innerHTML = worldContent;
|
| 40 |
|
| 41 |
+
GAME.resetBuilder();
|
| 42 |
+
|
| 43 |
+
const gameProxy = {
|
| 44 |
+
withSystem: (system: System) => {
|
| 45 |
+
GAME.withSystem(system);
|
| 46 |
+
return gameProxy;
|
| 47 |
+
},
|
| 48 |
+
withPlugin: (plugin: Plugin) => {
|
| 49 |
+
GAME.withPlugin(plugin);
|
| 50 |
+
return gameProxy;
|
| 51 |
+
},
|
| 52 |
+
withComponent: (name: string, component: Component) => {
|
| 53 |
+
GAME.withComponent(name, component);
|
| 54 |
+
return gameProxy;
|
| 55 |
+
},
|
| 56 |
+
configure: (options: BuilderOptions) => {
|
| 57 |
+
GAME.configure(options);
|
| 58 |
+
return gameProxy;
|
| 59 |
+
},
|
| 60 |
+
withoutDefaultPlugins: () => {
|
| 61 |
+
GAME.withoutDefaultPlugins();
|
| 62 |
+
return gameProxy;
|
| 63 |
+
},
|
| 64 |
+
run: () => {
|
| 65 |
+
console.warn(
|
| 66 |
+
"GAME.run() is not available in user scripts - the framework handles game lifecycle",
|
| 67 |
+
);
|
| 68 |
+
return Promise.resolve({
|
| 69 |
+
stop: () => {},
|
| 70 |
+
destroy: () => {},
|
| 71 |
+
step: () => {},
|
| 72 |
+
getState: () => null,
|
| 73 |
+
});
|
| 74 |
+
},
|
| 75 |
+
defineComponent: GAME.defineComponent,
|
| 76 |
+
defineQuery: GAME.defineQuery,
|
| 77 |
+
Types: GAME.Types,
|
| 78 |
+
};
|
| 79 |
+
|
| 80 |
+
(window as unknown as { GAME: typeof gameProxy }).GAME = gameProxy;
|
| 81 |
+
|
| 82 |
+
let scriptExecutionFailed = false;
|
| 83 |
+
for (const script of scripts) {
|
| 84 |
+
try {
|
| 85 |
+
const cleanedScript = script.replace(/GAME\.run\(\)/g, "");
|
| 86 |
+
eval(cleanedScript);
|
| 87 |
+
} catch (scriptError) {
|
| 88 |
+
scriptExecutionFailed = true;
|
| 89 |
+
const errorMsg =
|
| 90 |
+
scriptError instanceof Error
|
| 91 |
+
? scriptError.message
|
| 92 |
+
: String(scriptError);
|
| 93 |
+
console.error("Error executing user script:", errorMsg);
|
| 94 |
+
consoleStore.addMessage("error", `Script error: ${errorMsg}`);
|
| 95 |
+
}
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
(window as unknown as { GAME: typeof gameProxy | null }).GAME = null;
|
| 99 |
+
|
| 100 |
+
if (scriptExecutionFailed) {
|
| 101 |
+
throw new Error("Script execution failed - game not started");
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
this.gameInstance = await GAME.run();
|
| 105 |
gameStore.setInstance(this.gameInstance);
|
| 106 |
consoleStore.addMessage("info", "✅ Game started!");
|
|
|
|
| 109 |
uiStore.setError(errorMsg);
|
| 110 |
consoleStore.addMessage("error", `❌ Error: ${errorMsg}`);
|
| 111 |
gameStore.setInstance(null);
|
| 112 |
+
this.gameInstance = null;
|
| 113 |
} finally {
|
| 114 |
gameStore.setStarting(false);
|
| 115 |
}
|
|
|
|
| 118 |
stop(): void {
|
| 119 |
if (this.gameInstance) {
|
| 120 |
try {
|
| 121 |
+
this.gameInstance.destroy();
|
| 122 |
+
consoleStore.addMessage("info", "Game instance destroyed");
|
|
|
|
|
|
|
|
|
|
| 123 |
} catch (error) {
|
| 124 |
+
console.error("Error destroying game:", error);
|
| 125 |
+
consoleStore.addMessage("error", `Error destroying game: ${error}`);
|
| 126 |
}
|
| 127 |
this.gameInstance = null;
|
| 128 |
gameStore.setInstance(null);
|
|
|
|
| 134 |
container.removeChild(container.firstChild);
|
| 135 |
}
|
| 136 |
}
|
| 137 |
+
|
| 138 |
+
GAME.resetBuilder();
|
| 139 |
}
|
| 140 |
|
| 141 |
isRunning(): boolean {
|
src/lib/services/html-parser.ts
CHANGED
|
@@ -1,7 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
export class HTMLParser {
|
| 2 |
-
static extractGameContent(html: string):
|
| 3 |
const worldMatch = html.match(/<world[^>]*>[\s\S]*?<\/world>/);
|
| 4 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
}
|
| 6 |
|
| 7 |
static validateGameHTML(html: string): { valid: boolean; error?: string } {
|
|
|
|
| 1 |
+
export interface GameContent {
|
| 2 |
+
world: string;
|
| 3 |
+
scripts: string[];
|
| 4 |
+
}
|
| 5 |
+
|
| 6 |
export class HTMLParser {
|
| 7 |
+
static extractGameContent(html: string): GameContent {
|
| 8 |
const worldMatch = html.match(/<world[^>]*>[\s\S]*?<\/world>/);
|
| 9 |
+
const world = worldMatch
|
| 10 |
+
? worldMatch[0]
|
| 11 |
+
: '<world canvas="#game-canvas"></world>';
|
| 12 |
+
|
| 13 |
+
const scripts: string[] = [];
|
| 14 |
+
const scriptRegex = /<script[^>]*>([\s\S]*?)<\/script>/gi;
|
| 15 |
+
let match;
|
| 16 |
+
|
| 17 |
+
while ((match = scriptRegex.exec(html)) !== null) {
|
| 18 |
+
const scriptContent = match[1].trim();
|
| 19 |
+
if (scriptContent) {
|
| 20 |
+
scripts.push(scriptContent);
|
| 21 |
+
}
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
return { world, scripts };
|
| 25 |
}
|
| 26 |
|
| 27 |
static validateGameHTML(html: string): { valid: boolean; error?: string } {
|
src/lib/stores/editor.ts
CHANGED
|
@@ -6,8 +6,7 @@ export interface EditorState {
|
|
| 6 |
theme: string;
|
| 7 |
}
|
| 8 |
|
| 9 |
-
const DEFAULT_CONTENT =
|
| 10 |
-
`<canvas id="game-canvas"></canvas>
|
| 11 |
|
| 12 |
<world canvas="#game-canvas" sky="#87ceeb">
|
| 13 |
<player pos="0 0 0"></player>
|
|
@@ -17,13 +16,41 @@ const DEFAULT_CONTENT =
|
|
| 17 |
|
| 18 |
<!-- Ball -->
|
| 19 |
<dynamic-part pos="-2 4 -3" shape="sphere" size="1" color="#ff4500"></dynamic-part>
|
|
|
|
|
|
|
|
|
|
| 20 |
</world>
|
| 21 |
|
| 22 |
-
<script
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
function createEditorStore() {
|
| 29 |
const { subscribe, set, update } = writable<EditorState>({
|
|
|
|
| 6 |
theme: string;
|
| 7 |
}
|
| 8 |
|
| 9 |
+
const DEFAULT_CONTENT = `<canvas id="game-canvas"></canvas>
|
|
|
|
| 10 |
|
| 11 |
<world canvas="#game-canvas" sky="#87ceeb">
|
| 12 |
<player pos="0 0 0"></player>
|
|
|
|
| 16 |
|
| 17 |
<!-- Ball -->
|
| 18 |
<dynamic-part pos="-2 4 -3" shape="sphere" size="1" color="#ff4500"></dynamic-part>
|
| 19 |
+
|
| 20 |
+
<!-- Custom component -->
|
| 21 |
+
<entity my-component="value: 0"></entity>
|
| 22 |
</world>
|
| 23 |
|
| 24 |
+
<script>
|
| 25 |
+
// GAME is automatically provided by the framework
|
| 26 |
+
console.log("Game script loaded!");
|
| 27 |
+
|
| 28 |
+
const MyComponent = GAME.defineComponent({
|
| 29 |
+
value: GAME.Types.i32,
|
| 30 |
+
});
|
| 31 |
+
|
| 32 |
+
const myQuery = GAME.defineQuery([MyComponent]);
|
| 33 |
+
|
| 34 |
+
const MySystem = {
|
| 35 |
+
update: (state) => {
|
| 36 |
+
const entities = myQuery(state.world);
|
| 37 |
+
for (const entity of entities) {
|
| 38 |
+
MyComponent.value[entity]++;
|
| 39 |
+
if (MyComponent.value[entity] === 100) {
|
| 40 |
+
console.log("Entity value is 100");
|
| 41 |
+
}
|
| 42 |
+
}
|
| 43 |
+
}
|
| 44 |
+
};
|
| 45 |
+
|
| 46 |
+
const MyPlugin = {
|
| 47 |
+
components: { MyComponent },
|
| 48 |
+
systems: [MySystem],
|
| 49 |
+
};
|
| 50 |
+
|
| 51 |
+
GAME.withPlugin(MyPlugin);
|
| 52 |
+
// .run() is handled by the framework
|
| 53 |
+
</script>`;
|
| 54 |
|
| 55 |
function createEditorStore() {
|
| 56 |
const { subscribe, set, update } = writable<EditorState>({
|
src/main.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
| 1 |
import "./app.css";
|
| 2 |
import App from "./App.svelte";
|
|
|
|
| 3 |
|
| 4 |
const app = new App({
|
| 5 |
target: document.getElementById("app")!,
|
| 6 |
});
|
| 7 |
|
|
|
|
|
|
|
| 8 |
export default app;
|
|
|
|
| 1 |
import "./app.css";
|
| 2 |
import App from "./App.svelte";
|
| 3 |
+
import { consoleSyncService } from "./lib/services/console-sync";
|
| 4 |
|
| 5 |
const app = new App({
|
| 6 |
target: document.getElementById("app")!,
|
| 7 |
});
|
| 8 |
|
| 9 |
+
consoleSyncService.setup();
|
| 10 |
+
|
| 11 |
export default app;
|