provideAgent wires the agent into DI;{' '}
- injectAgent() hands back signals — messages(), status(), error() — plus durable threads and tool progress.
+ injectAgent() hands back signals: messages(), status(), error(),
+ isLoading(), and tool progress. Nothing to subscribe to, nothing to tear down.
>
}
rows={[
{ claim: 'Signals, not promises', api: 'injectAgent()' },
- { claim: 'Threads that branch, resume, replay', api: 'threadId' },
- { claim: 'Same contract on LangGraph and AG-UI', api: 'runtime adapters' },
+ { claim: 'Tool progress as it happens', api: 'toolProgress()' },
+ { claim: 'Same contract on LangGraph and AG-UI', api: 'Agent' },
]}
cta={{ label: 'Read the streaming guide', href: '/docs/langgraph/guides/streaming' }}
visual={interrupt() freezes the run inside the checkpoint. Your UI renders the proposal;{' '}
- submit({'{ resume }'}) continues with the decision on the record.
+ interrupt() freezes the run inside the checkpoint. Your UI renders the
+ proposal; submit({'{ resume }'}) continues with the decision on the
+ record.
>
}
rows={[
@@ -126,15 +103,67 @@ export default async function HomePage() {
{ claim: 'The decision lands beside the action it gated', api: 'submit({ resume })' },
]}
cta={{ label: 'Interrupt patterns', href: '/docs/langgraph/guides/interrupts' }}
- visualLeft
visual={provideFakeAgent() streams canned tokens in-process; mock transports
+ script tool calls and interrupts. Your component specs stay deterministic and fast.
+ >
+ }
+ rows={[
+ { claim: 'No key, no server, no network', api: 'provideFakeAgent()' },
+ { claim: 'Script tool calls and interrupts', api: 'mockLangGraphAgent()' },
+ { claim: 'Same UI code in test and production', api: 'Agent' },
+ ]}
+ cta={{ label: 'Try without a backend', href: INSTALL_OPTIONS[0].quickstartHref }}
+ visual={
+ {CODING_AGENT_PROMPT}
+
+ + {caption} + {captionLink ? ( + <> + {caption ? ' · ' : null} + {captionLink.label} + > + ) : null} +
+ ) : null}- The streaming demo takes an afternoon.{' '} - - Everything after it takes six months. - {' '} - Threadplane is the Angular layer that closes the gap — and it{' '} - - keeps your backend exactly where it is. - -
-- Not another backend agent runtime. Keep LangGraph, Genkit, Mastra, CrewAI, or your own service. Threadplane solves the Angular UI layer. -
-{HERO_SUBHEAD}
+ +{HERO_TRUST_LINE}
@threadplane/ag-ui connects any AG-UI-compatible backend, and{' '}
+ @threadplane/langgraph is the direct LangGraph adapter.{' '}
+ AG-UI on Threadplane
+ >
+ ),
},
{
- q: 'Is the Pilot-to-Prod program required?',
- a: 'No. Every package is MIT-licensed and complete on its own. Pilot-to-Prod is for teams who want hands-on delivery, not a software paywall.',
+ q: 'What is the difference between the LangGraph and AG-UI adapters?',
+ a: (
+ <>
+ Both implement the same Agent contract. LangGraph adds native threads,
+ checkpoints, history, and branch mapping; AG-UI maps the protocol's events and depends
+ on what the backend emits. Choosing an adapter
+ >
+ ),
},
{
- q: 'What does it cost?',
- a: 'Every package is free under MIT. Production Assurance and Pilot-to-Prod are scoped support and delivery engagements — see the pricing page.',
+ q: 'Where are threads and checkpoints stored?',
+ a: (
+ <>
+ In your backend's persistence layer. Threadplane exposes thread, history, and resume
+ behavior in the UI; durability comes from the runtime you operate.{' '}
+ Persistence guide
+ >
+ ),
},
{
- q: 'Is this production-ready today?',
- a: 'It runs the full stack in the live workspace built into our docs, and breaking changes are called out in release notes. We support Angular’s current and previous LTS versions.',
+ q: 'Can I use my existing Angular component library and design system?',
+ a: (
+ <>
+ Yes. The chat compositions are stylable, the primitives are headless, and generated UI
+ renders components you register. Generated UI
+ >
+ ),
},
{
- q: 'Where do I report issues?',
- a: 'GitHub Issues. Pilot customers also get a private channel.',
+ q: 'Does generated UI execute arbitrary code?',
+ a: (
+ <>
+ No. The agent emits constrained structured output that is validated against a schema, and
+ Angular renders registered components with a per-component fallback.{' '}
+ json-render and A2UI
+ >
+ ),
},
{
- q: 'Does it work with Angular Universal / SSR?',
- a: 'Streaming is client-side by design — agents are stateful and signal-based. If your shell is SSR’d, the agent-talking parts stay client-only; render fallbacks during hydration via standard Angular SSR patterns.',
+ q: 'Can I test the UI without a model or a live backend?',
+ a: (
+ <>
+ Yes. provideFakeAgent() streams canned tokens in-process, and mock transports
+ script tool calls and interrupts.{' '}
+ Try it without a backend
+ >
+ ),
+ },
+ {
+ q: 'Which Angular versions are supported?',
+ a: (
+ <>
+ Threadplane supports {formatAngularRange(WEBSITE_SUPPORTED_ANGULAR_MAJORS)}. The installation
+ guide lists the peer ranges for every package.{' '}
+ Installation
+ >
+ ),
+ },
+ {
+ q: 'Does Threadplane require a hosted service or an account?',
+ a: (
+ <>
+ No. Every package is MIT and runs inside your Angular application against a backend you
+ host. Pricing
+ >
+ ),
+ },
+ {
+ // The absolute framing this question used to carry ("installation is
+ // inert", linking the retired telemetry docs library) is barred copy —
+ // see lib/public-copy-contract.ts. /privacy is the canonical answer.
+ q: 'What does Threadplane report about my application?',
+ a: (
+ <>
+ Operational facts about how the product is running — activity, not content. Prompts,
+ messages, tool inputs and outputs, application state, and source code are outside what
+ that reporting is designed to carry. Privacy policy
+ >
+ ),
+ },
+ {
+ q: 'How does Threadplane differ from a raw streaming SDK?',
+ a: (
+ <>
+ A streaming SDK gives you events. Threadplane gives you the Angular state model, chat UX,
+ threads, approvals, generated UI, recovery, and tests on top of them.{' '}
+ Chat
+ >
+ ),
+ },
+ {
+ q: 'How does Threadplane compare with other Angular agent UI libraries?',
+ a: (
+ <>
+ Threadplane is the runtime-neutral Angular UI layer: direct LangGraph and AG-UI adapters, a
+ fake-agent test path, design-system-owned generated UI, and no hosted layer in the loop. A
+ dated, sourced comparison page is planned.{' '}
+ Choosing an adapter
+ >
+ ),
},
];
diff --git a/apps/website/src/components/landing/InstallDialog.spec.tsx b/apps/website/src/components/landing/InstallDialog.spec.tsx
new file mode 100644
index 000000000..4c7105115
--- /dev/null
+++ b/apps/website/src/components/landing/InstallDialog.spec.tsx
@@ -0,0 +1,67 @@
+// @vitest-environment jsdom
+import React from 'react';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { fireEvent, render, screen, within } from '@testing-library/react';
+import { INSTALL_OPTIONS } from '../../lib/positioning';
+
+const trackCtaClickMock = vi.hoisted(() => vi.fn());
+const writeTextMock = vi.hoisted(() => vi.fn().mockResolvedValue(undefined));
+vi.mock('../../lib/analytics/client', () => ({ trackCtaClick: trackCtaClickMock, track: vi.fn() }));
+vi.mock('../ui/Button', () => ({
+ Button: ({ children, href, onClick }: { children: React.ReactNode; href?: string; onClick?: () => void }) =>
+ href ? {children} : ,
+}));
+
+beforeEach(() => {
+ trackCtaClickMock.mockClear();
+ writeTextMock.mockClear();
+ Object.assign(navigator, { clipboard: { writeText: writeTextMock } });
+});
+
+describe('InstallDialog', () => {
+ it('opens on the fake-agent variant and shows its command and snippet', async () => {
+ const { InstallDialog } = await import('./InstallDialog');
+ render(
+ Three steps to a running <chat> in your Angular app. No account, no key.
+
{option.description}
+{option.command}
+ {option.peersNote}
+{option.providerSnippet}
+ {COMPONENT_SNIPPET}
+ {HERO_TRUST_LINE}
++ Keep your agent stack. Standardize the Angular surface. +
++ Threadplane adapts LangGraph and AG-UI into one signal-shaped Agent contract. Your model + provider stays behind the backend you already operate. +
+- Pilot-to-Prod is a concierge delivery — concrete outcomes, your engineers in the driver's seat, no lock-in. + Bring your backend, security model, and design system. Work directly with Threadplane + engineers on architecture, rollout, testing, and production hardening.
+ Not every backend emits every capability. Interrupts, subagents and checkpoints depend on
+ what the runtime sends.{' '}
+
AGUI CONFIG}} + componentPane={
COMPONENT} + />, + ); + expect(screen.getByText('LG CONFIG')).toBeTruthy(); + expect(screen.queryByText('AGUI CONFIG')).toBeNull(); + expect(screen.getByText('COMPONENT')).toBeTruthy(); + expect(screen.getByText('same in both')).toBeTruthy(); + }); + + it('switches to AG-UI and tracks the toggle with adapter', async () => { + const { RuntimeParityToggle } = await import('./RuntimeParityToggle'); + render( +
AGUI CONFIG}} + componentPane={
COMPONENT} + />, + ); + fireEvent.click(screen.getByRole('radio', { name: 'AG-UI' })); + expect(screen.getByText('AGUI CONFIG')).toBeTruthy(); + expect(screen.queryByText('LG CONFIG')).toBeNull(); + expect(screen.getByText('COMPONENT')).toBeTruthy(); + expect(trackCtaClickMock).toHaveBeenCalledWith( + expect.objectContaining({ cta_id: 'home_runtime_parity_toggle', adapter: 'ag_ui' }), + ); + }); + + it('moves DOM focus to the newly checked radio on ArrowRight', async () => { + const { RuntimeParityToggle } = await import('./RuntimeParityToggle'); + render( +
AGUI CONFIG}} + componentPane={
COMPONENT} + />, + ); + const langgraphRadio = screen.getByRole('radio', { name: 'LangGraph' }); + const agUiRadio = screen.getByRole('radio', { name: 'AG-UI' }); + langgraphRadio.focus(); + fireEvent.keyDown(langgraphRadio, { key: 'ArrowRight' }); + expect(document.activeElement).toBe(agUiRadio); + }); +}); diff --git a/apps/website/src/components/landing/RuntimeParityToggle.tsx b/apps/website/src/components/landing/RuntimeParityToggle.tsx new file mode 100644 index 000000000..a233c8a38 --- /dev/null +++ b/apps/website/src/components/landing/RuntimeParityToggle.tsx @@ -0,0 +1,90 @@ +'use client'; + +import { useRef, useState, type KeyboardEvent, type ReactNode } from 'react'; +import { trackCtaClick } from '../../lib/analytics/client'; + +type Adapter = 'langgraph' | 'ag_ui'; + +const ADAPTERS: { key: Adapter; label: string }[] = [ + { key: 'langgraph', label: 'LangGraph' }, + { key: 'ag_ui', label: 'AG-UI' }, +]; + +interface RuntimeParityToggleProps { + /** + * Both panes are highlighted on the server and handed down as elements, so + * switching never round-trips or re-highlights. Only the selected one is + * mounted — `hidden` would keep the inactive copy in the accessibility tree + * for `getByText`/screen readers, which is the wrong reading of "what + * changes". + */ + configPanes: Record
+ What changes app.config.ts +
+ {configPanes[adapter]} ++ What does not same in both +
+ {componentPane} +| Starting point | +What it gives you | +What Threadplane adds | +
|---|---|---|
| {row.start} | +{row.gives} | +{row.adds} | +
{step.body}
+hi
hi
hi
hi