Skip to content
5 changes: 4 additions & 1 deletion apps/papillon/frontend/src/pages/dashboard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ pub fn DashboardPage() -> impl IntoView {
agents.get().iter().filter(|a| a.source == "compiled").count()
};
let total_count = move || agents.get().len();
let active_agents = move || -> Vec<_> {
agents.get().into_iter().filter(|a| a.source == "compiled").collect()
};

view! {
<div class="fleet-page">
Expand Down Expand Up @@ -66,7 +69,7 @@ pub fn DashboardPage() -> impl IntoView {
fallback=move || view! {
<div class="fleet-grid">
<For
each=move || agents.get()
each=active_agents
key=|a| a.content_hash.clone()
children=move |agent| {
view! { <AgentCard agent=agent /> }
Expand Down
13 changes: 12 additions & 1 deletion apps/papillon/frontend/src/pages/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,17 @@ fn GeneralTab() -> impl IntoView {
});
};

let orch_status_text = move || match orchestrator.status.get() {
OrchestratorStatus::Ready | OrchestratorStatus::Unconfigured => "ACTIVE",
OrchestratorStatus::Downloading { .. } => "LOADING",
OrchestratorStatus::Disconnected => "OFFLINE",
};
let orch_status_color = move || match orchestrator.status.get() {
OrchestratorStatus::Ready | OrchestratorStatus::Unconfigured => "#00b894",
OrchestratorStatus::Downloading { .. } => "#fdcb6e",
OrchestratorStatus::Disconnected => "#b2bec3",
};

view! {
<div class="card">
// ── PAP Orchestrator ─────────────────────────────────────
Expand All @@ -223,7 +234,7 @@ fn GeneralTab() -> impl IntoView {
<div style="display: flex; flex-direction: column; gap: 8px;">
<div style="display: flex; align-items: center; gap: 8px; font-size: 12px;">
<span style="color: var(--text-tertiary); font-family: var(--font-mono); font-size: 10px; min-width: 100px;">"STATUS"</span>
<span style="color: #00b894; font-family: var(--font-mono); font-size: 11px;">"ACTIVE"</span>
<span style=move || format!("color: {}; font-family: var(--font-mono); font-size: 11px;", orch_status_color())>{move || orch_status_text()}</span>
</div>
<div style="display: flex; align-items: center; gap: 8px; font-size: 12px;">
<span style="color: var(--text-tertiary); font-family: var(--font-mono); font-size: 10px; min-width: 100px;">"MANDATE TTL"</span>
Expand Down
5 changes: 4 additions & 1 deletion e2e/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ export default defineConfig({
testDir: "./tests",
// WASM compile+mount takes ~15ms; 60s covers slow CI runners.
timeout: 60_000,
retries: 0,
retries: 1,
// Limit parallel workers in CI to reduce CPU contention from 8 concurrent
// WASM + Playwright instances (prevents typed-block render timeouts).
workers: isCI ? 4 : undefined,
expect: {
timeout: 30_000,
},
Expand Down
16 changes: 8 additions & 8 deletions e2e/tests/agent-prompts.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,11 +179,11 @@ test.describe("LLM provider configuration", () => {
expect(saved.mandate_ttl_hours).toBe(24);
});

test("settings UI General tab shows LLM provider select", async ({ page }) => {
test("settings UI General tab shows inference substrate select", async ({ page }) => {
await page.goto("/settings", { waitUntil: "commit" });
await waitForApp(page);

await expect(page.locator("text=LLM Provider")).toBeVisible();
await expect(page.locator("text=INFERENCE_SUBSTRATE")).toBeVisible();
// First select on the page is the provider dropdown
await expect(page.locator("select").first()).toBeVisible();
});
Expand All @@ -209,7 +209,7 @@ async function submitAndAwaitTypedBlock(
page: import("@playwright/test").Page,
mockKeyword: string,
cssClass: string,
timeout = 8000
timeout = 15000
): Promise<void> {
// Navigate to canvas
await page.goto("/", { waitUntil: "commit" });
Expand Down Expand Up @@ -503,9 +503,9 @@ test.describe("Chrysalis federation commands", () => {
})
);

// Local agents have explicit source fields (catalog, user_created)
// Local agents have explicit source fields (compiled, catalog, user_created)
for (const a of localAgents) {
expect(["catalog", "user_created"]).toContain(a.source);
expect(["compiled", "catalog", "user_created"]).toContain(a.source);
}

// Remote agents from a registry are a separate list
Expand Down Expand Up @@ -690,7 +690,7 @@ test.describe("Canvas block lifecycle", () => {
await page.locator(".topbar-address-input").press("Enter");

// Eventually resolves (event fires after 200ms mock delay)
await expect(page.locator(".typed-book").first()).toBeVisible({ timeout: 8000 });
await expect(page.locator(".typed-book").first()).toBeVisible({ timeout: 15000 });

// After resolution the block should NOT have the resolving class
const block = page.locator(".canvas-block").first();
Expand All @@ -702,7 +702,7 @@ test.describe("Canvas block lifecycle", () => {
await page.addInitScript(`
const origInvoke = window.__TAURI__.core.invoke;
window.__TAURI__.core.invoke = async function(cmd, args) {
if (cmd === 'canvas_prompt') {
if (cmd === 'canvas_plan_prompt') {
const blockId = (args && (args.block_id || args.blockId)) || 'block-fail';
setTimeout(function() {
window.__TAURI__.event.emit('block_resolved', {
Expand Down Expand Up @@ -741,7 +741,7 @@ test.describe("Canvas block lifecycle", () => {
await page.addInitScript(`
const origInvoke = window.__TAURI__.core.invoke;
window.__TAURI__.core.invoke = async function(cmd, args) {
if (cmd === 'canvas_prompt') {
if (cmd === 'canvas_plan_prompt') {
const blockId = (args && (args.block_id || args.blockId)) || 'block-ghost';
setTimeout(function() {
window.__TAURI__.event.emit('block_resolved', {
Expand Down
34 changes: 18 additions & 16 deletions e2e/tests/agents.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,41 +44,41 @@ test.describe("Agent fleet page", () => {
test("active badge shows correct count", async ({ page }) => {
await page.goto("/fleet", { waitUntil: "commit" });
await waitForApp(page);
// 2 catalog agents in mock → 2 ACTIVE
// 2 compiled agents in mock (Web Page Reader, On-Device AI) → 2 ACTIVE
const activeBadge = page.locator(".fleet-badge.active");
await expect(activeBadge).toBeVisible();
await expect(activeBadge).toContainText("2 ACTIVE");
});

test("total badge shows all 3 agents", async ({ page }) => {
test("total badge shows all 5 agents", async ({ page }) => {
await page.goto("/fleet", { waitUntil: "commit" });
await waitForApp(page);
const totalBadge = page.locator(".fleet-badge.total");
await expect(totalBadge).toBeVisible();
await expect(totalBadge).toContainText("3 TOTAL");
await expect(totalBadge).toContainText("5 TOTAL");
});

test("renders 3 agent cards from list_local_agents", async ({ page }) => {
test("ACTIVE AGENTS section renders 2 compiled agent cards", async ({ page }) => {
await page.goto("/fleet", { waitUntil: "commit" });
await waitForApp(page);
await expect(page.locator(".agent-card")).toHaveCount(3);
// Only compiled agents appear in the ACTIVE AGENTS grid
await expect(page.locator(".agent-card")).toHaveCount(2);
});

test("first agent card shows DuckDuckGo Search", async ({ page }) => {
test("first agent card shows Web Page Reader (first compiled agent)", async ({ page }) => {
await page.goto("/fleet", { waitUntil: "commit" });
await waitForApp(page);
const firstCard = page.locator(".agent-card").first();
await expect(firstCard.locator(".agent-card-name")).toContainText(
"DuckDuckGo Search"
"Web Page Reader"
);
});

test("agent cards show source badges", async ({ page }) => {
test("agent cards show compiled source badges", async ({ page }) => {
await page.goto("/fleet", { waitUntil: "commit" });
await waitForApp(page);
// Two catalog + one user_created
await expect(page.locator(".agent-source-badge.catalog")).toHaveCount(2);
await expect(page.locator(".agent-source-badge.user")).toHaveCount(1);
// Only compiled agents are shown in the ACTIVE AGENTS grid
await expect(page.locator(".agent-source-badge.compiled")).toHaveCount(2);
});

test("agent cards show truncated DID", async ({ page }) => {
Expand All @@ -98,7 +98,7 @@ test.describe("Agent fleet page", () => {
await waitForApp(page);
const firstCard = page.locator(".agent-card").first();
const actionStat = firstCard.locator(".agent-stat").nth(1);
await expect(actionStat).toContainText("SearchAction");
await expect(actionStat).toContainText("ReadAction");
// Must NOT include raw "schema:" prefix
await expect(actionStat).not.toContainText("schema:");
});
Expand All @@ -119,7 +119,7 @@ test.describe("Agent fleet page", () => {
// ── Agent command round-trips ─────────────────────────────────

test.describe("Agent command round-trips", () => {
test("list_local_agents returns 3 seeded agents with new fields", async ({
test("list_local_agents returns 5 seeded agents with new fields", async ({
page,
}) => {
await page.goto("/", { waitUntil: "commit" });
Expand All @@ -129,7 +129,8 @@ test.describe("Agent command round-trips", () => {
window.__TAURI__.core.invoke("list_local_agents")
);

expect(agents).toHaveLength(3);
// 2 compiled + 2 catalog + 1 user_created = 5
expect(agents).toHaveLength(5);

// Every agent must have the new AgentInfo fields
for (const agent of agents) {
Expand All @@ -142,6 +143,7 @@ test.describe("Agent command round-trips", () => {

// Sources must be valid
const sources = agents.map((a: any) => a.source);
expect(sources).toContain("compiled");
expect(sources).toContain("catalog");
expect(sources).toContain("user_created");
});
Expand Down Expand Up @@ -176,7 +178,7 @@ test.describe("Agent command round-trips", () => {
const agents = await page.evaluate(() =>
window.__TAURI__.core.invoke("list_local_agents")
);
expect(agents).toHaveLength(4);
expect(agents).toHaveLength(6); // 5 seeded + 1 new
expect(agents.find((a: any) => a.name === "Test Agent")).toBeDefined();
});

Expand Down Expand Up @@ -227,7 +229,7 @@ test.describe("Agent command round-trips", () => {
const after = await page.evaluate(() =>
window.__TAURI__.core.invoke("list_local_agents")
);
expect(after).toHaveLength(2);
expect(after).toHaveLength(4); // 5 seeded - 1 deleted = 4
expect(after.find((a: any) => a.agent_did === custom.agent_did)).toBeUndefined();
});

Expand Down
35 changes: 17 additions & 18 deletions e2e/tests/app.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,15 @@ test.describe("App shell", () => {
// ── Canvas Page (Home) ───────────────────────────────────────

test.describe("Canvas page", () => {
test("shows empty state with inspiration lines on new canvas", async ({ page }) => {
test("shows empty state with agent tiles on new canvas", async ({ page }) => {
await page.goto("/", { waitUntil: "commit" });
await waitForApp(page);
await expect(page.locator(".canvas-area")).toBeVisible();
await expect(page.locator(".canvas-stream")).toBeVisible();
// Seed canvas has blocks — create a new empty canvas via brand dropdown
await page.locator(".topbar-brand").click();
await page.locator("text=+ New Canvas").click();
await expect(page.locator(".canvas-empty")).toBeVisible();
await expect(page.locator(".inspiration-line").first()).toBeVisible();
await expect(page.locator(".canvas-empty-state")).toBeVisible();
await expect(page.locator(".agent-tile").first()).toBeVisible();
});

test("shows address bar prompt when orchestrator is ready", async ({ page }) => {
Expand All @@ -67,23 +67,24 @@ test.describe("Canvas page", () => {
await expect(page.locator(".topbar-address-input")).toBeVisible();
});

test("address bar shows pap:// suggestion buttons", async ({ page }) => {
test("address bar shows pap:// suggestion buttons when typing pap://", async ({ page }) => {
await page.goto("/", { waitUntil: "commit" });
await waitForApp(page);
await page.locator(".topbar-address-input").click();
await expect(page.locator(".palette-suggestion").first()).toBeVisible();
// Suggestions appear only when user types "pap://" prefix
await page.locator(".topbar-address-input").fill("pap://");
await expect(page.locator(".palette-suggestion").first()).toBeVisible({ timeout: 10000 });
});

test("address bar input accepts text", async ({ page }) => {
await page.goto("/", { waitUntil: "commit" });
await waitForApp(page);
await page.locator(".topbar-address-input").fill("Search for flights");
await expect(page.locator(".topbar-address-input")).toHaveValue("Search for flights");
// Suggestions should hide when input has text
// Suggestions should hide when input has non-pap:// text
await expect(page.locator(".palette-suggestion").first()).not.toBeVisible();
});

test("shows setup prompt when orchestrator is disconnected", async ({ page }) => {
test("canvas renders normally when orchestrator is disconnected", async ({ page }) => {
// Override mock to return Disconnected status
await page.addInitScript(`
const origInvoke = window.__TAURI__.core.invoke;
Expand All @@ -94,12 +95,10 @@ test.describe("Canvas page", () => {
`);
await page.goto("/", { waitUntil: "commit" });
await waitForApp(page);
// Seed canvas has blocks — create a new empty canvas to see the setup prompt
await page.locator(".topbar-brand").click();
await page.locator("text=+ New Canvas").click();
await expect(page.locator(".canvas-prompt-setup")).toBeVisible();
await expect(page.locator("text=Configure an LLM provider")).toBeVisible();
await expect(page.locator("text=Open Settings")).toBeVisible();
// Canvas still renders — deterministic routing works without LLM
await expect(page.locator(".canvas-stream")).toBeVisible();
// Topbar address input is still available
await expect(page.locator(".topbar-address-input")).toBeVisible();
});
});

Expand Down Expand Up @@ -130,10 +129,10 @@ test.describe("Settings page", () => {
await expect(page.locator(".settings-tab").nth(5)).toHaveText("MANDATES");
});

test("General tab shows LLM Provider config", async ({ page }) => {
test("General tab shows inference substrate config", async ({ page }) => {
await page.goto("/settings", { waitUntil: "commit" });
await waitForApp(page);
await expect(page.locator("text=LLM Provider")).toBeVisible();
await expect(page.locator("text=INFERENCE_SUBSTRATE")).toBeVisible();
// Provider select is the first select on the page
await expect(page.locator("select").first()).toBeVisible();
});
Expand Down Expand Up @@ -218,7 +217,7 @@ test.describe("Settings page", () => {
await waitForApp(page);

// Start on General
await expect(page.locator("text=LLM Provider")).toBeVisible();
await expect(page.locator("text=INFERENCE_SUBSTRATE")).toBeVisible();

// Switch to Identity
await page.locator(".settings-tab").nth(3).click();
Expand Down
Loading
Loading