Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

### Changed

- **DocsPage updated for Gemini/Ollama stack** — all nine documentation
sections rewritten to reflect the current project state: Anthropic/Claude
references replaced with Gemini/Ollama, directory tree updated, env-var
section shows dual-provider setup, tech stack table corrected, FAQ updated
with Gemini key and provider-switching questions, roadmap marks completed
features (multi-agent collab, Playwright Dashboard, run history, AI
summaries, Gemini/Ollama switching) as done (`ui/src/pages/DocsPage.tsx`).

### Added

- **Multi-agent collaboration loop** in `/api/qa-agent` — when 2+ agents are
Expand Down Expand Up @@ -50,8 +60,6 @@ All notable changes to this project will be documented in this file.
layout JSON are untouched (`ui/src/pages/PlaywrightDashboard.tsx`).
- `archiveRun()` now logs success/failure to the console — silent
archive failures are no longer invisible.
- Migrated AI logic from numbered demo folders into `src/`; shared LLM client at `src/core/llm-client.ts`.
- Moved marketplace HTML to `docs/` with `css/` and `js/` assets; updated all script paths and README links.

### Changed

Expand Down
24 changes: 12 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,18 @@ Type a request in the chat. **Edi M**, the Team Manager, analyses your intent an

## ✨ Features

| | Feature | Description |
| ---- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 🏢 | **2D Pixel-Art Office** | 7 specialist agents at animated desks — click any desk to start a conversation |
| 🤖 | **Edi M — Team Manager** | Orchestrator agent that analyses your request and routes it to the right specialist |
| 🤝 | **Multi-Agent Collaboration** | Tag two or more specialists (or @Edi M) and they iterate: primary drafts → critic reviews → primary refines → manager delivers the polished synthesis. Each turn streams into its own chat bubble with a role pill (primary / critic / synthesis) |
| ✨🦙 | **Gemini + Ollama** | Switch between cloud Gemini and fully-local Ollama models with a single click — no restart required |
| 🎭 | **Playwright Dashboard** | Run your test suite, stream live terminal output, and view pass/fail telemetry in real time. Every run now also lands an Edi M post-mortem directly in chat |
| 🧠 | **Post-Run AI Summaries** | After a test run Edi M reads the JSON results, classifies each failure (Timeout / Assertion / Locator / Network), and streams a structured executive summary with TypeScript fix snippets |
| 📜 | **Persistent Run History + Logs** | Each run is archived under `test-results/runs/` with its full stdout — click any past run in the history table to replay results AND the original log lines |
| 🔧 | **Self-Healing Locators** | Broken selectors are ranked by resilience: `getByTestId` → `getByRole` → `getByLabel` → CSS/XPath with confidence scores and caveats |
| 🔬 | **6 CLI Agents** | Standalone Node.js agents: Self-Healing, Auto-POM, Bug Triage, Visual Regression, A11y Scanner, Data Generator |
| 📚 | **Built-in QA Course** | 12-chapter automation curriculum with animated, interactive lessons built directly into the UI |
| | Feature | Description |
| ---- | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 🏢 | **2D Pixel-Art Office** | 7 specialist agents at animated desks — click any desk to start a conversation |
| 🤖 | **Edi M — Team Manager** | Orchestrator agent that analyses your request and routes it to the right specialist |
| 🤝 | **Multi-Agent Collaboration** | Tag two or more specialists (or @Edi M) and they iterate: primary drafts → critic reviews → primary refines → manager delivers the polished synthesis. Each turn streams into its own chat bubble with a role pill (primary / critic / synthesis). Chat history and image attachments are forwarded to the initial primary turn so vision prompts and follow-up context are preserved across collaborative rounds |
| ✨🦙 | **Gemini + Ollama** | Switch between cloud Gemini and fully-local Ollama models with a single click — no restart required |
| 🎭 | **Playwright Dashboard** | Run your test suite, stream live terminal output, and view pass/fail telemetry in real time. Every run now also lands an Edi M post-mortem directly in chat |
| 🧠 | **Post-Run AI Summaries** | After a test run Edi M reads the JSON results, classifies each failure (Timeout / Assertion / Locator / Network), and streams a structured executive summary with TypeScript fix snippets |
| 📜 | **Persistent Run History + Logs** | Each run is archived under `test-results/runs/` with its full stdout — click any past run in the history table to replay results AND the original log lines |
| 🔧 | **Self-Healing Locators** | Broken selectors are ranked by resilience: `getByTestId` → `getByRole` → `getByLabel` → CSS/XPath with confidence scores and caveats |
| 🔬 | **6 CLI Agents** | Standalone Node.js agents: Self-Healing, Auto-POM, Bug Triage, Visual Regression, A11y Scanner, Data Generator |
| 📚 | **Built-in QA Course** | 12-chapter automation curriculum with animated, interactive lessons built directly into the UI |

---

Expand Down
81 changes: 65 additions & 16 deletions server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,12 +316,21 @@ async function runCollabTurn(
systemInstruction: string,
prompt: string,
onChunk: (text: string) => void,
history?: HistoryItem[],
imageAttachments?: ImageAttachment[],
): Promise<string> {
const { GoogleGenerativeAI } = await import('@google/generative-ai');
const genAI = new GoogleGenerativeAI(apiKey);
const genModel = genAI.getGenerativeModel({ model, systemInstruction });
const chat = genModel.startChat({ history: [] });
const result = await chat.sendMessageStream([{ text: prompt }]);
// Forward prior chat history so follow-up questions and context are preserved.
const chat = genModel.startChat({ history: toGeminiHistory(history ?? []) });
// Build message parts: text prompt + any inline image attachments.
type GeminiPart = { text: string } | { inlineData: { mimeType: string; data: string } };
const parts: GeminiPart[] = [{ text: prompt }];
for (const img of imageAttachments ?? []) {
parts.push({ inlineData: { mimeType: img.type, data: stripDataUrl(img.content) } });
}
const result = await chat.sendMessageStream(parts);
let full = '';
for await (const chunk of result.stream) {
const t = chunk.text();
Expand Down Expand Up @@ -443,12 +452,20 @@ app.post('/api/qa-agent', async (req, res) => {
label: string,
systemPrompt: string,
userPrompt: string,
turnHistory?: HistoryItem[],
turnImages?: ImageAttachment[],
): Promise<string> => {
send({ evt: 'turn_start', agentId: agent.id, agentName: agent.name, role, round, label });
let full = '';
try {
full = await runCollabTurn(apiKey!, model, systemPrompt, userPrompt, (txt) =>
send({ chunk: txt }),
full = await runCollabTurn(
apiKey!,
model,
systemPrompt,
userPrompt,
(txt) => send({ chunk: txt }),
turnHistory,
turnImages,
Comment on lines +455 to +468

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Forward the original chat context to every collaboration turn.

playTurn can now carry turnHistory and turnImages, but only the initial primary call uses them. The critic, refinement, and synthesis turns still run without the user's prior chat or screenshots, so image-based reviews and the final synthesis are working from incomplete context.

Suggested fix
       const critiqueText = await playTurn(
         critic,
         'critic',
         round,
         `Critique (round ${round})`,
         critic.systemPrompt +
           "\n\nYou are reviewing a peer's draft. Be specific, constructive, and decisive — your goal is to make the final answer the best it can be.",
-        critiquePrompt,
+        critiquePrompt,
+        history,
+        imageAttachments,
       );
@@
       primaryAnswer = await playTurn(
         primary,
         'primary',
         round + 1,
         `Refined response (round ${round + 1})`,
         primary.systemPrompt +
           "\n\nYou are refining your previous draft based on peer feedback. Be focused — incorporate the feedback that's right, push back briefly on anything that's wrong, and produce a stronger answer.",
-        refinePrompt,
+        refinePrompt,
+        history,
+        imageAttachments,
       );
@@
     await playTurn(
       manager,
       'synthesis',
       rounds + 1,
       'Final synthesis',
       manager.systemPrompt +
         "\n\nYou are delivering the team's final answer to the user. Be clear, decisive, and useful.",
-      synthPrompt,
+      synthPrompt,
+      history,
+      imageAttachments,
     );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/index.ts` around lines 455 - 468, playTurn is only passing turnHistory
and turnImages into the initial runCollabTurn call, leaving the
critic/refinement/synthesis steps without chat or image context; update the
calls to the downstream functions (e.g., runCriticTurn, runRefinementTurn,
runSynthesisTurn or whatever functions invoke follow-up turns from playTurn) to
accept and forward the same turnHistory and turnImages parameters so every
collaboration turn receives the original chat history and image attachments;
locate the follow-up calls inside playTurn and add the turnHistory and
turnImages arguments (and corresponding parameters on those functions if needed)
so image-based reviews and final synthesis run with full context.

);
} catch (err: unknown) {
const m = err instanceof Error ? err.message : String(err);
Expand All @@ -469,6 +486,8 @@ app.post('/api/qa-agent', async (req, res) => {
primary.systemPrompt +
'\n\nYou are drafting an initial response. Be thorough but concise; another specialist will review your work.',
userBlock,
history, // forward chat history so prior context is preserved
imageAttachments, // forward image attachments so vision prompts work
Comment on lines +489 to +490

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard message before trimming in the new image-only flow.

The handler now accepts requests with only imageAttachments, but collaboration still builds userBlock from message.trim() on Line 479, and the single-agent Ollama/Gemini paths do the same on Lines 581 and 664. A screenshot-only prompt will throw before the vision request is sent.

Suggested fix
   const {
     message,
@@
     collaborate = false,
     maxRounds = 2,
   } = req.body as QaAgentBody;
+  const promptText = message?.trim() ?? '';
@@
-  if (!message?.trim() && imageAttachments.length === 0) {
+  if (!promptText && imageAttachments.length === 0) {
     send({ error: 'Body field "message" is required.' });
     res.end();
     return;
   }
@@
-    const userBlock = `# User request\n${message.trim()}\n`;
+    const userBlock = `# User request\n${promptText}\n`;
@@
           content:
-            message.trim() ||
+            promptText ||
             '(image attached — vision not supported by Ollama in this integration)',
@@
-    if (message.trim()) parts.push({ text: message });
+    if (promptText) parts.push({ text: promptText });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/index.ts` around lines 489 - 490, Guard use of message before calling
message.trim(): where you construct userBlock from message.trim() (the
collaboration flow and the single-agent Ollama/Gemini paths that also call
message.trim()), first check that message is defined and contains non-whitespace
(e.g., if (message && message.trim().length) ...) and only then build or append
userBlock; otherwise treat it as empty/omit user text so imageAttachments-only
requests proceed without throwing. Update the userBlock construction sites
(references: userBlock, message.trim(), imageAttachments, Ollama, Gemini) to use
this guard and ensure downstream code handles an empty userBlock gracefully.

);

let verdict: 'lgtm' | 'needs_work' = 'needs_work';
Expand Down Expand Up @@ -1224,7 +1243,8 @@ app.post('/api/playwright/run', async (req, res) => {
.split('\n')
.filter(Boolean)
.forEach((line) => {
if (capturedLog.length < 5000) capturedLog.push(line);
capturedLog.push(line);
if (capturedLog.length > 5000) capturedLog.shift(); // sliding window: keep newest 5000
send(line);
});

Expand All @@ -1233,18 +1253,26 @@ app.post('/api/playwright/run', async (req, res) => {

child.on('close', async (code) => {
// Archive FIRST so history is ready by the time the client calls fetchHistory().
// Pass capturedLog so the run page can re-display the stdout next time it's loaded.
// Capture the returned runId so streamSummary reads the immutable snapshot.
let runId: string | null = null;
try {
await archiveRun(archiveSpec, capturedLog);
runId = await archiveRun(archiveSpec, capturedLog);
} catch {
/* non-fatal */
}
send(`[DONE] Finished with exit code ${code ?? 0}`);

// ── Phase 2: Edi M AI Summary — routed to chat via summary_* SSE events.
// streamSummary is no-op-safe when credentials are missing; emits a friendly
// placeholder and a final summary_done. The client decides whether to render.
await streamSummary(res, { agentName, apiKey, model, provider, ollamaBaseUrl, ollamaModel });
// Pass runId so the helper reads the archived snapshot, not the shared file.
await streamSummary(res, {
agentName,
apiKey,
model,
provider,
ollamaBaseUrl,
ollamaModel,
runId: runId ?? undefined,
});
Comment on lines +1267 to +1275

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

runId fixes the read side, but the archive still races on the shared results file.

Both endpoints now pass runId into streamSummary, but archiveRun() still snapshots the single RESULTS_PATH after the child exits. Two overlapping runs can therefore still archive whichever pw-results.json was written last, and the “immutable” summary/history will belong to the wrong run.

Use a per-run reporter output path (or serialize runs) and archive that file directly instead of reading the shared RESULTS_PATH.

Also applies to: 1348-1350, 1604-1612

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/index.ts` around lines 1267 - 1275, The archive path races because
archiveRun() snapshots the shared RESULTS_PATH (pw-results.json) rather than the
per-run output used by streamSummary; modify archiveRun, and any callers, to
accept and archive a per-run reporter output path derived from runId (e.g.,
pw-results-{runId}.json) instead of reading the global RESULTS_PATH, and update
streamSummary invocation sites (where runId is passed) to also pass the same
reporter path; ensure functions/methods named archiveRun, streamSummary and any
uses of RESULTS_PATH are updated to use the per-run path so each run archives
its own file atomically.

res.end();
});

Expand Down Expand Up @@ -1293,6 +1321,12 @@ interface SummaryCreds {
provider?: 'gemini' | 'ollama';
ollamaBaseUrl?: string;
ollamaModel?: string;
/**
* ID of the archived run file (e.g. "run-2026-…"). When provided,
* streamSummary reads the immutable per-run snapshot instead of the shared
* RESULTS_PATH so concurrent runs don't clobber each other's summary.
*/
runId?: string;
}

/**
Expand All @@ -1311,7 +1345,11 @@ async function streamSummary(res: import('express').Response, creds: SummaryCred
const sendEvt = (payload: Record<string, unknown>) =>
res.write(`data: ${JSON.stringify(payload)}\n\n`);

if (!existsSync(RESULTS_PATH)) {
// Prefer the immutable per-run archive over the shared RESULTS_PATH so that
// concurrent runs don't cause this summary to read the wrong results file.
const resultsFile = creds.runId ? path.join(RUNS_DIR, `${creds.runId}.json`) : RESULTS_PATH;

if (!existsSync(resultsFile)) {
sendEvt({ evt: 'summary_done', failures: [] });
return;
}
Expand All @@ -1324,7 +1362,7 @@ async function streamSummary(res: import('express').Response, creds: SummaryCred
let totals = { total: 0, passed: 0, failed: 0, skipped: 0, duration: 0 };

try {
const rawResults = JSON.parse(await fs.readFile(RESULTS_PATH, 'utf-8')) as Record<
const rawResults = JSON.parse(await fs.readFile(resultsFile, 'utf-8')) as Record<
string,
unknown
>;
Expand Down Expand Up @@ -1537,17 +1575,20 @@ app.post('/api/run-dynamic-test', async (req, res) => {
.split('\n')
.filter(Boolean)
.forEach((line) => {
if (capturedLog.length < 5000) capturedLog.push(line);
capturedLog.push(line);
if (capturedLog.length > 5000) capturedLog.shift(); // sliding window: keep newest 5000
sendStr(line);
});

child.stdout.on('data', streamLine);
child.stderr.on('data', streamLine);

child.on('close', async (exitCode) => {
// Archive run first (with captured log), then clean up temp file
// Archive run first (with captured log), then clean up temp file.
// Capture runId so streamSummary reads the immutable per-run snapshot.
let runId: string | null = null;
try {
await archiveRun(`agent:${agentName ?? 'dynamic'}`, capturedLog);
runId = await archiveRun(`agent:${agentName ?? 'dynamic'}`, capturedLog);
} catch {
/* non-fatal */
}
Expand All @@ -1560,7 +1601,15 @@ app.post('/api/run-dynamic-test', async (req, res) => {
sendStr(`[DONE] Finished with exit code ${exitCode ?? 0}`);

// ── Phase 2: Edi M AI Summary (shared helper) ────────────────────────────
await streamSummary(res, { agentName, apiKey, model, provider, ollamaBaseUrl, ollamaModel });
await streamSummary(res, {
agentName,
apiKey,
model,
provider,
ollamaBaseUrl,
ollamaModel,
runId: runId ?? undefined,
});
res.end();
});

Expand Down
Loading
Loading