chore: dashboard task view update

This commit is contained in:
larchanka
2026-03-23 19:29:11 +01:00
parent b75b630d6a
commit 6076126ecb
6 changed files with 154 additions and 36 deletions

View File

@@ -383,7 +383,7 @@ export class ExecutorAgent extends BaseProcess {
}
}
this.sendTaskComplete(taskId);
this.sendTaskFinalize(taskId);
this.sendResponse(request, { taskId, result: aggregated });
}
@@ -949,15 +949,15 @@ export class ExecutorAgent extends BaseProcess {
});
}
private sendTaskComplete(taskId: string): void {
private sendTaskFinalize(taskId: string): void {
this.send({
id: randomUUID(),
timestamp: Date.now(),
from: PROCESS_NAME,
to: "task-memory",
type: "task.complete",
type: "task.updateStatus",
version: PROTOCOL_VERSION,
payload: { taskId },
payload: { taskId, status: "finalizing" },
});
}

View File

@@ -480,6 +480,16 @@ export class Orchestrator {
if (isRetry) {
this.sendToTelegram(chatId, "⏳ Re-planning with error feedback...", true);
} else {
// Create initial 'planning' task in memory
this.sendAndWait(taskMemory, "task.create", {
taskId,
userId: String(userId),
conversationId: conversationId ?? String(chatId),
goal,
status: "planning",
nodes: [],
edges: []
}).catch(() => { });
this.sendToTelegram(chatId, "🪏 Planning started...", true);
}
@@ -527,18 +537,15 @@ export class Orchestrator {
previousPlan = plan;
const nodes = plan.nodes as Array<{ id: string; type: string; service: string; input?: unknown }>;
const edges = (plan.edges ?? []) as Array<{ from: string; to: string }>;
const taskCreatePayload = {
// Update task DAG in memory
this.sendAndWait(taskMemory, "task.updateDag", {
taskId,
userId: String(userId),
conversationId: conversationId ?? String(chatId),
goal,
complexity: plan.complexity,
nodes: nodes.map((n) => ({ id: n.id, type: n.type, service: n.service, input: n.input })),
edges: edges
.filter((e) => e && typeof e === "object" && e.from && e.to)
.map((e) => ({ fromNode: e.from, toNode: e.to })),
};
this.sendAndWait(taskMemory, "task.create", taskCreatePayload).catch(() => { });
}).catch(() => { });
this.sendToTelegram(chatId, "💨 Planning complete. Execution started...", true);
let execEnv: Envelope;
@@ -598,6 +605,8 @@ export class Orchestrator {
}
}
this.sendToTelegram(chatId, text, false, "HTML");
// Mark task as completed after message is sent
this.sendAndWait(taskMemory, "task.complete", { taskId }).catch(() => { });
return;
}

View File

@@ -225,8 +225,8 @@ export class DashboardService extends BaseProcess {
SELECT t.id, t.goal, t.status, t.complexity, t.updated_at,
(SELECT json_group_array(json_object('id', id, 'type', type, 'status', status, 'input', input)) FROM task_nodes WHERE task_id = t.id ORDER BY started_at ASC) as nodes,
(SELECT json_group_array(json_object('from', from_node, 'to', to_node)) FROM task_edges WHERE task_id = t.id) as edges
FROM tasks t WHERE t.status IN (?, ?) ORDER BY t.updated_at DESC
`).all('pending', 'running').map((t: any) => ({
FROM tasks t WHERE t.status IN (?, ?, ?, ?) ORDER BY t.updated_at DESC
`).all('planning', 'pending', 'running', 'finalizing').map((t: any) => ({
...t,
nodes: JSON.parse(t.nodes || '[]'),
edges: JSON.parse(t.edges || '[]')
@@ -263,7 +263,7 @@ export class DashboardService extends BaseProcess {
if (!total) return `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}"><text x="50%" y="50%" text-anchor="middle" fill="#8b8b8b">No Data</text></svg>`;
const radius = 75, circumference = 2 * Math.PI * radius;
let offset = 0;
const colors: any = { completed: '#0b6e4f', failed: '#df2a5f', pending: '#d9730d', running: '#2383e2' };
const colors: any = { completed: '#0b6e4f', failed: '#df2a5f', pending: '#d9730d', running: '#2383e2', planning: '#8b8b8b', finalizing: '#9333ea' };
const slices = Object.entries(data).map(([k, v]: [string, any]) => {
const p = v / total, dash = (p * circumference) + ' ' + circumference, currentOffset = -offset;
offset += p * circumference;

View File

@@ -236,8 +236,11 @@ function updateDashboard() {
if (d.pendingTasks && d.pendingTasks.length > 0) {
qs.style.display = "block";
qt.innerHTML = d.pendingTasks.map(t => {
const isRunning = t.status === 'running';
const tc = isRunning ? "running" : "warning";
const isRunning = t.status === 'running' || t.status === 'finalizing';
let tc = "warning";
if (t.status === 'running') tc = "running";
else if (t.status === 'planning') tc = "planning";
else if (t.status === 'finalizing') tc = "finalizing";
const indicator = isRunning ? '<div class="pulse"></div>' : '';
return `<tr>
<td style="padding-left: 20px; color: var(--text-muted); white-space: nowrap; vertical-align: top; padding-top: 15px; border-bottom: none;">${fmtDate(t.updated_at)}</td>

View File

@@ -8,6 +8,24 @@
--success: #0b6e4f;
--error: #df2a5f;
--warning: #d9730d;
--purple: #9333ea;
--primary-bg: rgba(35, 131, 226, 0.1);
--success-bg: rgba(11, 110, 79, 0.1);
--error-bg: rgba(223, 42, 95, 0.1);
--warning-bg: rgba(217, 115, 13, 0.1);
--planning-bg: rgba(139, 139, 139, 0.1);
--finalizing-bg: rgba(147, 51, 234, 0.1);
--stage-bg: rgba(0, 0, 0, 0.02);
--chip-running-bg: rgba(35, 131, 226, 0.05);
--chip-completed-bg: rgba(11, 110, 79, 0.05);
--chip-failed-bg: rgba(223, 42, 95, 0.05);
--tooltip-bg: #1e1e1e;
--tooltip-text: #ffffff;
--tooltip-shadow: rgba(0, 0, 0, 0.2);
--tooltip-border: rgba(255, 255, 255, 0.1);
}
@media (prefers-color-scheme: dark) {
@@ -21,6 +39,24 @@
--success: #529e72;
--error: #ff4d4d;
--warning: #ffdc4d;
--purple: #bf7fff;
--primary-bg: rgba(46, 167, 255, 0.15);
--success-bg: rgba(82, 158, 114, 0.15);
--error-bg: rgba(255, 77, 77, 0.15);
--warning-bg: rgba(255, 220, 77, 0.15);
--planning-bg: rgba(255, 255, 255, 0.05);
--finalizing-bg: rgba(191, 127, 255, 0.15);
--stage-bg: rgba(255, 255, 255, 0.03);
--chip-running-bg: rgba(46, 167, 255, 0.08);
--chip-completed-bg: rgba(82, 158, 114, 0.08);
--chip-failed-bg: rgba(255, 77, 77, 0.08);
--tooltip-bg: #2a2a2a;
--tooltip-text: #e0e0e0;
--tooltip-shadow: rgba(0, 0, 0, 0.4);
--tooltip-border: rgba(255, 255, 255, 0.1);
}
}
@@ -241,20 +277,30 @@
font-weight: 500;
}
.tag.success { background: rgba(11, 110, 79, 0.1); color: var(--success); }
.tag.error { background: rgba(223, 42, 95, 0.1); color: var(--error); }
.tag.warning { background: rgba(217, 115, 13, 0.1); color: var(--warning); }
.tag.success { background: var(--success-bg); color: var(--success); }
.tag.error { background: var(--error-bg); color: var(--error); }
.tag.warning { background: var(--warning-bg); color: var(--warning); }
.tag.running {
background: rgba(35, 131, 226, 0.1);
background: var(--primary-bg);
color: var(--primary);
display: inline-flex;
align-items: center;
gap: 4px;
}
.tag.planning { background: var(--planning-bg); color: var(--text-muted); padding: 2px 8px; border-radius: 4px; }
.tag.finalizing {
background: var(--finalizing-bg);
color: var(--purple);
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
border-radius: 4px;
}
.tag.complexity-small { background: var(--subtle); color: var(--text-muted); border: 1px solid var(--border); }
.tag.complexity-medium { background: rgba(35, 131, 226, 0.1); color: var(--primary); }
.tag.complexity-large { background: rgba(147, 51, 234, 0.1); color: #9333ea; }
.tag.complexity-medium { background: var(--primary-bg); color: var(--primary); }
.tag.complexity-large { background: var(--finalizing-bg); color: var(--purple); }
.pulse {
width: 6px;
@@ -285,7 +331,7 @@
padding: 4px;
border-radius: 6px;
border: 1px dashed var(--border);
background: rgba(0,0,0,0.02);
background: var(--stage-bg);
}
.node-chip {
font-size: 11px;
@@ -302,18 +348,18 @@
.node-chip.running {
border-color: var(--primary);
color: var(--primary);
background: rgba(35, 131, 226, 0.05);
background: var(--chip-running-bg);
font-weight: 600;
}
.node-chip.completed {
border-color: var(--success);
color: var(--success);
background: rgba(11, 110, 79, 0.05);
background: var(--chip-completed-bg);
}
.node-chip.failed {
border-color: var(--error);
color: var(--error);
background: rgba(223, 42, 95, 0.05);
background: var(--chip-failed-bg);
}
.goal-text {
@@ -396,15 +442,15 @@
left: 50%;
transform: translateX(-50%);
padding: 6px 10px;
background: #1e1e1e;
color: #fff;
background: var(--tooltip-bg);
color: var(--tooltip-text);
font-size: 11px;
font-weight: 500;
border-radius: 6px;
white-space: nowrap;
z-index: 1000;
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
border: 1px solid rgba(255,255,255,0.1);
box-shadow: 0 4px 12px var(--tooltip-shadow);
border: 1px solid var(--tooltip-border);
pointer-events: none;
}
@@ -416,7 +462,7 @@
transform: translateX(-50%);
border-width: 5px;
border-style: solid;
border-color: #1e1e1e transparent transparent transparent;
border-color: var(--tooltip-bg) transparent transparent transparent;
z-index: 1001;
pointer-events: none;
}

View File

@@ -22,7 +22,7 @@ CREATE TABLE IF NOT EXISTS tasks (
user_id TEXT,
conversation_id TEXT,
goal TEXT NOT NULL,
status TEXT CHECK(status IN ('pending','running','completed','failed')),
status TEXT CHECK(status IN ('planning','running','finalizing','completed','failed','pending')),
complexity TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
@@ -92,6 +92,7 @@ interface TaskCreatePayload {
conversationId?: string;
goal: string;
complexity?: string;
status?: "planning" | "pending" | "running" | "finalizing" | "completed" | "failed";
nodes: Array<{ id: string; type: string; service: string; input?: unknown }>;
edges: Array<{ fromNode: string; toNode: string }>;
}
@@ -129,6 +130,11 @@ interface TaskGetByConversationIdPayload {
conversationId: string;
}
interface TaskUpdateStatusPayload {
taskId: string;
status: "planning" | "pending" | "running" | "finalizing" | "completed" | "failed";
}
// --- Store ---
export class TaskMemoryStore {
@@ -179,13 +185,14 @@ export class TaskMemoryStore {
this.db
.prepare(
`INSERT INTO tasks (id, user_id, conversation_id, goal, status, complexity, created_at, updated_at, metadata)
VALUES (?, ?, ?, ?, 'pending', ?, ?, ?, ?)`,
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
)
.run(
payload.taskId,
payload.userId ?? null,
payload.conversationId ?? null,
payload.goal,
payload.status || 'pending',
payload.complexity ?? null,
now,
now,
@@ -243,9 +250,9 @@ export class TaskMemoryStore {
)
.run(status, output, startedAt, completedAt, taskId, nodeId);
// Also update task status to 'running' if it was 'pending'
// Also update task status to 'running' if it was 'pending' or 'planning'
this.db
.prepare(`UPDATE tasks SET status = 'running', updated_at = ? WHERE id = ? AND status = 'pending'`)
.prepare(`UPDATE tasks SET status = 'running', updated_at = ? WHERE id = ? AND status IN ('pending', 'planning')`)
.run(now, taskId);
// Always touch updated_at even if status didn't change (e.g. from running to running with more nodes)
@@ -291,6 +298,45 @@ export class TaskMemoryStore {
.run(now, reason ? this.json({ reason }) : "", taskId);
}
updateTaskStatus(taskId: string, status: string): void {
const now = this.now();
this.db
.prepare(`UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?`)
.run(status, now, taskId);
}
updateTaskDag(taskId: string, nodes: TaskCreatePayload['nodes'], edges: TaskCreatePayload['edges']): void {
const now = this.now();
// Clear existing nodes and edges if any
this.db.prepare(`DELETE FROM task_edges WHERE task_id = ?`).run(taskId);
this.db.prepare(`DELETE FROM task_nodes WHERE task_id = ?`).run(taskId);
const insertNode = this.db.prepare(
`INSERT INTO task_nodes (task_id, id, type, service, status, input)
VALUES (?, ?, ?, ?, 'pending', ?)`,
);
for (const n of nodes) {
insertNode.run(
taskId,
n.id,
n.type,
n.service,
this.json(n.input),
);
}
const insertEdge = this.db.prepare(
`INSERT INTO task_edges (id, task_id, from_node, to_node)
VALUES (?, ?, ?, ?)`,
);
for (const e of edges) {
insertEdge.run(randomUUID(), taskId, e.fromNode, e.toNode);
}
this.db.prepare(`UPDATE tasks SET updated_at = ? WHERE id = ?`).run(now, taskId);
}
getTask(taskId: string): unknown {
const task = this.db.prepare(`SELECT * FROM tasks WHERE id = ?`).get(taskId) as Record<string, unknown> | undefined;
if (!task) return null;
@@ -339,7 +385,9 @@ function isTaskMessage(type: string): boolean {
type === "task.getByConversationId" ||
type === "task.appendReflection" ||
type === "task.complete" ||
type === "task.fail"
type === "task.fail" ||
type === "task.updateStatus" ||
type === "task.updateDag"
);
}
@@ -409,6 +457,18 @@ export class TaskMemoryService extends BaseProcess {
result = { taskId: p.taskId };
break;
}
case "task.updateStatus": {
const p = payload as unknown as TaskUpdateStatusPayload;
this.store.updateTaskStatus(p.taskId, p.status);
result = { taskId: p.taskId, status: p.status };
break;
}
case "task.updateDag": {
const p = payload as unknown as TaskCreatePayload;
this.store.updateTaskDag(p.taskId, p.nodes, p.edges);
result = { taskId: p.taskId };
break;
}
default:
this.sendError(envelope, "UNKNOWN_TYPE", `Unknown type: ${envelope.type}`);
return;