(run: RunInfo, stream: 'stdout' | 'stderr', chunk: string)
| 223 | } |
| 224 | |
| 225 | function appendLog(run: RunInfo, stream: 'stdout' | 'stderr', chunk: string) { |
| 226 | const prefix = stream === 'stderr' ? '[stderr] ' : '' |
| 227 | const text = prefix + chunk |
| 228 | run.log += text |
| 229 | // cap memory usage (keep last ~200KB) |
| 230 | if (run.log.length > 200_000) { |
| 231 | run.log = run.log.slice(run.log.length - 200_000) |
| 232 | } |
| 233 | for (const sub of run.subscribers) { |
| 234 | try { |
| 235 | sseWrite(sub, 'log', { id: run.id, stream, chunk }) |
| 236 | } catch { |
| 237 | // ignore broken pipes; cleanup happens on 'close' |
| 238 | } |
| 239 | } |
| 240 | |
| 241 | // UI input requests: detect sentinel lines emitted by the agent and broadcast as SSE. |
| 242 | // Format: [ui-input-request]{...json...} |
| 243 | try { |
| 244 | const parts = text.split(/\r?\n/) |
| 245 | for (const line of parts) { |
| 246 | const m = line.match(/^\[ui-input-request\](\{.*\})\s*$/) |
| 247 | if (!m) continue |
| 248 | const payload = JSON.parse(m[1]) |
| 249 | for (const sub of run.subscribers) { |
| 250 | try { |
| 251 | sseWrite(sub, 'input_request', payload) |
| 252 | } catch { |
| 253 | // ignore |
| 254 | } |
| 255 | } |
| 256 | } |
| 257 | } catch { |
| 258 | // ignore parse errors |
| 259 | } |
| 260 | |
| 261 | // Discover dashboard port from run output (printed by scripts/run_agent.sh). |
| 262 | // We proxy it through Vite so it works via port-forwarding / remote dev envs. |
| 263 | if (!run.dashboardPort) { |
| 264 | const m = text.match(DASHBOARD_PORT_RE) |
| 265 | const port = m?.[1] ? Number(m[1]) : NaN |
| 266 | if (Number.isFinite(port) && port > 0) { |
| 267 | run.dashboardPort = port |
| 268 | writeRunMeta(getProjectRoot(), run) |
| 269 | broadcastStatus(run) |
| 270 | } |
| 271 | } |
| 272 | } |
| 273 | |
| 274 | function broadcastStatus(run: RunInfo) { |
| 275 | const payload = { |
no test coverage detected