diff --git a/src/lib/components/agents/copy-prompt-button.svelte b/src/lib/components/agents/copy-prompt-button.svelte new file mode 100644 index 0000000000..2b6be30f87 --- /dev/null +++ b/src/lib/components/agents/copy-prompt-button.svelte @@ -0,0 +1,31 @@ + + + + + diff --git a/src/lib/components/agents/feature-card-background.svelte b/src/lib/components/agents/feature-card-background.svelte new file mode 100644 index 0000000000..37092925f5 --- /dev/null +++ b/src/lib/components/agents/feature-card-background.svelte @@ -0,0 +1,24 @@ + + +{#if stage} + +{/if} + diff --git a/src/lib/components/agents/mcp-example-question.svelte b/src/lib/components/agents/mcp-example-question.svelte new file mode 100644 index 0000000000..3872157bb8 --- /dev/null +++ b/src/lib/components/agents/mcp-example-question.svelte @@ -0,0 +1,26 @@ + + +
  • + +
  • diff --git a/src/lib/components/agents/mcp-example-questions.svelte b/src/lib/components/agents/mcp-example-questions.svelte new file mode 100644 index 0000000000..3dc534bf35 --- /dev/null +++ b/src/lib/components/agents/mcp-example-questions.svelte @@ -0,0 +1,29 @@ + + +
    +

    {label}

    + + +
    diff --git a/src/lib/components/agents/skills-terminal.svelte b/src/lib/components/agents/skills-terminal.svelte new file mode 100644 index 0000000000..8ae869c665 --- /dev/null +++ b/src/lib/components/agents/skills-terminal.svelte @@ -0,0 +1,47 @@ + + + + +
    + {#each skills as skill (skill.slug)} +
    +

    + + npx skills add {skill.repository} +

    +

    + + Installed + {skill.slug} + {#if skill.preview} + · Public Preview + {/if} +

    +
    + {/each} + +
    + Example: installing the skills with the skills CLI +
    +
    diff --git a/src/lib/pages/agent-marketing.svelte b/src/lib/pages/agent-marketing.svelte index 9ac5b853af..a00f1507be 100644 --- a/src/lib/pages/agent-marketing.svelte +++ b/src/lib/pages/agent-marketing.svelte @@ -1,4 +1,6 @@
    -

    Agents

    -
    -
    -
    -
    -
    -

    Build Durable AI Agents on Temporal

    -

    - Orchestrate your workflows, AI apps, and agents across any models, - tools, systems, and length of time. -

    -
    - -
    -
    - -
    -
    -

    Temporal Agent Harness

    -

    - A durable agent harness and a matching session UI in one - repo. The harness adds durable turns, tool-approval - policies, and a structured AgentEvents stream; the UI reads - that stream and shows your agents in agent language, turns, - messages, tool calls, sub-agents, and live operational - state. -

    -
    -
    - - -
    -
    -
    +
    +

    Agents

    +

    + Build durable AI agents on Temporal that survive failures and run for as + long as the work takes, with any model or tool. +

    +
    -
    +
    +
    + +
    +
    +

    Agent Harness

    + +
    +

    + A durable agent harness and a matching session UI in one repo. The + harness adds durable turns, tool-approval policies, and a structured + AgentEvents stream; the UI reads that stream and shows your agents + in agent language, turns, messages, tool calls, sub-agents, and live + operational state. +

    +
    +
    +
    +
    + Watch Demo of the Agent Harness + + {/if}
    -
    - -
    -
    -

    Temporal Skills

    -

    - Install the Developer, Ops, Serverless, Cloud and Cloud Setup - skills in the assistant you already code with, and point it at the - knowledgebase for answers grounded in Temporal docs. -

    + +
    + + -
    + +
    + -
    -

    Cookbooks and Demos

    -

    + +

    +
    +

    MCP Server

    + +
    +

    + Ask your agent why a Workflow failed or is stuck, and let it find + out. It reads Event History, checks a Task Queue's pollers and + backlog, and searches runs with Visibility queries, with read-only + access to the Namespaces you choose. +

    +
    +
    + +
    +
    + + + + +
    +
    + +
    + +
    +

    Skills

    +

    + Install the Developer, Ops, Serverless, Cloud and Cloud Setup skills + in the assistant you already code with, and point it at the + knowledgebase for answers grounded in Temporal docs. +

    +
    + +
    + +
    +
    +
    + +
    +

    Resources

    +
    +
    +

    Cookbooks and Demos

    +
      + {#each cookbooks as cookbook (cookbook.href)} +
    • {cookbook.name} - {/each} -
    -
    + + {/each} +
    -
    - -
    - - -
    -

    Blog

    -
    -

    - Read practical guidance and ideas for building reliable agents - with durable execution. -

    - - Browse agent articles + +
    +
    +

    Blog

    +

    + + Agents on Temporal Blog - - +

    +
    - + diff --git a/tests/integration/agents.spec.ts b/tests/integration/agents.spec.ts index a7b0223686..8f97ffb2a8 100644 --- a/tests/integration/agents.spec.ts +++ b/tests/integration/agents.spec.ts @@ -3,6 +3,13 @@ import { expect, test } from '@playwright/test'; import { mockNamespaceApis } from '~/test-utilities/mock-apis'; +const exampleQuestions = [ + 'Which Workflows are stuck, and why?', + 'How many Workflows failed in the last hour?', + 'Do my running Workflows have Workers polling?', + 'Which Namespaces can you read?', +]; + test.beforeEach(async ({ page }) => { await mockNamespaceApis(page); await page.goto('/namespaces/default/agents'); @@ -10,44 +17,160 @@ test.beforeEach(async ({ page }) => { test('shows the agents resource hub', async ({ page }) => { await expect( - page.getByRole('heading', { - name: 'Build Durable AI Agents on Temporal', - level: 2, - }), + page.getByRole('heading', { name: 'Agents', level: 1 }), + ).toBeVisible(); + await expect( + page.getByRole('heading', { name: 'Agent Harness', level: 2 }), ).toBeVisible(); await expect( - page.getByRole('link', { name: 'Open Agent Harness' }), + page.getByRole('link', { name: 'GitHub repository for Agent Harness' }), ).toHaveAttribute( 'href', 'https://github.com/temporal-community/temporal-agent-harness', ); + await expect( + page.getByRole('heading', { name: 'Skills', level: 2 }), + ).toBeVisible(); + await expect(page.getByText('· Public Preview')).toHaveCount(2); + await expect( + page.getByText('npx skills add temporalio/skill-temporal-ops'), + ).toBeVisible(); + await expect( page.getByRole('link', { name: 'Open Google ADK integration' }), ).toBeVisible(); await expect( - page.getByRole('heading', { name: 'Cookbooks and Demos' }), + page.getByRole('heading', { name: 'Resources', level: 2 }), + ).toBeVisible(); + await expect( + page.getByRole('heading', { + name: 'Cookbooks and Demos', + level: 3, + }), ).toBeVisible(); }); -test('does not overflow the viewport horizontally', async ({ page }) => { - const content = page.locator('#content'); +test('shows the MCP Server card and copies its prompts', async ({ + page, + context, +}) => { + await context.grantPermissions(['clipboard-read', 'clipboard-write']); + + await expect( + page.getByRole('heading', { name: 'MCP Server', level: 2 }), + ).toBeVisible(); + await expect( + page.getByRole('link', { + name: 'GitHub repository for MCP Server', + }), + ).toHaveAttribute('href', 'https://github.com/temporalio/temporal-mcp'); + await expect( + page.getByRole('button', { name: /, copy question$/ }), + ).toHaveText(exampleQuestions); + + await page + .getByRole('button', { name: `${exampleQuestions[0]}, copy question` }) + .click(); + await expect( + page.getByRole('button', { name: `${exampleQuestions[0]}, copied` }), + ).toBeVisible(); + await expect( + page.getByRole('button', { name: `${exampleQuestions[1]}, copy question` }), + ).toBeVisible(); + await expect + .poll(() => page.evaluate(() => navigator.clipboard.readText())) + .toBe(exampleQuestions[0]); + + await page + .getByRole('button', { + name: 'Install MCP server with your agent', + exact: true, + }) + .click(); + + await expect( + page.getByRole('button', { + name: 'Install MCP server with your agent, copied', + }), + ).toBeVisible(); await expect - .poll(async () => - content.evaluate((element) => element.scrollWidth <= element.clientWidth), - ) - .toBe(true); + .poll(() => page.evaluate(() => navigator.clipboard.readText())) + .toContain('https://github.com/temporalio/temporal-mcp'); +}); + +test('keeps each copied question on its own timer', async ({ + page, + context, +}) => { + await context.grantPermissions(['clipboard-read', 'clipboard-write']); + await page.clock.install(); + await page.reload(); + + const question = (index: number, state: 'copy question' | 'copied') => + page.getByRole('button', { name: `${exampleQuestions[index]}, ${state}` }); + + await expect(question(0, 'copy question')).toBeVisible(); + await page.clock.pauseAt((await page.evaluate(() => Date.now())) + 1000); + + await question(0, 'copy question').click(); + await expect(question(0, 'copied')).toBeVisible(); + await page.clock.runFor(1500); + + await question(1, 'copy question').click(); + await expect(question(1, 'copied')).toBeVisible(); + await page.clock.runFor(700); + + await expect(question(0, 'copy question')).toBeVisible(); + await expect(question(1, 'copied')).toBeVisible(); + + await page.clock.runFor(1400); + await expect(question(1, 'copy question')).toBeVisible(); }); +for (const width of [320, 375, 1280, 1440, 1800]) { + test(`keeps every card within the page at ${width}px`, async ({ + page, + isMobile, + }) => { + // eslint-disable-next-line playwright/no-skipped-test -- Each width sets its own viewport. + test.skip(isMobile, 'Viewport is set explicitly'); + + await page.setViewportSize({ width, height: 900 }); + await expect( + page.getByRole('heading', { name: 'MCP Server', level: 2 }), + ).toBeVisible(); + + const overflowing = await page + .locator('.agents-page') + .evaluate((agentsPage) => { + const pageRight = agentsPage.getBoundingClientRect().right; + + return Array.from(agentsPage.querySelectorAll('section')) + .filter( + (section) => + section.scrollWidth > section.clientWidth || + section.getBoundingClientRect().right > pageRight + 0.5, + ) + .map((section) => section.getAttribute('aria-labelledby')); + }); + + expect(overflowing).toEqual([]); + }); +} + test('has no automatically detectable accessibility violations', async ({ page, }) => { - await expect(page.locator('.agents-page')).toBeVisible(); + await expect( + page.getByRole('button', { name: /, copy question$/ }), + ).toHaveCount(4); const results = await new AxeBuilder({ page }) .include('.agents-page') + .options({ rules: { 'label-content-name-mismatch': { enabled: true } } }) .analyze(); expect(results.violations).toEqual([]);