Move more code to anthropic.js

This commit is contained in:
Alessandro Pignotti 2025-03-11 19:44:03 +01:00
parent e251f2af20
commit 0ec99ddf9d
3 changed files with 336 additions and 327 deletions

View File

@ -1,5 +1,5 @@
<script> <script>
import { apiState, setApiKey, addMessage, clearMessageHistory, forceStop, messageList, currentMessage, enableThinking } from '$lib/anthropic.js'; import { apiState, setApiKey, addMessage, clearMessageHistory, forceStop, messageList, currentMessage, enableThinking, getMessageDetails } from '$lib/anthropic.js';
import { tick } from 'svelte'; import { tick } from 'svelte';
import { get } from 'svelte/store'; import { get } from 'svelte/store';
import PanelButton from './PanelButton.svelte'; import PanelButton from './PanelButton.svelte';
@ -54,59 +54,6 @@
} }
} }
} }
function getMessageDetails(msg) {
const isToolUse = Array.isArray(msg.content) && msg.content[0].type === "tool_use";
const isToolResult = Array.isArray(msg.content) && msg.content[0].type === "tool_result";
const isThinking = Array.isArray(msg.content) && msg.content[0].type === "thinking";
let icon = "";
let messageContent = "";
if (isToolUse) {
let tool = msg.content[0].input;
if (tool.action === "screenshot") {
icon = "fa-desktop";
messageContent = "Screenshot";
} else if (tool.action === "mouse_move") {
icon = "fa-mouse-pointer";
var coords = tool.coordinate;
messageContent = `Mouse at (${coords[0]}, ${coords[1]})`;
} else if (tool.action === "left_click") {
icon = "fa-mouse-pointer";
var coords = tool.coordinate;
messageContent = `Left click at (${coords[0]}, ${coords[1]})`;
} else if (tool.action === "right_click") {
icon = "fa-mouse-pointer";
var coords = tool.coordinate;
messageContent = `Right click at (${coords[0]}, ${coords[1]})`;
} else if (tool.action === "wait") {
icon = "fa-hourglass-half";
messageContent = "Waiting";
} else if (tool.action === "key") {
icon = "fa-keyboard";
messageContent = `Key press: ${tool.text}`;
} else if (tool.action === "type") {
icon = "fa-keyboard";
messageContent = "Type text";
} else {
icon = "fa-screwdriver-wrench";
messageContent = "Use the system";
}
} else if (isThinking) {
icon = "fa-brain";
messageContent = "Thinking...";
} else {
icon = msg.role === "user" ? "fa-user" : "fa-robot";
messageContent = msg.content;
}
return {
isToolUse,
isToolResult,
icon,
messageContent,
role: msg.role
};
}
async function handleStop() { async function handleStop() {
stopRequested = true; stopRequested = true;
await forceStop(); await forceStop();

View File

@ -9,7 +9,7 @@
import { networkInterface, startLogin } from '$lib/network.js' import { networkInterface, startLogin } from '$lib/network.js'
import { cpuActivity, diskActivity, cpuPercentage, diskLatency } from '$lib/activities.js' import { cpuActivity, diskActivity, cpuPercentage, diskLatency } from '$lib/activities.js'
import { introMessage, errorMessage, unexpectedErrorMessage } from '$lib/messages.js' import { introMessage, errorMessage, unexpectedErrorMessage } from '$lib/messages.js'
import { displayConfig } from '$lib/anthropic.js' import { displayConfig, handleToolImpl } from '$lib/anthropic.js'
import { tryPlausible } from '$lib/plausible.js' import { tryPlausible } from '$lib/plausible.js'
export let configObj = null; export let configObj = null;
@ -26,9 +26,6 @@
var blockCache = null; var blockCache = null;
var processCount = 0; var processCount = 0;
var curVT = 0; var curVT = 0;
var lastScreenshot = null;
var screenshotCanvas = null;
var screenshotCtx = null;
var sideBarPinned = false; var sideBarPinned = false;
function writeData(buf, vt) function writeData(buf, vt)
{ {
@ -349,277 +346,9 @@
await blockCache.reset(); await blockCache.reset();
location.reload(); location.reload();
} }
function getKmsInputElement()
{
// Find the CheerpX textare, it's attached to the body element
for(const node of document.body.children)
{
if(node.tagName == "TEXTAREA")
return node;
}
return null;
}
async function yieldHelper(timeout)
{
return new Promise(function(f2, r2)
{
setTimeout(f2, timeout);
});
}
async function kmsSendChar(textArea, charStr)
{
textArea.value = "_" + charStr;
var ke = new KeyboardEvent("keydown");
textArea.dispatchEvent(ke);
var ke = new KeyboardEvent("keyup");
textArea.dispatchEvent(ke);
await yieldHelper(0);
}
async function handleTool(tool) async function handleTool(tool)
{ {
if(tool.command) return await handleToolImpl(tool, term);
{
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 if(tool.action)
{
// Desktop control
// TODO: We should have an explicit API to interact with CheerpX display
switch(tool.action)
{
case "screenshot":
{
// Insert a 3 seconds delay unconditionally, the reference implementation uses 2
await yieldHelper(3000);
var delayCount = 0;
var display = document.getElementById("display");
var dc = get(displayConfig);
if(screenshotCanvas == null)
{
screenshotCanvas = document.createElement("canvas");
screenshotCtx = screenshotCanvas.getContext("2d");
}
if(screenshotCanvas.width != dc.width || screenshotCanvas.height != dc.height)
{
screenshotCanvas.width = dc.width;
screenshotCanvas.height = dc.height;
}
while(1)
{
// Resize the canvas to a Claude compatible size
screenshotCtx.drawImage(display, 0, 0, display.width, display.height, 0, 0, dc.width, dc.height);
var dataUrl = screenshotCanvas.toDataURL("image/png");
if(dataUrl == lastScreenshot)
{
// Delay at most 3 times
if(delayCount < 3)
{
// TODO: Defensive message, validate and remove
console.warn("Identical screenshot, rate limiting");
delayCount++;
// Wait some time and retry
await yieldHelper(5000);
continue;
}
}
lastScreenshot = dataUrl;
// Remove prefix from the encoded data
dataUrl = dataUrl.substring("data:image/png;base64,".length);
var imageSrc = { type: "base64", media_type: "image/png", data: dataUrl };
var contentObj = { type: "image", source: imageSrc };
return [ contentObj ];
}
}
case "mouse_move":
{
var coords = tool.coordinate;
var dc = get(displayConfig);
var mouseX = coords[0] / dc.mouseMult;
var mouseY = coords[1] / dc.mouseMult;
var display = document.getElementById("display");
var clientRect = display.getBoundingClientRect();
var me = new MouseEvent('mousemove', { clientX: mouseX + clientRect.left, clientY: mouseY + clientRect.top });
display.dispatchEvent(me);
return null;
}
case "left_click":
{
var coords = tool.coordinate;
var dc = get(displayConfig);
var mouseX = coords[0] / dc.mouseMult;
var mouseY = coords[1] / dc.mouseMult;
var display = document.getElementById("display");
var clientRect = display.getBoundingClientRect();
var me = new MouseEvent('mousedown', { clientX: mouseX + clientRect.left, clientY: mouseY + clientRect.top, button: 0 });
display.dispatchEvent(me);
// This delay prevent X11 logic from debouncing the mouseup
await yieldHelper(60)
me = new MouseEvent('mouseup', { clientX: mouseX + clientRect.left, clientY: mouseY + clientRect.top, button: 0 });
display.dispatchEvent(me);
return null;
}
case "right_click":
{
var coords = tool.coordinate;
var dc = get(displayConfig);
var mouseX = coords[0] / dc.mouseMult;
var mouseY = coords[1] / dc.mouseMult;
var display = document.getElementById("display");
var clientRect = display.getBoundingClientRect();
var me = new MouseEvent('mousedown', { clientX: mouseX + clientRect.left, clientY: mouseY + clientRect.top, button: 2 });
display.dispatchEvent(me);
// This delay prevent X11 logic from debouncing the mouseup
await yieldHelper(60)
me = new MouseEvent('mouseup', { clientX: mouseX + clientRect.left, clientY: mouseY + clientRect.top, button: 2 });
display.dispatchEvent(me);
return null;
}
case "type":
{
var str = tool.text;
return new Promise(async function(f, r)
{
var textArea = getKmsInputElement();
for(var i=0;i<str.length;i++)
{
await kmsSendChar(textArea, str[i]);
}
f(null);
});
}
case "key":
{
var textArea = getKmsInputElement();
var key = tool.text;
// Support arbitrary order of modifiers
var isCtrl = false;
var isAlt = false;
var isShift = false;
while(1)
{
if(key.startsWith("shift+"))
{
isShift = true;
key = key.substr("shift+".length);
var ke = new KeyboardEvent("keydown", {keyCode: 0x10});
textArea.dispatchEvent(ke);
await yieldHelper(0);
continue;
}
else if(key.startsWith("ctrl+"))
{
isCtrl = true;
key = key.substr("ctrl+".length);
var ke = new KeyboardEvent("keydown", {keyCode: 0x11});
textArea.dispatchEvent(ke);
await yieldHelper(0);
continue;
}
else if(key.startsWith("alt+"))
{
isAlt = true;
key = key.substr("alt+".length);
var ke = new KeyboardEvent("keydown", {keyCode: 0x12});
textArea.dispatchEvent(ke);
await yieldHelper(0);
continue;
}
break;
}
var ret = null;
// Dispatch single chars directly and parse the rest
if(key.length == 1)
{
await kmsSendChar(textArea, key);
}
else
{
switch(tool.text)
{
case "Return":
await kmsSendChar(textArea, "\n");
break;
case "Escape":
var ke = new KeyboardEvent("keydown", {keyCode: 0x1b});
textArea.dispatchEvent(ke);
await yieldHelper(0);
ke = new KeyboardEvent("keyup", {keyCode: 0x1b});
textArea.dispatchEvent(ke);
await yieldHelper(0);
break;
default:
// TODO: Support more key combinations
ret = new Error(`Error: Invalid key '${tool.text}'`);
}
}
if(isShift)
{
var ke = new KeyboardEvent("keyup", {keyCode: 0x10});
textArea.dispatchEvent(ke);
await yieldHelper(0);
}
if(isCtrl)
{
var ke = new KeyboardEvent("keyup", {keyCode: 0x11});
textArea.dispatchEvent(ke);
await yieldHelper(0);
}
if(isAlt)
{
var ke = new KeyboardEvent("keyup", {keyCode: 0x12});
textArea.dispatchEvent(ke);
await yieldHelper(0);
}
return ret;
}
case "wait":
{
// Wait 2x what the machine expects to compensate for virtualization slowdown
await yieldHelper(tool.duration * 2 * 1000);
return null;
}
default:
{
break;
}
}
return new Error("Error: Invalid action");
}
else
{
// We can get there due to model hallucinations
return new Error("Error: Invalid tool syntax");
}
} }
async function handleSidebarPinChange(event) async function handleSidebarPinChange(event)
{ {

View File

@ -8,6 +8,9 @@ import Anthropic from '@anthropic-ai/sdk';
var client = null; var client = null;
var messages = []; var messages = [];
var stopFlag = false; var stopFlag = false;
var lastScreenshot = null;
var screenshotCanvas = null;
var screenshotCtx = null;
export function setApiKey(key) export function setApiKey(key)
{ {
@ -153,6 +156,336 @@ export function forceStop() {
}); });
} }
export function getMessageDetails(msg) {
const isToolUse = Array.isArray(msg.content) && msg.content[0].type === "tool_use";
const isToolResult = Array.isArray(msg.content) && msg.content[0].type === "tool_result";
const isThinking = Array.isArray(msg.content) && msg.content[0].type === "thinking";
let icon = "";
let messageContent = "";
if (isToolUse) {
let tool = msg.content[0].input;
if (tool.action === "screenshot") {
icon = "fa-desktop";
messageContent = "Screenshot";
} else if (tool.action === "mouse_move") {
icon = "fa-mouse-pointer";
var coords = tool.coordinate;
messageContent = `Mouse at (${coords[0]}, ${coords[1]})`;
} else if (tool.action === "left_click") {
icon = "fa-mouse-pointer";
var coords = tool.coordinate;
messageContent = `Left click at (${coords[0]}, ${coords[1]})`;
} else if (tool.action === "right_click") {
icon = "fa-mouse-pointer";
var coords = tool.coordinate;
messageContent = `Right click at (${coords[0]}, ${coords[1]})`;
} else if (tool.action === "wait") {
icon = "fa-hourglass-half";
messageContent = "Waiting";
} else if (tool.action === "key") {
icon = "fa-keyboard";
messageContent = `Key press: ${tool.text}`;
} else if (tool.action === "type") {
icon = "fa-keyboard";
messageContent = "Type text";
} else {
icon = "fa-screwdriver-wrench";
messageContent = "Use the system";
}
} else if (isThinking) {
icon = "fa-brain";
messageContent = "Thinking...";
} else {
icon = msg.role === "user" ? "fa-user" : "fa-robot";
messageContent = msg.content;
}
return {
isToolUse,
isToolResult,
icon,
messageContent,
role: msg.role
};
}
async function yieldHelper(timeout)
{
return new Promise(function(f2, r2)
{
setTimeout(f2, timeout);
});
}
async function kmsSendChar(textArea, charStr)
{
textArea.value = "_" + charStr;
var ke = new KeyboardEvent("keydown");
textArea.dispatchEvent(ke);
var ke = new KeyboardEvent("keyup");
textArea.dispatchEvent(ke);
await yieldHelper(0);
}
function getKmsInputElement()
{
// Find the CheerpX textare, it's attached to the body element
for(const node of document.body.children)
{
if(node.tagName == "TEXTAREA")
return node;
}
return null;
}
export async function handleToolImpl(tool, term)
{
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 if(tool.action)
{
// Desktop control
// TODO: We should have an explicit API to interact with CheerpX display
switch(tool.action)
{
case "screenshot":
{
// Insert a 3 seconds delay unconditionally, the reference implementation uses 2
await yieldHelper(3000);
var delayCount = 0;
var display = document.getElementById("display");
var dc = get(displayConfig);
if(screenshotCanvas == null)
{
screenshotCanvas = document.createElement("canvas");
screenshotCtx = screenshotCanvas.getContext("2d");
}
if(screenshotCanvas.width != dc.width || screenshotCanvas.height != dc.height)
{
screenshotCanvas.width = dc.width;
screenshotCanvas.height = dc.height;
}
while(1)
{
// Resize the canvas to a Claude compatible size
screenshotCtx.drawImage(display, 0, 0, display.width, display.height, 0, 0, dc.width, dc.height);
var dataUrl = screenshotCanvas.toDataURL("image/png");
if(dataUrl == lastScreenshot)
{
// Delay at most 3 times
if(delayCount < 3)
{
// TODO: Defensive message, validate and remove
console.warn("Identical screenshot, rate limiting");
delayCount++;
// Wait some time and retry
await yieldHelper(5000);
continue;
}
}
lastScreenshot = dataUrl;
// Remove prefix from the encoded data
dataUrl = dataUrl.substring("data:image/png;base64,".length);
var imageSrc = { type: "base64", media_type: "image/png", data: dataUrl };
var contentObj = { type: "image", source: imageSrc };
return [ contentObj ];
}
}
case "mouse_move":
{
var coords = tool.coordinate;
var dc = get(displayConfig);
var mouseX = coords[0] / dc.mouseMult;
var mouseY = coords[1] / dc.mouseMult;
var display = document.getElementById("display");
var clientRect = display.getBoundingClientRect();
var me = new MouseEvent('mousemove', { clientX: mouseX + clientRect.left, clientY: mouseY + clientRect.top });
display.dispatchEvent(me);
return null;
}
case "left_click":
{
var coords = tool.coordinate;
var dc = get(displayConfig);
var mouseX = coords[0] / dc.mouseMult;
var mouseY = coords[1] / dc.mouseMult;
var display = document.getElementById("display");
var clientRect = display.getBoundingClientRect();
var me = new MouseEvent('mousedown', { clientX: mouseX + clientRect.left, clientY: mouseY + clientRect.top, button: 0 });
display.dispatchEvent(me);
// This delay prevent X11 logic from debouncing the mouseup
await yieldHelper(60)
me = new MouseEvent('mouseup', { clientX: mouseX + clientRect.left, clientY: mouseY + clientRect.top, button: 0 });
display.dispatchEvent(me);
return null;
}
case "right_click":
{
var coords = tool.coordinate;
var dc = get(displayConfig);
var mouseX = coords[0] / dc.mouseMult;
var mouseY = coords[1] / dc.mouseMult;
var display = document.getElementById("display");
var clientRect = display.getBoundingClientRect();
var me = new MouseEvent('mousedown', { clientX: mouseX + clientRect.left, clientY: mouseY + clientRect.top, button: 2 });
display.dispatchEvent(me);
// This delay prevent X11 logic from debouncing the mouseup
await yieldHelper(60)
me = new MouseEvent('mouseup', { clientX: mouseX + clientRect.left, clientY: mouseY + clientRect.top, button: 2 });
display.dispatchEvent(me);
return null;
}
case "type":
{
var str = tool.text;
return new Promise(async function(f, r)
{
var textArea = getKmsInputElement();
for(var i=0;i<str.length;i++)
{
await kmsSendChar(textArea, str[i]);
}
f(null);
});
}
case "key":
{
var textArea = getKmsInputElement();
var key = tool.text;
// Support arbitrary order of modifiers
var isCtrl = false;
var isAlt = false;
var isShift = false;
while(1)
{
if(key.startsWith("shift+"))
{
isShift = true;
key = key.substr("shift+".length);
var ke = new KeyboardEvent("keydown", {keyCode: 0x10});
textArea.dispatchEvent(ke);
await yieldHelper(0);
continue;
}
else if(key.startsWith("ctrl+"))
{
isCtrl = true;
key = key.substr("ctrl+".length);
var ke = new KeyboardEvent("keydown", {keyCode: 0x11});
textArea.dispatchEvent(ke);
await yieldHelper(0);
continue;
}
else if(key.startsWith("alt+"))
{
isAlt = true;
key = key.substr("alt+".length);
var ke = new KeyboardEvent("keydown", {keyCode: 0x12});
textArea.dispatchEvent(ke);
await yieldHelper(0);
continue;
}
break;
}
var ret = null;
// Dispatch single chars directly and parse the rest
if(key.length == 1)
{
await kmsSendChar(textArea, key);
}
else
{
switch(tool.text)
{
case "Return":
await kmsSendChar(textArea, "\n");
break;
case "Escape":
var ke = new KeyboardEvent("keydown", {keyCode: 0x1b});
textArea.dispatchEvent(ke);
await yieldHelper(0);
ke = new KeyboardEvent("keyup", {keyCode: 0x1b});
textArea.dispatchEvent(ke);
await yieldHelper(0);
break;
default:
// TODO: Support more key combinations
ret = new Error(`Error: Invalid key '${tool.text}'`);
}
}
if(isShift)
{
var ke = new KeyboardEvent("keyup", {keyCode: 0x10});
textArea.dispatchEvent(ke);
await yieldHelper(0);
}
if(isCtrl)
{
var ke = new KeyboardEvent("keyup", {keyCode: 0x11});
textArea.dispatchEvent(ke);
await yieldHelper(0);
}
if(isAlt)
{
var ke = new KeyboardEvent("keyup", {keyCode: 0x12});
textArea.dispatchEvent(ke);
await yieldHelper(0);
}
return ret;
}
case "wait":
{
// Wait 2x what the machine expects to compensate for virtualization slowdown
await yieldHelper(tool.duration * 2 * 1000);
return null;
}
default:
{
break;
}
}
return new Error("Error: Invalid action");
}
else
{
// We can get there due to model hallucinations
return new Error("Error: Invalid tool syntax");
}
}
function initialize() function initialize()
{ {
var savedApiKey = localStorage.getItem("anthropic-api-key"); var savedApiKey = localStorage.getItem("anthropic-api-key");