feat(#1290): show per-agent loose-end question count in dashboard

Add a ' N' badge to each container row showing the count of pending
questions where the agent is the asker (awaiting an answer) or the
target (owes a reply). Derived live from questionsState — no backend
field needed.

Details:
- New .badge-loose-ends CSS class (purple, matching Q33R1ES section colour)
- Badge tooltip breaks down the count: 'N asked, N to answer'
- renderContainersFromState() called on QuestionAdded/QuestionResolved
  so the badge updates instantly when question state changes
- Existing  reminders badge kept separate (different signal)
This commit is contained in:
iris 2026-06-04 18:55:16 +02:00 committed by mara
commit 750f64b7f0
2 changed files with 26 additions and 0 deletions

View file

@ -78,6 +78,10 @@ code {
color: var(--cyan); border-color: var(--cyan);
text-shadow: 0 0 6px rgba(137, 220, 235, 0.4);
}
.badge-loose-ends {
color: var(--purple); border-color: var(--purple);
text-shadow: 0 0 6px rgba(203, 166, 247, 0.4);
}
/* Context-window usage badges on dashboard container rows. */
.badge-ctx-ok {
color: var(--green); border-color: var(--green);

View file

@ -805,6 +805,26 @@ window.marked = marked;
},
`${c.pending_reminders}`));
}
// Pending questions where this agent is the asker (awaiting an
// answer) or the target (owes a reply). Derived live from
// questionsState so the badge updates instantly on QuestionAdded /
// QuestionResolved without a separate backend field.
const agentQCount = questionsState.pending.filter(
(q) => q.asker === c.name || q.target === c.name,
).length;
if (agentQCount > 0) {
const askerCount = questionsState.pending.filter((q) => q.asker === c.name).length;
const targetCount = questionsState.pending.filter((q) => q.target === c.name).length;
const parts = [];
if (askerCount > 0) parts.push(`${askerCount} asked`);
if (targetCount > 0) parts.push(`${targetCount} to answer`);
head.append(el('span',
{
class: 'badge badge-loose-ends',
title: `pending questions: ${parts.join(', ')} — see the Q33R1ES tab`,
},
`${agentQCount}`));
}
if (c.ctx_tokens != null) {
const k = Math.round(c.ctx_tokens / 1000);
// Thresholds track the model's real context window when the
@ -1459,6 +1479,7 @@ window.marked = marked;
question_refs: ev.question_refs || [],
});
renderQuestions();
renderContainersFromState();
}
function applyQuestionResolved(ev) {
const idx = questionsState.pending.findIndex((q) => q.id === ev.id);
@ -1488,6 +1509,7 @@ window.marked = marked;
}
}
renderQuestions();
renderContainersFromState();
}
// Filter selection for the questions section. Persisted so the
// operator's preferred view (all / operator-targeted / peer)