From 7cefe1d1701b4d911a328a92a1dc76bbea4034eb Mon Sep 17 00:00:00 2001 From: Khaliq Date: Sun, 19 Jul 2026 16:16:32 +0200 Subject: [PATCH 1/3] agents --- web/content/docs/agents/build.mdx | 98 ++++++++++++++++++------ web/content/docs/agents/cli.mdx | 63 +++++++++++++++ web/content/docs/agents/deploy.mdx | 33 -------- web/content/docs/agents/introduction.mdx | 96 ++++++++++++++++++++--- web/content/docs/agents/patterns.mdx | 34 -------- web/content/docs/agents/quickstart.mdx | 43 ++++------- web/lib/product-docs-nav.ts | 7 +- 7 files changed, 240 insertions(+), 134 deletions(-) create mode 100644 web/content/docs/agents/cli.mdx delete mode 100644 web/content/docs/agents/deploy.mdx delete mode 100644 web/content/docs/agents/patterns.mdx diff --git a/web/content/docs/agents/build.mdx b/web/content/docs/agents/build.mdx index 0db2a95..9071121 100644 --- a/web/content/docs/agents/build.mdx +++ b/web/content/docs/agents/build.mdx @@ -1,47 +1,99 @@ --- title: 'Build your own' -description: 'Define a custom agent by making the trigger, context, permissions, and handoff path explicit.' +description: 'Author a cloud persona: persona.json for deploy wiring, agent.ts for behavior.' --- -Start with the contract. The implementation can change, but the contract keeps the agent understandable. - -## Define the job - -Write the job in one sentence: +Start with the job in one sentence: ```text When happens, inspect , take , and report . ``` -If the sentence needs a paragraph, split the agent into smaller agents. +If that needs a paragraph, split it into smaller agents. + +## Use the skill + +Don't hand-write the shape from memory — it moves. Install the skill and let your coding agent do the authoring: + +```bash +# with prpm +npx prpm install @agent-relay/creating-cloud-persona + +# with skills.sh +npx skills add https://github.com/agentworkforce/skills --skill creating-cloud-persona +``` + +Then ask it to "create a cloud persona for <job>". The skill covers the current persona shape, the event model, integration scopes and adapter config, inputs, memory, sandbox modes, and the production traps that break deploys. + +The rest of this page is what the skill encodes — worth reading so you can tell whether what it wrote is right. + +## The two files + +A cloud persona is always a pair: -## Give it a persona +- **`persona.json`** — deploy and runtime wiring: the integrations it requires and their scopes, inputs, model and harness, memory, sandbox mode, and the system prompt that defines its role. +- **`agent.ts`** — the behavior. `defineAgent(...)` declares the triggers, schedules, and watch rules; the handler branches on `event.type` and does the work. -The persona is not a brand voice. It is the operating policy: +Wakeups are declarative, behavior is imperative. Every provider your `agent.ts` listens on must also be declared in `persona.json` — an undeclared integration never mounts, and its triggers never fire. -- What the agent optimizes for -- What it must never do without approval -- Which files, tools, providers, and channels it can use -- What a good final report looks like +## Complexity stays declarative + +Complex agents aren't more code — they're more declarations. The [`review`](https://github.com/AgentWorkforce/agents/tree/main/review) agent reviews PRs, pushes fixes, and merges on approval. Its entire wakeup surface: + +```ts +export default defineAgent({ + triggers: { + github: [ + { on: 'pull_request.opened' }, + { on: 'pull_request.synchronize' }, + { on: 'pull_request_review.submitted' }, + { on: 'pull_request_review_comment.created' }, + { on: 'check_run.completed' }, + { on: 'issue_comment.created' } + ], + slack: [{ on: 'message.created', paths: ['/slack/channels/${SLACK_CHANNEL}/**'] }] + }, + handler: async (ctx, event) => { + const data = (await event.expand('full')).data; + if (event.type.startsWith('slack.')) return handleSlackMergeRequest(ctx, data); + // ...route the github events + } +}); +``` + +Seven event sources across two providers, and `event` is fully narrowed to exactly those types — an unhandled case is a type error, not a 3am surprise. + +The heavy lifting delegates to a coding harness in one call: + +```ts +const run = await ctx.harness.run({ cwd: ctx.sandbox.cwd, prompt: reviewHarnessPrompt(pr) }); +``` + +The PR's repo is already materialized at `ctx.sandbox.cwd`; the agent edits files there and cloud commits and pushes after the harness exits. No git, no `gh`, no checkout logic in your code. ## Keep context structured -Prefer durable references over pasted prose: +Prefer durable references over pasted prose: Relay channels and threads, Relayfile paths, PR URLs and commit SHAs, Linear issue IDs. Structured context lets the agent resume, verify, and explain itself. -- Relay channels and threads for coordination -- Relayfile paths for provider-backed records -- GitHub PR URLs and commit SHAs for code review -- Linear issue IDs for product work +## Verify before deploying + +```bash +npx agentworkforce persona compile ./my-agent/persona.ts # if typed +npx agentworkforce invoke ./my-agent/persona.json --schedule daily +npx agentworkforce deploy ./my-agent/persona.json --mode cloud --dry-run +``` -Structured context lets the agent resume, verify, and explain itself without reconstructing the world from a prompt. +`invoke` runs the handler locally against a fixture or a named schedule and prints a run record. The dry-run validates the deploy without side effects. -## Build from the repo + + Every command takes the `persona.json` **file**, not the directory — a directory path fails with `EISDIR`. If you author a typed `persona.ts`, `persona compile` writes the JSON next to it. + - Fork the examples and keep your agent's persona, trigger, and implementation together. + Fork the examples — persona, trigger, and implementation live together. - - Use Relay for the messaging, workspace, and handoff layer. + + Ship it, then keep it observable. diff --git a/web/content/docs/agents/cli.mdx b/web/content/docs/agents/cli.mdx new file mode 100644 index 0000000..b5169d9 --- /dev/null +++ b/web/content/docs/agents/cli.mdx @@ -0,0 +1,63 @@ +--- +title: 'CLI' +description: 'The agentworkforce command surface — deploy, run locally, trigger, inspect, and tear down.' +--- + +Everything an agent needs after it's written. Anything in the [gallery](/agents) can also be deployed from the web with **Launch agent** — same result, no terminal. + +```bash +npx agentworkforce login # browser OAuth, writes the workspace token +npx agentworkforce logout +``` + +## Deploy + +```bash +npx agentworkforce deploy [flags] +``` + +| Flag | What it does | +| --- | --- | +| `--mode dev \| sandbox \| cloud` | Local run, Daytona sandbox, or managed cloud. | +| `--dry-run` | Validate with no side effects. Prints integration and schedule counts. | +| `--on-exists update` | Redeploy over an existing persona. **Default is `cancel`** — a silent no-op. | +| `--input KEY=value` | Override a declared input. Repeatable. | +| `--reconnect ` | Force a fresh provider connect. | +| `--no-connect` | Fail instead of prompting for an unconnected provider. | +| `--workspace ` | Target a specific workspace. | +| `--detach` | Background the runner. | +| `--bundle-out ` | Write the deploy bundle to disk. | + +Deploy opens a connect flow for any declared provider that isn't connected. An unconnected provider means its triggers never fire. + +## Run and test locally + +```bash +npx agentworkforce agent # interactive local session +npx agentworkforce invoke --schedule daily # fire a named schedule +npx agentworkforce invoke --fixture ev.json +npx agentworkforce invoke --scaffold slack.message.created +npx agentworkforce local-surface # real webhooks, local handler +``` + +`invoke` emits a run record. `--reads fixtures|live` and `--model stub|fixture|live` control how much of the real world it touches; `--case ` runs a YAML case with assertions. + +## Operate + +```bash +npx agentworkforce deployments list # what's running +npx agentworkforce deployments logs # --tail defaults to 50 +npx agentworkforce trigger # fire an active agent now +npx agentworkforce runs export # replay fixture for a cloud run +npx agentworkforce destroy # cancel schedules, mark destroyed +``` + +`deployments list` accepts `--status`, `--persona `, and `--json`. `deployments logs` with no agent lists the available workspace log files. + +## Promote slowly + +Start report-only and graduate one permission at a time: comment, label, open a PR, merge, deploy. An unattended agent should leave enough in Relay that a teammate can answer why it woke up, what it inspected, what it changed, and what needs review. + + + Tighten the persona contract before increasing autonomy. + diff --git a/web/content/docs/agents/deploy.mdx b/web/content/docs/agents/deploy.mdx deleted file mode 100644 index 9d6ce43..0000000 --- a/web/content/docs/agents/deploy.mdx +++ /dev/null @@ -1,33 +0,0 @@ ---- -title: 'Deploy and operate' -description: 'Run proactive agents with clear triggers, scoped credentials, observable output, and human approval where it matters.' ---- - -Deployment is where an agent becomes part of the team. Treat it like a small service with a narrower blast radius and a louder paper trail. - -## Before enabling a trigger - -- Scope credentials to the paths, repos, channels, and providers the agent actually needs. -- Set a clear reporting channel or thread. -- Decide which actions require approval. -- Put a spending or runtime limit around open-ended work. -- Test with a real event and confirm the final report is useful. - -## Operating loop - -An unattended agent should leave enough evidence that a teammate can answer: - -1. Why did it wake up? -2. What context did it inspect? -3. What did it change? -4. What does a human need to review? - -If those answers are not visible in Relay, the agent is too quiet. - -## Promote slowly - -Start in report-only mode. Let the agent recommend the action, then graduate one permission at a time: comment, label, open a PR, merge, deploy. - - - Design the agent contract before increasing autonomy. - diff --git a/web/content/docs/agents/introduction.mdx b/web/content/docs/agents/introduction.mdx index 8f5a8d2..1f065fd 100644 --- a/web/content/docs/agents/introduction.mdx +++ b/web/content/docs/agents/introduction.mdx @@ -1,11 +1,89 @@ --- title: 'Agents' -description: 'Proactive workers that watch the systems your team already uses, decide when action is needed, and report back through Relay.' +description: 'Proactive workers that watch the systems your team already uses, decide when action is needed, and report back.' --- -Agents turn Relay from a coordination layer into an operating system for work. They can watch channels, files, provider events, schedules, and pull requests, then take a bounded action without waiting for someone to paste context into a chat box. +Relay agents are your team of proactive agents. Built on top of [file](/docs/file) they can react to any signal and take action. That signal can be any third party event, a schedule, or a message from another agent. -The important part is not autonomy for its own sake. A useful agent has a small job, clear inputs, visible output, and a human-friendly trail of what it did. +A relay agent is specialized and focused agent that has a task or a few small tasks and that it does very well. They are completely customizable +and live in your codebase and deployable by a simple CLI command or a one click deploy on the web. You can deploy several +relay agents and each agent on the relay can communicate with each other enabling as complex of workflows as you want with +full traceability and control in straightforward and concise TypeScript code. + +## A whole agent + +This is [`granola-prospect`](https://github.com/AgentWorkforce/agents/tree/main/granola). A meeting recording syncs in; it decides whether the call contained a real feature ask, files a Linear issue, and hands the implementation to a coding agent that opens the PR. + +The persona declares what it can reach: + +```ts +export default definePersona({ + id: 'granola-prospect', + description: 'Detects prospect calls, files a Linear issue with the ask, and opens a PR implementing it.', + cloud: true, + useSubscription: true, + integrations: { + granola: {}, + linear: { scope: { issues: '/linear/issues/**', teams: '/linear/teams/**' } }, + github: { scope: { repo: 'your-org/your-repo' } } + }, + harness: 'claude', + model: 'claude-sonnet-4-6', + systemPrompt: 'Turn prospect asks from meeting transcripts into a Linear issue and a small implementing PR.', + harnessSettings: { reasoning: 'high', timeoutSeconds: 1800, sandboxMode: 'workspace-write' }, + onEvent: './agent.ts' +}); +``` + +And the handler decides what to do: + +```ts +export default defineAgent({ + triggers: { granola: [{ on: 'file.created' }] }, + handler: async (ctx, event) => { + const notePath = readNotePath((await event.expand('full')).data); + const transcript = await readNote(ctx, notePath); + if (!transcript) return; + + // The judgment call: was this a prospect asking for something? + const ask = await classify(ctx, transcript); + if (!ask.isProspect) return; + + const linear = relayClient('linear'); + const issue = await linear.write('issues', {}, { + teamId: await resolveTeamId(ctx), + title: ask.title, + description: ask.summary + }); + + // Hand the actual work to a coding agent. The repo is already checked out. + const run = await ctx.harness.run({ + cwd: ctx.sandbox.cwd, + prompt: `A prospect asked for the following. Implement it, then open a pull request. + Put the PR URL on the last line.\n\n${ask.summary}` + }); + + const prUrl = run.output.match(/https?:\/\/\S*\/pull\/\d+/g)?.pop(); + if (prUrl) { + await linear.write('comments', { issueId: issue.receipt?.id }, { body: `:rocket: ${prUrl}` }); + } + } +}); +``` + +`classify` is a model call, not a rule: + +```ts +const ask = await ctx.llm.complete(` + Read this meeting transcript. Decide if it is a sales/prospect call where the + prospect asked for a feature or change. Reply with JSON only: + {"isProspect": boolean, "title": "short issue title", "summary": "what they asked for"} + + ${transcript.slice(0, 8000)} +`, { maxTokens: 400 }); +``` + +The agent makes decisions based on the inputs and allows the code to lean but accomplish complicated functionality. ## What an agent needs @@ -14,19 +92,17 @@ The important part is not autonomy for its own sake. A useful agent has a small - **A policy**: what the agent may read, write, spend, and deploy. - **A reporting path**: where the agent posts progress, findings, and handoffs. -Relay gives that work a shared substrate: messages for coordination, Relayfile for provider-backed state, and profiles for repeatable behavior. - ## Start here - Launch a small repo-review agent and see how it reports through Relay. - - - Pick the shape that matches the job: reviewer, monitor, triager, or worker pair. + Get an agent from the gallery running in one click. - Define the trigger, behavior, permissions, and handoff path for a custom agent. + Author a cloud persona: `persona.json` for wiring, `agent.ts` for behavior. + + + Deploy, test locally, trigger, read logs, and tear down. diff --git a/web/content/docs/agents/patterns.mdx b/web/content/docs/agents/patterns.mdx deleted file mode 100644 index 7a9dcd9..0000000 --- a/web/content/docs/agents/patterns.mdx +++ /dev/null @@ -1,34 +0,0 @@ ---- -title: 'Agent patterns' -description: 'Use a small set of repeatable patterns for proactive agents instead of designing every workflow from scratch.' ---- - -Good agents are boring in the best way. They have one reason to wake up, one definition of done, and a clear path back to the team. - -## Reviewer - -Reviews pull requests, migrations, release notes, or generated work. The agent should cite files, produce concrete findings, and hand back a short pass/fail summary. - -Use it when quality gates are frequent and repetitive. - -## Monitor - -Watches outside signals: package releases, vendor changelogs, HN keywords, inbound emails, or provider webhooks. It should dedupe aggressively and only post when the signal clears a threshold. - -Use it when the job is mostly "notice the important thing." - -## Triager - -Turns unstructured input into routed work. It can classify Linear issues, summarize customer requests, open follow-up tickets, or assign an owner. - -Use it when the next step is known, but humans spend time sorting the queue. - -## Worker pair - -Splits a job between an implementer and a reviewer. One agent changes the system, the other checks the result and asks for corrections before a human sees it. - -Use it when the task benefits from independent review before handoff. - - - See concrete proactive-agent use cases. - diff --git a/web/content/docs/agents/quickstart.mdx b/web/content/docs/agents/quickstart.mdx index 76207e2..92c5127 100644 --- a/web/content/docs/agents/quickstart.mdx +++ b/web/content/docs/agents/quickstart.mdx @@ -1,50 +1,35 @@ --- title: 'Quickstart' -description: 'Fork an existing proactive agent, connect it to a Relay workspace, and run it against a real event.' +description: 'Get a proactive agent running in one click.' --- -The fastest way to start is to fork an existing agent and keep the first job narrow. Review a PR. Triage one Linear project. Watch one package. Send one digest. +Start with someone else's agent. Pick one from the [gallery](/agents) and hit **Launch agent**. -## 1. Pick an agent +The deploy wizard opens with that agent's persona already loaded. Pick a workspace, connect the providers it asks for, choose how it gets model access, fill in its inputs — Slack channel, repo, whatever it declared — and deploy. -Start from the open-source agents repo: +That's it. Its triggers and schedules are registered and it's live. Sandboxes spin up on the first trigger fire, so an idle agent costs nothing. -```bash -gh repo clone AgentWorkforce/agents -cd agents -``` - -Choose a small agent directory as the reference. Each agent should make the trigger, persona, and implementation obvious from the files in that folder. - -## 2. Connect Relay - -Install the Relay skill in the coding harness you use for setup work: +## Prefer the terminal? ```bash -npx @agent-relay@latest skills add +gh repo clone AgentWorkforce/agents && cd agents +npx agentworkforce login +npx agentworkforce deploy ./hn-monitor/persona.json --mode cloud ``` -Then create or select the workspace where the agent should report progress. The same workspace can host the agent's channel, human handoffs, and durable context. - -## 3. Run the first event - -Use a real event, but keep the permission surface small: +Then fire it without waiting for its schedule: ```bash -agent-relay spawn repo-reviewer \ - --channel dev \ - --task "Review the latest pull request and report findings." +npx agentworkforce trigger hn-monitor ``` -The first successful run should prove three things: the agent got the right context, stayed inside its policy, and reported its work somewhere humans already look. - ## Next - - Use a known shape instead of inventing every workflow from scratch. + + Write a persona from scratch — or have your coding agent write it. - - Add schedules, secrets, limits, and reporting before leaving it unattended. + + Deploy, test locally, trigger, read logs, and tear down. diff --git a/web/lib/product-docs-nav.ts b/web/lib/product-docs-nav.ts index 0d2991c..3b01858 100644 --- a/web/lib/product-docs-nav.ts +++ b/web/lib/product-docs-nav.ts @@ -151,14 +151,11 @@ export const agentsSection: ProductDocSection = { }, { title: 'Design', - items: [ - { title: 'Agent patterns', slug: 'patterns' }, - { title: 'Build your own', slug: 'build' }, - ], + items: [{ title: 'Build your own', slug: 'build' }], }, { title: 'Run', - items: [{ title: 'Deploy and operate', slug: 'deploy' }], + items: [{ title: 'CLI', slug: 'cli' }], }, ], }; From 191139cf0ed714f797516f679e83a7ca51f6f951 Mon Sep 17 00:00:00 2001 From: Khaliq Date: Sun, 19 Jul 2026 16:53:54 +0200 Subject: [PATCH 2/3] feat(agents): dense gallery rows with provider logos MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the 3-up card grid on /agents with a horizontal list. The card artwork was carrying most of the vertical space while saying little, so rows now lead with name + tagline and show which providers each agent touches as logo marks. - Vendor provider logos from Nango's template logos (the same source pear resolves icons from) via scripts/sync-integration-logos.sh, rather than hotlinking on every render. Nango answers 200 with its SPA shell for slugs it has no logo for, so the script sniffs for an SVG root; daytona, gcp, hacker-news, neon, and npm have none and fall back to text chips. granola's logo is a ~525KB SVG wrapping a base64 PNG, so it is unwrapped and downscaled. - Logos sit on a light chip — several marks are solid black and would disappear on the dark surface. - Trigger detail moves to its own reserved line that fades in on hover, so the tagline stays visible and the row never shifts. - Show logos on the agent detail page too, labeled with the provider name. - hn-monitor and spotify-releases declare telegram as an optional transport gated by enabledByInput; reflect that in their integrations and inputs. Co-Authored-By: Claude Opus 4.8 --- web/app/agents/AgentsGallery.tsx | 62 +++-- web/app/agents/[slug]/AgentDetail.tsx | 31 +-- web/app/agents/agents.module.css | 252 ++++++++++++++----- web/components/agents/IntegrationLogos.tsx | 61 +++++ web/lib/agents.ts | 38 ++- web/public/integration-logos/cloudflare.svg | 1 + web/public/integration-logos/github.svg | 1 + web/public/integration-logos/google-mail.svg | 1 + web/public/integration-logos/granola.png | Bin 0 -> 5968 bytes web/public/integration-logos/linear.svg | 1 + web/public/integration-logos/notion.svg | 1 + web/public/integration-logos/slack.svg | 1 + web/public/integration-logos/spotify.svg | 1 + web/public/integration-logos/telegram.svg | 1 + web/scripts/sync-integration-logos.sh | 98 ++++++++ 15 files changed, 451 insertions(+), 99 deletions(-) create mode 100644 web/components/agents/IntegrationLogos.tsx create mode 100644 web/public/integration-logos/cloudflare.svg create mode 100644 web/public/integration-logos/github.svg create mode 100644 web/public/integration-logos/google-mail.svg create mode 100644 web/public/integration-logos/granola.png create mode 100644 web/public/integration-logos/linear.svg create mode 100644 web/public/integration-logos/notion.svg create mode 100644 web/public/integration-logos/slack.svg create mode 100644 web/public/integration-logos/spotify.svg create mode 100644 web/public/integration-logos/telegram.svg create mode 100755 web/scripts/sync-integration-logos.sh diff --git a/web/app/agents/AgentsGallery.tsx b/web/app/agents/AgentsGallery.tsx index 5331a3c..b053797 100644 --- a/web/app/agents/AgentsGallery.tsx +++ b/web/app/agents/AgentsGallery.tsx @@ -4,9 +4,9 @@ import Link from 'next/link'; import { Box, Database, Zap } from 'lucide-react'; import { FadeIn } from '../../components/FadeIn'; -import { AgentArt } from '../../components/agents/AgentArt'; import { BuildYourOwn } from '../../components/agents/BuildYourOwn'; -import { AGENTS, INTEGRATION_LABELS } from '../../lib/agents'; +import { IntegrationLogos } from '../../components/agents/IntegrationLogos'; +import { AGENTS } from '../../lib/agents'; import s from './agents.module.css'; const CAPABILITIES = [ @@ -89,31 +89,45 @@ export function AgentsGallery() {
-
+ +
    {AGENTS.map((agent, i) => ( - - -
    - -
    -
    -

    {agent.name}

    -

    {agent.tagline}

    -
    - {agent.integrations.map((integration) => ( - - {INTEGRATION_LABELS[integration]} +
  • + + + + {agent.name} + {agent.tagline} + {/* Its own reserved line, so revealing it on hover/focus + never shifts the row or hides the tagline. */} + + + {agent.trigger.kind} - ))} -
  • -
    - View agent → -
    -
    - -
    + {agent.trigger.summary} + + + + + + + + + ))} -
+
diff --git a/web/app/agents/[slug]/AgentDetail.tsx b/web/app/agents/[slug]/AgentDetail.tsx index 4ebc951..b734613 100644 --- a/web/app/agents/[slug]/AgentDetail.tsx +++ b/web/app/agents/[slug]/AgentDetail.tsx @@ -5,7 +5,8 @@ import Link from 'next/link'; import { FadeIn } from '../../../components/FadeIn'; import { AgentArt } from '../../../components/agents/AgentArt'; import { ForkAgentButton } from '../../../components/agents/ForkAgentButton'; -import { INTEGRATION_LABELS, sourceUrl, type Agent } from '../../../lib/agents'; +import { IntegrationLogos } from '../../../components/agents/IntegrationLogos'; +import { sourceUrl, type Agent } from '../../../lib/agents'; import s from '../agents.module.css'; export function AgentDetail({ agent }: { agent: Agent }) { @@ -42,13 +43,13 @@ export function AgentDetail({ agent }: { agent: Agent }) {

{agent.tagline}

-
- {agent.integrations.map((integration) => ( - - {INTEGRATION_LABELS[integration]} - - ))} -
+ @@ -98,13 +99,13 @@ export function AgentDetail({ agent }: { agent: Agent }) {
Integrations -
- {agent.integrations.map((integration) => ( - - {INTEGRATION_LABELS[integration]} - - ))} -
+
{agent.inputs.length > 0 && (
diff --git a/web/app/agents/agents.module.css b/web/app/agents/agents.module.css index 6261b1c..9ead1b5 100644 --- a/web/app/agents/agents.module.css +++ b/web/app/agents/agents.module.css @@ -203,104 +203,244 @@ } } -/* ── Gallery grid ── */ +/* ── Gallery list ── */ .gallery { - max-width: 1280px; + max-width: 1180px; margin: 0 auto; padding: 24px 40px 96px; } -.grid { +/* Column headers. Mirrors the .rowLink grid; hidden once the row stacks. */ +.listHead { display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 24px; -} - -.col { - display: flex; + grid-template-columns: minmax(0, 1fr) 190px 20px; + gap: 20px; + align-items: center; + padding: 0 18px 10px; + font-size: 0.7rem; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--fg-subtle, var(--fg-muted)); } -.card { - display: flex; - flex-direction: column; - min-height: 100%; - width: 100%; - border-radius: 18px; +.list { + list-style: none; + margin: 0; + padding: 0; border: 1px solid var(--line); + border-radius: 14px; background: var(--card-bg); overflow: hidden; - text-decoration: none; - color: inherit; - box-shadow: 0 8px 24px color-mix(in srgb, var(--primary) 4%, transparent); - transition: - border-color 0.2s, - box-shadow 0.2s, - transform 0.2s; } -.card:hover { - border-color: var(--card-hover-border); - box-shadow: 0 14px 36px color-mix(in srgb, var(--primary) 12%, transparent); - transform: translateY(-3px); +.row { + display: block; } -.cardMedia { - position: relative; - aspect-ratio: 16 / 9; - width: 100%; - overflow: hidden; - border-bottom: 1px solid var(--line); +.row + .row { + border-top: 1px solid var(--line); } -.cardMedia img { - width: 100%; - height: 100%; - object-fit: cover; - display: block; +.rowLink { + display: grid; + grid-template-columns: minmax(0, 1fr) 190px 20px; + gap: 20px; + align-items: center; + padding: 13px 18px; + text-decoration: none; + color: inherit; + transition: background 0.15s; +} + +.rowLink:hover { + background: color-mix(in srgb, var(--primary) 5%, transparent); } -.cardBody { +.rowMain { display: flex; flex-direction: column; - gap: 10px; - padding: 20px 22px 22px; - flex: 1; + gap: 3px; + min-width: 0; } -.cardName { +.rowName { font-family: var(--font-heading), sans-serif; - font-size: 1.18rem; + font-size: 1rem; font-weight: 600; color: var(--fg); - margin: 0; } -.cardTagline { - font-size: 0.94rem; - line-height: 1.55; +.rowTagline { + font-size: 0.86rem; + line-height: 1.45; color: var(--fg-muted); - margin: 0; - flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } -.cardFooter { +/* Occupies a permanently reserved line under the tagline, so fading it in on + hover/focus neither shifts the row nor hides the description. */ +.rowTrigger { display: flex; align-items: center; - justify-content: space-between; - gap: 12px; - margin-top: 4px; + gap: 8px; + min-width: 0; + font-size: 0.78rem; + line-height: 1.45; + color: var(--fg-muted); + opacity: 0; + transition: opacity 0.15s; +} + +.rowLink:hover .rowTrigger, +.rowLink:focus-visible .rowTrigger { + opacity: 1; } -.cardArrow { - font-size: 0.9rem; +.rowTriggerText { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* No hover on touch — keep the trigger visible rather than unreachable. */ +@media (hover: none) { + .rowTrigger { + opacity: 1; + } +} + +.kindSchedule, +.kindEvent { + flex: none; + padding: 2px 8px; + border-radius: 100px; + font-size: 0.68rem; font-weight: 600; + letter-spacing: 0.02em; + text-transform: uppercase; +} + +.kindSchedule { + border: 1px solid color-mix(in srgb, var(--primary) 22%, transparent); + background: color-mix(in srgb, var(--primary) 9%, transparent); color: var(--primary); - white-space: nowrap; +} + +.kindEvent { + border: 1px solid color-mix(in srgb, var(--fg-muted) 26%, transparent); + background: color-mix(in srgb, var(--fg-muted) 9%, transparent); + color: var(--fg-muted); +} + +.rowLogos { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 6px; +} + +/* Provider marks are drawn for light surfaces and several are solid black + (github, granola, notion), so they get their own light chip rather than + sitting directly on the row — otherwise they vanish in dark mode. + Height-constrained with auto width so wordmarks keep their aspect ratio. */ +.rowLogo { + height: 16px; + width: auto; + max-width: 64px; + object-fit: contain; + display: block; + box-sizing: content-box; + padding: 3px 5px; + border-radius: 5px; + background: #fff; + border: 1px solid color-mix(in srgb, var(--fg) 10%, transparent); +} + +.rowArrow { + justify-self: end; + font-size: 0.95rem; + color: var(--fg-muted); + transition: transform 0.15s, color 0.15s; +} + +.rowLink:hover .rowArrow { + color: var(--primary); + transform: translateX(2px); +} + +@media (max-width: 900px) { + .gallery { + padding: 24px 20px 72px; + } + + .listHead { + display: none; + } + + .rowLink { + grid-template-columns: minmax(0, 1fr) auto; + grid-template-areas: + 'main arrow' + 'logos arrow'; + gap: 10px 16px; + align-items: start; + padding: 16px 18px; + } + + .rowMain { + grid-area: main; + } + + .rowLogos { + grid-area: logos; + } + + .rowArrow { + grid-area: arrow; + align-self: center; + } } /* ── Integration / meta chips ── */ +/* Labeled provider chip used on the detail page — mark plus name, where + there's room to spell it out. */ +.logoChip { + display: inline-flex; + align-items: center; + gap: 7px; + padding: 4px 11px 4px 5px; + border-radius: 100px; + border: 1px solid color-mix(in srgb, var(--primary) 16%, transparent); + background: color-mix(in srgb, var(--primary) 7%, transparent); + font-size: 0.78rem; + font-weight: 600; + color: var(--primary); +} + +/* Same light chip as the gallery marks — several provider logos are solid + black and would disappear on the dark surface. */ +.logoChipIcon { + height: 14px; + width: auto; + max-width: 52px; + object-fit: contain; + display: block; + box-sizing: content-box; + padding: 3px 4px; + border-radius: 100px; + background: #fff; +} + +/* A chip with no logo loses the icon's left inset, so even it back out. */ +.logoChip:not(:has(img)) { + padding-left: 11px; +} + .chips { display: flex; flex-wrap: wrap; diff --git a/web/components/agents/IntegrationLogos.tsx b/web/components/agents/IntegrationLogos.tsx new file mode 100644 index 0000000..57be732 --- /dev/null +++ b/web/components/agents/IntegrationLogos.tsx @@ -0,0 +1,61 @@ +import { INTEGRATION_LABELS, integrationLogo, type Integration } from '../../lib/agents'; + +/** + * Provider logos for an agent. + * + * Bare (gallery rows): just the marks, for density. + * `withLabel` (detail page): mark + provider name, where there's room to spell + * it out. Providers Nango has no logo for fall back to a text-only chip so the + * set still reads correctly either way. + * + * Plain (not next/image) to match AgentArt — the SST/OpenNext optimizer + * 500s without sharp. + */ +export function IntegrationLogos({ + integrations, + withLabel = false, + className, + chipClassName, + logoClassName, +}: { + integrations: Integration[]; + /** Render the provider name next to its mark. */ + withLabel?: boolean; + className?: string; + /** Text-only chip: the labeled variant, and the no-logo fallback. */ + chipClassName?: string; + logoClassName?: string; +}) { + return ( + + {integrations.map((integration) => { + const label = INTEGRATION_LABELS[integration]; + const src = integrationLogo(integration); + + if (withLabel) { + return ( + + {src ? : null} + {label} + + ); + } + + return src ? ( + {label} + ) : ( + + {label} + + ); + })} + + ); +} diff --git a/web/lib/agents.ts b/web/lib/agents.ts index d48636b..4068169 100644 --- a/web/lib/agents.ts +++ b/web/lib/agents.ts @@ -199,9 +199,11 @@ export const AGENTS: Agent[] = [ summary: 'Twice a day · 9am & 5pm ET', detail: '0 9,17 * * *', }, - integrations: ['slack', 'hacker-news'], + // slack and telegram are both optional transports gated by + // enabledByInput (SLACK_CHANNEL / TELEGRAM_CHAT) — set either or both. + integrations: ['slack', 'telegram', 'hacker-news'], runtime: 'Claude · claude-haiku-4-5', - inputs: ['TOPICS', 'SLACK_CHANNEL'], + inputs: ['TOPICS', 'SLACK_CHANNEL', 'TELEGRAM_CHAT'], accent: '#c1674b', hasCustomArt: true, }, @@ -249,9 +251,11 @@ export const AGENTS: Agent[] = [ summary: 'Every day · 10am ET', detail: '0 10 * * *', }, - integrations: ['spotify', 'slack'], + // Delivery over Slack DM, Telegram, or both — each optional and gated by + // enabledByInput (SLACK_USER / TELEGRAM_CHAT). + integrations: ['spotify', 'slack', 'telegram'], runtime: 'Default harness', - inputs: ['SLACK_USER', 'SPOTIFY_TOKEN'], + inputs: ['SLACK_USER', 'TELEGRAM_CHAT', 'SPOTIFY_TOKEN'], accent: '#1db954', hasCustomArt: true, }, @@ -610,6 +614,32 @@ export function agentAsset(slug: string, asset: 'banner' | 'card' | 'card-sm'): return `/agent-art/${slug}/${asset}.png`; } +/** + * Provider logos vendored from Nango's template logos by + * scripts/sync-integration-logos.sh — the same source pear resolves icons from. + * Nango has no logo for every provider we support (it answers 200 with its app + * shell for unknown slugs), so this maps only the ones that resolve to a real + * image. Anything absent renders as a text chip instead; re-run the sync script + * after adding an integration and add it here only if a file lands. + */ +const INTEGRATION_LOGO_FILES: Partial> = { + cloudflare: 'cloudflare.svg', + github: 'github.svg', + 'google-mail': 'google-mail.svg', + granola: 'granola.png', + linear: 'linear.svg', + notion: 'notion.svg', + slack: 'slack.svg', + spotify: 'spotify.svg', + telegram: 'telegram.svg', +}; + +/** Logo URL for an integration, or undefined when no vendored logo exists. */ +export function integrationLogo(integration: Integration): string | undefined { + const file = INTEGRATION_LOGO_FILES[integration]; + return file ? `/integration-logos/${file}` : undefined; +} + function agentRepo(agent: Agent): string { return agent.repo ?? DEFAULT_REPO; } diff --git a/web/public/integration-logos/cloudflare.svg b/web/public/integration-logos/cloudflare.svg new file mode 100644 index 0000000..2c84b33 --- /dev/null +++ b/web/public/integration-logos/cloudflare.svg @@ -0,0 +1 @@ + diff --git a/web/public/integration-logos/github.svg b/web/public/integration-logos/github.svg new file mode 100644 index 0000000..b7c3ee7 --- /dev/null +++ b/web/public/integration-logos/github.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/public/integration-logos/google-mail.svg b/web/public/integration-logos/google-mail.svg new file mode 100644 index 0000000..2136fcd --- /dev/null +++ b/web/public/integration-logos/google-mail.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/public/integration-logos/granola.png b/web/public/integration-logos/granola.png new file mode 100644 index 0000000000000000000000000000000000000000..6216a095b60c2aa249f06e14b3f9f0295e95fab3 GIT binary patch literal 5968 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 literal 0 HcmV?d00001 diff --git a/web/public/integration-logos/linear.svg b/web/public/integration-logos/linear.svg new file mode 100644 index 0000000..c89d91c --- /dev/null +++ b/web/public/integration-logos/linear.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/public/integration-logos/notion.svg b/web/public/integration-logos/notion.svg new file mode 100644 index 0000000..b685add --- /dev/null +++ b/web/public/integration-logos/notion.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/public/integration-logos/slack.svg b/web/public/integration-logos/slack.svg new file mode 100644 index 0000000..ae95ae0 --- /dev/null +++ b/web/public/integration-logos/slack.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/public/integration-logos/spotify.svg b/web/public/integration-logos/spotify.svg new file mode 100644 index 0000000..097076b --- /dev/null +++ b/web/public/integration-logos/spotify.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/public/integration-logos/telegram.svg b/web/public/integration-logos/telegram.svg new file mode 100644 index 0000000..d2ecb2c --- /dev/null +++ b/web/public/integration-logos/telegram.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/scripts/sync-integration-logos.sh b/web/scripts/sync-integration-logos.sh new file mode 100755 index 0000000..de04751 --- /dev/null +++ b/web/scripts/sync-integration-logos.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env bash +# +# Vendor the integration provider logos into public/integration-logos/ so the +# /agents gallery and agent detail pages serve them as static assets instead of +# hotlinking Nango on every render. +# +# Source of truth: Nango's template logos, the same convention pear uses to +# resolve a provider icon (see pear/src/main/integrations.ts nangoTemplateLogoUrl). +# +# Two things this script has to defend against: +# +# 1. Nango answers 200 with its SPA HTML shell for slugs it has no logo for, +# so a status check is not enough — we sniff the body for an SVG root and +# skip anything else. As of this writing daytona, gcp, hacker-news, neon, +# and npm have no logo and intentionally fall through to a text chip in +# the UI (see INTEGRATION_LOGO_FILES in lib/agents.ts). +# 2. granola's logo is a ~525KB SVG wrapping a base64 PNG — absurd for a 16px +# mark — so it is unwrapped and downscaled to a small PNG instead. +# +# Re-running is safe and idempotent. After adding an integration, run this and +# then add it to INTEGRATION_LOGO_FILES only if a file actually lands here. +# +# NOTE: destination is public/integration-logos, NOT public/integrations — +# top-level public/ folders become CloudFront → S3 behaviors under SST/OpenNext +# and would shadow any same-named page route (see sync-agent-assets.sh). +# +# Usage: web/scripts/sync-integration-logos.sh + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WEB_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +DEST="$WEB_DIR/public/integration-logos" +BASE="https://app.nango.dev/images/template-logos" +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT + +# Every provider in the `Integration` union in lib/agents.ts. Slugs that Nango +# has no logo for are listed anyway so the skip is visible in the output. +SLUGS=( + cloudflare + daytona + gcp + github + google-mail + granola + hacker-news + linear + neon + notion + npm + slack + spotify + telegram +) + +mkdir -p "$DEST" +echo "Syncing integration logos from $BASE -> $DEST" + +for slug in "${SLUGS[@]}"; do + raw="$TMP/$slug.svg" + + if ! curl -sfL "$BASE/$slug.svg" -o "$raw"; then + echo " skip $slug (fetch failed)" + continue + fi + + # Nango serves its app shell for unknown slugs — only keep real SVGs. + if ! head -c 512 "$raw" | grep -qi '/dev/null 2>&1 + rm -f "$DEST/granola.svg" + echo " granola.png (unwrapped from svg)" + continue + fi + echo " WARN: granola svg had no embedded raster; keeping svg as-is" >&2 + fi + + cp "$raw" "$DEST/$slug.svg" + echo " $slug.svg" +done + +echo "Done." From 0c2dc1e3ad713a15e12f96393977c6d2cc6d1c27 Mon Sep 17 00:00:00 2001 From: Khaliq Date: Sun, 19 Jul 2026 16:56:26 +0200 Subject: [PATCH 3/3] fix(docs): redirect the removed agent doc URLs /docs/agents/deploy and /docs/agents/patterns were deleted in this branch without redirects, so bookmarked and indexed links hit notFound. Point deploy at its successor (cli) and patterns at build, which is the nearest page still covering agent shape. Co-Authored-By: Claude Opus 4.8 --- web/next.config.mjs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/web/next.config.mjs b/web/next.config.mjs index 2be9a29..08fe1c1 100644 --- a/web/next.config.mjs +++ b/web/next.config.mjs @@ -81,6 +81,11 @@ const nextConfig = { { source: '/relaycast/:path*', destination: '/primitives#message', permanent: true }, { source: '/docs/reference-sdk', destination: '/docs/typescript-sdk', permanent: true }, { source: '/docs/reference-sdk-py', destination: '/docs/typescript-sdk', permanent: true }, + // The agents docs lost these two pages: 'Deploy and operate' became the + // CLI reference, and 'Agent patterns' was dropped without a successor + // (the build guide is the nearest thing that still covers agent shape). + { source: '/docs/agents/deploy', destination: '/docs/agents/cli', permanent: true }, + { source: '/docs/agents/patterns', destination: '/docs/agents/build', permanent: true }, ]; }, };