Spaces:
Running
Running
Commit
Β·
9434357
1
Parent(s):
3342a1d
better tools
Browse files- .claude/commands/peel.md +0 -1
- bun.lock +9 -9
- llms.txt +1 -1
- package.json +1 -1
- src/lib/components/chat/ChatPanel.svelte +25 -71
- src/lib/components/chat/InProgressBlock.svelte +43 -23
- src/lib/components/chat/ToolCallBlock.svelte +0 -373
- src/lib/components/chat/ToolCallDisplay.svelte +0 -89
- src/lib/components/chat/ToolInvocation.svelte +73 -11
- src/lib/components/chat/context.md +5 -6
- src/lib/server/context.md +4 -4
- src/lib/server/langgraph-agent.ts +189 -34
- src/lib/server/tools.ts +216 -4
- src/lib/services/agent.ts +120 -0
- src/lib/services/console-forward.ts +2 -2
- src/lib/services/context.md +8 -1
- src/lib/services/message-handler.ts +202 -0
- src/lib/services/websocket.ts +104 -0
- src/lib/stores/agent.ts +148 -578
- src/lib/stores/context.md +1 -1
.claude/commands/peel.md
CHANGED
|
@@ -6,7 +6,6 @@ Load relevant context for the current conversation.
|
|
| 6 |
|
| 7 |
@CLAUDE.md
|
| 8 |
@layers/structure.md
|
| 9 |
-
@llms.txt
|
| 10 |
|
| 11 |
User arguments: "$ARGUMENTS"
|
| 12 |
|
|
|
|
| 6 |
|
| 7 |
@CLAUDE.md
|
| 8 |
@layers/structure.md
|
|
|
|
| 9 |
|
| 10 |
User arguments: "$ARGUMENTS"
|
| 11 |
|
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": {
|
|
@@ -108,7 +108,7 @@
|
|
| 108 |
|
| 109 |
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.3.5", "", { "dependencies": { "@eslint/core": "^0.15.2", "levn": "^0.4.1" } }, "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w=="],
|
| 110 |
|
| 111 |
-
"@huggingface/hub": ["@huggingface/hub@2.6.
|
| 112 |
|
| 113 |
"@huggingface/inference": ["@huggingface/inference@4.8.0", "", { "dependencies": { "@huggingface/jinja": "^0.5.1", "@huggingface/tasks": "^0.19.45" } }, "sha512-Eq98EAXqYn4rKMfrbEXuhc3IjKfaeIO6eXNOZk9xk6v5akrIWRtd6d1h0fjAWyX4zRbdUpXRh6MvsqXnzGvXCA=="],
|
| 114 |
|
|
@@ -196,7 +196,7 @@
|
|
| 196 |
|
| 197 |
"@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@2.1.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^3.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "vite": "^5.0.0" } }, "sha512-9QX28IymvBlSCqsCll5t0kQVxipsfhFFL+L2t3nTWfXnddYwxBuAEtTtlaVQpRz9c37BhJjltSeY4AJSC03SSg=="],
|
| 198 |
|
| 199 |
-
"@types/bun": ["@types/bun@1.2.
|
| 200 |
|
| 201 |
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
| 202 |
|
|
@@ -206,7 +206,7 @@
|
|
| 206 |
|
| 207 |
"@types/marked": ["@types/marked@6.0.0", "", { "dependencies": { "marked": "*" } }, "sha512-jmjpa4BwUsmhxcfsgUit/7A9KbrC48Q0q8KvnY107ogcjGgTFDlIL3RpihNpx2Mu1hM4mdFQjoVc4O6JoGKHsA=="],
|
| 208 |
|
| 209 |
-
"@types/node": ["@types/node@24.
|
| 210 |
|
| 211 |
"@types/pug": ["@types/pug@2.0.10", "", {}, "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA=="],
|
| 212 |
|
|
@@ -286,7 +286,7 @@
|
|
| 286 |
|
| 287 |
"buffer-crc32": ["buffer-crc32@1.0.0", "", {}, "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w=="],
|
| 288 |
|
| 289 |
-
"bun-types": ["bun-types@1.2.
|
| 290 |
|
| 291 |
"call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],
|
| 292 |
|
|
@@ -326,7 +326,7 @@
|
|
| 326 |
|
| 327 |
"data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="],
|
| 328 |
|
| 329 |
-
"debug": ["debug@4.4.
|
| 330 |
|
| 331 |
"decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="],
|
| 332 |
|
|
@@ -570,7 +570,7 @@
|
|
| 570 |
|
| 571 |
"magic-string": ["magic-string@0.30.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="],
|
| 572 |
|
| 573 |
-
"marked": ["marked@16.
|
| 574 |
|
| 575 |
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
| 576 |
|
|
@@ -774,13 +774,13 @@
|
|
| 774 |
|
| 775 |
"unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="],
|
| 776 |
|
| 777 |
-
"undici-types": ["undici-types@7.
|
| 778 |
|
| 779 |
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
| 780 |
|
| 781 |
"uuid": ["uuid@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="],
|
| 782 |
|
| 783 |
-
"vibegame": ["vibegame@0.1.
|
| 784 |
|
| 785 |
"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=="],
|
| 786 |
|
|
|
|
| 14 |
"marked": "^16.2.1",
|
| 15 |
"monaco-editor": "^0.50.0",
|
| 16 |
"svelte-splitpanes": "^8.0.5",
|
| 17 |
+
"vibegame": "^0.1.3",
|
| 18 |
"zod": "^4.1.8",
|
| 19 |
},
|
| 20 |
"devDependencies": {
|
|
|
|
| 108 |
|
| 109 |
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.3.5", "", { "dependencies": { "@eslint/core": "^0.15.2", "levn": "^0.4.1" } }, "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w=="],
|
| 110 |
|
| 111 |
+
"@huggingface/hub": ["@huggingface/hub@2.6.4", "", { "dependencies": { "@huggingface/tasks": "^0.19.45" }, "optionalDependencies": { "cli-progress": "^3.12.0" }, "bin": { "hfjs": "dist/cli.js" } }, "sha512-eqJjP0DAIShc8O90neoK1SFAlgikK6jpreTcOue4hXkKRa+BkC4WCLGintI8HIDk2Y+pHgQwUvsdb4Ruo4inSg=="],
|
| 112 |
|
| 113 |
"@huggingface/inference": ["@huggingface/inference@4.8.0", "", { "dependencies": { "@huggingface/jinja": "^0.5.1", "@huggingface/tasks": "^0.19.45" } }, "sha512-Eq98EAXqYn4rKMfrbEXuhc3IjKfaeIO6eXNOZk9xk6v5akrIWRtd6d1h0fjAWyX4zRbdUpXRh6MvsqXnzGvXCA=="],
|
| 114 |
|
|
|
|
| 196 |
|
| 197 |
"@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@2.1.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^3.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "vite": "^5.0.0" } }, "sha512-9QX28IymvBlSCqsCll5t0kQVxipsfhFFL+L2t3nTWfXnddYwxBuAEtTtlaVQpRz9c37BhJjltSeY4AJSC03SSg=="],
|
| 198 |
|
| 199 |
+
"@types/bun": ["@types/bun@1.2.22", "", { "dependencies": { "bun-types": "1.2.22" } }, "sha512-5A/KrKos2ZcN0c6ljRSOa1fYIyCKhZfIVYeuyb4snnvomnpFqC0tTsEkdqNxbAgExV384OETQ//WAjl3XbYqQA=="],
|
| 200 |
|
| 201 |
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
| 202 |
|
|
|
|
| 206 |
|
| 207 |
"@types/marked": ["@types/marked@6.0.0", "", { "dependencies": { "marked": "*" } }, "sha512-jmjpa4BwUsmhxcfsgUit/7A9KbrC48Q0q8KvnY107ogcjGgTFDlIL3RpihNpx2Mu1hM4mdFQjoVc4O6JoGKHsA=="],
|
| 208 |
|
| 209 |
+
"@types/node": ["@types/node@24.4.0", "", { "dependencies": { "undici-types": "~7.11.0" } }, "sha512-gUuVEAK4/u6F9wRLznPUU4WGUacSEBDPoC2TrBkw3GAnOLHBL45QdfHOXp1kJ4ypBGLxTOB+t7NJLpKoC3gznQ=="],
|
| 210 |
|
| 211 |
"@types/pug": ["@types/pug@2.0.10", "", {}, "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA=="],
|
| 212 |
|
|
|
|
| 286 |
|
| 287 |
"buffer-crc32": ["buffer-crc32@1.0.0", "", {}, "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w=="],
|
| 288 |
|
| 289 |
+
"bun-types": ["bun-types@1.2.22", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-hwaAu8tct/Zn6Zft4U9BsZcXkYomzpHJX28ofvx7k0Zz2HNz54n1n+tDgxoWFGB4PcFvJXJQloPhaV2eP3Q6EA=="],
|
| 290 |
|
| 291 |
"call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],
|
| 292 |
|
|
|
|
| 326 |
|
| 327 |
"data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="],
|
| 328 |
|
| 329 |
+
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
| 330 |
|
| 331 |
"decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="],
|
| 332 |
|
|
|
|
| 570 |
|
| 571 |
"magic-string": ["magic-string@0.30.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="],
|
| 572 |
|
| 573 |
+
"marked": ["marked@16.3.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-K3UxuKu6l6bmA5FUwYho8CfJBlsUWAooKtdGgMcERSpF7gcBUrCGsLH7wDaaNOzwq18JzSUDyoEb/YsrqMac3w=="],
|
| 574 |
|
| 575 |
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
| 576 |
|
|
|
|
| 774 |
|
| 775 |
"unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="],
|
| 776 |
|
| 777 |
+
"undici-types": ["undici-types@7.11.0", "", {}, "sha512-kt1ZriHTi7MU+Z/r9DOdAI3ONdaR3M3csEaRc6ewa4f4dTvX4cQCbJ4NkEn0ohE4hHtq85+PhPSTY+pO/1PwgA=="],
|
| 778 |
|
| 779 |
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
| 780 |
|
| 781 |
"uuid": ["uuid@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="],
|
| 782 |
|
| 783 |
+
"vibegame": ["vibegame@0.1.3", "", { "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-pUUK8yjHhb9UCgaBKZerKovn3rR+RD8AKoF8iFoYRIOhtDBXjGV+hLzJ2Z9QLpymCCjEuo+vDTdfatagARQ1wg=="],
|
| 784 |
|
| 785 |
"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=="],
|
| 786 |
|
llms.txt
CHANGED
|
@@ -487,7 +487,7 @@ Available in all systems via the `state` parameter:
|
|
| 487 |
### Time
|
| 488 |
- `time.delta: number` - Frame time in seconds
|
| 489 |
- `time.elapsed: number` - Total time in seconds
|
| 490 |
-
- `time.fixed: number` - Fixed timestep (1/
|
| 491 |
|
| 492 |
### Physics Helpers
|
| 493 |
- `addComponent(entity, ApplyImpulse, {x, y, z})` - One-time push
|
|
|
|
| 487 |
### Time
|
| 488 |
- `time.delta: number` - Frame time in seconds
|
| 489 |
- `time.elapsed: number` - Total time in seconds
|
| 490 |
+
- `time.fixed: number` - Fixed timestep (1/50)
|
| 491 |
|
| 492 |
### Physics Helpers
|
| 493 |
- `addComponent(entity, ApplyImpulse, {x, y, z})` - One-time push
|
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.3",
|
| 50 |
"zod": "^4.1.8"
|
| 51 |
}
|
| 52 |
}
|
src/lib/components/chat/ChatPanel.svelte
CHANGED
|
@@ -3,11 +3,10 @@
|
|
| 3 |
import { fade } from "svelte/transition";
|
| 4 |
import { agentStore, isConnected, isProcessing } from "../../stores/agent";
|
| 5 |
import { authStore } from "../../services/auth";
|
|
|
|
| 6 |
import gsap from "gsap";
|
| 7 |
import ReasoningBlock from "./ReasoningBlock.svelte";
|
| 8 |
import MarkdownRenderer from "./MarkdownRenderer.svelte";
|
| 9 |
-
import ToolCallDisplay from "./ToolCallDisplay.svelte";
|
| 10 |
-
import ToolCallBlock from "./ToolCallBlock.svelte";
|
| 11 |
import InProgressBlock from "./InProgressBlock.svelte";
|
| 12 |
import MessageSegment from "./MessageSegment.svelte";
|
| 13 |
|
|
@@ -19,43 +18,9 @@
|
|
| 19 |
|
| 20 |
let hasConnected = false;
|
| 21 |
|
| 22 |
-
const TOOL_PATTERN = /\[TOOL:\s*(\w+)(?:\s+({[^}]+}))?\]/g;
|
| 23 |
-
|
| 24 |
-
function parseMessageContent(content: string) {
|
| 25 |
-
const parts: Array<{ type: 'text' | 'tool', content: string, toolName?: string, params?: any }> = [];
|
| 26 |
-
let lastIndex = 0;
|
| 27 |
-
|
| 28 |
-
const matches = Array.from(content.matchAll(TOOL_PATTERN));
|
| 29 |
-
|
| 30 |
-
for (const match of matches) {
|
| 31 |
-
const [fullMatch, toolName, paramsStr] = match;
|
| 32 |
-
const index = match.index!;
|
| 33 |
-
|
| 34 |
-
if (index > lastIndex) {
|
| 35 |
-
parts.push({ type: 'text', content: content.slice(lastIndex, index) });
|
| 36 |
-
}
|
| 37 |
-
|
| 38 |
-
let params = null;
|
| 39 |
-
if (paramsStr) {
|
| 40 |
-
try {
|
| 41 |
-
params = JSON.parse(paramsStr);
|
| 42 |
-
} catch {}
|
| 43 |
-
}
|
| 44 |
-
|
| 45 |
-
parts.push({ type: 'tool', content: fullMatch, toolName, params });
|
| 46 |
-
lastIndex = index + fullMatch.length;
|
| 47 |
-
}
|
| 48 |
-
|
| 49 |
-
if (lastIndex < content.length) {
|
| 50 |
-
parts.push({ type: 'text', content: content.slice(lastIndex) });
|
| 51 |
-
}
|
| 52 |
-
|
| 53 |
-
return parts.length > 0 ? parts : [{ type: 'text' as const, content }];
|
| 54 |
-
}
|
| 55 |
-
|
| 56 |
onMount(() => {
|
| 57 |
if ($authStore.isAuthenticated && !$authStore.loading) {
|
| 58 |
-
|
| 59 |
hasConnected = true;
|
| 60 |
}
|
| 61 |
|
|
@@ -71,11 +36,11 @@
|
|
| 71 |
});
|
| 72 |
|
| 73 |
onDestroy(() => {
|
| 74 |
-
|
| 75 |
});
|
| 76 |
|
| 77 |
$: if ($authStore.isAuthenticated && !$authStore.loading && !hasConnected) {
|
| 78 |
-
|
| 79 |
hasConnected = true;
|
| 80 |
}
|
| 81 |
|
|
@@ -164,7 +129,7 @@
|
|
| 164 |
});
|
| 165 |
}
|
| 166 |
|
| 167 |
-
|
| 168 |
inputValue = "";
|
| 169 |
}
|
| 170 |
}
|
|
@@ -263,37 +228,26 @@
|
|
| 263 |
{/if}
|
| 264 |
|
| 265 |
{#each $agentStore.messages as message (message.id)}
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
{
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
{:else if part.type === 'tool' && part.toolName}
|
| 287 |
-
<ToolCallDisplay toolName={part.toolName} parameters={part.params} />
|
| 288 |
-
{/if}
|
| 289 |
-
{/each}
|
| 290 |
-
{:else}
|
| 291 |
-
{message.content.trim()}
|
| 292 |
-
{/if}
|
| 293 |
-
</div>
|
| 294 |
-
{/if}
|
| 295 |
-
</div>
|
| 296 |
-
{/if}
|
| 297 |
{/each}
|
| 298 |
|
| 299 |
{#if $agentStore.streamingStatus !== "idle" && (!$agentStore.streamingContent || $agentStore.streamingStatus === "thinking")}
|
|
|
|
| 3 |
import { fade } from "svelte/transition";
|
| 4 |
import { agentStore, isConnected, isProcessing } from "../../stores/agent";
|
| 5 |
import { authStore } from "../../services/auth";
|
| 6 |
+
import { agentService } from "../../services/agent";
|
| 7 |
import gsap from "gsap";
|
| 8 |
import ReasoningBlock from "./ReasoningBlock.svelte";
|
| 9 |
import MarkdownRenderer from "./MarkdownRenderer.svelte";
|
|
|
|
|
|
|
| 10 |
import InProgressBlock from "./InProgressBlock.svelte";
|
| 11 |
import MessageSegment from "./MessageSegment.svelte";
|
| 12 |
|
|
|
|
| 18 |
|
| 19 |
let hasConnected = false;
|
| 20 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
onMount(() => {
|
| 22 |
if ($authStore.isAuthenticated && !$authStore.loading) {
|
| 23 |
+
agentService.connect();
|
| 24 |
hasConnected = true;
|
| 25 |
}
|
| 26 |
|
|
|
|
| 36 |
});
|
| 37 |
|
| 38 |
onDestroy(() => {
|
| 39 |
+
agentService.disconnect();
|
| 40 |
});
|
| 41 |
|
| 42 |
$: if ($authStore.isAuthenticated && !$authStore.loading && !hasConnected) {
|
| 43 |
+
agentService.connect();
|
| 44 |
hasConnected = true;
|
| 45 |
}
|
| 46 |
|
|
|
|
| 129 |
});
|
| 130 |
}
|
| 131 |
|
| 132 |
+
agentService.sendMessage(inputValue.trim());
|
| 133 |
inputValue = "";
|
| 134 |
}
|
| 135 |
}
|
|
|
|
| 228 |
{/if}
|
| 229 |
|
| 230 |
{#each $agentStore.messages as message (message.id)}
|
| 231 |
+
<div class="message {message.role}">
|
| 232 |
+
{#if message.reasoning && message.role === "assistant"}
|
| 233 |
+
<ReasoningBlock reasoning={message.reasoning} />
|
| 234 |
+
{/if}
|
| 235 |
+
{#if message.segments && message.segments.length > 0}
|
| 236 |
+
<div class="message-segments">
|
| 237 |
+
{#each message.segments as segment (segment.id)}
|
| 238 |
+
<MessageSegment {segment} />
|
| 239 |
+
{/each}
|
| 240 |
+
</div>
|
| 241 |
+
{:else if message.content && message.content.trim()}
|
| 242 |
+
<div class="message-content">
|
| 243 |
+
{#if message.role === "assistant"}
|
| 244 |
+
<MarkdownRenderer content={message.content.trim()} streaming={false} />
|
| 245 |
+
{:else}
|
| 246 |
+
{message.content.trim()}
|
| 247 |
+
{/if}
|
| 248 |
+
</div>
|
| 249 |
+
{/if}
|
| 250 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
{/each}
|
| 252 |
|
| 253 |
{#if $agentStore.streamingStatus !== "idle" && (!$agentStore.streamingContent || $agentStore.streamingStatus === "thinking")}
|
src/lib/components/chat/InProgressBlock.svelte
CHANGED
|
@@ -53,7 +53,7 @@
|
|
| 53 |
contentElement,
|
| 54 |
{ opacity: 0, maxHeight: 0, y: -10 },
|
| 55 |
{ opacity: 1, maxHeight: 500, y: 0, duration: 0.3, ease: "power2.out" },
|
| 56 |
-
"-=0.1"
|
| 57 |
);
|
| 58 |
} else {
|
| 59 |
timeline
|
|
@@ -62,16 +62,20 @@
|
|
| 62 |
duration: 0.2,
|
| 63 |
ease: "power2.in",
|
| 64 |
})
|
| 65 |
-
.to(
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
|
|
|
|
|
|
|
|
|
| 73 |
},
|
| 74 |
-
|
|
|
|
| 75 |
}
|
| 76 |
}
|
| 77 |
|
|
@@ -83,9 +87,9 @@
|
|
| 83 |
tl.to(blockElement, {
|
| 84 |
duration: 0.4,
|
| 85 |
ease: "power2.inOut",
|
| 86 |
-
onUpdate: function() {
|
| 87 |
updateBlockStyle(to);
|
| 88 |
-
}
|
| 89 |
});
|
| 90 |
|
| 91 |
tl.to(statusElement, {
|
|
@@ -93,13 +97,12 @@
|
|
| 93 |
opacity: 0,
|
| 94 |
duration: 0.15,
|
| 95 |
ease: "power2.in",
|
| 96 |
-
onComplete: () => {}
|
| 97 |
-
})
|
| 98 |
-
.to(statusElement, {
|
| 99 |
scale: 1,
|
| 100 |
opacity: 1,
|
| 101 |
duration: 0.15,
|
| 102 |
-
ease: "power2.out"
|
| 103 |
});
|
| 104 |
|
| 105 |
if (progressBar) {
|
|
@@ -107,13 +110,13 @@
|
|
| 107 |
gsap.to(progressBar, {
|
| 108 |
width: "60%",
|
| 109 |
duration: 1,
|
| 110 |
-
ease: "power2.out"
|
| 111 |
});
|
| 112 |
} else if (to === "completing") {
|
| 113 |
gsap.to(progressBar, {
|
| 114 |
-
width: "
|
| 115 |
duration: 0.5,
|
| 116 |
-
ease: "power2.out"
|
| 117 |
});
|
| 118 |
}
|
| 119 |
}
|
|
@@ -168,9 +171,10 @@
|
|
| 168 |
}
|
| 169 |
|
| 170 |
if (progressBar) {
|
| 171 |
-
gsap.fromTo(
|
|
|
|
| 172 |
{ width: "0%" },
|
| 173 |
-
{ width: "30%", duration: 0.5, ease: "power2.out" }
|
| 174 |
);
|
| 175 |
}
|
| 176 |
|
|
@@ -184,6 +188,20 @@
|
|
| 184 |
onDestroy(() => {
|
| 185 |
if (collapseTimeout) clearTimeout(collapseTimeout);
|
| 186 |
if (timeline) timeline.kill();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
});
|
| 188 |
</script>
|
| 189 |
|
|
@@ -267,10 +285,12 @@
|
|
| 267 |
|
| 268 |
.progress-bar {
|
| 269 |
height: 100%;
|
| 270 |
-
background: linear-gradient(
|
|
|
|
| 271 |
rgba(255, 210, 30, 0.6) 0%,
|
| 272 |
rgba(65, 105, 225, 0.6) 50%,
|
| 273 |
-
rgba(0, 255, 0, 0.6) 100%
|
|
|
|
| 274 |
box-shadow: 0 0 10px rgba(65, 105, 225, 0.4);
|
| 275 |
transition: width 0.3s ease;
|
| 276 |
}
|
|
|
|
| 53 |
contentElement,
|
| 54 |
{ opacity: 0, maxHeight: 0, y: -10 },
|
| 55 |
{ opacity: 1, maxHeight: 500, y: 0, duration: 0.3, ease: "power2.out" },
|
| 56 |
+
"-=0.1",
|
| 57 |
);
|
| 58 |
} else {
|
| 59 |
timeline
|
|
|
|
| 62 |
duration: 0.2,
|
| 63 |
ease: "power2.in",
|
| 64 |
})
|
| 65 |
+
.to(
|
| 66 |
+
contentElement,
|
| 67 |
+
{
|
| 68 |
+
opacity: 0,
|
| 69 |
+
maxHeight: 0,
|
| 70 |
+
y: -5,
|
| 71 |
+
duration: 0.2,
|
| 72 |
+
ease: "power2.in",
|
| 73 |
+
onComplete: () => {
|
| 74 |
+
gsap.set(contentElement, { display: "none" });
|
| 75 |
+
},
|
| 76 |
},
|
| 77 |
+
"-=0.1",
|
| 78 |
+
);
|
| 79 |
}
|
| 80 |
}
|
| 81 |
|
|
|
|
| 87 |
tl.to(blockElement, {
|
| 88 |
duration: 0.4,
|
| 89 |
ease: "power2.inOut",
|
| 90 |
+
onUpdate: function () {
|
| 91 |
updateBlockStyle(to);
|
| 92 |
+
},
|
| 93 |
});
|
| 94 |
|
| 95 |
tl.to(statusElement, {
|
|
|
|
| 97 |
opacity: 0,
|
| 98 |
duration: 0.15,
|
| 99 |
ease: "power2.in",
|
| 100 |
+
onComplete: () => {},
|
| 101 |
+
}).to(statusElement, {
|
|
|
|
| 102 |
scale: 1,
|
| 103 |
opacity: 1,
|
| 104 |
duration: 0.15,
|
| 105 |
+
ease: "power2.out",
|
| 106 |
});
|
| 107 |
|
| 108 |
if (progressBar) {
|
|
|
|
| 110 |
gsap.to(progressBar, {
|
| 111 |
width: "60%",
|
| 112 |
duration: 1,
|
| 113 |
+
ease: "power2.out",
|
| 114 |
});
|
| 115 |
} else if (to === "completing") {
|
| 116 |
gsap.to(progressBar, {
|
| 117 |
+
width: "100%",
|
| 118 |
duration: 0.5,
|
| 119 |
+
ease: "power2.out",
|
| 120 |
});
|
| 121 |
}
|
| 122 |
}
|
|
|
|
| 171 |
}
|
| 172 |
|
| 173 |
if (progressBar) {
|
| 174 |
+
gsap.fromTo(
|
| 175 |
+
progressBar,
|
| 176 |
{ width: "0%" },
|
| 177 |
+
{ width: "30%", duration: 0.5, ease: "power2.out" },
|
| 178 |
);
|
| 179 |
}
|
| 180 |
|
|
|
|
| 188 |
onDestroy(() => {
|
| 189 |
if (collapseTimeout) clearTimeout(collapseTimeout);
|
| 190 |
if (timeline) timeline.kill();
|
| 191 |
+
|
| 192 |
+
if (progressBar && blockElement) {
|
| 193 |
+
gsap.to(progressBar, {
|
| 194 |
+
width: "100%",
|
| 195 |
+
duration: 0.2,
|
| 196 |
+
ease: "power2.out",
|
| 197 |
+
});
|
| 198 |
+
gsap.to(blockElement, {
|
| 199 |
+
opacity: 0,
|
| 200 |
+
scale: 0.95,
|
| 201 |
+
duration: 0.3,
|
| 202 |
+
ease: "power2.in",
|
| 203 |
+
});
|
| 204 |
+
}
|
| 205 |
});
|
| 206 |
</script>
|
| 207 |
|
|
|
|
| 285 |
|
| 286 |
.progress-bar {
|
| 287 |
height: 100%;
|
| 288 |
+
background: linear-gradient(
|
| 289 |
+
90deg,
|
| 290 |
rgba(255, 210, 30, 0.6) 0%,
|
| 291 |
rgba(65, 105, 225, 0.6) 50%,
|
| 292 |
+
rgba(0, 255, 0, 0.6) 100%
|
| 293 |
+
);
|
| 294 |
box-shadow: 0 0 10px rgba(65, 105, 225, 0.4);
|
| 295 |
transition: width 0.3s ease;
|
| 296 |
}
|
src/lib/components/chat/ToolCallBlock.svelte
DELETED
|
@@ -1,373 +0,0 @@
|
|
| 1 |
-
<script lang="ts">
|
| 2 |
-
import { onMount } from "svelte";
|
| 3 |
-
import gsap from "gsap";
|
| 4 |
-
import type { ToolExecution } from "../../stores/agent";
|
| 5 |
-
|
| 6 |
-
export let toolExecutions: ToolExecution[] = [];
|
| 7 |
-
|
| 8 |
-
let blockElement: HTMLDivElement;
|
| 9 |
-
let expandedTools: Set<string> = new Set();
|
| 10 |
-
|
| 11 |
-
const toolIcons: Record<string, string> = {
|
| 12 |
-
read_editor: "π",
|
| 13 |
-
write_editor: "βοΈ",
|
| 14 |
-
observe_console: "π",
|
| 15 |
-
default: "π§",
|
| 16 |
-
};
|
| 17 |
-
|
| 18 |
-
const statusIcons: Record<string, string> = {
|
| 19 |
-
pending: "β³",
|
| 20 |
-
running: "β‘",
|
| 21 |
-
completed: "β
",
|
| 22 |
-
error: "β",
|
| 23 |
-
};
|
| 24 |
-
|
| 25 |
-
const statusMessages: Record<string, (name: string) => string> = {
|
| 26 |
-
read_editor: (name) => ({
|
| 27 |
-
pending: "Preparing to read code...",
|
| 28 |
-
running: "Reading editor content...",
|
| 29 |
-
completed: "Code read successfully",
|
| 30 |
-
error: "Failed to read code"
|
| 31 |
-
}[name] || name),
|
| 32 |
-
write_editor: (name) => ({
|
| 33 |
-
pending: "Preparing to write code...",
|
| 34 |
-
running: "Writing code and reloading game...",
|
| 35 |
-
completed: "Code updated successfully",
|
| 36 |
-
error: "Failed to write code"
|
| 37 |
-
}[name] || name),
|
| 38 |
-
observe_console: (name) => ({
|
| 39 |
-
pending: "Preparing to read console...",
|
| 40 |
-
running: "Reading console output...",
|
| 41 |
-
completed: "Console read successfully",
|
| 42 |
-
error: "Failed to read console"
|
| 43 |
-
}[name] || name),
|
| 44 |
-
};
|
| 45 |
-
|
| 46 |
-
function toggleTool(toolId: string) {
|
| 47 |
-
if (expandedTools.has(toolId)) {
|
| 48 |
-
expandedTools.delete(toolId);
|
| 49 |
-
} else {
|
| 50 |
-
expandedTools.add(toolId);
|
| 51 |
-
}
|
| 52 |
-
expandedTools = expandedTools;
|
| 53 |
-
}
|
| 54 |
-
|
| 55 |
-
function getStatusMessage(tool: ToolExecution): string {
|
| 56 |
-
const messageFunc = statusMessages[tool.name] || statusMessages.default;
|
| 57 |
-
return messageFunc ? messageFunc(tool.status) : `${tool.name}: ${tool.status}`;
|
| 58 |
-
}
|
| 59 |
-
|
| 60 |
-
function formatDuration(startTime: number, endTime?: number): string {
|
| 61 |
-
const duration = (endTime || Date.now()) - startTime;
|
| 62 |
-
if (duration < 1000) {
|
| 63 |
-
return `${duration}ms`;
|
| 64 |
-
}
|
| 65 |
-
return `${(duration / 1000).toFixed(1)}s`;
|
| 66 |
-
}
|
| 67 |
-
|
| 68 |
-
onMount(() => {
|
| 69 |
-
gsap.fromTo(
|
| 70 |
-
blockElement,
|
| 71 |
-
{ opacity: 0, y: -10 },
|
| 72 |
-
{ opacity: 1, y: 0, duration: 0.3, ease: "power2.out" }
|
| 73 |
-
);
|
| 74 |
-
});
|
| 75 |
-
</script>
|
| 76 |
-
|
| 77 |
-
<div class="tool-block" bind:this={blockElement}>
|
| 78 |
-
{#each toolExecutions as tool (tool.id)}
|
| 79 |
-
<div class="tool-item {tool.status}" class:expanded={expandedTools.has(tool.id)}>
|
| 80 |
-
<button
|
| 81 |
-
class="tool-item-header"
|
| 82 |
-
on:click={() => toggleTool(tool.id)}
|
| 83 |
-
aria-expanded={expandedTools.has(tool.id)}
|
| 84 |
-
>
|
| 85 |
-
<span class="tool-status-icon">
|
| 86 |
-
{#if tool.status === "running"}
|
| 87 |
-
<span class="spinner-small">{statusIcons[tool.status]}</span>
|
| 88 |
-
{:else}
|
| 89 |
-
{statusIcons[tool.status]}
|
| 90 |
-
{/if}
|
| 91 |
-
</span>
|
| 92 |
-
<span class="tool-icon">{toolIcons[tool.name] || toolIcons.default}</span>
|
| 93 |
-
<span class="tool-name">{getStatusMessage(tool)}</span>
|
| 94 |
-
<span class="tool-duration">
|
| 95 |
-
{formatDuration(tool.startTime, tool.endTime)}
|
| 96 |
-
</span>
|
| 97 |
-
<span class="expand-icon" class:rotated={expandedTools.has(tool.id)}>
|
| 98 |
-
βΆ
|
| 99 |
-
</span>
|
| 100 |
-
</button>
|
| 101 |
-
|
| 102 |
-
{#if expandedTools.has(tool.id)}
|
| 103 |
-
<div class="tool-details">
|
| 104 |
-
{#if tool.args && Object.keys(tool.args).length > 0}
|
| 105 |
-
<div class="tool-section">
|
| 106 |
-
<div class="section-title">Parameters:</div>
|
| 107 |
-
<div class="params">
|
| 108 |
-
{#each Object.entries(tool.args) as [key, value]}
|
| 109 |
-
<div class="param">
|
| 110 |
-
<span class="param-key">{key}:</span>
|
| 111 |
-
<span class="param-value">
|
| 112 |
-
{#if typeof value === 'string' && value.length > 100}
|
| 113 |
-
<pre>{value}</pre>
|
| 114 |
-
{:else}
|
| 115 |
-
{JSON.stringify(value)}
|
| 116 |
-
{/if}
|
| 117 |
-
</span>
|
| 118 |
-
</div>
|
| 119 |
-
{/each}
|
| 120 |
-
</div>
|
| 121 |
-
</div>
|
| 122 |
-
{/if}
|
| 123 |
-
|
| 124 |
-
{#if tool.output}
|
| 125 |
-
<div class="tool-section">
|
| 126 |
-
<div class="section-title">Output:</div>
|
| 127 |
-
<pre class="tool-output">{tool.output}</pre>
|
| 128 |
-
</div>
|
| 129 |
-
{/if}
|
| 130 |
-
|
| 131 |
-
{#if tool.consoleOutput && tool.consoleOutput.length > 0}
|
| 132 |
-
<div class="tool-section">
|
| 133 |
-
<div class="section-title">Console Output:</div>
|
| 134 |
-
<div class="console-output">
|
| 135 |
-
{#each tool.consoleOutput as line}
|
| 136 |
-
<div class="console-line">{line}</div>
|
| 137 |
-
{/each}
|
| 138 |
-
</div>
|
| 139 |
-
</div>
|
| 140 |
-
{/if}
|
| 141 |
-
|
| 142 |
-
{#if tool.error}
|
| 143 |
-
<div class="tool-section error">
|
| 144 |
-
<div class="section-title">Error:</div>
|
| 145 |
-
<div class="error-message">{tool.error}</div>
|
| 146 |
-
</div>
|
| 147 |
-
{/if}
|
| 148 |
-
|
| 149 |
-
{#if tool.status === "running" && !tool.output}
|
| 150 |
-
<div class="tool-section">
|
| 151 |
-
<div class="loading-indicator">
|
| 152 |
-
<span class="loading-dots">Processing</span>
|
| 153 |
-
</div>
|
| 154 |
-
</div>
|
| 155 |
-
{/if}
|
| 156 |
-
</div>
|
| 157 |
-
{/if}
|
| 158 |
-
</div>
|
| 159 |
-
{/each}
|
| 160 |
-
</div>
|
| 161 |
-
|
| 162 |
-
<style>
|
| 163 |
-
.tool-block {
|
| 164 |
-
margin: 0.25rem 0;
|
| 165 |
-
display: flex;
|
| 166 |
-
flex-direction: column;
|
| 167 |
-
gap: 0.25rem;
|
| 168 |
-
}
|
| 169 |
-
|
| 170 |
-
.tool-item {
|
| 171 |
-
border-radius: 4px;
|
| 172 |
-
overflow: hidden;
|
| 173 |
-
transition: all 0.2s ease;
|
| 174 |
-
border: 1px solid rgba(65, 105, 225, 0.2);
|
| 175 |
-
background: rgba(65, 105, 225, 0.05);
|
| 176 |
-
}
|
| 177 |
-
|
| 178 |
-
.tool-item.running {
|
| 179 |
-
background: rgba(255, 210, 30, 0.08);
|
| 180 |
-
border: 1px solid rgba(255, 210, 30, 0.3);
|
| 181 |
-
}
|
| 182 |
-
|
| 183 |
-
.tool-item.completed {
|
| 184 |
-
background: rgba(0, 255, 0, 0.05);
|
| 185 |
-
border: 1px solid rgba(0, 255, 0, 0.2);
|
| 186 |
-
}
|
| 187 |
-
|
| 188 |
-
.tool-item.error {
|
| 189 |
-
background: rgba(255, 0, 0, 0.08);
|
| 190 |
-
border: 1px solid rgba(255, 0, 0, 0.3);
|
| 191 |
-
}
|
| 192 |
-
|
| 193 |
-
.tool-item-header {
|
| 194 |
-
display: flex;
|
| 195 |
-
align-items: center;
|
| 196 |
-
gap: 0.5rem;
|
| 197 |
-
width: 100%;
|
| 198 |
-
padding: 0.4rem 0.6rem;
|
| 199 |
-
background: transparent;
|
| 200 |
-
border: none;
|
| 201 |
-
color: inherit;
|
| 202 |
-
font: inherit;
|
| 203 |
-
text-align: left;
|
| 204 |
-
cursor: pointer;
|
| 205 |
-
transition: background 0.2s ease;
|
| 206 |
-
}
|
| 207 |
-
|
| 208 |
-
.tool-item-header:hover {
|
| 209 |
-
background: rgba(255, 255, 255, 0.02);
|
| 210 |
-
}
|
| 211 |
-
|
| 212 |
-
.tool-status-icon {
|
| 213 |
-
font-size: 0.9rem;
|
| 214 |
-
width: 1.2rem;
|
| 215 |
-
text-align: center;
|
| 216 |
-
}
|
| 217 |
-
|
| 218 |
-
.tool-icon {
|
| 219 |
-
font-size: 1rem;
|
| 220 |
-
}
|
| 221 |
-
|
| 222 |
-
.tool-name {
|
| 223 |
-
flex: 1;
|
| 224 |
-
color: rgba(255, 255, 255, 0.9);
|
| 225 |
-
font-size: 0.825rem;
|
| 226 |
-
}
|
| 227 |
-
|
| 228 |
-
.tool-duration {
|
| 229 |
-
color: rgba(255, 255, 255, 0.4);
|
| 230 |
-
font-size: 0.75rem;
|
| 231 |
-
font-family: "Monaco", "Menlo", monospace;
|
| 232 |
-
}
|
| 233 |
-
|
| 234 |
-
.expand-icon {
|
| 235 |
-
font-size: 0.7rem;
|
| 236 |
-
color: rgba(255, 255, 255, 0.4);
|
| 237 |
-
transition: transform 0.2s ease;
|
| 238 |
-
}
|
| 239 |
-
|
| 240 |
-
.expand-icon.rotated {
|
| 241 |
-
transform: rotate(90deg);
|
| 242 |
-
}
|
| 243 |
-
|
| 244 |
-
.tool-details {
|
| 245 |
-
padding: 0.75rem;
|
| 246 |
-
background: rgba(0, 0, 0, 0.2);
|
| 247 |
-
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
| 248 |
-
}
|
| 249 |
-
|
| 250 |
-
.tool-section {
|
| 251 |
-
margin-bottom: 0.75rem;
|
| 252 |
-
}
|
| 253 |
-
|
| 254 |
-
.tool-section:last-child {
|
| 255 |
-
margin-bottom: 0;
|
| 256 |
-
}
|
| 257 |
-
|
| 258 |
-
.section-title {
|
| 259 |
-
color: rgba(255, 255, 255, 0.5);
|
| 260 |
-
font-size: 0.75rem;
|
| 261 |
-
font-weight: 600;
|
| 262 |
-
margin-bottom: 0.25rem;
|
| 263 |
-
text-transform: uppercase;
|
| 264 |
-
letter-spacing: 0.5px;
|
| 265 |
-
}
|
| 266 |
-
|
| 267 |
-
.params {
|
| 268 |
-
font-family: "Monaco", "Menlo", monospace;
|
| 269 |
-
font-size: 0.8rem;
|
| 270 |
-
}
|
| 271 |
-
|
| 272 |
-
.param {
|
| 273 |
-
display: flex;
|
| 274 |
-
gap: 0.5rem;
|
| 275 |
-
margin: 0.25rem 0;
|
| 276 |
-
}
|
| 277 |
-
|
| 278 |
-
.param-key {
|
| 279 |
-
color: rgba(255, 255, 255, 0.5);
|
| 280 |
-
}
|
| 281 |
-
|
| 282 |
-
.param-value {
|
| 283 |
-
color: rgba(255, 210, 30, 0.8);
|
| 284 |
-
word-break: break-all;
|
| 285 |
-
}
|
| 286 |
-
|
| 287 |
-
.param-value pre {
|
| 288 |
-
margin: 0;
|
| 289 |
-
padding: 0.5rem;
|
| 290 |
-
background: rgba(0, 0, 0, 0.3);
|
| 291 |
-
border-radius: 4px;
|
| 292 |
-
font-size: 0.75rem;
|
| 293 |
-
overflow-x: auto;
|
| 294 |
-
max-height: 200px;
|
| 295 |
-
}
|
| 296 |
-
|
| 297 |
-
.tool-output, .console-output {
|
| 298 |
-
background: rgba(0, 0, 0, 0.3);
|
| 299 |
-
border-radius: 4px;
|
| 300 |
-
padding: 0.5rem;
|
| 301 |
-
font-family: "Monaco", "Menlo", monospace;
|
| 302 |
-
font-size: 0.75rem;
|
| 303 |
-
color: rgba(255, 255, 255, 0.8);
|
| 304 |
-
overflow-x: auto;
|
| 305 |
-
max-height: 300px;
|
| 306 |
-
overflow-y: auto;
|
| 307 |
-
}
|
| 308 |
-
|
| 309 |
-
.console-line {
|
| 310 |
-
margin: 0.1rem 0;
|
| 311 |
-
}
|
| 312 |
-
|
| 313 |
-
.error-message {
|
| 314 |
-
color: #ff6b6b;
|
| 315 |
-
font-family: "Monaco", "Menlo", monospace;
|
| 316 |
-
font-size: 0.8rem;
|
| 317 |
-
padding: 0.5rem;
|
| 318 |
-
background: rgba(255, 0, 0, 0.1);
|
| 319 |
-
border-radius: 4px;
|
| 320 |
-
}
|
| 321 |
-
|
| 322 |
-
.loading-indicator {
|
| 323 |
-
text-align: center;
|
| 324 |
-
padding: 1rem;
|
| 325 |
-
color: rgba(255, 255, 255, 0.5);
|
| 326 |
-
font-size: 0.85rem;
|
| 327 |
-
}
|
| 328 |
-
|
| 329 |
-
.loading-dots::after {
|
| 330 |
-
content: "";
|
| 331 |
-
animation: dots 1.5s steps(4, end) infinite;
|
| 332 |
-
}
|
| 333 |
-
|
| 334 |
-
@keyframes dots {
|
| 335 |
-
0%, 20% { content: ""; }
|
| 336 |
-
40% { content: "."; }
|
| 337 |
-
60% { content: ".."; }
|
| 338 |
-
80%, 100% { content: "..."; }
|
| 339 |
-
}
|
| 340 |
-
|
| 341 |
-
.spinner {
|
| 342 |
-
display: inline-block;
|
| 343 |
-
animation: spin 1s linear infinite;
|
| 344 |
-
}
|
| 345 |
-
|
| 346 |
-
.spinner-small {
|
| 347 |
-
display: inline-block;
|
| 348 |
-
animation: spin 0.8s linear infinite;
|
| 349 |
-
}
|
| 350 |
-
|
| 351 |
-
@keyframes spin {
|
| 352 |
-
from { transform: rotate(0deg); }
|
| 353 |
-
to { transform: rotate(360deg); }
|
| 354 |
-
}
|
| 355 |
-
|
| 356 |
-
::-webkit-scrollbar {
|
| 357 |
-
width: 6px;
|
| 358 |
-
height: 6px;
|
| 359 |
-
}
|
| 360 |
-
|
| 361 |
-
::-webkit-scrollbar-track {
|
| 362 |
-
background: transparent;
|
| 363 |
-
}
|
| 364 |
-
|
| 365 |
-
::-webkit-scrollbar-thumb {
|
| 366 |
-
background: rgba(255, 255, 255, 0.1);
|
| 367 |
-
border-radius: 3px;
|
| 368 |
-
}
|
| 369 |
-
|
| 370 |
-
::-webkit-scrollbar-thumb:hover {
|
| 371 |
-
background: rgba(255, 255, 255, 0.2);
|
| 372 |
-
}
|
| 373 |
-
</style>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/lib/components/chat/ToolCallDisplay.svelte
DELETED
|
@@ -1,89 +0,0 @@
|
|
| 1 |
-
<script lang="ts">
|
| 2 |
-
import { onMount } from "svelte";
|
| 3 |
-
import gsap from "gsap";
|
| 4 |
-
|
| 5 |
-
export let toolName: string;
|
| 6 |
-
export let parameters: any = null;
|
| 7 |
-
|
| 8 |
-
let element: HTMLDivElement;
|
| 9 |
-
|
| 10 |
-
onMount(() => {
|
| 11 |
-
gsap.fromTo(
|
| 12 |
-
element,
|
| 13 |
-
{ opacity: 0, scale: 0.9, y: -5 },
|
| 14 |
-
{ opacity: 1, scale: 1, y: 0, duration: 0.3, ease: "back.out(1.5)" }
|
| 15 |
-
);
|
| 16 |
-
});
|
| 17 |
-
|
| 18 |
-
const toolIcons: Record<string, string> = {
|
| 19 |
-
read_game_code: "π",
|
| 20 |
-
edit_game_code: "βοΈ",
|
| 21 |
-
read_console: "π",
|
| 22 |
-
clear_console: "π§Ή",
|
| 23 |
-
run_game: "βΆοΈ",
|
| 24 |
-
stop_game: "βΉ",
|
| 25 |
-
default: "π§",
|
| 26 |
-
};
|
| 27 |
-
|
| 28 |
-
$: icon = toolIcons[toolName] || toolIcons.default;
|
| 29 |
-
</script>
|
| 30 |
-
|
| 31 |
-
<div class="tool-call" bind:this={element}>
|
| 32 |
-
<span class="tool-icon">{icon}</span>
|
| 33 |
-
<span class="tool-name">{toolName}</span>
|
| 34 |
-
{#if parameters && Object.keys(parameters).length > 0}
|
| 35 |
-
<span class="tool-params">
|
| 36 |
-
{#each Object.entries(parameters) as [key, value]}
|
| 37 |
-
<span class="param">
|
| 38 |
-
<span class="param-key">{key}:</span>
|
| 39 |
-
<span class="param-value">{typeof value === 'string' && value.length > 20 ? value.substring(0, 20) + '...' : value}</span>
|
| 40 |
-
</span>
|
| 41 |
-
{/each}
|
| 42 |
-
</span>
|
| 43 |
-
{/if}
|
| 44 |
-
</div>
|
| 45 |
-
|
| 46 |
-
<style>
|
| 47 |
-
.tool-call {
|
| 48 |
-
display: inline-flex;
|
| 49 |
-
align-items: center;
|
| 50 |
-
gap: 0.5rem;
|
| 51 |
-
background: rgba(65, 105, 225, 0.1);
|
| 52 |
-
border: 1px solid rgba(65, 105, 225, 0.3);
|
| 53 |
-
border-radius: 4px;
|
| 54 |
-
padding: 0.25rem 0.5rem;
|
| 55 |
-
margin: 0.25rem 0;
|
| 56 |
-
font-size: 0.8rem;
|
| 57 |
-
font-family: "Monaco", "Menlo", monospace;
|
| 58 |
-
}
|
| 59 |
-
|
| 60 |
-
.tool-icon {
|
| 61 |
-
font-size: 1rem;
|
| 62 |
-
line-height: 1;
|
| 63 |
-
}
|
| 64 |
-
|
| 65 |
-
.tool-name {
|
| 66 |
-
color: rgba(65, 105, 225, 1);
|
| 67 |
-
font-weight: 600;
|
| 68 |
-
}
|
| 69 |
-
|
| 70 |
-
.tool-params {
|
| 71 |
-
display: flex;
|
| 72 |
-
gap: 0.5rem;
|
| 73 |
-
color: rgba(255, 255, 255, 0.6);
|
| 74 |
-
font-size: 0.75rem;
|
| 75 |
-
}
|
| 76 |
-
|
| 77 |
-
.param {
|
| 78 |
-
display: flex;
|
| 79 |
-
gap: 0.25rem;
|
| 80 |
-
}
|
| 81 |
-
|
| 82 |
-
.param-key {
|
| 83 |
-
color: rgba(255, 255, 255, 0.5);
|
| 84 |
-
}
|
| 85 |
-
|
| 86 |
-
.param-value {
|
| 87 |
-
color: rgba(255, 210, 30, 0.7);
|
| 88 |
-
}
|
| 89 |
-
</style>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/lib/components/chat/ToolInvocation.svelte
CHANGED
|
@@ -76,8 +76,8 @@
|
|
| 76 |
|
| 77 |
if (to === "completed") {
|
| 78 |
gsap.to(element, {
|
| 79 |
-
borderColor: "rgba(
|
| 80 |
-
backgroundColor: "rgba(
|
| 81 |
duration: 0.3,
|
| 82 |
ease: "power2.out"
|
| 83 |
});
|
|
@@ -266,8 +266,8 @@
|
|
| 266 |
}
|
| 267 |
|
| 268 |
.tool-invocation.completed {
|
| 269 |
-
background: rgba(
|
| 270 |
-
border-color: rgba(
|
| 271 |
}
|
| 272 |
|
| 273 |
.tool-invocation.error {
|
|
@@ -315,7 +315,7 @@
|
|
| 315 |
}
|
| 316 |
|
| 317 |
.tool-invocation.completed .progress-ring {
|
| 318 |
-
color: rgba(
|
| 319 |
}
|
| 320 |
|
| 321 |
.tool-invocation.error .progress-ring {
|
|
@@ -375,18 +375,22 @@
|
|
| 375 |
}
|
| 376 |
|
| 377 |
.param {
|
| 378 |
-
display:
|
| 379 |
-
|
| 380 |
-
margin: 0.2rem 0;
|
| 381 |
}
|
| 382 |
|
| 383 |
.param-key {
|
| 384 |
color: rgba(255, 255, 255, 0.5);
|
|
|
|
|
|
|
|
|
|
| 385 |
}
|
| 386 |
|
| 387 |
.param-value {
|
| 388 |
color: rgba(255, 210, 30, 0.8);
|
| 389 |
word-break: break-all;
|
|
|
|
|
|
|
| 390 |
}
|
| 391 |
|
| 392 |
.param-value pre {
|
|
@@ -396,9 +400,29 @@
|
|
| 396 |
border-radius: 3px;
|
| 397 |
font-size: 0.7rem;
|
| 398 |
overflow-x: auto;
|
|
|
|
| 399 |
max-height: 150px;
|
| 400 |
}
|
| 401 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 402 |
.spinner {
|
| 403 |
display: inline-block;
|
| 404 |
animation: spin 0.8s linear infinite;
|
|
@@ -412,8 +436,8 @@
|
|
| 412 |
.tool-output {
|
| 413 |
margin-top: 0.5rem;
|
| 414 |
padding: 0.5rem;
|
| 415 |
-
background: rgba(
|
| 416 |
-
border: 1px solid rgba(
|
| 417 |
border-radius: 3px;
|
| 418 |
}
|
| 419 |
|
|
@@ -421,13 +445,32 @@
|
|
| 421 |
margin: 0;
|
| 422 |
font-family: "Monaco", "Menlo", monospace;
|
| 423 |
font-size: 0.7rem;
|
| 424 |
-
color: rgba(
|
| 425 |
white-space: pre-wrap;
|
| 426 |
word-wrap: break-word;
|
| 427 |
max-height: 200px;
|
| 428 |
overflow-y: auto;
|
| 429 |
}
|
| 430 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 431 |
.console-output {
|
| 432 |
margin-top: 0.5rem;
|
| 433 |
background: rgba(0, 0, 0, 0.3);
|
|
@@ -441,6 +484,25 @@
|
|
| 441 |
overflow-y: auto;
|
| 442 |
}
|
| 443 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 444 |
.console-line {
|
| 445 |
margin: 0.1rem 0;
|
| 446 |
}
|
|
|
|
| 76 |
|
| 77 |
if (to === "completed") {
|
| 78 |
gsap.to(element, {
|
| 79 |
+
borderColor: "rgba(100, 200, 180, 0.3)",
|
| 80 |
+
backgroundColor: "rgba(100, 200, 180, 0.05)",
|
| 81 |
duration: 0.3,
|
| 82 |
ease: "power2.out"
|
| 83 |
});
|
|
|
|
| 266 |
}
|
| 267 |
|
| 268 |
.tool-invocation.completed {
|
| 269 |
+
background: rgba(100, 200, 180, 0.04);
|
| 270 |
+
border-color: rgba(100, 200, 180, 0.15);
|
| 271 |
}
|
| 272 |
|
| 273 |
.tool-invocation.error {
|
|
|
|
| 315 |
}
|
| 316 |
|
| 317 |
.tool-invocation.completed .progress-ring {
|
| 318 |
+
color: rgba(100, 200, 180, 0.6);
|
| 319 |
}
|
| 320 |
|
| 321 |
.tool-invocation.error .progress-ring {
|
|
|
|
| 375 |
}
|
| 376 |
|
| 377 |
.param {
|
| 378 |
+
display: block;
|
| 379 |
+
margin: 0.4rem 0;
|
|
|
|
| 380 |
}
|
| 381 |
|
| 382 |
.param-key {
|
| 383 |
color: rgba(255, 255, 255, 0.5);
|
| 384 |
+
display: block;
|
| 385 |
+
margin-bottom: 0.2rem;
|
| 386 |
+
font-size: 0.7rem;
|
| 387 |
}
|
| 388 |
|
| 389 |
.param-value {
|
| 390 |
color: rgba(255, 210, 30, 0.8);
|
| 391 |
word-break: break-all;
|
| 392 |
+
display: block;
|
| 393 |
+
margin-left: 0.5rem;
|
| 394 |
}
|
| 395 |
|
| 396 |
.param-value pre {
|
|
|
|
| 400 |
border-radius: 3px;
|
| 401 |
font-size: 0.7rem;
|
| 402 |
overflow-x: auto;
|
| 403 |
+
overflow-y: auto;
|
| 404 |
max-height: 150px;
|
| 405 |
}
|
| 406 |
|
| 407 |
+
.param-value pre::-webkit-scrollbar {
|
| 408 |
+
width: 8px;
|
| 409 |
+
height: 8px;
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
.param-value pre::-webkit-scrollbar-track {
|
| 413 |
+
background: rgba(0, 0, 0, 0.2);
|
| 414 |
+
border-radius: 4px;
|
| 415 |
+
}
|
| 416 |
+
|
| 417 |
+
.param-value pre::-webkit-scrollbar-thumb {
|
| 418 |
+
background: rgba(100, 200, 180, 0.3);
|
| 419 |
+
border-radius: 4px;
|
| 420 |
+
}
|
| 421 |
+
|
| 422 |
+
.param-value pre::-webkit-scrollbar-thumb:hover {
|
| 423 |
+
background: rgba(100, 200, 180, 0.5);
|
| 424 |
+
}
|
| 425 |
+
|
| 426 |
.spinner {
|
| 427 |
display: inline-block;
|
| 428 |
animation: spin 0.8s linear infinite;
|
|
|
|
| 436 |
.tool-output {
|
| 437 |
margin-top: 0.5rem;
|
| 438 |
padding: 0.5rem;
|
| 439 |
+
background: rgba(100, 200, 180, 0.05);
|
| 440 |
+
border: 1px solid rgba(100, 200, 180, 0.1);
|
| 441 |
border-radius: 3px;
|
| 442 |
}
|
| 443 |
|
|
|
|
| 445 |
margin: 0;
|
| 446 |
font-family: "Monaco", "Menlo", monospace;
|
| 447 |
font-size: 0.7rem;
|
| 448 |
+
color: rgba(100, 200, 180, 0.9);
|
| 449 |
white-space: pre-wrap;
|
| 450 |
word-wrap: break-word;
|
| 451 |
max-height: 200px;
|
| 452 |
overflow-y: auto;
|
| 453 |
}
|
| 454 |
|
| 455 |
+
.tool-output pre::-webkit-scrollbar {
|
| 456 |
+
width: 8px;
|
| 457 |
+
height: 8px;
|
| 458 |
+
}
|
| 459 |
+
|
| 460 |
+
.tool-output pre::-webkit-scrollbar-track {
|
| 461 |
+
background: rgba(0, 0, 0, 0.2);
|
| 462 |
+
border-radius: 4px;
|
| 463 |
+
}
|
| 464 |
+
|
| 465 |
+
.tool-output pre::-webkit-scrollbar-thumb {
|
| 466 |
+
background: rgba(100, 200, 180, 0.3);
|
| 467 |
+
border-radius: 4px;
|
| 468 |
+
}
|
| 469 |
+
|
| 470 |
+
.tool-output pre::-webkit-scrollbar-thumb:hover {
|
| 471 |
+
background: rgba(100, 200, 180, 0.5);
|
| 472 |
+
}
|
| 473 |
+
|
| 474 |
.console-output {
|
| 475 |
margin-top: 0.5rem;
|
| 476 |
background: rgba(0, 0, 0, 0.3);
|
|
|
|
| 484 |
overflow-y: auto;
|
| 485 |
}
|
| 486 |
|
| 487 |
+
.console-output::-webkit-scrollbar {
|
| 488 |
+
width: 8px;
|
| 489 |
+
height: 8px;
|
| 490 |
+
}
|
| 491 |
+
|
| 492 |
+
.console-output::-webkit-scrollbar-track {
|
| 493 |
+
background: rgba(0, 0, 0, 0.2);
|
| 494 |
+
border-radius: 4px;
|
| 495 |
+
}
|
| 496 |
+
|
| 497 |
+
.console-output::-webkit-scrollbar-thumb {
|
| 498 |
+
background: rgba(65, 105, 225, 0.3);
|
| 499 |
+
border-radius: 4px;
|
| 500 |
+
}
|
| 501 |
+
|
| 502 |
+
.console-output::-webkit-scrollbar-thumb:hover {
|
| 503 |
+
background: rgba(65, 105, 225, 0.5);
|
| 504 |
+
}
|
| 505 |
+
|
| 506 |
.console-line {
|
| 507 |
margin: 0.1rem 0;
|
| 508 |
}
|
src/lib/components/chat/context.md
CHANGED
|
@@ -4,19 +4,18 @@ AI chat interface with real-time streaming.
|
|
| 4 |
|
| 5 |
## Components
|
| 6 |
|
| 7 |
-
- `ChatPanel.svelte` - Main chat UI with smooth scroll
|
| 8 |
- `MessageSegment.svelte` - Renders text, tool invocations, and results
|
| 9 |
- `StreamingText.svelte` - Optimized character streaming with state persistence
|
| 10 |
-
- `ToolInvocation.svelte` -
|
| 11 |
- `InProgressBlock.svelte` - Status indicator with progress bar
|
| 12 |
- `ReasoningBlock.svelte` - Auto-collapsing thinking viewer
|
| 13 |
- `MarkdownRenderer.svelte` - Markdown parser with configurable streaming speed
|
| 14 |
-
- `ToolCallDisplay.svelte`, `ToolCallBlock.svelte` - Legacy tool rendering (deprecated)
|
| 15 |
|
| 16 |
## Architecture
|
| 17 |
|
| 18 |
-
-
|
| 19 |
- StreamingText tracks processed content length to prevent duplication
|
| 20 |
- Batch character processing (3 chars/cycle) at 120 chars/sec
|
| 21 |
-
- Tool invocations
|
| 22 |
-
-
|
|
|
|
| 4 |
|
| 5 |
## Components
|
| 6 |
|
| 7 |
+
- `ChatPanel.svelte` - Main chat UI with smooth scroll
|
| 8 |
- `MessageSegment.svelte` - Renders text, tool invocations, and results
|
| 9 |
- `StreamingText.svelte` - Optimized character streaming with state persistence
|
| 10 |
+
- `ToolInvocation.svelte` - Tool execution and results display
|
| 11 |
- `InProgressBlock.svelte` - Status indicator with progress bar
|
| 12 |
- `ReasoningBlock.svelte` - Auto-collapsing thinking viewer
|
| 13 |
- `MarkdownRenderer.svelte` - Markdown parser with configurable streaming speed
|
|
|
|
| 14 |
|
| 15 |
## Architecture
|
| 16 |
|
| 17 |
+
- Segment-based messaging with unique IDs
|
| 18 |
- StreamingText tracks processed content length to prevent duplication
|
| 19 |
- Batch character processing (3 chars/cycle) at 120 chars/sec
|
| 20 |
+
- Tool invocations displayed through dedicated segments
|
| 21 |
+
- Clean separation between text and tool content
|
src/lib/server/context.md
CHANGED
|
@@ -5,8 +5,8 @@ WebSocket server with LangGraph agent for AI-assisted game development.
|
|
| 5 |
## Key Components
|
| 6 |
|
| 7 |
- **api.ts** - WebSocket message routing
|
| 8 |
-
- **langgraph-agent.ts** - LangGraph agent with
|
| 9 |
-
- **tools.ts** - Editor read/write
|
| 10 |
- **console-buffer.ts** - Console message storage
|
| 11 |
- **documentation.ts** - VibeGame documentation loader
|
| 12 |
|
|
@@ -14,8 +14,8 @@ WebSocket server with LangGraph agent for AI-assisted game development.
|
|
| 14 |
|
| 15 |
LangGraph state machine with real-time streaming:
|
| 16 |
|
| 17 |
-
-
|
| 18 |
-
- Tool invocations
|
| 19 |
- Explicit message IDs required for all segment operations
|
| 20 |
|
| 21 |
## Message Protocol
|
|
|
|
| 5 |
## Key Components
|
| 6 |
|
| 7 |
- **api.ts** - WebSocket message routing
|
| 8 |
+
- **langgraph-agent.ts** - LangGraph agent with buffered streaming
|
| 9 |
+
- **tools.ts** - Editor manipulation: full read/write, line-based reading, text/regex search, search-replace editing
|
| 10 |
- **console-buffer.ts** - Console message storage
|
| 11 |
- **documentation.ts** - VibeGame documentation loader
|
| 12 |
|
|
|
|
| 14 |
|
| 15 |
LangGraph state machine with real-time streaming:
|
| 16 |
|
| 17 |
+
- Buffers and filters tool patterns from text segments
|
| 18 |
+
- Tool invocations handled separately from text content
|
| 19 |
- Explicit message IDs required for all segment operations
|
| 20 |
|
| 21 |
## Message Protocol
|
src/lib/server/langgraph-agent.ts
CHANGED
|
@@ -8,6 +8,9 @@ import {
|
|
| 8 |
} from "@langchain/core/messages";
|
| 9 |
import {
|
| 10 |
readEditorTool,
|
|
|
|
|
|
|
|
|
|
| 11 |
writeEditorTool,
|
| 12 |
observeConsoleTool,
|
| 13 |
setWebSocketConnection,
|
|
@@ -67,17 +70,60 @@ export class LangGraphAgent {
|
|
| 67 |
let fullResponse = "";
|
| 68 |
let currentSegmentId: string | null = null;
|
| 69 |
let currentSegmentContent = "";
|
| 70 |
-
let
|
| 71 |
const messageId = config?.metadata?.messageId;
|
|
|
|
| 72 |
|
| 73 |
for await (const token of this.streamModelResponse(messages)) {
|
| 74 |
fullResponse += token;
|
| 75 |
config?.writer?.({ type: "token", content: token });
|
|
|
|
| 76 |
|
| 77 |
-
|
| 78 |
-
|
|
|
|
|
|
|
| 79 |
|
| 80 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
if (currentSegmentId && currentSegmentContent.trim() && this.ws) {
|
| 82 |
this.ws.send(
|
| 83 |
JSON.stringify({
|
|
@@ -90,14 +136,19 @@ export class LangGraphAgent {
|
|
| 90 |
timestamp: Date.now(),
|
| 91 |
}),
|
| 92 |
);
|
|
|
|
|
|
|
| 93 |
}
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
|
|
|
|
|
|
|
|
|
| 101 |
currentSegmentId = `seg_${Date.now()}_${Math.random()}`;
|
| 102 |
currentSegmentContent = "";
|
| 103 |
this.ws.send(
|
|
@@ -114,14 +165,14 @@ export class LangGraphAgent {
|
|
| 114 |
}
|
| 115 |
|
| 116 |
if (currentSegmentId) {
|
| 117 |
-
currentSegmentContent +=
|
| 118 |
if (this.ws) {
|
| 119 |
this.ws.send(
|
| 120 |
JSON.stringify({
|
| 121 |
type: "segment_token",
|
| 122 |
payload: {
|
| 123 |
segmentId: currentSegmentId,
|
| 124 |
-
token,
|
| 125 |
messageId,
|
| 126 |
},
|
| 127 |
timestamp: Date.now(),
|
|
@@ -129,6 +180,46 @@ export class LangGraphAgent {
|
|
| 129 |
);
|
| 130 |
}
|
| 131 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
}
|
| 133 |
}
|
| 134 |
|
|
@@ -207,28 +298,69 @@ VIBEGAME DOCUMENTATION:
|
|
| 207 |
${this.documentation}
|
| 208 |
|
| 209 |
AVAILABLE TOOLS:
|
| 210 |
-
- read_editor: Read the current code in the editor
|
| 211 |
-
- write_editor: Write new code to the editor and wait for game to reload
|
| 212 |
-
- observe_console: Read recent console messages from the game
|
| 213 |
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
or
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
Example:
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 232 |
|
| 233 |
Be concise, accurate, and focus on practical solutions.`;
|
| 234 |
}
|
|
@@ -359,6 +491,29 @@ Be concise, accurate, and focus on practical solutions.`;
|
|
| 359 |
|
| 360 |
if (call.name === "read_editor") {
|
| 361 |
result = await readEditorTool.func("");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 362 |
} else if (call.name === "write_editor") {
|
| 363 |
result = await writeEditorTool.func(call.args as { content: string });
|
| 364 |
|
|
|
|
| 8 |
} from "@langchain/core/messages";
|
| 9 |
import {
|
| 10 |
readEditorTool,
|
| 11 |
+
readEditorLinesTool,
|
| 12 |
+
searchEditorTool,
|
| 13 |
+
editEditorTool,
|
| 14 |
writeEditorTool,
|
| 15 |
observeConsoleTool,
|
| 16 |
setWebSocketConnection,
|
|
|
|
| 70 |
let fullResponse = "";
|
| 71 |
let currentSegmentId: string | null = null;
|
| 72 |
let currentSegmentContent = "";
|
| 73 |
+
let buffer = "";
|
| 74 |
const messageId = config?.metadata?.messageId;
|
| 75 |
+
const toolRegex = /\[TOOL:\s*(\w+)(?:\s+({[^}]+}))?\]/g;
|
| 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; // Reset regex state
|
| 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 && this.ws) {
|
| 93 |
+
currentSegmentId = `seg_${Date.now()}_${Math.random()}`;
|
| 94 |
+
currentSegmentContent = "";
|
| 95 |
+
this.ws.send(
|
| 96 |
+
JSON.stringify({
|
| 97 |
+
type: "segment_start",
|
| 98 |
+
payload: {
|
| 99 |
+
segmentId: currentSegmentId,
|
| 100 |
+
segmentType: "text",
|
| 101 |
+
messageId,
|
| 102 |
+
},
|
| 103 |
+
timestamp: Date.now(),
|
| 104 |
+
}),
|
| 105 |
+
);
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
if (currentSegmentId) {
|
| 109 |
+
currentSegmentContent += textBefore;
|
| 110 |
+
if (this.ws) {
|
| 111 |
+
this.ws.send(
|
| 112 |
+
JSON.stringify({
|
| 113 |
+
type: "segment_token",
|
| 114 |
+
payload: {
|
| 115 |
+
segmentId: currentSegmentId,
|
| 116 |
+
token: textBefore,
|
| 117 |
+
messageId,
|
| 118 |
+
},
|
| 119 |
+
timestamp: Date.now(),
|
| 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({
|
|
|
|
| 136 |
timestamp: Date.now(),
|
| 137 |
}),
|
| 138 |
);
|
| 139 |
+
currentSegmentId = null;
|
| 140 |
+
currentSegmentContent = "";
|
| 141 |
}
|
| 142 |
+
|
| 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("[TOOL:")) {
|
| 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 = "";
|
| 154 |
this.ws.send(
|
|
|
|
| 165 |
}
|
| 166 |
|
| 167 |
if (currentSegmentId) {
|
| 168 |
+
currentSegmentContent += buffer;
|
| 169 |
if (this.ws) {
|
| 170 |
this.ws.send(
|
| 171 |
JSON.stringify({
|
| 172 |
type: "segment_token",
|
| 173 |
payload: {
|
| 174 |
segmentId: currentSegmentId,
|
| 175 |
+
token: buffer,
|
| 176 |
messageId,
|
| 177 |
},
|
| 178 |
timestamp: Date.now(),
|
|
|
|
| 180 |
);
|
| 181 |
}
|
| 182 |
}
|
| 183 |
+
buffer = "";
|
| 184 |
+
}
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
// Flush remaining buffer after stream ends
|
| 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 = "";
|
| 195 |
+
this.ws.send(
|
| 196 |
+
JSON.stringify({
|
| 197 |
+
type: "segment_start",
|
| 198 |
+
payload: {
|
| 199 |
+
segmentId: currentSegmentId,
|
| 200 |
+
segmentType: "text",
|
| 201 |
+
messageId,
|
| 202 |
+
},
|
| 203 |
+
timestamp: Date.now(),
|
| 204 |
+
}),
|
| 205 |
+
);
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
if (currentSegmentId) {
|
| 209 |
+
currentSegmentContent += buffer;
|
| 210 |
+
if (this.ws) {
|
| 211 |
+
this.ws.send(
|
| 212 |
+
JSON.stringify({
|
| 213 |
+
type: "segment_token",
|
| 214 |
+
payload: {
|
| 215 |
+
segmentId: currentSegmentId,
|
| 216 |
+
token: buffer,
|
| 217 |
+
messageId,
|
| 218 |
+
},
|
| 219 |
+
timestamp: Date.now(),
|
| 220 |
+
}),
|
| 221 |
+
);
|
| 222 |
+
}
|
| 223 |
}
|
| 224 |
}
|
| 225 |
|
|
|
|
| 298 |
${this.documentation}
|
| 299 |
|
| 300 |
AVAILABLE TOOLS:
|
|
|
|
|
|
|
|
|
|
| 301 |
|
| 302 |
+
1. search_editor - Search for text or patterns to locate code elements
|
| 303 |
+
Use FIRST when: Looking for specific elements, functions, or components
|
| 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 |
}
|
|
|
|
| 491 |
|
| 492 |
if (call.name === "read_editor") {
|
| 493 |
result = await readEditorTool.func("");
|
| 494 |
+
} else if (call.name === "read_editor_lines") {
|
| 495 |
+
result = await readEditorLinesTool.func(
|
| 496 |
+
call.args as { startLine: number; endLine?: number },
|
| 497 |
+
);
|
| 498 |
+
} else if (call.name === "search_editor") {
|
| 499 |
+
result = await searchEditorTool.func(
|
| 500 |
+
call.args as {
|
| 501 |
+
query: string;
|
| 502 |
+
mode?: "text" | "regex";
|
| 503 |
+
contextLines?: number;
|
| 504 |
+
},
|
| 505 |
+
);
|
| 506 |
+
} else if (call.name === "edit_editor") {
|
| 507 |
+
result = await editEditorTool.func(
|
| 508 |
+
call.args as { oldText: string; newText: string },
|
| 509 |
+
);
|
| 510 |
+
|
| 511 |
+
const consoleMatch = result.match(/Console output:\n([\s\S]*?)$/);
|
| 512 |
+
if (consoleMatch) {
|
| 513 |
+
consoleOutput = consoleMatch[1]
|
| 514 |
+
.split("\n")
|
| 515 |
+
.filter((line) => line.trim());
|
| 516 |
+
}
|
| 517 |
} else if (call.name === "write_editor") {
|
| 518 |
result = await writeEditorTool.func(call.args as { content: string });
|
| 519 |
|
src/lib/server/tools.ts
CHANGED
|
@@ -34,15 +34,127 @@ export function updateEditorContent(content: string) {
|
|
| 34 |
|
| 35 |
export const readEditorTool = new DynamicTool({
|
| 36 |
name: "read_editor",
|
| 37 |
-
description:
|
|
|
|
| 38 |
func: async () => {
|
| 39 |
return `Current editor content (html):\n${currentEditorContent}`;
|
| 40 |
},
|
| 41 |
});
|
| 42 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
export const writeEditorTool = new DynamicStructuredTool({
|
| 44 |
name: "write_editor",
|
| 45 |
-
description:
|
|
|
|
| 46 |
schema: z.object({
|
| 47 |
content: z.string().describe("The code content to write to the editor"),
|
| 48 |
}),
|
|
@@ -87,9 +199,102 @@ export const writeEditorTool = new DynamicStructuredTool({
|
|
| 87 |
},
|
| 88 |
});
|
| 89 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
export const observeConsoleTool = new DynamicTool({
|
| 91 |
name: "observe_console",
|
| 92 |
-
description:
|
|
|
|
| 93 |
func: async () => {
|
| 94 |
const messages = consoleBuffer.getRecentMessages();
|
| 95 |
const gameState = consoleBuffer.getGameStateFromMessages();
|
|
@@ -115,4 +320,11 @@ export const observeConsoleTool = new DynamicTool({
|
|
| 115 |
},
|
| 116 |
});
|
| 117 |
|
| 118 |
-
export const tools = [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
|
| 35 |
export const readEditorTool = new DynamicTool({
|
| 36 |
name: "read_editor",
|
| 37 |
+
description:
|
| 38 |
+
"Read the complete editor content - use for initial exploration or when search returns no results",
|
| 39 |
func: async () => {
|
| 40 |
return `Current editor content (html):\n${currentEditorContent}`;
|
| 41 |
},
|
| 42 |
});
|
| 43 |
|
| 44 |
+
export const readEditorLinesTool = new DynamicStructuredTool({
|
| 45 |
+
name: "read_editor_lines",
|
| 46 |
+
description:
|
| 47 |
+
"Read specific lines from the editor - use AFTER search_editor to examine found code sections in detail",
|
| 48 |
+
schema: z.object({
|
| 49 |
+
startLine: z
|
| 50 |
+
.number()
|
| 51 |
+
.min(1)
|
| 52 |
+
.describe("The starting line number (1-indexed)"),
|
| 53 |
+
endLine: z
|
| 54 |
+
.number()
|
| 55 |
+
.min(1)
|
| 56 |
+
.optional()
|
| 57 |
+
.describe(
|
| 58 |
+
"The ending line number (inclusive). If not provided, only the start line is returned",
|
| 59 |
+
),
|
| 60 |
+
}),
|
| 61 |
+
func: async (input: { startLine: number; endLine?: number }) => {
|
| 62 |
+
const lines = currentEditorContent.split("\n");
|
| 63 |
+
const totalLines = lines.length;
|
| 64 |
+
|
| 65 |
+
if (input.startLine > totalLines) {
|
| 66 |
+
return `Error: Start line ${input.startLine} exceeds total lines (${totalLines})`;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
const endLine = input.endLine || input.startLine;
|
| 70 |
+
if (endLine > totalLines) {
|
| 71 |
+
return `Error: End line ${endLine} exceeds total lines (${totalLines})`;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
if (input.startLine > endLine) {
|
| 75 |
+
return `Error: Start line (${input.startLine}) cannot be greater than end line (${endLine})`;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
const selectedLines = lines.slice(input.startLine - 1, endLine);
|
| 79 |
+
const lineNumbers: number[] = [];
|
| 80 |
+
for (let i = input.startLine; i <= endLine; i++) {
|
| 81 |
+
lineNumbers.push(i);
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
const result = selectedLines
|
| 85 |
+
.map((line, index) => `${lineNumbers[index]}: ${line}`)
|
| 86 |
+
.join("\n");
|
| 87 |
+
|
| 88 |
+
return `Lines ${input.startLine}-${endLine} of ${totalLines}:\n${result}`;
|
| 89 |
+
},
|
| 90 |
+
});
|
| 91 |
+
|
| 92 |
+
export const editEditorTool = new DynamicStructuredTool({
|
| 93 |
+
name: "edit_editor",
|
| 94 |
+
description:
|
| 95 |
+
"Replace specific text in the editor - use for targeted changes after locating code with search_editor",
|
| 96 |
+
schema: z.object({
|
| 97 |
+
oldText: z.string().describe("The exact text to find and replace"),
|
| 98 |
+
newText: z.string().describe("The text to replace it with"),
|
| 99 |
+
}),
|
| 100 |
+
func: async (input: { oldText: string; newText: string }) => {
|
| 101 |
+
if (!currentEditorContent.includes(input.oldText)) {
|
| 102 |
+
return `Error: Could not find the specified text to replace. Make sure the oldText matches exactly, including whitespace.`;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
const occurrences = currentEditorContent.split(input.oldText).length - 1;
|
| 106 |
+
if (occurrences > 1) {
|
| 107 |
+
return `Warning: Found ${occurrences} occurrences of the text. Use write_editor for multiple replacements or be more specific.`;
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
const newContent = currentEditorContent.replace(
|
| 111 |
+
input.oldText,
|
| 112 |
+
input.newText,
|
| 113 |
+
);
|
| 114 |
+
currentEditorContent = newContent;
|
| 115 |
+
|
| 116 |
+
consoleBuffer.clear();
|
| 117 |
+
|
| 118 |
+
if (wsConnection) {
|
| 119 |
+
wsConnection.send({
|
| 120 |
+
type: "editor_update",
|
| 121 |
+
payload: { content: newContent },
|
| 122 |
+
});
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
const startTime = Date.now();
|
| 126 |
+
const maxWaitTime = 3000;
|
| 127 |
+
|
| 128 |
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
| 129 |
+
|
| 130 |
+
while (Date.now() - startTime < maxWaitTime) {
|
| 131 |
+
const gameState = consoleBuffer.getGameStateFromMessages();
|
| 132 |
+
|
| 133 |
+
if (gameState.isReady) {
|
| 134 |
+
const messages = consoleBuffer.getRecentMessages();
|
| 135 |
+
consoleBuffer.markAsRead();
|
| 136 |
+
return `Text replaced successfully. Game reloaded without errors.\nRecent console output:\n${messages.map((m) => `[${m.type}] ${m.message}`).join("\n")}`;
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
if (gameState.hasError) {
|
| 140 |
+
const messages = consoleBuffer.getRecentMessages();
|
| 141 |
+
consoleBuffer.markAsRead();
|
| 142 |
+
return `Text replaced but game failed to start.\nError: ${gameState.lastError}\nFull console output:\n${messages.map((m) => `[${m.type}] ${m.message}`).join("\n")}`;
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
const messages = consoleBuffer.getRecentMessages();
|
| 149 |
+
consoleBuffer.markAsRead();
|
| 150 |
+
return `Text replaced. Game reload status uncertain (timeout).\nConsole output:\n${messages.map((m) => `[${m.type}] ${m.message}`).join("\n")}`;
|
| 151 |
+
},
|
| 152 |
+
});
|
| 153 |
+
|
| 154 |
export const writeEditorTool = new DynamicStructuredTool({
|
| 155 |
name: "write_editor",
|
| 156 |
+
description:
|
| 157 |
+
"Replace entire editor content - use for creating new files or complete rewrites",
|
| 158 |
schema: z.object({
|
| 159 |
content: z.string().describe("The code content to write to the editor"),
|
| 160 |
}),
|
|
|
|
| 199 |
},
|
| 200 |
});
|
| 201 |
|
| 202 |
+
export const searchEditorTool = new DynamicStructuredTool({
|
| 203 |
+
name: "search_editor",
|
| 204 |
+
description:
|
| 205 |
+
"Search for code elements and get line numbers - use FIRST to locate specific functions, classes, or components before reading or editing",
|
| 206 |
+
schema: z.object({
|
| 207 |
+
query: z.string().describe("Text or regex pattern to search for"),
|
| 208 |
+
mode: z
|
| 209 |
+
.enum(["text", "regex"])
|
| 210 |
+
.optional()
|
| 211 |
+
.describe(
|
| 212 |
+
"Search mode: 'text' for literal text search, 'regex' for pattern matching (default: text)",
|
| 213 |
+
),
|
| 214 |
+
contextLines: z
|
| 215 |
+
.number()
|
| 216 |
+
.min(0)
|
| 217 |
+
.max(5)
|
| 218 |
+
.optional()
|
| 219 |
+
.describe(
|
| 220 |
+
"Number of context lines before/after match (default: 2, max: 5)",
|
| 221 |
+
),
|
| 222 |
+
}),
|
| 223 |
+
func: async (input: {
|
| 224 |
+
query: string;
|
| 225 |
+
mode?: "text" | "regex";
|
| 226 |
+
contextLines?: number;
|
| 227 |
+
}) => {
|
| 228 |
+
const lines = currentEditorContent.split("\n");
|
| 229 |
+
const totalLines = lines.length;
|
| 230 |
+
const mode = input.mode || "text";
|
| 231 |
+
const contextLines = input.contextLines ?? 2;
|
| 232 |
+
|
| 233 |
+
const matches: Array<{
|
| 234 |
+
lineNumber: number;
|
| 235 |
+
line: string;
|
| 236 |
+
context: string[];
|
| 237 |
+
}> = [];
|
| 238 |
+
|
| 239 |
+
for (let i = 0; i < lines.length; i++) {
|
| 240 |
+
let isMatch = false;
|
| 241 |
+
|
| 242 |
+
if (mode === "text") {
|
| 243 |
+
isMatch = lines[i].includes(input.query);
|
| 244 |
+
} else if (mode === "regex") {
|
| 245 |
+
try {
|
| 246 |
+
const regex = new RegExp(input.query);
|
| 247 |
+
isMatch = regex.test(lines[i]);
|
| 248 |
+
} catch (e) {
|
| 249 |
+
return `Error: Invalid regex pattern "${input.query}"`;
|
| 250 |
+
}
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
if (isMatch) {
|
| 254 |
+
const startContext = Math.max(0, i - contextLines);
|
| 255 |
+
const endContext = Math.min(lines.length - 1, i + contextLines);
|
| 256 |
+
|
| 257 |
+
const contextArray: string[] = [];
|
| 258 |
+
for (let j = startContext; j <= endContext; j++) {
|
| 259 |
+
const lineNum = j + 1;
|
| 260 |
+
const prefix = j === i ? ">>> " : " ";
|
| 261 |
+
contextArray.push(`${prefix}${lineNum}: ${lines[j]}`);
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
matches.push({
|
| 265 |
+
lineNumber: i + 1,
|
| 266 |
+
line: lines[i],
|
| 267 |
+
context: contextArray,
|
| 268 |
+
});
|
| 269 |
+
}
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
if (matches.length === 0) {
|
| 273 |
+
return `No matches found for "${input.query}" in editor content (${totalLines} lines searched)`;
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
const totalMatches = matches.length;
|
| 277 |
+
const displayMatches = matches.slice(0, 10);
|
| 278 |
+
|
| 279 |
+
let output = `Found ${totalMatches} match${totalMatches > 1 ? "es" : ""} for "${input.query}":\n\n`;
|
| 280 |
+
|
| 281 |
+
displayMatches.forEach((match, index) => {
|
| 282 |
+
if (index > 0) output += "\n---\n\n";
|
| 283 |
+
output += match.context.join("\n");
|
| 284 |
+
});
|
| 285 |
+
|
| 286 |
+
if (totalMatches > 10) {
|
| 287 |
+
output += `\n\n(Showing first 10 of ${totalMatches} matches. Use more specific search terms to narrow results)`;
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
return output;
|
| 291 |
+
},
|
| 292 |
+
});
|
| 293 |
+
|
| 294 |
export const observeConsoleTool = new DynamicTool({
|
| 295 |
name: "observe_console",
|
| 296 |
+
description:
|
| 297 |
+
"Read console messages and game state - use to check for errors after making changes",
|
| 298 |
func: async () => {
|
| 299 |
const messages = consoleBuffer.getRecentMessages();
|
| 300 |
const gameState = consoleBuffer.getGameStateFromMessages();
|
|
|
|
| 320 |
},
|
| 321 |
});
|
| 322 |
|
| 323 |
+
export const tools = [
|
| 324 |
+
readEditorTool,
|
| 325 |
+
readEditorLinesTool,
|
| 326 |
+
searchEditorTool,
|
| 327 |
+
editEditorTool,
|
| 328 |
+
writeEditorTool,
|
| 329 |
+
observeConsoleTool,
|
| 330 |
+
];
|
src/lib/services/agent.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { websocketService, type WebSocketMessage } from "./websocket";
|
| 2 |
+
import { messageHandler } from "./message-handler";
|
| 3 |
+
import { authStore } from "./auth";
|
| 4 |
+
import { agentStore, type ChatMessage } from "../stores/agent";
|
| 5 |
+
import { editorStore } from "../stores/editor";
|
| 6 |
+
import { get } from "svelte/store";
|
| 7 |
+
|
| 8 |
+
export class AgentService {
|
| 9 |
+
private isInitialized = false;
|
| 10 |
+
private unsubscribeHandlers: Array<() => void> = [];
|
| 11 |
+
|
| 12 |
+
connect(): void {
|
| 13 |
+
if (this.isInitialized) return;
|
| 14 |
+
|
| 15 |
+
this.unsubscribeHandlers.push(
|
| 16 |
+
websocketService.onConnection(this.handleConnection.bind(this)),
|
| 17 |
+
websocketService.onMessage(this.handleMessage.bind(this)),
|
| 18 |
+
);
|
| 19 |
+
|
| 20 |
+
websocketService.connect();
|
| 21 |
+
this.isInitialized = true;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
disconnect(): void {
|
| 25 |
+
websocketService.disconnect();
|
| 26 |
+
this.unsubscribeHandlers.forEach((unsubscribe) => unsubscribe());
|
| 27 |
+
this.unsubscribeHandlers = [];
|
| 28 |
+
this.isInitialized = false;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
sendMessage(content: string): void {
|
| 32 |
+
if (!websocketService.isConnected()) {
|
| 33 |
+
agentStore.setError("Not connected to server");
|
| 34 |
+
return;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
const userMessage: ChatMessage = {
|
| 38 |
+
id: `user_${Date.now()}`,
|
| 39 |
+
role: "user",
|
| 40 |
+
content,
|
| 41 |
+
timestamp: Date.now(),
|
| 42 |
+
streaming: false,
|
| 43 |
+
};
|
| 44 |
+
|
| 45 |
+
agentStore.addMessage(userMessage);
|
| 46 |
+
agentStore.setError(null);
|
| 47 |
+
|
| 48 |
+
websocketService.send({
|
| 49 |
+
type: "chat",
|
| 50 |
+
payload: { content },
|
| 51 |
+
timestamp: Date.now(),
|
| 52 |
+
});
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
sendRawMessage(message: unknown): void {
|
| 56 |
+
if (websocketService.isConnected()) {
|
| 57 |
+
websocketService.send(message as WebSocketMessage);
|
| 58 |
+
}
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
reauthenticate(): void {
|
| 62 |
+
if (websocketService.isConnected()) {
|
| 63 |
+
const token = authStore.getToken();
|
| 64 |
+
if (token) {
|
| 65 |
+
websocketService.send({
|
| 66 |
+
type: "auth",
|
| 67 |
+
payload: { token },
|
| 68 |
+
timestamp: Date.now(),
|
| 69 |
+
});
|
| 70 |
+
}
|
| 71 |
+
}
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
private handleConnection(connected: boolean): void {
|
| 75 |
+
agentStore.setConnected(connected);
|
| 76 |
+
|
| 77 |
+
if (connected) {
|
| 78 |
+
this.authenticate();
|
| 79 |
+
this.syncEditor();
|
| 80 |
+
}
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
private handleMessage(message: WebSocketMessage): void {
|
| 84 |
+
messageHandler.handleMessage(message);
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
private authenticate(): void {
|
| 88 |
+
const token = authStore.getToken();
|
| 89 |
+
if (token) {
|
| 90 |
+
websocketService.send({
|
| 91 |
+
type: "auth",
|
| 92 |
+
payload: { token },
|
| 93 |
+
timestamp: Date.now(),
|
| 94 |
+
});
|
| 95 |
+
} else {
|
| 96 |
+
agentStore.setError(
|
| 97 |
+
"Authentication required. Please sign in with Hugging Face.",
|
| 98 |
+
);
|
| 99 |
+
agentStore.setConnected(false);
|
| 100 |
+
websocketService.disconnect();
|
| 101 |
+
}
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
private syncEditor(): void {
|
| 105 |
+
const editorState = get(editorStore);
|
| 106 |
+
if (editorState?.content) {
|
| 107 |
+
setTimeout(() => {
|
| 108 |
+
if (websocketService.isConnected()) {
|
| 109 |
+
websocketService.send({
|
| 110 |
+
type: "editor_sync",
|
| 111 |
+
payload: { content: editorState.content },
|
| 112 |
+
timestamp: Date.now(),
|
| 113 |
+
});
|
| 114 |
+
}
|
| 115 |
+
}, 500);
|
| 116 |
+
}
|
| 117 |
+
}
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
export const agentService = new AgentService();
|
src/lib/services/console-forward.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
import { consoleStore, type ConsoleMessage } from "../stores/console";
|
| 2 |
-
import {
|
| 3 |
|
| 4 |
export class ConsoleForwarder {
|
| 5 |
private static instance: ConsoleForwarder | null = null;
|
|
@@ -40,7 +40,7 @@ export class ConsoleForwarder {
|
|
| 40 |
|
| 41 |
private forwardMessages(messages: ConsoleMessage[]): void {
|
| 42 |
messages.forEach((message) => {
|
| 43 |
-
|
| 44 |
type: "console_sync",
|
| 45 |
payload: {
|
| 46 |
id: message.id,
|
|
|
|
| 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;
|
|
|
|
| 40 |
|
| 41 |
private forwardMessages(messages: ConsoleMessage[]): void {
|
| 42 |
messages.forEach((message) => {
|
| 43 |
+
agentService.sendRawMessage({
|
| 44 |
type: "console_sync",
|
| 45 |
payload: {
|
| 46 |
id: message.id,
|
src/lib/services/context.md
CHANGED
|
@@ -14,6 +14,9 @@ Business logic layer with pure functions and singleton services
|
|
| 14 |
services/
|
| 15 |
βββ context.md # This file
|
| 16 |
βββ auth.ts # Hugging Face OAuth authentication
|
|
|
|
|
|
|
|
|
|
| 17 |
βββ game-engine.ts # Game lifecycle management
|
| 18 |
βββ console-capture.ts # Console interception
|
| 19 |
βββ console-forward.ts # WebSocket console forwarding
|
|
@@ -31,10 +34,14 @@ services/
|
|
| 31 |
- `authStore.logout()` - Clear authentication
|
| 32 |
- `authStore.getToken()` - Get current token
|
| 33 |
- `authStore.isTokenValid()` - Check token validity
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
- `gameEngine.start()` - Start game with world content
|
| 35 |
- `gameEngine.stop()` - Clean up game instance
|
| 36 |
- `consoleCapture.setup()` - Begin console interception
|
| 37 |
-
- `consoleForwarder.start()` - Start forwarding console
|
| 38 |
- `consoleForwarder.stop()` - Stop console forwarding
|
| 39 |
- `HTMLParser.extractGameContent()` - Parse world from HTML
|
| 40 |
|
|
|
|
| 14 |
services/
|
| 15 |
βββ context.md # This file
|
| 16 |
βββ auth.ts # Hugging Face OAuth authentication
|
| 17 |
+
βββ agent.ts # High-level agent coordination
|
| 18 |
+
βββ websocket.ts # WebSocket connection management
|
| 19 |
+
βββ message-handler.ts # WebSocket message processing
|
| 20 |
βββ game-engine.ts # Game lifecycle management
|
| 21 |
βββ console-capture.ts # Console interception
|
| 22 |
βββ console-forward.ts # WebSocket console forwarding
|
|
|
|
| 34 |
- `authStore.logout()` - Clear authentication
|
| 35 |
- `authStore.getToken()` - Get current token
|
| 36 |
- `authStore.isTokenValid()` - Check token validity
|
| 37 |
+
- `agentService.connect()` - Connect to WebSocket server
|
| 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 content
|
| 42 |
- `gameEngine.stop()` - Clean up game instance
|
| 43 |
- `consoleCapture.setup()` - Begin console interception
|
| 44 |
+
- `consoleForwarder.start()` - Start forwarding console messages
|
| 45 |
- `consoleForwarder.stop()` - Stop console forwarding
|
| 46 |
- `HTMLParser.extractGameContent()` - Parse world from HTML
|
| 47 |
|
src/lib/services/message-handler.ts
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { WebSocketMessage } from "./websocket";
|
| 2 |
+
import { agentStore } from "../stores/agent";
|
| 3 |
+
import { editorStore } from "../stores/editor";
|
| 4 |
+
import type {
|
| 5 |
+
ChatMessage,
|
| 6 |
+
MessageSegment,
|
| 7 |
+
MessageSegmentType,
|
| 8 |
+
} from "../stores/agent";
|
| 9 |
+
|
| 10 |
+
interface StreamState {
|
| 11 |
+
currentStreamId: string | null;
|
| 12 |
+
streamingContent: string;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
export class MessageHandler {
|
| 16 |
+
private streamState: StreamState = {
|
| 17 |
+
currentStreamId: null,
|
| 18 |
+
streamingContent: "",
|
| 19 |
+
};
|
| 20 |
+
|
| 21 |
+
handleMessage(message: WebSocketMessage): void {
|
| 22 |
+
const handlers: Record<string, () => void> = {
|
| 23 |
+
status: () => this.handleStatus(message),
|
| 24 |
+
stream_start: () => this.handleStreamStart(message),
|
| 25 |
+
stream_token: () => this.handleStreamToken(message),
|
| 26 |
+
stream_end: () => this.handleStreamEnd(message),
|
| 27 |
+
chat: () => this.handleChat(message),
|
| 28 |
+
error: () => this.handleError(message),
|
| 29 |
+
editor_update: () => this.handleEditorUpdate(message),
|
| 30 |
+
segment_start: () => this.handleSegmentStart(message),
|
| 31 |
+
segment_token: () => this.handleSegmentToken(message),
|
| 32 |
+
segment_end: () => this.handleSegmentEnd(message),
|
| 33 |
+
};
|
| 34 |
+
|
| 35 |
+
const handler = handlers[message.type];
|
| 36 |
+
if (handler) {
|
| 37 |
+
handler();
|
| 38 |
+
}
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
private handleStatus(message: WebSocketMessage): void {
|
| 42 |
+
const { processing, connected } = message.payload;
|
| 43 |
+
|
| 44 |
+
if (processing !== undefined) {
|
| 45 |
+
agentStore.setProcessing(processing as boolean);
|
| 46 |
+
}
|
| 47 |
+
if (connected !== undefined) {
|
| 48 |
+
agentStore.setConnected(connected as boolean);
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
private handleStreamStart(message: WebSocketMessage): void {
|
| 53 |
+
const messageId =
|
| 54 |
+
(message.payload.messageId as string) || `assistant_${Date.now()}`;
|
| 55 |
+
this.streamState.currentStreamId = messageId;
|
| 56 |
+
this.streamState.streamingContent = "";
|
| 57 |
+
|
| 58 |
+
const newMessage: ChatMessage = {
|
| 59 |
+
id: messageId,
|
| 60 |
+
role: "assistant",
|
| 61 |
+
content: "",
|
| 62 |
+
timestamp: Date.now(),
|
| 63 |
+
streaming: true,
|
| 64 |
+
segments: [],
|
| 65 |
+
};
|
| 66 |
+
|
| 67 |
+
agentStore.addMessage(newMessage);
|
| 68 |
+
agentStore.setStreamingStatus("streaming");
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
private handleStreamToken(message: WebSocketMessage): void {
|
| 72 |
+
const messageId = message.payload.messageId as string;
|
| 73 |
+
const token = (message.payload.token as string) || "";
|
| 74 |
+
|
| 75 |
+
if (!messageId) {
|
| 76 |
+
console.error("stream_token without messageId");
|
| 77 |
+
return;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
this.streamState.streamingContent += token;
|
| 81 |
+
agentStore.updateMessageContent(
|
| 82 |
+
messageId,
|
| 83 |
+
this.streamState.streamingContent,
|
| 84 |
+
);
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
private handleStreamEnd(message: WebSocketMessage): void {
|
| 88 |
+
const messageId = message.payload.messageId as string;
|
| 89 |
+
const content =
|
| 90 |
+
(message.payload.content as string) || this.streamState.streamingContent;
|
| 91 |
+
|
| 92 |
+
if (!messageId) {
|
| 93 |
+
console.error("stream_end without messageId");
|
| 94 |
+
return;
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
agentStore.updateMessage(messageId, { content, streaming: false });
|
| 98 |
+
agentStore.setStreamingStatus("idle");
|
| 99 |
+
|
| 100 |
+
this.streamState.currentStreamId = null;
|
| 101 |
+
this.streamState.streamingContent = "";
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
private handleChat(message: WebSocketMessage): void {
|
| 105 |
+
if (this.streamState.currentStreamId) {
|
| 106 |
+
agentStore.updateMessage(this.streamState.currentStreamId, {
|
| 107 |
+
streaming: false,
|
| 108 |
+
});
|
| 109 |
+
agentStore.setStreamingStatus("idle");
|
| 110 |
+
this.streamState.currentStreamId = null;
|
| 111 |
+
this.streamState.streamingContent = "";
|
| 112 |
+
} else {
|
| 113 |
+
const { role, content } = message.payload;
|
| 114 |
+
if (role && content) {
|
| 115 |
+
const newMessage: ChatMessage = {
|
| 116 |
+
id: `msg_${Date.now()}`,
|
| 117 |
+
role: role as "user" | "assistant" | "system",
|
| 118 |
+
content: content as string,
|
| 119 |
+
timestamp: Date.now(),
|
| 120 |
+
};
|
| 121 |
+
agentStore.addMessage(newMessage);
|
| 122 |
+
}
|
| 123 |
+
}
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
private handleError(message: WebSocketMessage): void {
|
| 127 |
+
agentStore.setError((message.payload.error as string) || null);
|
| 128 |
+
agentStore.setProcessing(false);
|
| 129 |
+
agentStore.setStreamingStatus("idle");
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
private handleEditorUpdate(message: WebSocketMessage): void {
|
| 133 |
+
const content = message.payload.content as string;
|
| 134 |
+
if (content) {
|
| 135 |
+
editorStore.setContent(content);
|
| 136 |
+
}
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
private handleSegmentStart(message: WebSocketMessage): void {
|
| 140 |
+
const messageId = message.payload.messageId as string;
|
| 141 |
+
const segmentId =
|
| 142 |
+
(message.payload.segmentId as string) || `seg_${Date.now()}`;
|
| 143 |
+
const segmentType = message.payload.segmentType as MessageSegmentType;
|
| 144 |
+
|
| 145 |
+
if (!messageId) {
|
| 146 |
+
console.error("segment_start without messageId");
|
| 147 |
+
return;
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
const newSegment: MessageSegment = {
|
| 151 |
+
id: segmentId,
|
| 152 |
+
type: segmentType,
|
| 153 |
+
content: "",
|
| 154 |
+
toolName: message.payload.toolName as string | undefined,
|
| 155 |
+
toolArgs: message.payload.toolArgs as Record<string, unknown> | undefined,
|
| 156 |
+
startTime: Date.now(),
|
| 157 |
+
streaming: segmentType === "text",
|
| 158 |
+
toolStatus: segmentType === "tool-invocation" ? "pending" : undefined,
|
| 159 |
+
};
|
| 160 |
+
|
| 161 |
+
agentStore.addSegment(messageId, newSegment);
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
private handleSegmentToken(message: WebSocketMessage): void {
|
| 165 |
+
const messageId = message.payload.messageId as string;
|
| 166 |
+
const segmentId = message.payload.segmentId as string;
|
| 167 |
+
const token = (message.payload.token as string) || "";
|
| 168 |
+
|
| 169 |
+
if (!messageId || !segmentId) {
|
| 170 |
+
console.error("segment_token missing messageId or segmentId");
|
| 171 |
+
return;
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
agentStore.updateSegmentContent(messageId, segmentId, token);
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
private handleSegmentEnd(message: WebSocketMessage): void {
|
| 178 |
+
const messageId = message.payload.messageId as string;
|
| 179 |
+
const segmentId = message.payload.segmentId as string;
|
| 180 |
+
|
| 181 |
+
if (!messageId || !segmentId) {
|
| 182 |
+
console.error("segment_end missing messageId or segmentId");
|
| 183 |
+
return;
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
const updates: Partial<MessageSegment> = {
|
| 187 |
+
streaming: false,
|
| 188 |
+
content: message.payload.content as string | undefined,
|
| 189 |
+
toolOutput: message.payload.toolOutput as string | undefined,
|
| 190 |
+
toolResult: message.payload.toolResult as string | undefined,
|
| 191 |
+
toolStatus: message.payload.toolStatus as MessageSegment["toolStatus"],
|
| 192 |
+
toolError: message.payload.toolError as string | undefined,
|
| 193 |
+
endTime: Date.now(),
|
| 194 |
+
consoleOutput: message.payload.consoleOutput as string[] | undefined,
|
| 195 |
+
};
|
| 196 |
+
|
| 197 |
+
agentStore.updateSegment(messageId, segmentId, updates);
|
| 198 |
+
agentStore.mergeSegmentsIfNeeded(messageId);
|
| 199 |
+
}
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
export const messageHandler = new MessageHandler();
|
src/lib/services/websocket.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export type WebSocketMessage = {
|
| 2 |
+
type: string;
|
| 3 |
+
payload: Record<string, unknown>;
|
| 4 |
+
timestamp?: number;
|
| 5 |
+
};
|
| 6 |
+
|
| 7 |
+
export type MessageHandler = (message: WebSocketMessage) => void;
|
| 8 |
+
export type ConnectionHandler = (connected: boolean) => void;
|
| 9 |
+
|
| 10 |
+
export class WebSocketService {
|
| 11 |
+
private ws: WebSocket | null = null;
|
| 12 |
+
private messageHandlers = new Set<MessageHandler>();
|
| 13 |
+
private connectionHandlers = new Set<ConnectionHandler>();
|
| 14 |
+
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
| 15 |
+
private url: string;
|
| 16 |
+
|
| 17 |
+
constructor() {
|
| 18 |
+
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
| 19 |
+
this.url = `${protocol}//${window.location.host}/ws`;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
connect(): void {
|
| 23 |
+
if (this.ws?.readyState === WebSocket.OPEN) return;
|
| 24 |
+
|
| 25 |
+
this.ws = new WebSocket(this.url);
|
| 26 |
+
|
| 27 |
+
this.ws.onopen = () => {
|
| 28 |
+
this.notifyConnection(true);
|
| 29 |
+
if (this.reconnectTimer) {
|
| 30 |
+
clearTimeout(this.reconnectTimer);
|
| 31 |
+
this.reconnectTimer = null;
|
| 32 |
+
}
|
| 33 |
+
};
|
| 34 |
+
|
| 35 |
+
this.ws.onmessage = (event) => {
|
| 36 |
+
try {
|
| 37 |
+
const message = JSON.parse(event.data) as WebSocketMessage;
|
| 38 |
+
this.notifyMessage(message);
|
| 39 |
+
} catch (error) {
|
| 40 |
+
console.error("Error parsing WebSocket message:", error);
|
| 41 |
+
}
|
| 42 |
+
};
|
| 43 |
+
|
| 44 |
+
this.ws.onerror = (error) => {
|
| 45 |
+
console.error("WebSocket error:", error);
|
| 46 |
+
};
|
| 47 |
+
|
| 48 |
+
this.ws.onclose = () => {
|
| 49 |
+
this.notifyConnection(false);
|
| 50 |
+
this.scheduleReconnect();
|
| 51 |
+
};
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
disconnect(): void {
|
| 55 |
+
if (this.reconnectTimer) {
|
| 56 |
+
clearTimeout(this.reconnectTimer);
|
| 57 |
+
this.reconnectTimer = null;
|
| 58 |
+
}
|
| 59 |
+
if (this.ws) {
|
| 60 |
+
this.ws.close();
|
| 61 |
+
this.ws = null;
|
| 62 |
+
}
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
send(message: WebSocketMessage): void {
|
| 66 |
+
if (this.ws?.readyState !== WebSocket.OPEN) {
|
| 67 |
+
throw new Error("WebSocket not connected");
|
| 68 |
+
}
|
| 69 |
+
this.ws.send(JSON.stringify(message));
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
isConnected(): boolean {
|
| 73 |
+
return this.ws?.readyState === WebSocket.OPEN;
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
onMessage(handler: MessageHandler): () => void {
|
| 77 |
+
this.messageHandlers.add(handler);
|
| 78 |
+
return () => this.messageHandlers.delete(handler);
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
onConnection(handler: ConnectionHandler): () => void {
|
| 82 |
+
this.connectionHandlers.add(handler);
|
| 83 |
+
return () => this.connectionHandlers.delete(handler);
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
private notifyMessage(message: WebSocketMessage): void {
|
| 87 |
+
this.messageHandlers.forEach((handler) => handler(message));
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
private notifyConnection(connected: boolean): void {
|
| 91 |
+
this.connectionHandlers.forEach((handler) => handler(connected));
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
private scheduleReconnect(): void {
|
| 95 |
+
if (!this.reconnectTimer) {
|
| 96 |
+
this.reconnectTimer = setTimeout(() => {
|
| 97 |
+
this.reconnectTimer = null;
|
| 98 |
+
this.connect();
|
| 99 |
+
}, 3000);
|
| 100 |
+
}
|
| 101 |
+
}
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
export const websocketService = new WebSocketService();
|
src/lib/stores/agent.ts
CHANGED
|
@@ -1,19 +1,4 @@
|
|
| 1 |
-
import { writable, derived
|
| 2 |
-
import { authStore } from "../services/auth";
|
| 3 |
-
import { editorStore } from "./editor";
|
| 4 |
-
|
| 5 |
-
export interface ToolExecution {
|
| 6 |
-
id: string;
|
| 7 |
-
name: string;
|
| 8 |
-
status: "pending" | "running" | "completed" | "error";
|
| 9 |
-
args?: Record<string, unknown>;
|
| 10 |
-
output?: string;
|
| 11 |
-
error?: string;
|
| 12 |
-
startTime: number;
|
| 13 |
-
endTime?: number;
|
| 14 |
-
consoleOutput?: string[];
|
| 15 |
-
expanded?: boolean;
|
| 16 |
-
}
|
| 17 |
|
| 18 |
export type MessageSegmentType =
|
| 19 |
| "text"
|
|
@@ -41,13 +26,12 @@ export interface MessageSegment {
|
|
| 41 |
|
| 42 |
export interface ChatMessage {
|
| 43 |
id: string;
|
| 44 |
-
role: "user" | "assistant" | "system"
|
| 45 |
content: string;
|
| 46 |
timestamp: number;
|
| 47 |
streaming?: boolean;
|
| 48 |
reasoning?: string;
|
| 49 |
showReasoning?: boolean;
|
| 50 |
-
toolExecutions?: ToolExecution[];
|
| 51 |
segments?: MessageSegment[];
|
| 52 |
}
|
| 53 |
|
|
@@ -72,579 +56,165 @@ function createAgentStore() {
|
|
| 72 |
thinkingStartTime: null,
|
| 73 |
});
|
| 74 |
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
function connect() {
|
| 79 |
-
if (ws && ws.readyState === WebSocket.OPEN) {
|
| 80 |
-
return;
|
| 81 |
-
}
|
| 82 |
-
|
| 83 |
-
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
| 84 |
-
const wsUrl = `${protocol}//${window.location.host}/ws`;
|
| 85 |
-
|
| 86 |
-
ws = new WebSocket(wsUrl);
|
| 87 |
-
|
| 88 |
-
ws.onopen = () => {
|
| 89 |
-
update((state) => ({ ...state, connected: true, error: null }));
|
| 90 |
-
|
| 91 |
-
const token = authStore.getToken();
|
| 92 |
-
if (token && ws) {
|
| 93 |
-
ws.send(
|
| 94 |
-
JSON.stringify({
|
| 95 |
-
type: "auth",
|
| 96 |
-
payload: { token },
|
| 97 |
-
timestamp: Date.now(),
|
| 98 |
-
}),
|
| 99 |
-
);
|
| 100 |
-
|
| 101 |
-
const editorState = get(editorStore);
|
| 102 |
-
if (editorState && editorState.content) {
|
| 103 |
-
setTimeout(() => {
|
| 104 |
-
if (ws && ws.readyState === WebSocket.OPEN) {
|
| 105 |
-
ws.send(
|
| 106 |
-
JSON.stringify({
|
| 107 |
-
type: "editor_sync",
|
| 108 |
-
payload: { content: editorState.content },
|
| 109 |
-
timestamp: Date.now(),
|
| 110 |
-
}),
|
| 111 |
-
);
|
| 112 |
-
}
|
| 113 |
-
}, 500);
|
| 114 |
-
}
|
| 115 |
-
} else {
|
| 116 |
-
update((state) => ({
|
| 117 |
-
...state,
|
| 118 |
-
error: "Authentication required. Please sign in with Hugging Face.",
|
| 119 |
-
connected: false,
|
| 120 |
-
}));
|
| 121 |
-
if (ws) {
|
| 122 |
-
ws.close();
|
| 123 |
-
}
|
| 124 |
-
}
|
| 125 |
-
};
|
| 126 |
-
|
| 127 |
-
ws.onmessage = (event) => {
|
| 128 |
-
try {
|
| 129 |
-
const message = JSON.parse(event.data);
|
| 130 |
-
handleWebSocketMessage(message);
|
| 131 |
-
} catch (error) {
|
| 132 |
-
console.error("Error parsing WebSocket message:", error);
|
| 133 |
-
}
|
| 134 |
-
};
|
| 135 |
-
|
| 136 |
-
ws.onerror = (error) => {
|
| 137 |
-
console.error("WebSocket error:", error);
|
| 138 |
-
update((state) => ({ ...state, error: "Connection error" }));
|
| 139 |
-
};
|
| 140 |
-
|
| 141 |
-
ws.onclose = () => {
|
| 142 |
-
update((state) => ({ ...state, connected: false }));
|
| 143 |
-
|
| 144 |
-
const token = authStore.getToken();
|
| 145 |
-
if (token) {
|
| 146 |
-
setTimeout(() => {
|
| 147 |
-
connect();
|
| 148 |
-
}, 3000);
|
| 149 |
-
}
|
| 150 |
-
};
|
| 151 |
-
}
|
| 152 |
-
|
| 153 |
-
function handleWebSocketMessage(message: {
|
| 154 |
-
type: string;
|
| 155 |
-
payload: {
|
| 156 |
-
processing?: boolean;
|
| 157 |
-
connected?: boolean;
|
| 158 |
-
chunk?: string;
|
| 159 |
-
token?: string;
|
| 160 |
-
messageId?: string;
|
| 161 |
-
reasoning?: string;
|
| 162 |
-
role?: string;
|
| 163 |
-
content?: string;
|
| 164 |
-
error?: string;
|
| 165 |
-
toolName?: string;
|
| 166 |
-
toolArgs?: Record<string, unknown>;
|
| 167 |
-
toolResult?: string;
|
| 168 |
-
message?: string;
|
| 169 |
-
toolId?: string;
|
| 170 |
-
toolStatus?: "pending" | "running" | "completed" | "error";
|
| 171 |
-
toolOutput?: string;
|
| 172 |
-
toolError?: string;
|
| 173 |
-
consoleOutput?: string[];
|
| 174 |
-
segmentId?: string;
|
| 175 |
-
segmentType?: MessageSegmentType;
|
| 176 |
-
};
|
| 177 |
-
}) {
|
| 178 |
-
switch (message.type) {
|
| 179 |
-
case "status":
|
| 180 |
-
if (message.payload.processing !== undefined) {
|
| 181 |
-
const isProcessing = message.payload.processing as boolean;
|
| 182 |
-
update((state) => ({
|
| 183 |
-
...state,
|
| 184 |
-
processing: isProcessing,
|
| 185 |
-
streamingStatus: isProcessing ? "thinking" : "idle",
|
| 186 |
-
thinkingStartTime: isProcessing ? Date.now() : null,
|
| 187 |
-
}));
|
| 188 |
-
}
|
| 189 |
-
if (message.payload.connected !== undefined) {
|
| 190 |
-
update((state) => ({
|
| 191 |
-
...state,
|
| 192 |
-
connected: message.payload.connected as boolean,
|
| 193 |
-
}));
|
| 194 |
-
}
|
| 195 |
-
break;
|
| 196 |
-
|
| 197 |
-
case "stream_start": {
|
| 198 |
-
const assistantId =
|
| 199 |
-
message.payload.messageId || `assistant_${Date.now()}`;
|
| 200 |
-
currentStreamId = assistantId;
|
| 201 |
-
update((state) => {
|
| 202 |
-
return {
|
| 203 |
-
...state,
|
| 204 |
-
streamingContent: "",
|
| 205 |
-
messages: [
|
| 206 |
-
...state.messages,
|
| 207 |
-
{
|
| 208 |
-
id: assistantId,
|
| 209 |
-
role: "assistant",
|
| 210 |
-
content: "",
|
| 211 |
-
timestamp: Date.now(),
|
| 212 |
-
streaming: true,
|
| 213 |
-
segments: [],
|
| 214 |
-
},
|
| 215 |
-
],
|
| 216 |
-
streamingStatus: "streaming",
|
| 217 |
-
thinkingStartTime: null,
|
| 218 |
-
};
|
| 219 |
-
});
|
| 220 |
-
break;
|
| 221 |
-
}
|
| 222 |
-
|
| 223 |
-
case "stream_token":
|
| 224 |
-
if (!message.payload.messageId) {
|
| 225 |
-
console.error("stream_token without messageId");
|
| 226 |
-
break;
|
| 227 |
-
}
|
| 228 |
-
update((state) => {
|
| 229 |
-
const newContent =
|
| 230 |
-
state.streamingContent + (message.payload.token || "");
|
| 231 |
-
return {
|
| 232 |
-
...state,
|
| 233 |
-
streamingContent: newContent,
|
| 234 |
-
messages: state.messages.map((msg) =>
|
| 235 |
-
msg.id === message.payload.messageId
|
| 236 |
-
? { ...msg, content: newContent }
|
| 237 |
-
: msg,
|
| 238 |
-
),
|
| 239 |
-
streamingStatus: "streaming",
|
| 240 |
-
};
|
| 241 |
-
});
|
| 242 |
-
break;
|
| 243 |
-
|
| 244 |
-
case "stream_end":
|
| 245 |
-
if (!message.payload.messageId) {
|
| 246 |
-
console.error("stream_end without messageId");
|
| 247 |
-
break;
|
| 248 |
-
}
|
| 249 |
-
update((state) => {
|
| 250 |
-
const finalContent =
|
| 251 |
-
message.payload.content || state.streamingContent;
|
| 252 |
-
currentStreamId = null;
|
| 253 |
-
return {
|
| 254 |
-
...state,
|
| 255 |
-
streamingContent: "",
|
| 256 |
-
messages: state.messages.map((msg) =>
|
| 257 |
-
msg.id === message.payload.messageId
|
| 258 |
-
? { ...msg, content: finalContent, streaming: false }
|
| 259 |
-
: msg,
|
| 260 |
-
),
|
| 261 |
-
streamingStatus: "idle",
|
| 262 |
-
thinkingStartTime: null,
|
| 263 |
-
};
|
| 264 |
-
});
|
| 265 |
-
break;
|
| 266 |
-
|
| 267 |
-
case "chat":
|
| 268 |
-
update((state) => {
|
| 269 |
-
if (currentStreamId) {
|
| 270 |
-
const messages = state.messages.map((msg) => {
|
| 271 |
-
if (msg.id === currentStreamId) {
|
| 272 |
-
return { ...msg, streaming: false };
|
| 273 |
-
}
|
| 274 |
-
return msg;
|
| 275 |
-
});
|
| 276 |
-
currentStreamId = null;
|
| 277 |
-
return {
|
| 278 |
-
...state,
|
| 279 |
-
streamingContent: "",
|
| 280 |
-
messages,
|
| 281 |
-
streamingStatus: "idle",
|
| 282 |
-
thinkingStartTime: null,
|
| 283 |
-
};
|
| 284 |
-
} else {
|
| 285 |
-
if (message.payload.role && message.payload.content) {
|
| 286 |
-
const newMessage: ChatMessage = {
|
| 287 |
-
id: `msg_${Date.now()}`,
|
| 288 |
-
role: message.payload.role as "user" | "assistant" | "system",
|
| 289 |
-
content: message.payload.content,
|
| 290 |
-
timestamp: Date.now(),
|
| 291 |
-
};
|
| 292 |
-
return {
|
| 293 |
-
...state,
|
| 294 |
-
messages: [...state.messages, newMessage],
|
| 295 |
-
};
|
| 296 |
-
}
|
| 297 |
-
return state;
|
| 298 |
-
}
|
| 299 |
-
});
|
| 300 |
-
break;
|
| 301 |
-
|
| 302 |
-
case "error":
|
| 303 |
-
update((state) => ({
|
| 304 |
-
...state,
|
| 305 |
-
error: message.payload.error || null,
|
| 306 |
-
processing: false,
|
| 307 |
-
streamingStatus: "idle",
|
| 308 |
-
thinkingStartTime: null,
|
| 309 |
-
}));
|
| 310 |
-
break;
|
| 311 |
-
|
| 312 |
-
case "editor_update":
|
| 313 |
-
if (message.payload.content) {
|
| 314 |
-
editorStore.setContent(message.payload.content);
|
| 315 |
-
}
|
| 316 |
-
break;
|
| 317 |
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 328 |
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 332 |
);
|
| 333 |
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
if (
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
toolExecution,
|
| 354 |
-
],
|
| 355 |
-
};
|
| 356 |
}
|
| 357 |
-
return msg;
|
| 358 |
-
});
|
| 359 |
-
return { ...state, messages };
|
| 360 |
-
}
|
| 361 |
-
});
|
| 362 |
-
break;
|
| 363 |
-
|
| 364 |
-
case "tool_output":
|
| 365 |
-
update((state) => {
|
| 366 |
-
const messages = state.messages.map((msg) => {
|
| 367 |
-
if (msg.role === "tool" && msg.toolExecutions) {
|
| 368 |
-
const toolExecutions = msg.toolExecutions.map((exec) => {
|
| 369 |
-
if (exec.id === message.payload.toolId) {
|
| 370 |
-
return {
|
| 371 |
-
...exec,
|
| 372 |
-
output:
|
| 373 |
-
(exec.output || "") + (message.payload.toolOutput || ""),
|
| 374 |
-
consoleOutput:
|
| 375 |
-
message.payload.consoleOutput || exec.consoleOutput,
|
| 376 |
-
};
|
| 377 |
-
}
|
| 378 |
-
return exec;
|
| 379 |
-
});
|
| 380 |
-
return { ...msg, toolExecutions };
|
| 381 |
-
}
|
| 382 |
-
return msg;
|
| 383 |
-
});
|
| 384 |
-
return { ...state, messages };
|
| 385 |
-
});
|
| 386 |
-
break;
|
| 387 |
-
|
| 388 |
-
case "tool_complete":
|
| 389 |
-
update((state) => {
|
| 390 |
-
const messages = state.messages.map((msg) => {
|
| 391 |
-
if (msg.role === "tool" && msg.toolExecutions) {
|
| 392 |
-
const toolExecutions = msg.toolExecutions.map((exec) => {
|
| 393 |
-
if (exec.id === message.payload.toolId) {
|
| 394 |
-
return {
|
| 395 |
-
...exec,
|
| 396 |
-
status: "completed" as const,
|
| 397 |
-
output: message.payload.toolResult || exec.output,
|
| 398 |
-
endTime: Date.now(),
|
| 399 |
-
consoleOutput:
|
| 400 |
-
message.payload.consoleOutput || exec.consoleOutput,
|
| 401 |
-
};
|
| 402 |
-
}
|
| 403 |
-
return exec;
|
| 404 |
-
});
|
| 405 |
-
return { ...msg, toolExecutions };
|
| 406 |
}
|
| 407 |
-
return
|
| 408 |
});
|
| 409 |
-
return { ...state, messages };
|
| 410 |
-
});
|
| 411 |
-
break;
|
| 412 |
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
});
|
| 428 |
-
return { ...msg, toolExecutions };
|
| 429 |
}
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
};
|
| 451 |
-
|
| 452 |
-
if (!message.payload.messageId) {
|
| 453 |
-
console.error("segment_start without messageId");
|
| 454 |
-
return state;
|
| 455 |
-
}
|
| 456 |
-
return {
|
| 457 |
-
...state,
|
| 458 |
-
messages: state.messages.map((msg) => {
|
| 459 |
-
if (msg.id === message.payload.messageId) {
|
| 460 |
-
return {
|
| 461 |
-
...msg,
|
| 462 |
-
segments: [...(msg.segments || []), newSegment],
|
| 463 |
-
};
|
| 464 |
-
}
|
| 465 |
-
return msg;
|
| 466 |
-
}),
|
| 467 |
-
};
|
| 468 |
-
});
|
| 469 |
-
break;
|
| 470 |
-
|
| 471 |
-
case "segment_token":
|
| 472 |
-
if (!message.payload.messageId) {
|
| 473 |
-
console.error("segment_token without messageId");
|
| 474 |
-
break;
|
| 475 |
-
}
|
| 476 |
-
update((state) => {
|
| 477 |
-
return {
|
| 478 |
-
...state,
|
| 479 |
-
messages: state.messages.map((msg) => {
|
| 480 |
-
if (msg.id === message.payload.messageId) {
|
| 481 |
-
const segments = msg.segments?.map((seg) => {
|
| 482 |
-
if (seg.id === message.payload.segmentId) {
|
| 483 |
-
return {
|
| 484 |
-
...seg,
|
| 485 |
-
content: seg.content + (message.payload.token || ""),
|
| 486 |
-
};
|
| 487 |
-
}
|
| 488 |
-
return seg;
|
| 489 |
-
});
|
| 490 |
-
return { ...msg, segments };
|
| 491 |
-
}
|
| 492 |
-
return msg;
|
| 493 |
-
}),
|
| 494 |
-
};
|
| 495 |
-
});
|
| 496 |
-
break;
|
| 497 |
-
|
| 498 |
-
case "segment_end":
|
| 499 |
-
if (!message.payload.messageId) {
|
| 500 |
-
console.error("segment_end without messageId");
|
| 501 |
-
break;
|
| 502 |
-
}
|
| 503 |
-
update((state) => {
|
| 504 |
-
return {
|
| 505 |
-
...state,
|
| 506 |
-
messages: state.messages.map((msg) => {
|
| 507 |
-
if (msg.id === message.payload.messageId) {
|
| 508 |
-
const segments = msg.segments?.map((seg, index, allSegs) => {
|
| 509 |
-
if (seg.id === message.payload.segmentId) {
|
| 510 |
-
const updatedSegment = {
|
| 511 |
-
...seg,
|
| 512 |
-
streaming: false,
|
| 513 |
-
content: message.payload.content || seg.content,
|
| 514 |
-
toolOutput: message.payload.toolOutput,
|
| 515 |
-
toolResult: message.payload.toolResult,
|
| 516 |
-
toolStatus: message.payload.toolStatus || seg.toolStatus,
|
| 517 |
-
toolError: message.payload.toolError,
|
| 518 |
-
endTime: Date.now(),
|
| 519 |
-
consoleOutput: message.payload.consoleOutput,
|
| 520 |
-
};
|
| 521 |
-
|
| 522 |
-
if (seg.type === "tool-result" && index > 0) {
|
| 523 |
-
const prevSegment = allSegs[index - 1];
|
| 524 |
-
if (
|
| 525 |
-
prevSegment.type === "tool-invocation" &&
|
| 526 |
-
prevSegment.toolName === seg.toolName
|
| 527 |
-
) {
|
| 528 |
-
return { ...updatedSegment, mergeWithPrevious: true };
|
| 529 |
-
}
|
| 530 |
-
}
|
| 531 |
-
return updatedSegment;
|
| 532 |
-
}
|
| 533 |
-
return seg;
|
| 534 |
-
});
|
| 535 |
-
|
| 536 |
-
const mergedSegments = segments?.reduce((acc, seg, index) => {
|
| 537 |
-
if (seg.mergeWithPrevious && index > 0 && acc.length > 0) {
|
| 538 |
-
const prevIndex = acc.length - 1;
|
| 539 |
-
acc[prevIndex] = {
|
| 540 |
-
...acc[prevIndex],
|
| 541 |
-
toolOutput: seg.toolOutput || acc[prevIndex].toolOutput,
|
| 542 |
-
toolResult: seg.toolResult || seg.toolOutput,
|
| 543 |
-
toolError: seg.toolError || acc[prevIndex].toolError,
|
| 544 |
-
toolStatus: seg.toolStatus || acc[prevIndex].toolStatus,
|
| 545 |
-
consoleOutput:
|
| 546 |
-
seg.consoleOutput || acc[prevIndex].consoleOutput,
|
| 547 |
-
endTime: seg.endTime,
|
| 548 |
-
};
|
| 549 |
-
return acc;
|
| 550 |
-
}
|
| 551 |
-
const cleanSegment = { ...seg };
|
| 552 |
-
delete cleanSegment.mergeWithPrevious;
|
| 553 |
-
return [...acc, cleanSegment];
|
| 554 |
-
}, [] as MessageSegment[]);
|
| 555 |
-
|
| 556 |
-
return { ...msg, segments: mergedSegments };
|
| 557 |
-
}
|
| 558 |
-
return msg;
|
| 559 |
-
}),
|
| 560 |
-
};
|
| 561 |
-
});
|
| 562 |
-
break;
|
| 563 |
-
}
|
| 564 |
-
}
|
| 565 |
-
|
| 566 |
-
function sendMessage(content: string) {
|
| 567 |
-
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
| 568 |
-
update((state) => ({ ...state, error: "Not connected to server" }));
|
| 569 |
-
return;
|
| 570 |
-
}
|
| 571 |
-
|
| 572 |
-
const userMessage: ChatMessage = {
|
| 573 |
-
id: `user_${Date.now()}`,
|
| 574 |
-
role: "user",
|
| 575 |
-
content,
|
| 576 |
-
timestamp: Date.now(),
|
| 577 |
-
streaming: false,
|
| 578 |
-
};
|
| 579 |
-
|
| 580 |
-
update((state) => ({
|
| 581 |
-
...state,
|
| 582 |
-
messages: [...state.messages, userMessage],
|
| 583 |
-
error: null,
|
| 584 |
-
}));
|
| 585 |
-
|
| 586 |
-
ws.send(
|
| 587 |
-
JSON.stringify({
|
| 588 |
-
type: "chat",
|
| 589 |
-
payload: { content },
|
| 590 |
-
timestamp: Date.now(),
|
| 591 |
-
}),
|
| 592 |
-
);
|
| 593 |
-
}
|
| 594 |
-
|
| 595 |
-
function sendRawMessage(message: unknown) {
|
| 596 |
-
if (ws && ws.readyState === WebSocket.OPEN) {
|
| 597 |
-
ws.send(JSON.stringify(message));
|
| 598 |
-
}
|
| 599 |
-
}
|
| 600 |
-
|
| 601 |
-
function clearMessages() {
|
| 602 |
-
update((state) => ({
|
| 603 |
-
...state,
|
| 604 |
-
messages: [],
|
| 605 |
-
streamingContent: "",
|
| 606 |
-
error: null,
|
| 607 |
-
streamingStatus: "idle",
|
| 608 |
-
thinkingStartTime: null,
|
| 609 |
-
}));
|
| 610 |
-
currentStreamId = null;
|
| 611 |
-
}
|
| 612 |
-
|
| 613 |
-
function disconnect() {
|
| 614 |
-
if (ws) {
|
| 615 |
-
ws.close();
|
| 616 |
-
ws = null;
|
| 617 |
-
}
|
| 618 |
-
}
|
| 619 |
-
|
| 620 |
-
function reauthenticate() {
|
| 621 |
-
if (ws && ws.readyState === WebSocket.OPEN) {
|
| 622 |
-
const token = authStore.getToken();
|
| 623 |
-
if (token) {
|
| 624 |
-
ws.send(
|
| 625 |
-
JSON.stringify({
|
| 626 |
-
type: "auth",
|
| 627 |
-
payload: { token },
|
| 628 |
-
timestamp: Date.now(),
|
| 629 |
-
}),
|
| 630 |
-
);
|
| 631 |
-
}
|
| 632 |
-
}
|
| 633 |
-
}
|
| 634 |
-
|
| 635 |
-
return {
|
| 636 |
-
subscribe,
|
| 637 |
-
connect,
|
| 638 |
-
disconnect,
|
| 639 |
-
sendMessage,
|
| 640 |
-
sendRawMessage,
|
| 641 |
-
clearMessages,
|
| 642 |
-
reauthenticate,
|
| 643 |
};
|
| 644 |
}
|
| 645 |
|
| 646 |
export const agentStore = createAgentStore();
|
| 647 |
|
| 648 |
export const isProcessing = derived(agentStore, ($agent) => $agent.processing);
|
| 649 |
-
|
| 650 |
export const isConnected = derived(agentStore, ($agent) => $agent.connected);
|
|
|
|
| 1 |
+
import { writable, derived } from "svelte/store";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
export type MessageSegmentType =
|
| 4 |
| "text"
|
|
|
|
| 26 |
|
| 27 |
export interface ChatMessage {
|
| 28 |
id: string;
|
| 29 |
+
role: "user" | "assistant" | "system";
|
| 30 |
content: string;
|
| 31 |
timestamp: number;
|
| 32 |
streaming?: boolean;
|
| 33 |
reasoning?: string;
|
| 34 |
showReasoning?: boolean;
|
|
|
|
| 35 |
segments?: MessageSegment[];
|
| 36 |
}
|
| 37 |
|
|
|
|
| 56 |
thinkingStartTime: null,
|
| 57 |
});
|
| 58 |
|
| 59 |
+
return {
|
| 60 |
+
subscribe,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
|
| 62 |
+
setConnected(connected: boolean) {
|
| 63 |
+
update((state) => ({ ...state, connected }));
|
| 64 |
+
},
|
| 65 |
+
|
| 66 |
+
setProcessing(processing: boolean) {
|
| 67 |
+
update((state) => ({
|
| 68 |
+
...state,
|
| 69 |
+
processing,
|
| 70 |
+
streamingStatus: processing ? "thinking" : state.streamingStatus,
|
| 71 |
+
thinkingStartTime: processing ? Date.now() : state.thinkingStartTime,
|
| 72 |
+
}));
|
| 73 |
+
},
|
| 74 |
+
|
| 75 |
+
setStreamingStatus(status: AgentState["streamingStatus"]) {
|
| 76 |
+
update((state) => ({
|
| 77 |
+
...state,
|
| 78 |
+
streamingStatus: status,
|
| 79 |
+
thinkingStartTime: status === "idle" ? null : state.thinkingStartTime,
|
| 80 |
+
}));
|
| 81 |
+
},
|
| 82 |
+
|
| 83 |
+
setError(error: string | null) {
|
| 84 |
+
update((state) => ({ ...state, error }));
|
| 85 |
+
},
|
| 86 |
+
|
| 87 |
+
addMessage(message: ChatMessage) {
|
| 88 |
+
update((state) => ({
|
| 89 |
+
...state,
|
| 90 |
+
messages: [...state.messages, message],
|
| 91 |
+
}));
|
| 92 |
+
},
|
| 93 |
+
|
| 94 |
+
updateMessage(messageId: string, updates: Partial<ChatMessage>) {
|
| 95 |
+
update((state) => ({
|
| 96 |
+
...state,
|
| 97 |
+
messages: state.messages.map((msg) =>
|
| 98 |
+
msg.id === messageId ? { ...msg, ...updates } : msg,
|
| 99 |
+
),
|
| 100 |
+
}));
|
| 101 |
+
},
|
| 102 |
+
|
| 103 |
+
updateMessageContent(messageId: string, content: string) {
|
| 104 |
+
update((state) => ({
|
| 105 |
+
...state,
|
| 106 |
+
streamingContent: content,
|
| 107 |
+
messages: state.messages.map((msg) =>
|
| 108 |
+
msg.id === messageId ? { ...msg, content } : msg,
|
| 109 |
+
),
|
| 110 |
+
}));
|
| 111 |
+
},
|
| 112 |
+
|
| 113 |
+
addSegment(messageId: string, segment: MessageSegment) {
|
| 114 |
+
update((state) => ({
|
| 115 |
+
...state,
|
| 116 |
+
messages: state.messages.map((msg) =>
|
| 117 |
+
msg.id === messageId
|
| 118 |
+
? { ...msg, segments: [...(msg.segments || []), segment] }
|
| 119 |
+
: msg,
|
| 120 |
+
),
|
| 121 |
+
}));
|
| 122 |
+
},
|
| 123 |
+
|
| 124 |
+
updateSegment(
|
| 125 |
+
messageId: string,
|
| 126 |
+
segmentId: string,
|
| 127 |
+
updates: Partial<MessageSegment>,
|
| 128 |
+
) {
|
| 129 |
+
update((state) => ({
|
| 130 |
+
...state,
|
| 131 |
+
messages: state.messages.map((msg) => {
|
| 132 |
+
if (msg.id !== messageId) return msg;
|
| 133 |
+
|
| 134 |
+
const segments = msg.segments?.map((seg) =>
|
| 135 |
+
seg.id === segmentId ? { ...seg, ...updates } : seg,
|
| 136 |
+
);
|
| 137 |
|
| 138 |
+
return { ...msg, segments };
|
| 139 |
+
}),
|
| 140 |
+
}));
|
| 141 |
+
},
|
| 142 |
+
|
| 143 |
+
updateSegmentContent(messageId: string, segmentId: string, token: string) {
|
| 144 |
+
update((state) => ({
|
| 145 |
+
...state,
|
| 146 |
+
messages: state.messages.map((msg) => {
|
| 147 |
+
if (msg.id !== messageId) return msg;
|
| 148 |
+
|
| 149 |
+
const segments = msg.segments?.map((seg) =>
|
| 150 |
+
seg.id === segmentId
|
| 151 |
+
? { ...seg, content: seg.content + token }
|
| 152 |
+
: seg,
|
| 153 |
);
|
| 154 |
|
| 155 |
+
return { ...msg, segments };
|
| 156 |
+
}),
|
| 157 |
+
}));
|
| 158 |
+
},
|
| 159 |
+
|
| 160 |
+
mergeSegmentsIfNeeded(messageId: string) {
|
| 161 |
+
update((state) => ({
|
| 162 |
+
...state,
|
| 163 |
+
messages: state.messages.map((msg) => {
|
| 164 |
+
if (msg.id !== messageId || !msg.segments) return msg;
|
| 165 |
+
|
| 166 |
+
const segments = msg.segments.map((seg, index, allSegs) => {
|
| 167 |
+
if (seg.type === "tool-result" && index > 0) {
|
| 168 |
+
const prevSegment = allSegs[index - 1];
|
| 169 |
+
if (
|
| 170 |
+
prevSegment.type === "tool-invocation" &&
|
| 171 |
+
prevSegment.toolName === seg.toolName
|
| 172 |
+
) {
|
| 173 |
+
return { ...seg, mergeWithPrevious: true };
|
|
|
|
|
|
|
|
|
|
| 174 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
}
|
| 176 |
+
return seg;
|
| 177 |
});
|
|
|
|
|
|
|
|
|
|
| 178 |
|
| 179 |
+
const mergedSegments = segments.reduce((acc, seg, index) => {
|
| 180 |
+
if (seg.mergeWithPrevious && index > 0 && acc.length > 0) {
|
| 181 |
+
const prevIndex = acc.length - 1;
|
| 182 |
+
acc[prevIndex] = {
|
| 183 |
+
...acc[prevIndex],
|
| 184 |
+
toolOutput: seg.toolOutput || acc[prevIndex].toolOutput,
|
| 185 |
+
toolResult: seg.toolResult || seg.toolOutput,
|
| 186 |
+
toolError: seg.toolError || acc[prevIndex].toolError,
|
| 187 |
+
toolStatus: seg.toolStatus || acc[prevIndex].toolStatus,
|
| 188 |
+
consoleOutput:
|
| 189 |
+
seg.consoleOutput || acc[prevIndex].consoleOutput,
|
| 190 |
+
endTime: seg.endTime,
|
| 191 |
+
};
|
| 192 |
+
return acc;
|
|
|
|
|
|
|
| 193 |
}
|
| 194 |
+
const cleanSegment = { ...seg };
|
| 195 |
+
delete cleanSegment.mergeWithPrevious;
|
| 196 |
+
return [...acc, cleanSegment];
|
| 197 |
+
}, [] as MessageSegment[]);
|
| 198 |
+
|
| 199 |
+
return { ...msg, segments: mergedSegments };
|
| 200 |
+
}),
|
| 201 |
+
}));
|
| 202 |
+
},
|
| 203 |
+
|
| 204 |
+
clearMessages() {
|
| 205 |
+
update((state) => ({
|
| 206 |
+
...state,
|
| 207 |
+
messages: [],
|
| 208 |
+
streamingContent: "",
|
| 209 |
+
error: null,
|
| 210 |
+
streamingStatus: "idle",
|
| 211 |
+
thinkingStartTime: null,
|
| 212 |
+
}));
|
| 213 |
+
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
};
|
| 215 |
}
|
| 216 |
|
| 217 |
export const agentStore = createAgentStore();
|
| 218 |
|
| 219 |
export const isProcessing = derived(agentStore, ($agent) => $agent.processing);
|
|
|
|
| 220 |
export const isConnected = derived(agentStore, ($agent) => $agent.connected);
|
src/lib/stores/context.md
CHANGED
|
@@ -17,7 +17,7 @@ stores/
|
|
| 17 |
βββ game.ts # Game instance and running state
|
| 18 |
βββ console.ts # Console messages with unique IDs
|
| 19 |
βββ editor.ts # Editor content and settings
|
| 20 |
-
βββ agent.ts #
|
| 21 |
βββ ui.ts # UI state (view mode, errors)
|
| 22 |
```
|
| 23 |
|
|
|
|
| 17 |
βββ game.ts # Game instance and running state
|
| 18 |
βββ console.ts # Console messages with unique IDs
|
| 19 |
βββ editor.ts # Editor content and settings
|
| 20 |
+
βββ agent.ts # Agent chat state and messages
|
| 21 |
βββ ui.ts # UI state (view mode, errors)
|
| 22 |
```
|
| 23 |
|