feat: display OpenCode todos with status on builder page

- Capture todowrite tool events and store todos on messages
- Add API endpoint GET /api/sessions/:sessionId/todos
- Clear todos on message finish, undo, and redo
- Create renderStructuredTodos function with status icons
- Integrate todo display into message rendering
- Add CSS styling for todo items by status and priority
This commit is contained in:
southseact-3d
2026-02-08 14:00:29 +00:00
parent 638f9ae5d2
commit 9ef54cf6ee
3 changed files with 371 additions and 2 deletions

View File

@@ -8807,6 +8807,36 @@ async function sendToOpencode({ session, model, content, message, cli, streamCal
}
}
// Capture todo tool events
if (event.type === 'tool' && event.tool === 'todowrite' && event.input?.todos) {
const todos = event.input.todos;
if (message) {
message.todos = todos;
log('Captured todos from todowrite tool', {
messageId: messageKey,
todoCount: todos.length,
todos: todos.map(t => ({ id: t.id, content: t.content?.substring(0, 50), status: t.status }))
});
// Broadcast todos to SSE clients
if (messageKey && activeStreams.has(messageKey)) {
const streams = activeStreams.get(messageKey);
const data = JSON.stringify({
type: 'todos',
todos: todos,
timestamp: new Date().toISOString()
});
streams.forEach(res => {
try {
res.write(`data: ${data}\n\n`);
} catch (err) {
log('SSE todos write error', { err: String(err) });
}
});
}
}
}
// Extract text from text events
if (event.type === 'text' && event.part?.text) {
partialOutput += event.part.text;
@@ -14622,6 +14652,29 @@ async function handleListSessions(req, res, userId) {
async function handleGetSession(_req, res, sessionId, userId) { const session = getSession(sessionId, userId); if (!session) return sendJson(res, 404, { error: 'Session not found' }); sendJson(res, 200, { session: serializeSession(session) }); }
async function handleGetSessionTodos(_req, res, sessionId, userId) {
const session = getSession(sessionId, userId);
if (!session) return sendJson(res, 404, { error: 'Session not found' });
// Get all todos from messages in the session
const allTodos = [];
if (session.messages) {
for (const msg of session.messages) {
if (msg.todos && Array.isArray(msg.todos)) {
// Add message ID to each todo for reference
const todosWithMeta = msg.todos.map(todo => ({
...todo,
messageId: msg.id,
messageStatus: msg.status
}));
allTodos.push(...todosWithMeta);
}
}
}
sendJson(res, 200, { todos: allTodos });
}
async function handleNewMessage(req, res, sessionId, userId) {
const session = getSession(sessionId, userId);
if (!session) return sendJson(res, 404, { error: 'Session not found' });
@@ -15112,6 +15165,13 @@ async function handleUndoMessage(req, res, sessionId, messageId, userId) {
timeout: 30000
});
// Clear todos from the message when undone
if (message.todos) {
const todoCount = message.todos.length;
message.todos = [];
log('Cleared todos from undone message', { sessionId, messageId, clearedCount: todoCount });
}
log('Undo command completed', { sessionId, messageId });
sendJson(res, 200, { ok: true, message: 'Undo command sent successfully' });
} catch (error) {
@@ -15148,6 +15208,13 @@ async function handleRedoMessage(req, res, sessionId, messageId, userId) {
timeout: 30000
});
// Clear todos from the message when redone (they will be regenerated during the new execution)
if (message.todos) {
const todoCount = message.todos.length;
message.todos = [];
log('Cleared todos from redone message', { sessionId, messageId, clearedCount: todoCount });
}
log('Redo command completed', { sessionId, messageId });
sendJson(res, 200, { ok: true, message: 'Redo command sent successfully' });
} catch (error) {
@@ -16320,6 +16387,15 @@ async function routeInternal(req, res, url, pathname) {
if (!userId) return;
return handleGetSession(req, res, sessionMatch[1], userId);
}
// GET todos for a session
const todosMatch = pathname.match(/^\/api\/sessions\/([a-f0-9\-]+)\/todos$/i);
if (req.method === 'GET' && todosMatch) {
const userId = requireUserId(req, res, url);
if (!userId) return;
return handleGetSessionTodos(req, res, todosMatch[1], userId);
}
const messageMatch = pathname.match(/^\/api\/sessions\/([a-f0-9\-]+)\/messages$/i);
if (req.method === 'POST' && messageMatch) {
const userId = requireUserId(req, res, url);