소스 검색

Integrate Claude AI

Currently only terminal mode is supported
Alessandro Pignotti 6 달 전
부모
커밋
9a6e488c8e
8개의 변경된 파일437개의 추가작업 그리고 7개의 파일을 삭제
  1. 215 2
      package-lock.json
  2. 4 1
      package.json
  3. 2 0
      postcss.config.js
  4. 66 0
      src/lib/AnthropicTab.svelte
  5. 10 3
      src/lib/SideBar.svelte
  6. 42 1
      src/lib/WebVM.svelte
  7. 1 0
      src/lib/activities.js
  8. 97 0
      src/lib/anthropic.js

+ 215 - 2
package-lock.json

@@ -7,6 +7,9 @@
 		"": {
 			"name": "webvm",
 			"version": "2.0.0",
+			"dependencies": {
+				"@anthropic-ai/sdk": "^0.33.0"
+			},
 			"devDependencies": {
 				"@fortawesome/fontawesome-free": "^6.6.0",
 				"@leaningtech/cheerpx": "latest",
@@ -54,6 +57,20 @@
 				"node": ">=6.0.0"
 			}
 		},
+		"node_modules/@anthropic-ai/sdk": {
+			"version": "0.33.0",
+			"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.33.0.tgz",
+			"integrity": "sha512-FnUnCNVgO8ayDyAD8hsBEmXMzQn4xGG3EnABtgFgSMmKg+zNgdyX2iKNIm01pAhmnH213MJUBMI7egEFF08cvA==",
+			"dependencies": {
+				"@types/node": "^18.11.18",
+				"@types/node-fetch": "^2.6.4",
+				"abort-controller": "^3.0.0",
+				"agentkeepalive": "^4.2.1",
+				"form-data-encoder": "1.7.2",
+				"formdata-node": "^4.3.2",
+				"node-fetch": "^2.6.7"
+			}
+		},
 		"node_modules/@esbuild/aix-ppc64": {
 			"version": "0.21.5",
 			"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
@@ -910,6 +927,23 @@
 			"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
 			"dev": true
 		},
+		"node_modules/@types/node": {
+			"version": "18.19.68",
+			"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.68.tgz",
+			"integrity": "sha512-QGtpFH1vB99ZmTa63K4/FU8twThj4fuVSBkGddTp7uIL/cuoLWIUSL2RcOaigBhfR+hg5pgGkBnkoOxrTVBMKw==",
+			"dependencies": {
+				"undici-types": "~5.26.4"
+			}
+		},
+		"node_modules/@types/node-fetch": {
+			"version": "2.6.12",
+			"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz",
+			"integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==",
+			"dependencies": {
+				"@types/node": "*",
+				"form-data": "^4.0.0"
+			}
+		},
 		"node_modules/@xterm/addon-fit": {
 			"version": "0.10.0",
 			"resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz",
@@ -934,6 +968,17 @@
 			"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
 			"dev": true
 		},
+		"node_modules/abort-controller": {
+			"version": "3.0.0",
+			"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
+			"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
+			"dependencies": {
+				"event-target-shim": "^5.0.0"
+			},
+			"engines": {
+				"node": ">=6.5"
+			}
+		},
 		"node_modules/acorn": {
 			"version": "8.14.0",
 			"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
@@ -946,6 +991,17 @@
 				"node": ">=0.4.0"
 			}
 		},
+		"node_modules/agentkeepalive": {
+			"version": "4.5.0",
+			"resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz",
+			"integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==",
+			"dependencies": {
+				"humanize-ms": "^1.2.1"
+			},
+			"engines": {
+				"node": ">= 8.0.0"
+			}
+		},
 		"node_modules/ansi-regex": {
 			"version": "6.1.0",
 			"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
@@ -1004,6 +1060,11 @@
 				"node": ">= 0.4"
 			}
 		},
+		"node_modules/asynckit": {
+			"version": "0.4.0",
+			"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+			"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
+		},
 		"node_modules/autoprefixer": {
 			"version": "10.4.20",
 			"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
@@ -1235,6 +1296,17 @@
 			"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
 			"dev": true
 		},
+		"node_modules/combined-stream": {
+			"version": "1.0.8",
+			"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+			"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+			"dependencies": {
+				"delayed-stream": "~1.0.0"
+			},
+			"engines": {
+				"node": ">= 0.8"
+			}
+		},
 		"node_modules/commander": {
 			"version": "4.1.1",
 			"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
@@ -1346,6 +1418,14 @@
 				"node": ">=0.10.0"
 			}
 		},
+		"node_modules/delayed-stream": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+			"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+			"engines": {
+				"node": ">=0.4.0"
+			}
+		},
 		"node_modules/devalue": {
 			"version": "5.1.1",
 			"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.1.1.tgz",
@@ -1511,6 +1591,14 @@
 				"@types/estree": "^1.0.0"
 			}
 		},
+		"node_modules/event-target-shim": {
+			"version": "5.0.1",
+			"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
+			"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
+			"engines": {
+				"node": ">=6"
+			}
+		},
 		"node_modules/fast-glob": {
 			"version": "3.3.2",
 			"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
@@ -1576,6 +1664,36 @@
 				"url": "https://github.com/sponsors/isaacs"
 			}
 		},
+		"node_modules/form-data": {
+			"version": "4.0.1",
+			"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
+			"integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
+			"dependencies": {
+				"asynckit": "^0.4.0",
+				"combined-stream": "^1.0.8",
+				"mime-types": "^2.1.12"
+			},
+			"engines": {
+				"node": ">= 6"
+			}
+		},
+		"node_modules/form-data-encoder": {
+			"version": "1.7.2",
+			"resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz",
+			"integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="
+		},
+		"node_modules/formdata-node": {
+			"version": "4.4.1",
+			"resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz",
+			"integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==",
+			"dependencies": {
+				"node-domexception": "1.0.0",
+				"web-streams-polyfill": "4.0.0-beta.3"
+			},
+			"engines": {
+				"node": ">= 12.20"
+			}
+		},
 		"node_modules/fraction.js": {
 			"version": "4.3.7",
 			"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
@@ -1697,6 +1815,14 @@
 				"he": "bin/he"
 			}
 		},
+		"node_modules/humanize-ms": {
+			"version": "1.2.1",
+			"resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
+			"integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
+			"dependencies": {
+				"ms": "^2.0.0"
+			}
+		},
 		"node_modules/import-meta-resolve": {
 			"version": "4.1.0",
 			"resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz",
@@ -1916,6 +2042,25 @@
 				"node": ">=8.6"
 			}
 		},
+		"node_modules/mime-db": {
+			"version": "1.52.0",
+			"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+			"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+			"engines": {
+				"node": ">= 0.6"
+			}
+		},
+		"node_modules/mime-types": {
+			"version": "2.1.35",
+			"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+			"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+			"dependencies": {
+				"mime-db": "1.52.0"
+			},
+			"engines": {
+				"node": ">= 0.6"
+			}
+		},
 		"node_modules/minimatch": {
 			"version": "9.0.5",
 			"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
@@ -1961,8 +2106,7 @@
 		"node_modules/ms": {
 			"version": "2.1.3",
 			"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
-			"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
-			"dev": true
+			"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
 		},
 		"node_modules/mz": {
 			"version": "2.7.0",
@@ -1993,6 +2137,43 @@
 				"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
 			}
 		},
+		"node_modules/node-domexception": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
+			"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
+			"funding": [
+				{
+					"type": "github",
+					"url": "https://github.com/sponsors/jimmywarting"
+				},
+				{
+					"type": "github",
+					"url": "https://paypal.me/jimmywarting"
+				}
+			],
+			"engines": {
+				"node": ">=10.5.0"
+			}
+		},
+		"node_modules/node-fetch": {
+			"version": "2.7.0",
+			"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+			"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+			"dependencies": {
+				"whatwg-url": "^5.0.0"
+			},
+			"engines": {
+				"node": "4.x || >=6.0.0"
+			},
+			"peerDependencies": {
+				"encoding": "^0.1.0"
+			},
+			"peerDependenciesMeta": {
+				"encoding": {
+					"optional": true
+				}
+			}
+		},
 		"node_modules/node-html-parser": {
 			"version": "6.1.13",
 			"resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-6.1.13.tgz",
@@ -2767,12 +2948,22 @@
 				"node": ">=6"
 			}
 		},
+		"node_modules/tr46": {
+			"version": "0.0.3",
+			"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+			"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
+		},
 		"node_modules/ts-interface-checker": {
 			"version": "0.1.13",
 			"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
 			"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
 			"dev": true
 		},
+		"node_modules/undici-types": {
+			"version": "5.26.5",
+			"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
+			"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
+		},
 		"node_modules/universalify": {
 			"version": "2.0.1",
 			"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
@@ -2909,6 +3100,28 @@
 				}
 			}
 		},
+		"node_modules/web-streams-polyfill": {
+			"version": "4.0.0-beta.3",
+			"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz",
+			"integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==",
+			"engines": {
+				"node": ">= 14"
+			}
+		},
+		"node_modules/webidl-conversions": {
+			"version": "3.0.1",
+			"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+			"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
+		},
+		"node_modules/whatwg-url": {
+			"version": "5.0.0",
+			"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+			"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+			"dependencies": {
+				"tr46": "~0.0.3",
+				"webidl-conversions": "^3.0.0"
+			}
+		},
 		"node_modules/which": {
 			"version": "2.0.2",
 			"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

+ 4 - 1
package.json

@@ -27,5 +27,8 @@
 		"vite": "^5.0.3",
 		"vite-plugin-static-copy": "^1.0.6"
 	},
-	"type": "module"
+	"type": "module",
+	"dependencies": {
+		"@anthropic-ai/sdk": "^0.33.0"
+	}
 }

+ 2 - 0
postcss.config.js

@@ -18,6 +18,8 @@ export default {
 				case '.fa-circle:before':
 				case '.fa-trash-can:before':
 				case '.fa-book-open:before':
+				case '.fa-brain:before':
+				case '.fa-user:before':
 				case '.fa-brands:before':
 				case '.fa-solid:before':
 				case '.fa-regular:before':

+ 66 - 0
src/lib/AnthropicTab.svelte

@@ -0,0 +1,66 @@
+<script>
+	import { apiState, setApiKey, addMessage, messageList } from '$lib/anthropic.js'
+	import PanelButton from './PanelButton.svelte';
+	export let handleTool;
+	function handleKeyEnter(e)
+	{
+		if(e.key != "Enter")
+			return;
+		setApiKey(e.target.value);
+	}
+	function handleMessage(e)
+	{
+		if(e.key != "Enter")
+			return;
+		addMessage(e.target.value, handleTool);
+		e.target.value = "";
+	}
+	function scrollMessage(node, messageList)
+	{
+		return {
+			update(messageList) {
+				node.scrollTop = node.scrollHeight;
+			}
+		}
+	}
+	function getIconForMsg(msg)
+	{
+		if(msg.role == "user")
+			return "fa-user"
+		else
+			return "fa-robot"
+	}
+	function isToolUse(msg)
+	{
+		if(!Array.isArray(msg.content))
+			return false;
+		return msg.content[0].type == "tool_use";
+	}
+	function isToolResult(msg)
+	{
+		if(!Array.isArray(msg.content))
+			return false;
+		return msg.content[0].type == "tool_result";
+	}
+</script>
+<h1 class="text-lg font-bold">Claude AI Integration</h1>
+<p>WebVM is integrated with Claude by Anthropic AI. You can prompt the AI to control the system.</p>
+<p>You need to provide your API key. The key is only saved locally to your browser.</p>
+<div class="flex grow overflow-y-scroll scrollbar" use:scrollMessage={$messageList}>
+	<div class="h-full w-full">
+		<div class="w-full min-h-full flex flex-col gap-2 justify-end">
+			{#each $messageList as msg}
+				{#if isToolUse(msg)}
+					<p class="bg-neutral-700 p-2 rounded-md italic"><i class='fas fa-screwdriver-wrench w-6 mr-2 text-center'></i>Use the system</p>
+				{:else if !isToolResult(msg)}
+					<p class="{msg.role == 'error' ? 'bg-red-900' : 'bg-neutral-700'} p-2 rounded-md"><i class='fas {getIconForMsg(msg)} w-6 mr-2 text-center'></i>{msg.content}</p>
+				{/if}
+			{/each}
+		</div>
+	</div>
+</div>
+{#if $apiState == "KEY_REQUIRED"}
+	<input class="bg-neutral-700 p-2 rounded-md placeholder-gray-400" placeholder="Insert your Claude API Key" on:keydown={handleKeyEnter}/>
+{:else}
+	<input class="bg-neutral-700 p-2 rounded-md placeholder-gray-400" placeholder="Prompt..." on:keydown={handleMessage}/>
+{/if}

+ 10 - 3
src/lib/SideBar.svelte

@@ -4,10 +4,11 @@
 	import NetworkingTab from './NetworkingTab.svelte';
 	import CpuTab from './CpuTab.svelte';
 	import DiskTab from './DiskTab.svelte';
+	import AnthropicTab from './AnthropicTab.svelte';
 	import PostsTab from './PostsTab.svelte';
 	import DiscordTab from './DiscordTab.svelte';
 	import GitHubTab from './GitHubTab.svelte';
-	import { cpuActivity, diskActivity } from './activities.js'
+	import { cpuActivity, diskActivity, aiActivity } from './activities.js'
 
 	const icons = [
 		{ icon: 'fas fa-info-circle', info: 'Information', activity: null },
@@ -15,6 +16,7 @@
 		{ icon: 'fas fa-microchip', info: 'CPU', activity: cpuActivity },
 		{ icon: 'fas fa-compact-disc', info: 'Disk', activity: diskActivity },
 		null,
+		{ icon: 'fas fa-brain', info: 'ClaudeAI', activity: aiActivity },
 		{ icon: 'fas fa-book-open', info: 'Posts', activity: null },
 		{ icon: 'fab fa-discord', info: 'Discord', activity: null },
 		{ icon: 'fab fa-github', info: 'GitHub', activity: null },
@@ -29,19 +31,22 @@
 	function hideInfo() {
 		activeInfo = null;
 	}
+
+	export let handleTool;
+	export let needsDisplay;
 </script>
 
 <div class="flex flex-row w-14 h-full bg-neutral-700" on:mouseleave={hideInfo}>
 	<div class="flex flex-col shrink-0 w-14 text-gray-300">
 		{#each icons as i}
-			{#if i}
+			{#if i && (!needsDisplay || i.info != 'ClaudeAI')}
 				<Icon
 					icon={i.icon}
 					info={i.info}
 					activity={i.activity}
 					on:mouseover={(e) => showInfo(e.detail)}
 				/>
-			{:else}
+			{:else if i == null}
 				<div class="grow" on:mouseover={(e) => showInfo(null)}></div>
 			{/if}
 		{/each}
@@ -57,6 +62,8 @@
 			<CpuTab/>
 		{:else if activeInfo === 'Disk'}
 			<DiskTab on:reset/>
+		{:else if activeInfo === 'ClaudeAI'}
+			<AnthropicTab handleTool={handleTool} />
 		{:else if activeInfo === 'Posts'}
 			<PostsTab/>
 		{:else if activeInfo === 'Discord'}

+ 42 - 1
src/lib/WebVM.svelte

@@ -324,12 +324,53 @@
 		await blockCache.reset();
 		location.reload();
 	}
+	async function handleTool(tool)
+	{
+		if(tool.command)
+		{
+			var sentinel = "# End of AI command";
+			var buffer = term.buffer.active;
+			// Get the current cursor position
+			var marker = term.registerMarker();
+			var startLine = marker.line;
+			marker.dispose();
+			var ret = new Promise(function(f, r)
+			{
+				var callbackDisposer = term.onWriteParsed(function()
+				{
+					var curLength = buffer.length;
+					// Accumulate the output and see if the sentinel has been printed
+					var output = "";
+					for(var i=startLine + 1;i<curLength;i++)
+					{
+						var curLine = buffer.getLine(i).translateToString(true, 0, term.cols);;
+						if(curLine.indexOf(sentinel) >= 0)
+						{
+							// We are done, cleanup and return
+							callbackDisposer.dispose();
+							return f(output);
+						}
+						output += curLine + "\n";
+					}
+				});
+			});
+			term.input(tool.command);
+			term.input("\n");
+			term.input(sentinel);
+			term.input("\n");
+			return ret;
+		}
+		else
+		{
+			debugger;
+		}
+	}
 </script>
 
 <main class="relative w-full h-full">
 	<Nav />
 	<div class="absolute top-10 bottom-0 left-0 right-0">
-		<SideBar on:connect={handleConnect} on:reset={handleReset}>
+		<SideBar on:connect={handleConnect} on:reset={handleReset} needsDisplay={configObj.needsDisplay} handleTool={handleTool}>
 			<slot></slot>
 		</SideBar>
 		{#if configObj.needsDisplay}

+ 1 - 0
src/lib/activities.js

@@ -2,5 +2,6 @@ import { writable } from 'svelte/store';
 
 export const cpuActivity = writable(false);
 export const diskActivity = writable(false);
+export const aiActivity = writable(false);
 export const cpuPercentage = writable(0);
 export const diskLatency = writable(0);

+ 97 - 0
src/lib/anthropic.js

@@ -0,0 +1,97 @@
+import { writable } from 'svelte/store';
+import { browser } from '$app/environment'
+import { aiActivity } from '$lib/activities.js'
+
+import Anthropic from '@anthropic-ai/sdk';
+
+var client = null;
+var messages = [];
+
+export function setApiKey(key)
+{
+	client = new Anthropic({apiKey: key, dangerouslyAllowBrowser: true});
+	// Reset messages
+	messages = []
+	messageList.set(messages);
+	localStorage.setItem("anthropic-api-key", key);
+	apiState.set("READY");
+}
+
+function clearApiKey()
+{
+	localStorage.removeItem("anthropic-api-key");
+	apiState.set("KEY_REQUIRED");
+}
+
+function addMessageInternal(role, content)
+{
+	messages.push({role: role, content: content});
+	messageList.set(messages);
+}
+
+async function sendMessages(handleTool)
+{
+	aiActivity.set(true);
+	try
+	{
+		var tools = [
+			{ "type": "bash_20241022", "name": "bash" }
+		];
+		const response = await client.beta.messages.create({max_tokens: 1024, messages: messages, model: 'claude-3-5-sonnet-20241022', tools: tools, betas: ["computer-use-2024-10-22"]}); 
+		var content = response.content;
+		// Be robust to multiple response
+		for(var i=0;i<content.length;i++)
+		{
+			var c = content[i];
+			if(c.type == "text")
+			{
+				addMessageInternal(response.role, c.text);
+			}
+			else if(c.type == "tool_use")
+			{
+				addMessageInternal(response.role, [c]);
+				var commandResponse = await handleTool(c.input);
+				addMessageInternal("user", [{type: "tool_result", tool_use_id: c.id, content: commandResponse}]);
+				sendMessages(handleTool);
+			}
+			else
+			{
+				debugger;
+			}
+		}
+		if(response.stop_reason == "end_turn")
+			aiActivity.set(false);
+	}
+	catch(e)
+	{
+		if(e.status == 401)
+		{
+			addMessageInternal('error', 'Invalid API key');
+			clearApiKey();
+		}
+		else
+		{
+			addMessageInternal('error', e.error.error.message);
+		}
+			
+	}
+}
+
+export function addMessage(text, handleTool)
+{
+	addMessageInternal('user', text);
+	sendMessages(handleTool);
+}
+
+function initialize()
+{
+	var savedApiKey = localStorage.getItem("anthropic-api-key");
+	if(savedApiKey)
+		setApiKey(savedApiKey);
+}
+
+export const apiState = writable("KEY_REQUIRED");
+export const messageList = writable(messages);
+
+if(browser)
+	initialize();