fix: push agent state to all WS connections and sync sidebar in background tabs

- agent.py: add finally block to _process_chain that calls mark_dirty_all()
  so non-active tabs see running:false without manual refresh
- message-queue-store.js: deduplicate allItems by id to prevent Alpine
  'Duplicate key' errors during server-poll vs pending-item race
- chats-list.html: add data-chat-id attribute for DOM-based targeting
- index.js: move chatsStore/tasksStore updates before context-equality
  check so background state changes always reach the sidebar
This commit is contained in:
Testing 2026-05-14 00:16:01 +00:00
parent 1e271be83e
commit 3bdbdaa80c
4 changed files with 21 additions and 10 deletions

View file

@ -300,6 +300,11 @@ class AgentContext:
return response
except Exception as e:
await self.handle_exception("process_chain", e)
finally:
# Push updated running state to ALL connections when agent finishes.
# Without this, non-active tabs don't see running:false until they click.
from helpers.state_monitor_integration import mark_dirty_all
mark_dirty_all(reason="agent.AgentContext._process_chain completed")
@extension.extensible
async def handle_exception(self, location: str, exception: Exception):

View file

@ -47,8 +47,15 @@ const model = {
},
// Combined items for display: confirmed first, then pending at the end
// Deduplicate by id to prevent Alpine "Duplicate key" errors during
// the race between server poll updates and pending item cleanup.
get allItems() {
return [...this.items, ...this.pendingItems];
const seen = new Set();
return [...this.items, ...this.pendingItems].filter((item) => {
if (seen.has(item.id)) return false;
seen.add(item.id);
return true;
});
},
async addToQueue(text, attachments = []) {

View file

@ -24,7 +24,7 @@
<ul class="config-list chats-config-list no-scrollbar" x-show="$store.chats.contexts.length > 0">
<template x-for="context in $store.chats.contexts" :key="context.id">
<li>
<li :data-chat-id="context.id">
<div :class="{'chat-container': true, 'chat-selected': context.id === $store.chats.selected}"
@click="$store.chats.selectChat(context.id)">
<div class="chat-list-button">

View file

@ -314,6 +314,13 @@ export async function applySnapshot(snapshot, options = {}) {
return { updated: false };
}
// Update chats/tasks list BEFORE context check so background chat
// state changes (running, etc.) are always reflected in the sidebar.
let contexts = snapshot.contexts || [];
chatsStore.applyContexts(contexts);
let tasks = snapshot.tasks || [];
tasksStore.applyTasks(tasks);
if (
snapshot.context != context &&
context !== null
@ -370,14 +377,6 @@ export async function applySnapshot(snapshot, options = {}) {
setConnectionStatus(true);
}
// Update chats list using store
let contexts = snapshot.contexts || [];
chatsStore.applyContexts(contexts);
// Update tasks list using store
let tasks = snapshot.tasks || [];
tasksStore.applyTasks(tasks);
// Make sure the active context is properly selected in both lists
if (context) {
// Update selection in both stores