dylanebert commited on
Commit
9434357
Β·
1 Parent(s): 3342a1d

better tools

Browse files
.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.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.3", "", { "dependencies": { "@huggingface/tasks": "^0.19.45" }, "optionalDependencies": { "cli-progress": "^3.12.0" }, "bin": { "hfjs": "dist/cli.js" } }, "sha512-IEZ67adV+gWqg98A//mU0Ed+Q6xGPQxMfK+aV36b0Ww7R4EXG1O0zyiCcbLE/cvryfCD8+PNEwQgiPU+v63tsQ=="],
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.21", "", { "dependencies": { "bun-types": "1.2.21" } }, "sha512-NiDnvEqmbfQ6dmZ3EeUO577s4P5bf4HCTXtI6trMc6f6RzirY5IrF3aIookuSpyslFzrnvv2lmEWv5HyC1X79A=="],
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.3.3", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-GKBNHjoNw3Kra1Qg5UXttsY5kiWMEfoHq2TmXb+b1rcm6N7B3wTrFYIf/oSZ1xNQ+hVVijgLkiDZh7jRRsh+Gw=="],
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.21", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-sa2Tj77Ijc/NTLS0/Odjq/qngmEPZfbfnOERi0KRUYhT9R8M4VBioWVmMWE5GrYbKMc+5lVybXygLdibHaqVqw=="],
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.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
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.2.1", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-r3UrXED9lMlHF97jJByry90cwrZBBvZmjG1L68oYfuPMW+uDTnuMbyJDymCWwbTE+f+3LhpNDKfpR3a3saFyjA=="],
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.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="],
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.1", "", { "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-HjWMlO4qUus9ChEBsD+abS9UGle/qiDZWukIqkve811N2oliKjWE7WYCDhqaDhWS18FaaYaY1a/Vc8yF0HaS1Q=="],
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/60)
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.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
- agentStore.connect();
59
  hasConnected = true;
60
  }
61
 
@@ -71,11 +36,11 @@
71
  });
72
 
73
  onDestroy(() => {
74
- agentStore.disconnect();
75
  });
76
 
77
  $: if ($authStore.isAuthenticated && !$authStore.loading && !hasConnected) {
78
- agentStore.connect();
79
  hasConnected = true;
80
  }
81
 
@@ -164,7 +129,7 @@
164
  });
165
  }
166
 
167
- agentStore.sendMessage(inputValue.trim());
168
  inputValue = "";
169
  }
170
  }
@@ -263,37 +228,26 @@
263
  {/if}
264
 
265
  {#each $agentStore.messages as message (message.id)}
266
- {#if message.role === "tool" && message.toolExecutions}
267
- <ToolCallBlock toolExecutions={message.toolExecutions} />
268
- {:else}
269
- <div class="message {message.role}">
270
- {#if message.reasoning && message.role === "assistant"}
271
- <ReasoningBlock reasoning={message.reasoning} />
272
- {/if}
273
- {#if message.segments && message.segments.length > 0}
274
- <div class="message-segments">
275
- {#each message.segments as segment (segment.id)}
276
- <MessageSegment {segment} />
277
- {/each}
278
- </div>
279
- {:else}
280
- <div class="message-content">
281
- {#if message.role === "assistant"}
282
- {@const parts = parseMessageContent(message.content.trim())}
283
- {#each parts as part}
284
- {#if part.type === 'text' && part.content.trim()}
285
- <MarkdownRenderer content={part.content} streaming={false} />
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(contentElement, {
66
- opacity: 0,
67
- maxHeight: 0,
68
- y: -5,
69
- duration: 0.2,
70
- ease: "power2.in",
71
- onComplete: () => {
72
- gsap.set(contentElement, { display: "none" });
 
 
 
73
  },
74
- }, "-=0.1");
 
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: "90%",
115
  duration: 0.5,
116
- ease: "power2.out"
117
  });
118
  }
119
  }
@@ -168,9 +171,10 @@
168
  }
169
 
170
  if (progressBar) {
171
- gsap.fromTo(progressBar,
 
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(90deg,
 
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(0, 255, 0, 0.3)",
80
- backgroundColor: "rgba(0, 255, 0, 0.05)",
81
  duration: 0.3,
82
  ease: "power2.out"
83
  });
@@ -266,8 +266,8 @@
266
  }
267
 
268
  .tool-invocation.completed {
269
- background: rgba(0, 255, 0, 0.04);
270
- border-color: rgba(0, 255, 0, 0.15);
271
  }
272
 
273
  .tool-invocation.error {
@@ -315,7 +315,7 @@
315
  }
316
 
317
  .tool-invocation.completed .progress-ring {
318
- color: rgba(0, 255, 0, 0.6);
319
  }
320
 
321
  .tool-invocation.error .progress-ring {
@@ -375,18 +375,22 @@
375
  }
376
 
377
  .param {
378
- display: flex;
379
- gap: 0.5rem;
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(0, 255, 0, 0.05);
416
- border: 1px solid rgba(0, 255, 0, 0.1);
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(0, 255, 0, 0.8);
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 and keyed message iteration
8
  - `MessageSegment.svelte` - Renders text, tool invocations, and results
9
  - `StreamingText.svelte` - Optimized character streaming with state persistence
10
- - `ToolInvocation.svelte` - Consolidated 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
- - `ToolCallDisplay.svelte`, `ToolCallBlock.svelte` - Legacy tool rendering (deprecated)
15
 
16
  ## Architecture
17
 
18
- - Explicit message IDs with proper component keying
19
  - StreamingText tracks processed content length to prevent duplication
20
  - Batch character processing (3 chars/cycle) at 120 chars/sec
21
- - Tool invocations and results merged in single visual blocks
22
- - Segments keyed by ID for stable component identity
 
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 character streaming
9
- - **tools.ts** - Editor read/write with game reload
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
- - Streams text segments character-by-character as they arrive
18
- - Tool invocations interrupt text streaming
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 inToolCall = false;
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
- const isToolStart = token.includes("[TOOL:");
78
- const isToolEnd = inToolCall && token.includes("]");
 
 
79
 
80
- if (isToolStart) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- currentSegmentId = null;
95
- currentSegmentContent = "";
96
- inToolCall = true;
97
- } else if (isToolEnd) {
98
- inToolCall = false;
99
- } else if (!inToolCall) {
100
- if (!currentSegmentId && token.trim() && this.ws) {
 
 
 
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 += token;
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
- TOOL USAGE FORMAT:
215
- To use a tool, format your response with the tool call in this exact format:
216
- [TOOL: tool_name]
217
- or with parameters:
218
- [TOOL: tool_name {"param": "value"}]
219
-
220
- Example:
221
- To read the editor: [TOOL: read_editor]
222
- To write code: [TOOL: write_editor {"content": "<world>...</world>"}]
223
- To check console: [TOOL: observe_console]
224
-
225
- IMPORTANT WORKFLOW:
226
- 1. When asked to modify code, FIRST use read_editor to see the current code
227
- 2. After receiving tool results, CONTINUE with your analysis and next steps
228
- 3. Use write_editor to make the requested changes
229
- 4. After writing, provide a clear explanation of what was changed
230
-
231
- When you receive tool results, use them to inform your next action. Do not stop after using a tool - continue until the task is complete.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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: "Read the current code in the editor",
 
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: "Write new code to the editor and wait for game to reload",
 
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: "Read recent console messages from the game",
 
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 = [readEditorTool, writeEditorTool, observeConsoleTool];
 
 
 
 
 
 
 
 
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 { agentStore } from "../stores/agent";
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
- agentStore.sendRawMessage({
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 to WebSocket
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, get } from "svelte/store";
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" | "tool";
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
- let ws: WebSocket | null = null;
76
- let currentStreamId: string | null = null;
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
- case "tool_start":
319
- update((state) => {
320
- const toolExecution: ToolExecution = {
321
- id: message.payload.toolId || `tool_${Date.now()}`,
322
- name: message.payload.toolName || "unknown",
323
- status: "running",
324
- args: message.payload.toolArgs,
325
- startTime: Date.now(),
326
- expanded: false,
327
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
328
 
329
- let toolMessage = state.messages.find(
330
- (msg) =>
331
- msg.role === "tool" && msg.id === `tools_${currentStreamId}`,
 
 
 
 
 
 
 
 
 
 
 
 
332
  );
333
 
334
- if (!toolMessage) {
335
- toolMessage = {
336
- id: `tools_${currentStreamId || Date.now()}`,
337
- role: "tool",
338
- content: "",
339
- timestamp: Date.now(),
340
- toolExecutions: [toolExecution],
341
- };
342
- return {
343
- ...state,
344
- messages: [...state.messages, toolMessage],
345
- };
346
- } else {
347
- const messages = state.messages.map((msg) => {
348
- if (msg.id === toolMessage!.id) {
349
- return {
350
- ...msg,
351
- toolExecutions: [
352
- ...(msg.toolExecutions || []),
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 msg;
408
  });
409
- return { ...state, messages };
410
- });
411
- break;
412
 
413
- case "tool_error":
414
- update((state) => {
415
- const messages = state.messages.map((msg) => {
416
- if (msg.role === "tool" && msg.toolExecutions) {
417
- const toolExecutions = msg.toolExecutions.map((exec) => {
418
- if (exec.id === message.payload.toolId) {
419
- return {
420
- ...exec,
421
- status: "error" as const,
422
- error: message.payload.error || "Unknown error",
423
- endTime: Date.now(),
424
- };
425
- }
426
- return exec;
427
- });
428
- return { ...msg, toolExecutions };
429
  }
430
- return msg;
431
- });
432
- return { ...state, messages };
433
- });
434
- break;
435
-
436
- case "segment_start":
437
- update((state) => {
438
- const segmentId = message.payload.segmentId || `seg_${Date.now()}`;
439
- const segmentType = message.payload.segmentType as MessageSegmentType;
440
- const newSegment: MessageSegment = {
441
- id: segmentId,
442
- type: segmentType,
443
- content: "",
444
- toolName: message.payload.toolName,
445
- toolArgs: message.payload.toolArgs,
446
- startTime: Date.now(),
447
- streaming: segmentType === "text",
448
- toolStatus:
449
- segmentType === "tool-invocation" ? "pending" : undefined,
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 # WebSocket agent connection
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