From 8e9f2dec8ec118229af5a8a3cf4cf02b825c74d8 Mon Sep 17 00:00:00 2001 From: southseact-3d Date: Tue, 10 Feb 2026 11:53:31 +0000 Subject: [PATCH] added support for todos --- chat/public/builder.js | 6 ++ chat/server.js | 124 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+) diff --git a/chat/public/builder.js b/chat/public/builder.js index a27a840..4e18ddb 100644 --- a/chat/public/builder.js +++ b/chat/public/builder.js @@ -3094,6 +3094,9 @@ function streamMessage(sessionId, messageId) { // Clear todos when message completes (to start fresh for next message) clearTodos(); + if (message.todos && Array.isArray(message.todos)) { + message.todos = []; + } renderMessages(session); setStatus('Complete'); @@ -3139,6 +3142,9 @@ function streamMessage(sessionId, messageId) { // Clear todos on error clearTodos(); + if (message.todos && Array.isArray(message.todos)) { + message.todos = []; + } renderMessages(session); scrollChatToBottom(); diff --git a/chat/server.js b/chat/server.js index e40e85c..2d4151f 100644 --- a/chat/server.js +++ b/chat/server.js @@ -105,6 +105,10 @@ const WORKSPACES_ROOT = path.join(DATA_ROOT, 'apps'); const STATIC_ROOT = path.join(__dirname, 'public'); const UPLOADS_DIR = path.join(STATE_DIR, 'uploads'); const REPO_ROOT = process.env.CHAT_REPO_ROOT || process.cwd(); +const OPENCODE_REPO_ROOT = path.join(REPO_ROOT, 'opencode'); +const OPENCODE_REPO_CLI = path.join(OPENCODE_REPO_ROOT, 'packages', 'opencode', 'bin', 'opencode'); +const OPENCODE_PROMPT_DIR = path.join(OPENCODE_REPO_ROOT, 'packages', 'opencode', 'src', 'session', 'prompt'); +const OPENCODE_REQUIRE_REPO = process.env.OPENCODE_REQUIRE_REPO !== 'false'; const DEFAULT_OPENROUTER_API_URL = 'https://openrouter.ai/api/v1/chat/completions'; const OPENROUTER_API_URL = process.env.OPENROUTER_API_URL || DEFAULT_OPENROUTER_API_URL; const OPENROUTER_API_KEY = process.env.OPENROUTER_API_KEY || process.env.OPENROUTER_API_TOKEN || ''; @@ -5279,6 +5283,7 @@ function resolveCliCommand(cli) { const normalized = normalizeCli(cli); const binDir = process.env.OPENCODE_BIN_DIR || '/root/.opencode/bin'; const candidates = []; + if (OPENCODE_REPO_CLI) candidates.push(OPENCODE_REPO_CLI); if (binDir) candidates.push(path.join(binDir, normalized)); candidates.push(`/usr/local/bin/${normalized}`); candidates.push(normalized); @@ -5292,6 +5297,85 @@ function resolveCliCommand(cli) { return normalized; } +const opencodeVerificationCache = { + checked: false, + ok: false, + error: null, + cliPath: null, +}; + +function verifyOpencodePrompts() { + const promptFiles = [ + 'codex_header.txt', + 'beast.txt', + 'gemini.txt', + 'anthropic.txt', + 'qwen.txt', + 'trinity.txt', + 'wordpress-plugin.txt', + 'wordpress-plugin-subsequent.txt' + ]; + const missing = []; + const nonWordPress = []; + + for (const file of promptFiles) { + const fullPath = path.join(OPENCODE_PROMPT_DIR, file); + if (!fsSync.existsSync(fullPath)) { + missing.push(fullPath); + continue; + } + const content = fsSync.readFileSync(fullPath, 'utf8'); + if (!/wordpress/i.test(content)) { + nonWordPress.push(fullPath); + } + } + + if (missing.length) { + throw new Error(`OpenCode prompt verification failed: missing prompt files: ${missing.join(', ')}`); + } + if (nonWordPress.length) { + throw new Error(`OpenCode prompt verification failed: prompts are not WordPress-specific: ${nonWordPress.join(', ')}`); + } +} + +function verifyOpencodeCli(cliCommand) { + if (!OPENCODE_REQUIRE_REPO) return; + const repoRootResolved = fsSync.existsSync(OPENCODE_REPO_ROOT) + ? fsSync.realpathSync(OPENCODE_REPO_ROOT) + : null; + if (!repoRootResolved) { + throw new Error('OpenCode repo root not found; cannot verify CLI build source.'); + } + let cliResolved = null; + try { + cliResolved = fsSync.realpathSync(cliCommand); + } catch (_) { + cliResolved = null; + } + if (!cliResolved || !cliResolved.startsWith(repoRootResolved)) { + throw new Error(`OpenCode CLI is not using the repo build. Expected CLI under ${repoRootResolved} but got ${cliCommand}.`); + } +} + +function verifyOpencodeSetup(cliCommand) { + if (opencodeVerificationCache.checked) { + if (opencodeVerificationCache.error) throw opencodeVerificationCache.error; + return; + } + try { + verifyOpencodeCli(cliCommand); + verifyOpencodePrompts(); + opencodeVerificationCache.checked = true; + opencodeVerificationCache.ok = true; + opencodeVerificationCache.cliPath = cliCommand; + } catch (err) { + opencodeVerificationCache.checked = true; + opencodeVerificationCache.ok = false; + opencodeVerificationCache.error = err; + throw err; + } +} + async function ensureStateFile() { try { await fs.mkdir(STATE_DIR, { recursive: true }); @@ -8898,6 +8982,7 @@ async function sendToOpencode({ session, model, content, message, cli, streamCal const workspaceDir = session.workspaceDir; const cliName = normalizeCli(cli || session?.cli); const cliCommand = resolveCliCommand(cliName); + verifyOpencodeSetup(cliCommand); // Ensure model is properly resolved const resolvedModel = model || session.model; @@ -9117,6 +9202,36 @@ async function sendToOpencode({ session, model, content, message, cli, streamCal } }); } + persistState().catch(() => { }); + } + } + + if (event.type === 'todo.updated' && event.properties?.todos) { + const todos = event.properties.todos; + if (message) { + message.todos = todos; + log('Captured todos from todo.updated event', { + messageId: messageKey, + todoCount: todos.length, + todos: todos.map(t => ({ id: t.id, content: t.content?.substring(0, 50), status: t.status })) + }); + + 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) }); + } + }); + } + persistState().catch(() => { }); } } @@ -9213,6 +9328,9 @@ async function sendToOpencode({ session, model, content, message, cli, streamCal if (session) { session.updatedAt = message.finishedAt; } + if (message.todos && Array.isArray(message.todos) && message.todos.length) { + message.todos = []; + } } // The reply is the final accumulated output @@ -10590,6 +10708,9 @@ async function processMessage(sessionId, message) { message.status = 'done'; message.reply = reply; + if (message.todos && Array.isArray(message.todos) && message.todos.length) { + message.todos = []; + } message.finishedAt = new Date().toISOString(); session.updatedAt = message.finishedAt; session.cli = activeCli; @@ -10599,6 +10720,9 @@ async function processMessage(sessionId, message) { } catch (error) { // Provide helpful and parseable error details in the message message.status = 'error'; + if (message.todos && Array.isArray(message.todos) && message.todos.length) { + message.todos = []; + } const details = []; if (error.code) details.push(`code: ${error.code}`); if (error.stderr) details.push(`stderr: ${error.stderr.trim()}`);