(args: z.infer<typeof LiveArgs>, ctx: ToolContext)
| 323 | captureNote = cap.note; |
| 324 | } |
| 325 | |
| 326 | // Ask a vision model to critique the screenshot. We reuse the vision_analyze tool so all |
| 327 | // the backend-selection + graceful "[VISION_NOT_CONFIGURED]" handling lives in one place. |
| 328 | const prompt = buildReviewPrompt({ type: manifest.type, title: manifest.title, intent: args.intent }); |
| 329 | let visionAnswer = ''; |
| 330 | let sawScreenshot = false; |
| 331 | if (screenshotPath) { |
| 332 | try { |
| 333 | const res = await this.visionFn({ imagePath: screenshotPath, prompt, ctx }); |
| 334 | const text = typeof res.content === 'string' ? res.content : ''; |
| 335 | if (res.isError || text.startsWith('[VISION_NOT_CONFIGURED]')) { |
| 336 | // Vision unavailable — we can still report runtime errors, just not a visual verdict. |
| 337 | visionAnswer = ''; |
| 338 | sawScreenshot = false; |
| 339 | } else { |
| 340 | // vision_analyze prefixes every answer with `[via <backend>, <n>KB]\n\n`. That pushes the |
| 341 | // verdict token (LOOKS_GOOD/…) off the first line, so classifyVisionAnswer — which keys off |
| 342 | // the LEADING token — would misread a clean render as `unverified` and the loop would never |
| 343 | // confirm success. Strip the banner so the model's actual verdict is what gets classified. |
| 344 | visionAnswer = text.replace(/^\[via [^\]]*\]\s*/, ''); |
| 345 | sawScreenshot = true; |
| 346 | } |
| 347 | } catch { |
| 348 | visionAnswer = ''; |
| 349 | sawScreenshot = false; |
| 350 | } |
| 351 | } |
| 352 | |
| 353 | const report = buildReviewReport({ visionAnswer, consoleErrors, pageErrors, sawScreenshot }); |
| 354 | |
| 355 | const out = formatReviewReport(manifest.id, version, report); |
| 356 | const notes: string[] = []; |
| 357 | if (captureNote) notes.push(captureNote); |
| 358 | if (!sawScreenshot && consoleErrors.length === 0 && pageErrors.length === 0 && screenshotPath) { |
| 359 | notes.push( |
| 360 | 'No vision backend configured — captured the render but could not get a visual verdict. ' + |
| 361 | 'Set QODEX_OLLAMA_VISION_MODEL / ANTHROPIC_API_KEY / OPENAI_API_KEY for one.', |
| 362 | ); |
| 363 | } |
| 364 | const extra = notes.length ? '\n' + notes.map(n => `(${n})`).join('\n') : ''; |
| 365 | |
| 366 | return { |
| 367 | content: out + extra, |
| 368 | // `issues` + `screenshotPath` are echoed so a front-end (the bot card) can render the verdict, |
| 369 | // the concrete problems, and the rendered screenshot from this single tool result. |
| 370 | metadata: { artifactId: manifest.id, version, verdict: report.verdict, issues: report.issues, issueCount: report.issues.length, sawScreenshot, screenshotPath, title: manifest.title, type: manifest.type }, |
| 371 | }; |
| 372 | } |
| 373 | } |
| 374 | |
| 375 | const LiveArgs = z.object({ |
| 376 | id: z.string().optional().describe('The artifact id to serve live.'), |
| 377 | name: z.string().optional().describe('Alias for id — some models pass the artifact title here; id takes precedence.'), |
| 378 | share: z.enum(['local', 'network', 'tunnel']).optional().describe( |
| 379 | 'Reach: "local" (default, localhost only) · "network" (same WiFi/LAN — teammates open ' + |
| 380 | 'http://<your-ip>:port) · "tunnel" (a public https link via cloudflared/ngrok, plus LAN). ' + |
| 381 | 'network/tunnel links carry a private access token, so only someone with the full URL gets in.'), |
| 382 | open: z.boolean().optional().describe( |
nothing calls this directly
no test coverage detected