diff --git a/static/panels.js b/static/panels.js
index 8928c77d8..240d66543 100644
--- a/static/panels.js
+++ b/static/panels.js
@@ -3159,7 +3159,7 @@ function loadTodos() {
if (!todos.length) {
if (typeof _todosLastRenderedHash !== 'undefined' && _todosLastRenderedHash === '__empty__') return;
- panel.innerHTML = `
${esc(t('todos_no_active'))}
`;
+ panel.innerHTML = renderTodoEmptyState();
if (typeof _todosLastRenderedHash !== 'undefined') _todosLastRenderedHash = '__empty__';
return;
}
@@ -3170,18 +3170,9 @@ function loadTodos() {
_todosLastRenderedHash = hash;
}
- const statusIcon = {pending:li('square',14), in_progress:li('loader',14), completed:li('check',14), cancelled:li('x',14)};
- const statusColor = {pending:'var(--muted)', in_progress:'var(--blue)', completed:'rgba(100,200,100,.8)', cancelled:'rgba(200,100,100,.5)'};
// Single innerHTML join is the cheapest correct way to materialize
// ~10–50 leaf nodes. All user-controlled content goes through esc().
- panel.innerHTML = todos.map(td => `
-
- ${statusIcon[td.status]||li('square',14)}
-
-
${esc(td.content)}
-
${esc(td.id)} · ${esc(td.status)}
-
-
`).join('');
+ panel.innerHTML = renderTodoRows(todos, {metadata:true});
}
// Legacy fallback: reverse-scan settled tool messages for the most
diff --git a/static/ui.js b/static/ui.js
index b3bb0fe2d..7060c2bf5 100644
--- a/static/ui.js
+++ b/static/ui.js
@@ -7215,7 +7215,7 @@ function clearInflightState(sid){
// `S.session = ...` settle point so cross-session navigation never
// leaves a stale list visible.
//
-// The hash is keyed on (id, content, status); the render itself uses
+// The hash is keyed on (id, content/text, status); the render itself uses
// `esc()` for any user-controlled string, so XSS surface is the same as
// any other innerHTML path in this file.
let _todosLastRenderedHash=null;
@@ -7232,11 +7232,73 @@ function _todosHash(items){
let h=items.length+'|';
for(let i=0;i${esc(t('todos_no_active'))}`;
+}
+
+function renderTodoRow(todo,options={}){
+ const td=todo||{};
+ const status=todoStatusKey(td.status);
+ const visual=todoStatusVisual(status);
+ const showMetadata=!(options&&options.metadata===false);
+ const isCompleted=status==='completed';
+ const contentColor=isCompleted?'var(--muted)':'var(--text)';
+ const completedStyle=isCompleted?'text-decoration:line-through;opacity:.5':'';
+ const metadata=showMetadata
+ ? `