feat: connect chat and runs, cancellable chat messages, unified frontend runs module

This commit is contained in:
Shivering Ocelot committed 2026-05-31 12:48:06 -05:00
1 parent d7e92f38f3
commit bc71fa4d59
7 files changed
+244 -119

No files matched your search

+142
View File
@@ -0,0 +1,142 @@
import * as backend from './backend';
// State
let runs = [];
let unsubscribeRunEvents = null;
// Event listeners for UI components
const listeners = {
runs: [],
chats: {},
};
class RunListener {
constructor() {
this.runIds = new Set();
this.callbacks = [];
this.cancelled = false;
}
setIdList(runIds) {
this.runIds = new Set(runIds);
}
addRunId(runId) {
this.runIds.add(runId);
}
addCallback(callback) {
this.callbacks.push(callback);
}
cancel() {
this.cancelled = true;
listeners.runs = listeners.runs.filter(l => l !== this);
}
hasRunId(runId) {
return this.runIds.has(runId);
}
emitChange(data) {
if (this.cancelled) return;
for (let cb of this.callbacks) cb(data);
}
}
// Update a run based on an incoming event
function updateRun(event) {
const { name, runId, data } = event.data;
// Find the run by ID
const runIndex = runs.findIndex(r => r.id === runId);
let run = runIndex >= 0 ? runs[runIndex] : null;
if (name == 'created') {
if (!run) {
run = Object.assign({}, data, {id: runId, events: []});
runs.push(run);
} else {
console.warn("Created but existing?", run, data);
}
} else if (name == 'done') {
run.done = true;
run.endTime = data.endTime;
run.output = data.output;
} else if (name == 'chat-message') {
let chatListeners = listeners.chats[data.chatId] || [];
for (let f of chatListeners) {
f({runId: runId, ...data});
}
}
if (run) {
run.events.push(event);
for (let listener of listeners.runs) {
if (listener.hasRunId(runId)) {
listener.emitChange(run);
}
}
} else {
console.error('Bad run event (non-existing run)', event);
}
}
// Initialize the runs module
async function init() {
// Listen for connection establishment
const res = await backend.sendAction("get-agent-runs");
runs = res.runs || [];
// Listen for run events
unsubscribeRunEvents = backend.onAction('run-event', updateRun);
}
init();
// Get all runs
export function getRuns() {
return runs;
}
// Get a specific run by ID
export function getRun(runId) {
return runs.find(r => r.id === runId);
}
// Cancel a run
export async function cancelRun(runId) {
try {
const res = await backend.sendAction("cancel-agent-run", { runId });
console.log(`Cancelled run ${runId}`, res);
return res;
} catch (err) {
console.error(`Failed to cancel run ${runId}:`, err);
throw err;
}
}
// Subscribe to runs changes
export function addRunListener(callback) {
const listener = new RunListener();
listener.addCallback(callback);
listeners.runs.push(listener);
return listener;
}
// Subscribe to run chat message events
export function onChatMessage(chatId, callback) {
let chatListeners = listeners.chats[chatId];
if (!chatListeners) {
chatListeners = [];
listeners.chats[chatId] = chatListeners;
}
chatListeners.push(callback);
return () => {
const index = chatListeners.indexOf(callback);
if (index > -1) {
chatListeners.splice(index, 1);
}
};
}