feat: connect chat and runs, cancellable chat messages, unified frontend runs module
This commit is contained in:
1 parent
d7e92f38f3
commit
bc71fa4d59
7 files changed
+244
-119
No files matched your search
@@ -9,6 +9,7 @@
|
||||
|
||||
import * as backend from '../backend';
|
||||
import * as ui from '../ui';
|
||||
import * as runs from '../runs';
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
@@ -33,6 +34,7 @@
|
||||
const expandedThreads = ref({}); // Object<int: bool>, to simulate a Set<int>
|
||||
const editingMessageId = ref(null);
|
||||
const noteMode = ref(false);
|
||||
const messageRunMap = ref({});
|
||||
|
||||
const editInfo = computed(() => {
|
||||
if (editingMessageId.value != null) {
|
||||
@@ -50,6 +52,8 @@
|
||||
|
||||
let _isMounted = false;
|
||||
let unsubscribeChat = null;
|
||||
let unsubscribeRuns = null;
|
||||
let runListener = null;
|
||||
|
||||
// TODO: user sent message is added without id.
|
||||
// User should receive his own message notification, and find its message to assign it an id.
|
||||
@@ -110,6 +114,8 @@
|
||||
});
|
||||
});
|
||||
|
||||
unsubscribeRuns = runs.onChatMessage(chatId, onRunMessage);
|
||||
|
||||
let response = await backend.sendAction("get-chat-messages", {chatId});
|
||||
// Always good to duplicate when assigning to a ref
|
||||
messages.value = response.messages.map(it => ({
|
||||
@@ -147,6 +153,29 @@
|
||||
async function unloadChatState () {
|
||||
if (unsubscribeChat) unsubscribeChat();
|
||||
unsubscribeChat = null;
|
||||
if (unsubscribeRuns) unsubscribeRuns();
|
||||
unsubscribeRuns = null;
|
||||
}
|
||||
|
||||
function onRunMessage (event) {
|
||||
if (!runListener) {
|
||||
runListener = runs.addRunListener(onRunUpdate);
|
||||
}
|
||||
runListener.addRunId(event.runId);
|
||||
messageRunMap.value[event.messageId] = event.runId;
|
||||
ui.alert("Chat Run Message: " + JSON.stringify(event));
|
||||
}
|
||||
|
||||
function onRunUpdate (newRun) {
|
||||
if (newRun.done) {
|
||||
runListener.runIds.delete(newRun.id);
|
||||
let map = messageRunMap.value;
|
||||
for (let messageId in map) {
|
||||
if (map[messageId] == newRun.id) {
|
||||
delete map[messageId];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This won't fire when called, the mounted callback will take care
|
||||
@@ -428,6 +457,9 @@
|
||||
:message="msg"
|
||||
@open-menu="chatEntryContextMenu"
|
||||
@toggle-thread="expandThread"
|
||||
|
||||
:cancellable="messageRunMap[msg.id]"
|
||||
@cancel="runs.cancelRun(messageRunMap[msg.id])"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
import TitleBar from '../components/TitleBar.vue';
|
||||
import * as backend from '../backend';
|
||||
import * as runs from '../runs';
|
||||
|
||||
const runs = ref([]);
|
||||
const runList = ref([]);
|
||||
const expandedRunId = ref(null);
|
||||
const loading = ref(true);
|
||||
let unsubscribeRunEvents = null;
|
||||
const runListener = runs.addRunListener(onRunEvent);
|
||||
|
||||
const toggleRun = (id) => {
|
||||
if (expandedRunId.value === id) {
|
||||
expandedRunId.value = null;
|
||||
} else {
|
||||
expandedRunId.value = id;
|
||||
}
|
||||
expandedRunId.value = expandedRunId.value == id ? null : id;
|
||||
};
|
||||
|
||||
const formatTime = (timestamp) => {
|
||||
@@ -21,72 +16,24 @@
|
||||
return new Date(timestamp * 1000).toLocaleString();
|
||||
};
|
||||
|
||||
const getStatusBadge = (run) => {
|
||||
return run.done ? 'Done' : 'Running';
|
||||
};
|
||||
|
||||
const updateRun = (event) => {
|
||||
const { name, data } = event;
|
||||
|
||||
// Find the run by ID
|
||||
const runIndex = runs.value.findIndex(r => r.id === data.runId);
|
||||
|
||||
if (runIndex === -1) {
|
||||
// New run was created
|
||||
if (name === 'run-started') {
|
||||
runs.value.push(data);
|
||||
}
|
||||
} else {
|
||||
// Update existing run
|
||||
const run = runs.value[runIndex];
|
||||
|
||||
if (name === 'run-completed') {
|
||||
run.done = true;
|
||||
run.endTime = data.endTime;
|
||||
run.output = data.output;
|
||||
|
||||
// Mark as expanded to show the completion
|
||||
if (expandedRunId.value !== data.runId) {
|
||||
expandedRunId.value = data.runId;
|
||||
}
|
||||
} else if (name === 'run-event') {
|
||||
// Add new event to the run
|
||||
if (!run.events) {
|
||||
run.events = [];
|
||||
}
|
||||
run.events.push(data.data);
|
||||
|
||||
// If this run is expanded, update it
|
||||
if (expandedRunId.value === data.runId) {
|
||||
// No DOM update needed as we're re-rendering the whole list
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
async function cancelRun (runId) {
|
||||
const res = await backend.sendAction("cancel-agent-run", {runId});
|
||||
console.log(`Cancelled run ${runId}`, res);
|
||||
await runs.cancelRun(runId);
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const res = await backend.sendAction("get-agent-runs");
|
||||
runs.value = res.runs || [];
|
||||
} catch (err) {
|
||||
console.error("Failed to load agent runs:", err);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
function onRunEvent (newRun) {
|
||||
let run = runList.value.findOne(r => r.id == newRun.id);
|
||||
Object.assign(run, newRun);
|
||||
}
|
||||
|
||||
// Listen for live run events
|
||||
unsubscribeRunEvents = backend.onRunEvents(updateRun);
|
||||
onMounted(() => {
|
||||
runList.value = runs.getRuns().map(
|
||||
run => Object.assign({}, run)
|
||||
);
|
||||
runListener.setIdList(runList.value.map(r => r.id));
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (unsubscribeRunEvents) {
|
||||
unsubscribeRunEvents();
|
||||
}
|
||||
runListener.setIdList([]);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -96,18 +43,16 @@
|
||||
<h1>Agent Runs</h1>
|
||||
</TitleBar>
|
||||
|
||||
<div v-if="loading" class="loading">Loading runs...</div>
|
||||
|
||||
<div v-else-if="runs.length === 0" class="empty">
|
||||
<div v-if="runList.length === 0" class="empty">
|
||||
No agent runs found.
|
||||
</div>
|
||||
|
||||
<div v-else v-for="run in runs" :key="run.id" class="run-entry">
|
||||
<div v-else v-for="run in runList" :key="run.id" class="run-entry">
|
||||
<div class="run-header" @click="toggleRun(run.id)">
|
||||
<span class="run-id">{{ run.id }}</span>
|
||||
<span class="run-agent">Agent: {{ run.agentId }}</span>
|
||||
<span :class="['run-status', run.done ? 'status-done' : 'status-running']">
|
||||
{{ getStatusBadge(run) }}
|
||||
{{ run.done ? 'Done' : 'Running' }}
|
||||
</span>
|
||||
<button v-if="!run.done" @click="cancelRun(run.id)">
|
||||
Cancel
|
||||
|
||||
Reference in new issue
Block a user