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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user