mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-13 08:15:59 +00:00
Feature: cancelling executions through dashboard
This commit is contained in:
1 parent
99d75be125
commit
322348ea51
7 files changed
+581
-11
No files matched your search
@@ -60,6 +60,5 @@ initial_prompt: ""
|
||||
project_name: "serena"
|
||||
languages:
|
||||
- python
|
||||
- typescript
|
||||
included_optional_tools: []
|
||||
encoding: utf-8
|
||||
@@ -198,6 +198,15 @@ class SerenaAgent:
|
||||
"""
|
||||
return self._task_executor.get_current_tasks()
|
||||
|
||||
def cancel_task(self, task_id: int) -> bool:
|
||||
"""
|
||||
Cancels a task with the given ID and removes it from the task queue.
|
||||
|
||||
:param task_id: the ID of the task to cancel
|
||||
:return: True if the task was found and cancelled, False otherwise
|
||||
"""
|
||||
return self._task_executor.cancel_task(task_id)
|
||||
|
||||
def get_last_executed_task(self) -> TaskExecutor.TaskInfo | None:
|
||||
"""
|
||||
Gets the last executed task.
|
||||
|
||||
+21
-5
@@ -86,11 +86,16 @@ class RequestDeleteMemory(BaseModel):
|
||||
memory_name: str
|
||||
|
||||
|
||||
class RequestCancelTaskExecution(BaseModel):
|
||||
task_id: int
|
||||
|
||||
|
||||
class QueuedExecution(BaseModel):
|
||||
task_id: int
|
||||
is_running: bool
|
||||
name: str
|
||||
finished_successfully: bool
|
||||
logged: bool
|
||||
|
||||
@classmethod
|
||||
def from_task_info(cls, task_info: TaskExecutor.TaskInfo) -> Self:
|
||||
@@ -99,6 +104,7 @@ class QueuedExecution(BaseModel):
|
||||
is_running=task_info.is_running,
|
||||
name=task_info.name,
|
||||
finished_successfully=task_info.finished_successfully(),
|
||||
logged=task_info.logged,
|
||||
)
|
||||
|
||||
|
||||
@@ -169,7 +175,7 @@ class SerenaDashboardAPI:
|
||||
|
||||
@self._app.route("/get_config_overview", methods=["GET"])
|
||||
def get_config_overview() -> dict[str, Any]:
|
||||
result = self._agent.execute_task(self._get_config_overview)
|
||||
result = self._agent.execute_task(self._get_config_overview, logged=False)
|
||||
return result.model_dump()
|
||||
|
||||
@self._app.route("/shutdown", methods=["PUT"])
|
||||
@@ -251,6 +257,16 @@ class SerenaDashboardAPI:
|
||||
except Exception as e:
|
||||
return {"status": "error", "message": str(e)}
|
||||
|
||||
@self._app.route("/cancel_task_execution", methods=["POST"])
|
||||
def cancel_task_execution() -> dict[str, Any]:
|
||||
request_data = request.get_json()
|
||||
try:
|
||||
request_cancel_task = RequestCancelTaskExecution.model_validate(request_data)
|
||||
task_was_cancelled = self._agent.cancel_task(request_cancel_task.task_id)
|
||||
return {"status": "success", "was_cancelled": task_was_cancelled}
|
||||
except Exception as e:
|
||||
return {"status": "error", "message": str(e), "was_cancelled": False}
|
||||
|
||||
@self._app.route("/last_execution", methods=["GET"])
|
||||
def get_last_execution() -> dict[str, Any]:
|
||||
try:
|
||||
@@ -417,7 +433,7 @@ class SerenaDashboardAPI:
|
||||
|
||||
return ResponseAvailableLanguages(languages=sorted(available_languages))
|
||||
|
||||
return self._agent.execute_task(run)
|
||||
return self._agent.execute_task(run, logged=False)
|
||||
|
||||
def _get_memory(self, request_get_memory: RequestGetMemory) -> ResponseGetMemory:
|
||||
def run() -> ResponseGetMemory:
|
||||
@@ -428,7 +444,7 @@ class SerenaDashboardAPI:
|
||||
content = project.memories_manager.load_memory(request_get_memory.memory_name)
|
||||
return ResponseGetMemory(content=content, memory_name=request_get_memory.memory_name)
|
||||
|
||||
return self._agent.execute_task(run)
|
||||
return self._agent.execute_task(run, logged=False)
|
||||
|
||||
def _save_memory(self, request_save_memory: RequestSaveMemory) -> None:
|
||||
def run() -> None:
|
||||
@@ -438,7 +454,7 @@ class SerenaDashboardAPI:
|
||||
|
||||
project.memories_manager.save_memory(request_save_memory.memory_name, request_save_memory.content)
|
||||
|
||||
self._agent.execute_task(run)
|
||||
self._agent.execute_task(run, logged=True, name="SaveMemory")
|
||||
|
||||
def _delete_memory(self, request_delete_memory: RequestDeleteMemory) -> None:
|
||||
def run() -> None:
|
||||
@@ -448,7 +464,7 @@ class SerenaDashboardAPI:
|
||||
|
||||
project.memories_manager.delete_memory(request_delete_memory.memory_name)
|
||||
|
||||
self._agent.execute_task(run)
|
||||
self._agent.execute_task(run, logged=True, name="DeleteMemory")
|
||||
|
||||
def _add_language(self, request_add_language: RequestAddLanguage) -> None:
|
||||
from solidlsp.ls_config import Language
|
||||
|
||||
@@ -338,6 +338,190 @@ body {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Executions Styles */
|
||||
.executions-section {
|
||||
background-color: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 5px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
transition: background-color 0.3s ease, border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.executions-section h2 {
|
||||
margin-top: 0;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.execution-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.execution-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
background-color: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 20px;
|
||||
padding: 8px 12px;
|
||||
min-height: 40px;
|
||||
transition: background-color 0.2s ease, border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.execution-item.running {
|
||||
border-color: var(--btn-primary);
|
||||
background: linear-gradient(to right, rgba(234, 164, 93, 0.1), var(--bg-primary));
|
||||
}
|
||||
|
||||
.execution-item.cancelled {
|
||||
border-color: var(--text-muted);
|
||||
background-color: var(--bg-primary);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.execution-item.abandoned {
|
||||
border-color: var(--log-error);
|
||||
background: linear-gradient(to right, rgba(255, 0, 0, 0.1), var(--bg-primary));
|
||||
}
|
||||
|
||||
.execution-spinner {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid var(--border-color);
|
||||
border-top-color: var(--btn-primary);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.7s linear infinite;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.execution-name {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.execution-meta {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.execution-cancel-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: background-color 0.15s ease, color 0.15s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.execution-cancel-btn:hover {
|
||||
background-color: rgba(255, 0, 0, 0.1);
|
||||
color: var(--log-error);
|
||||
}
|
||||
|
||||
.execution-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.execution-icon.success {
|
||||
background-color: rgba(34, 197, 94, 0.2);
|
||||
border: 1px solid rgba(34, 197, 94, 0.6);
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.execution-icon.cancelled {
|
||||
background-color: var(--border-color);
|
||||
border: 1px solid var(--text-muted);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.execution-icon.abandoned {
|
||||
background-color: rgba(255, 0, 0, 0.2);
|
||||
border: 1px solid var(--log-error);
|
||||
color: var(--log-error);
|
||||
}
|
||||
|
||||
.execution-icon.error {
|
||||
background-color: rgba(255, 0, 0, 0.2);
|
||||
border: 1px solid var(--log-error);
|
||||
color: var(--log-error);
|
||||
}
|
||||
|
||||
.last-execution-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
background: linear-gradient(to right, rgba(34, 197, 94, 0.08), transparent);
|
||||
border: 1px solid rgba(34, 197, 94, 0.2);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.last-execution-container.error {
|
||||
background: linear-gradient(to right, rgba(255, 0, 0, 0.08), transparent);
|
||||
border-color: rgba(255, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.last-execution-icon-container {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background-color: rgba(34, 197, 94, 0.2);
|
||||
border: 1px solid rgba(34, 197, 94, 0.6);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
color: #22c55e;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.last-execution-container.error .last-execution-icon-container {
|
||||
background-color: rgba(255, 0, 0, 0.2);
|
||||
border-color: var(--log-error);
|
||||
color: var(--log-error);
|
||||
}
|
||||
|
||||
.last-execution-body {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.last-execution-status {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.last-execution-name {
|
||||
font-size: 13px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.stat-bar-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -730,7 +914,7 @@ body {
|
||||
}
|
||||
|
||||
.language-add-btn:hover {
|
||||
background: var(--bg-tertiary);
|
||||
background: var(--border-color);
|
||||
border-color: var(--btn-primary);
|
||||
color: var(--btn-primary);
|
||||
}
|
||||
|
||||
@@ -62,11 +62,16 @@ class Dashboard {
|
||||
this.isAddingLanguage = false;
|
||||
this.waitingForPollingResult = false;
|
||||
|
||||
// Execution tracking
|
||||
this.cancelledExecutions = [];
|
||||
this.executionToCancel = null;
|
||||
|
||||
// Tool names and stats
|
||||
this.toolNames = [];
|
||||
this.currentMaxIdx = -1;
|
||||
this.pollInterval = null;
|
||||
this.configPollInterval = null;
|
||||
this.executionsPollInterval = null;
|
||||
this.failureCount = 0;
|
||||
|
||||
// jQuery elements
|
||||
@@ -117,6 +122,13 @@ class Dashboard {
|
||||
this.$createMemoryCreateBtn = $('#create-memory-create-btn');
|
||||
this.$createMemoryCancelBtn = $('#create-memory-cancel-btn');
|
||||
this.$modalCloseCreateMemory = $('.modal-close-create-memory');
|
||||
this.$activeExecutionQueueDisplay = $('#active-executions-display');
|
||||
this.$lastExecutionDisplay = $('#last-execution-display');
|
||||
this.$cancelledExecutionsDisplay = $('#cancelled-executions-display');
|
||||
this.$cancelExecutionModal = $('#cancel-execution-modal');
|
||||
this.$cancelExecutionOkBtn = $('#cancel-execution-ok-btn');
|
||||
this.$cancelExecutionCancelBtn = $('#cancel-execution-cancel-btn');
|
||||
this.$modalCloseCancelExecution = $('.modal-close-cancel-execution');
|
||||
|
||||
// Chart references
|
||||
this.countChart = null;
|
||||
@@ -156,6 +168,9 @@ class Dashboard {
|
||||
self.createMemoryFromModal();
|
||||
}
|
||||
});
|
||||
this.$cancelExecutionOkBtn.click(this.confirmCancelExecutionOk.bind(this));
|
||||
this.$cancelExecutionCancelBtn.click(this.closeCancelExecutionModal.bind(this));
|
||||
this.$modalCloseCancelExecution.click(this.closeCancelExecutionModal.bind(this));
|
||||
|
||||
// Page navigation
|
||||
$('[data-page]').click(function(e) {
|
||||
@@ -237,6 +252,7 @@ class Dashboard {
|
||||
// Start on overview page
|
||||
self.loadConfigOverview();
|
||||
self.startConfigPolling();
|
||||
self.startExecutionsPolling();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -268,6 +284,7 @@ class Dashboard {
|
||||
if (page === 'overview') {
|
||||
this.loadConfigOverview();
|
||||
this.startConfigPolling();
|
||||
this.startExecutionsPolling();
|
||||
} else if (page === 'logs') {
|
||||
this.loadLogs();
|
||||
} else if (page === 'stats') {
|
||||
@@ -284,6 +301,10 @@ class Dashboard {
|
||||
clearInterval(this.configPollInterval);
|
||||
this.configPollInterval = null;
|
||||
}
|
||||
if (this.executionsPollInterval) {
|
||||
clearInterval(this.executionsPollInterval);
|
||||
this.executionsPollInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Config Overview Methods =====
|
||||
@@ -299,7 +320,6 @@ class Dashboard {
|
||||
url: '/get_config_overview',
|
||||
type: 'GET',
|
||||
success: function(response) {
|
||||
console.log('Config overview response:', response);
|
||||
self.failureCount = 0;
|
||||
self.configData = response;
|
||||
self.jetbrainsMode = response.jetbrains_mode;
|
||||
@@ -336,6 +356,17 @@ class Dashboard {
|
||||
this.configPollInterval = setInterval(this.loadConfigOverview.bind(this), 2000);
|
||||
}
|
||||
|
||||
startExecutionsPolling() {
|
||||
// Poll every 1 second for executions (independent of config polling)
|
||||
// This ensures stuck executions can still be cancelled even if config polling is blocked
|
||||
this.loadExecutions();
|
||||
this.loadLastExecution();
|
||||
this.executionsPollInterval = setInterval(() => {
|
||||
this.loadExecutions();
|
||||
this.loadLastExecution();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
displayConfig(config) {
|
||||
try {
|
||||
// Check if tools and memories sections are currently expanded
|
||||
@@ -604,6 +635,279 @@ class Dashboard {
|
||||
this.$availableContextsDisplay.html(html);
|
||||
}
|
||||
|
||||
// ===== Executions Methods =====
|
||||
|
||||
loadExecutions() {
|
||||
let self = this;
|
||||
$.ajax({
|
||||
url: '/queued_task_executions',
|
||||
type: 'GET',
|
||||
success: function(response) {
|
||||
if (response.status === 'success') {
|
||||
self.displayActiveExecutionsQueue(response.queued_executions || []);
|
||||
} else {
|
||||
console.error('Error loading executions:', response.message);
|
||||
}
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('Error loading executions:', error);
|
||||
self.$activeExecutionQueueDisplay.html('<div class="error-message">Error loading executions</div>');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadLastExecution() {
|
||||
let self = this;
|
||||
$.ajax({
|
||||
url: '/last_execution',
|
||||
type: 'GET',
|
||||
success: function(response) {
|
||||
if (response.status === 'success') {
|
||||
if (response.last_execution !== null && response.last_execution.logged) {
|
||||
self.displayLastExecution(response.last_execution);
|
||||
}
|
||||
} else {
|
||||
console.error('Error loading last execution:', response.message);
|
||||
}
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('Error loading last execution:', error);
|
||||
self.$lastExecutionDisplay.html('<div class="error-message">Error loading last execution</div>');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
displayActiveExecutionsQueue(executions) {
|
||||
if (!executions || executions.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '<div class="execution-list">';
|
||||
let self = this;
|
||||
|
||||
executions.forEach(function(execution) {
|
||||
const isRunning = execution.is_running;
|
||||
const logged = execution.logged;
|
||||
|
||||
if (!logged) {
|
||||
return; // Skip unlogged executions
|
||||
}
|
||||
|
||||
let itemClass = 'execution-item';
|
||||
if (isRunning) {
|
||||
itemClass += ' running';
|
||||
}
|
||||
|
||||
// Escape JSON for HTML attribute - replace single quotes and use HTML entities
|
||||
const executionJson = JSON.stringify(execution).replace(/'/g, ''');
|
||||
|
||||
html += '<div class="' + itemClass + '" data-task-id="' + execution.task_id + '" data-execution=\'' + executionJson + '\'>';
|
||||
|
||||
if (isRunning) {
|
||||
html += '<div class="execution-spinner"></div>';
|
||||
}
|
||||
|
||||
html += '<div class="execution-name">' + self.escapeHtml(execution.name) + '</div>';
|
||||
|
||||
if (isRunning) {
|
||||
html += '<div class="execution-meta">#' + execution.task_id + '</div>';
|
||||
} else {
|
||||
html += '<div class="execution-meta">queued · #' + execution.task_id + '</div>';
|
||||
}
|
||||
|
||||
html += '<button class="execution-cancel-btn" data-task-id="' + execution.task_id + '" data-is-running="' + isRunning + '">✕</button>';
|
||||
html += '</div>';
|
||||
});
|
||||
|
||||
html += '</div>';
|
||||
this.$activeExecutionQueueDisplay.html(html);
|
||||
|
||||
// Attach event handlers for cancel buttons
|
||||
$('.execution-cancel-btn').click(function(e) {
|
||||
e.preventDefault();
|
||||
console.log('Cancel button clicked');
|
||||
const $item = $(this).closest('.execution-item');
|
||||
console.log('Found item:', $item.length);
|
||||
const executionDataStr = $item.attr('data-execution');
|
||||
console.log('Execution data string:', executionDataStr);
|
||||
if (executionDataStr) {
|
||||
// Unescape HTML entities
|
||||
const unescapedStr = executionDataStr.replace(/'/g, "'");
|
||||
const executionData = JSON.parse(unescapedStr);
|
||||
console.log('Parsed execution data:', executionData);
|
||||
self.confirmCancelExecution(executionData);
|
||||
} else {
|
||||
console.error('No execution data found on element');
|
||||
}
|
||||
});
|
||||
|
||||
// Update cancelled executions display
|
||||
this.displayCancelledExecutions(executions);
|
||||
}
|
||||
|
||||
displayLastExecution(execution) {
|
||||
if (!execution) {
|
||||
this.$lastExecutionDisplay.html('<div class="no-stats-message">No executions yet.</div>');
|
||||
return;
|
||||
}
|
||||
|
||||
const isSuccess = execution.finished_successfully;
|
||||
let html = '<div class="last-execution-container' + (isSuccess ? '' : ' error') + '">';
|
||||
|
||||
html += '<div class="last-execution-icon-container">';
|
||||
html += isSuccess ? '✓' : '✕';
|
||||
html += '</div>';
|
||||
|
||||
html += '<div class="last-execution-body">';
|
||||
html += '<div class="last-execution-status">' + (isSuccess ? 'Succeeded' : 'Failed') + '</div>';
|
||||
html += '<div class="last-execution-name">' + this.escapeHtml(execution.name) + '</div>';
|
||||
html += '</div>';
|
||||
|
||||
html += '<div class="execution-meta">#' + execution.task_id + '</div>';
|
||||
html += '</div>';
|
||||
|
||||
this.$lastExecutionDisplay.html(html);
|
||||
}
|
||||
|
||||
displayCancelledExecutions() {
|
||||
let self = this;
|
||||
const cancelledExecs = self.cancelledExecutions
|
||||
|
||||
if (cancelledExecs.length === 0) {
|
||||
// Hide the cancelled executions section
|
||||
$('.executions-section').eq(2).hide();
|
||||
return;
|
||||
}
|
||||
|
||||
// Show the cancelled executions section
|
||||
$('.executions-section').eq(2).show();
|
||||
|
||||
let html = '<div class="execution-list">';
|
||||
|
||||
cancelledExecs.forEach(function(execution) {
|
||||
const isAbandoned = execution.is_running;
|
||||
|
||||
html += '<div class="execution-item ' + (isAbandoned ? 'abandoned' : 'cancelled') + '">';
|
||||
html += '<div class="execution-icon ' + (isAbandoned ? 'abandoned' : 'cancelled') + '">';
|
||||
html += isAbandoned ? '!' : '✕';
|
||||
html += '</div>';
|
||||
html += '<div class="execution-name">' + self.escapeHtml(execution.name) + '</div>';
|
||||
html += '<div class="execution-meta">' + (isAbandoned ? 'abandoned · ' : '') + '#' + execution.task_id + '</div>';
|
||||
html += '</div>';
|
||||
});
|
||||
|
||||
html += '</div>';
|
||||
this.$cancelledExecutionsDisplay.html(html);
|
||||
}
|
||||
|
||||
confirmCancelExecution(executionData) {
|
||||
console.log('confirmCancelExecution called with:', executionData);
|
||||
this.executionToCancel = executionData;
|
||||
|
||||
if (executionData.is_running) {
|
||||
// Show modal for running executions
|
||||
console.log('Showing modal for running execution');
|
||||
this.$cancelExecutionModal.fadeIn(200);
|
||||
} else {
|
||||
// Directly cancel queued executions
|
||||
console.log('Directly cancelling queued execution');
|
||||
this.cancelExecution(executionData);
|
||||
}
|
||||
}
|
||||
|
||||
confirmCancelExecutionOk() {
|
||||
if (this.executionToCancel) {
|
||||
this.cancelExecution(this.executionToCancel);
|
||||
}
|
||||
this.closeCancelExecutionModal();
|
||||
}
|
||||
|
||||
cancelExecution(executionData) {
|
||||
const self = this;
|
||||
|
||||
console.log('cancelExecution called with full execution data:', executionData);
|
||||
console.log('Attempting to cancel task:', executionData.task_id);
|
||||
|
||||
// Call backend API to cancel the task
|
||||
$.ajax({
|
||||
url: '/cancel_task_execution',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({
|
||||
task_id: executionData.task_id
|
||||
}),
|
||||
success: function(response) {
|
||||
console.log('Cancel task response:', response);
|
||||
|
||||
if (response.status === 'error') {
|
||||
console.error('Backend returned error status:', response.message);
|
||||
alert('Error cancelling task: ' + response.message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.status === 'success') {
|
||||
if (response.was_cancelled) {
|
||||
console.log('Task ' + executionData.task_id + ' was successfully cancelled');
|
||||
// Add to cancelled list (only managed in JS, not persisted)
|
||||
const alreadyCancelled = self.cancelledExecutions.some(function(exec) {
|
||||
return exec.task_id === executionData.task_id;
|
||||
});
|
||||
if (!alreadyCancelled) {
|
||||
console.log('Adding execution to cancelled list:', executionData);
|
||||
self.cancelledExecutions.push(executionData);
|
||||
console.log('Cancelled executions array now contains:', self.cancelledExecutions);
|
||||
} else {
|
||||
console.log('Execution already in cancelled list');
|
||||
}
|
||||
} else {
|
||||
console.log('Task ' + executionData.task_id + ' could not be cancelled (may have already completed). ' + response.message );
|
||||
}
|
||||
// Refresh display regardless
|
||||
self.loadExecutions();
|
||||
} else {
|
||||
console.error('Unexpected response status:', response.status);
|
||||
alert('Unexpected response from server');
|
||||
}
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('AJAX error cancelling task:');
|
||||
console.error(' Status:', status);
|
||||
console.error(' Error:', error);
|
||||
console.error(' XHR:', xhr);
|
||||
console.error(' Response:', xhr.responseText);
|
||||
|
||||
let errorMessage = error;
|
||||
if (xhr.responseJSON && xhr.responseJSON.message) {
|
||||
errorMessage = xhr.responseJSON.message;
|
||||
} else if (xhr.responseText) {
|
||||
errorMessage = xhr.responseText;
|
||||
}
|
||||
|
||||
alert('Error cancelling task: ' + errorMessage);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
closeCancelExecutionModal() {
|
||||
this.$cancelExecutionModal.fadeOut(200);
|
||||
this.executionToCancel = null;
|
||||
}
|
||||
|
||||
escapeHtml(text) {
|
||||
if (typeof text !== 'string') return text;
|
||||
|
||||
const patterns = {
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'&': '&',
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
'`': '`'
|
||||
};
|
||||
|
||||
return text.replace(/[<>&"'`]/g, match => patterns[match]);
|
||||
}
|
||||
|
||||
// ===== Logs Methods =====
|
||||
|
||||
displayLogMessage(message) {
|
||||
@@ -1207,7 +1511,7 @@ class Dashboard {
|
||||
// Reload config to show updated language list
|
||||
self.loadConfigOverview();
|
||||
} else {
|
||||
alert('Error: ' + response.message);
|
||||
alert('Error removing language ' + language + ": " + response.message);
|
||||
}
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
@@ -1290,7 +1594,7 @@ class Dashboard {
|
||||
if (response.status === 'success') {
|
||||
console.log("Language added successfully");
|
||||
} else {
|
||||
alert('Error: ' + response.message);
|
||||
alert('Error adding language ' + selectedLanguage + ": " + response.message);
|
||||
// Restore button visibility on error
|
||||
$('#add-language-btn').show();
|
||||
$('#add-language-spinner').hide();
|
||||
|
||||
@@ -58,6 +58,27 @@
|
||||
<div class="loading">Loading stats...</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="executions-section">
|
||||
<h2>Executions Queue</h2>
|
||||
<div id="active-executions-display">
|
||||
<div class="loading">Loading executions...</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="executions-section">
|
||||
<h2>Last Execution</h2>
|
||||
<div id="last-execution-display">
|
||||
<div class="loading">Loading...</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="executions-section" style="display:none">
|
||||
<h2>Cancelled Executions</h2>
|
||||
<div id="cancelled-executions-display">
|
||||
<div class="no-stats-message">No cancelled executions.</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="overview-right">
|
||||
@@ -255,6 +276,23 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cancel Execution Confirmation Modal -->
|
||||
<div id="cancel-execution-modal" class="modal" style="display:none">
|
||||
<div class="modal-content">
|
||||
<span class="modal-close modal-close-cancel-execution">×</span>
|
||||
<p style="margin: 20px 0; color: var(--text-primary); line-height: 1.5; font-size: 15px;">
|
||||
Are you sure? The execution will continue running until timeout, it will simply no longer be in the queue.
|
||||
Abandoning a running execution is only advised as a measure for unblocking Serena.
|
||||
</p>
|
||||
<div style="display: flex; gap: 10px; justify-content: flex-end; margin-top: 20px;">
|
||||
<button id="cancel-execution-cancel-btn" class="btn"
|
||||
style="background: var(--bg-secondary); color: var(--text-primary); padding: 8px 16px;">Cancel
|
||||
</button>
|
||||
<button id="cancel-execution-ok-btn" class="btn" style="padding: 8px 16px;">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> <!-- End of Main -->
|
||||
|
||||
<script>
|
||||
|
||||
@@ -142,13 +142,14 @@ class TaskExecutor:
|
||||
"""
|
||||
unique identifier of the task
|
||||
"""
|
||||
logged: bool
|
||||
|
||||
def finished_successfully(self) -> bool:
|
||||
return self.future.done() and not self.future.cancelled() and self.future.exception() is None
|
||||
|
||||
@staticmethod
|
||||
def from_task(task: "TaskExecutor.Task", is_running: bool) -> "TaskExecutor.TaskInfo":
|
||||
return TaskExecutor.TaskInfo(name=task.name, is_running=is_running, future=task.future, task_id=id(task))
|
||||
return TaskExecutor.TaskInfo(name=task.name, is_running=is_running, future=task.future, task_id=id(task), logged=task.logged)
|
||||
|
||||
def cancel(self) -> None:
|
||||
self.future.cancel()
|
||||
@@ -215,3 +216,22 @@ class TaskExecutor:
|
||||
"""
|
||||
with self._task_executor_lock:
|
||||
return self._task_executor_last_executed_task_info
|
||||
|
||||
def cancel_task(self, task_id: int) -> bool:
|
||||
"""
|
||||
Cancels a task with the given task ID and removes it from the queue if found.
|
||||
|
||||
:param task_id: the unique identifier of the task to cancel
|
||||
:return: True if the task was found and cancelled, False otherwise
|
||||
"""
|
||||
with self._task_executor_lock:
|
||||
if self._task_executor_current_task is not None and id(self._task_executor_current_task) == task_id:
|
||||
self._task_executor_current_task.cancel()
|
||||
self._task_executor_current_task = None
|
||||
return True
|
||||
for task in self._task_executor_queue:
|
||||
if id(task) == task_id:
|
||||
task.cancel()
|
||||
self._task_executor_queue.remove(task)
|
||||
return True
|
||||
raise KeyError(f"Task with ID {task_id} not found in the queue. {self._task_executor_queue=}, {self._task_executor_current_task=}")
|
||||
Reference in new issue
Block a user