diff --git a/.github/workflows/claude-review.yml b/.github/workflows/claude-review.yml index 8ed710c29..0c35c758c 100644 --- a/.github/workflows/claude-review.yml +++ b/.github/workflows/claude-review.yml @@ -27,6 +27,7 @@ jobs: - name: Claude review uses: anthropics/claude-code-action@806af32823ef69c8ef357086c573a902af641307 # v1 + continue-on-error: true with: anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} # Post as the workflow's own GITHUB_TOKEN. Without this the action @@ -51,10 +52,17 @@ jobs: - Public API / DX regressions on the published `@threadplane/*` surface - Missing or weak test coverage for the change + For docs-heavy PRs, skip generated or bulky derived files unless + they are the only evidence for a problem. In particular, do not + spend review budget on `api-docs.json`, generated public context + files, build artifacts, or lockfile-sized output. Prefer reviewing + the source templates, generators, route code, docs config, and MDX + pages that produce or consume those artifacts. + Post a concise top-level summary via `gh pr comment`. Post specific issues as inline comments. Be brief; skip nitpicks and style unless they affect correctness. If the PR looks good, say so briefly. claude_args: | --model claude-sonnet-4-6 - --max-turns 15 + --max-turns 30 --allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)" diff --git a/apps/website/content/docs/chat/api/api-docs.json b/apps/website/content/docs/chat/api/api-docs.json index 94e7a6123..ad77d918f 100644 --- a/apps/website/content/docs/chat/api/api-docs.json +++ b/apps/website/content/docs/chat/api/api-docs.json @@ -1672,7 +1672,7 @@ { "name": "currentAssistantStreaming", "type": "Signal", - "description": "True iff there's a current (last-index) assistant message that's\nstill streaming. The bubble's own caret already signals loading;\nwe suppress the floor typing-indicator in that case so the user\ndoesn't see two loading affordances at once.\n\nMatches the same `streaming + current` condition the bubble uses\nto enable `.chat-message__caret`:\n `agent().isLoading() && i === agent().messages().length - 1`\n `i === agent().messages().length - 1`\n\nRestricted to assistant role because the caret only renders on\nassistant bubbles (`:host([data-role=\"assistant\"][data-current=...\n ][data-streaming=...])`).", + "description": "True iff there's a current (last-index) assistant message that's\nstill streaming. The bubble's own caret already signals loading;\nwe suppress the floor typing-indicator in that case so the user\ndoesn't see two loading affordances at once.\n\nMatches the same `streaming + current` condition the bubble uses\nto enable `.chat-message__caret`:\n `this.agent().isLoading() && i === this.agent().messages().length - 1`\n `i === this.agent().messages().length - 1`\n\nRestricted to assistant role because the caret only renders on\nassistant bubbles (`:host([data-role=\"assistant\"][data-current=...\n ][data-streaming=...])`).", "optional": false }, { diff --git a/apps/website/content/docs/chat/getting-started/installation.mdx b/apps/website/content/docs/chat/getting-started/installation.mdx index 56b2b2919..e9434cfc8 100644 --- a/apps/website/content/docs/chat/getting-started/installation.mdx +++ b/apps/website/content/docs/chat/getting-started/installation.mdx @@ -101,7 +101,7 @@ For me, this is the lowest-friction path for a small team — the tradeoff is th ## 3. Wire `provideChat()` and `provideAgent()` -Register both providers in your application config. Below is a minimal `app.config.ts` for a LangGraph backend (swap `@threadplane/langgraph` for `@threadplane/ag-ui` if you picked that adapter — the API surface is identical). +Register both providers in your application config. Below is a minimal `app.config.ts` for a LangGraph backend. If you picked AG-UI, component calls stay the same, but the provider import and config shape change (`apiUrl`/`assistantId` for LangGraph, `url` for AG-UI). ```ts // src/app/app.config.ts diff --git a/apps/website/content/docs/chat/guides/error-handling.mdx b/apps/website/content/docs/chat/guides/error-handling.mdx index c56c141e3..ccf83a39c 100644 --- a/apps/website/content/docs/chat/guides/error-handling.mdx +++ b/apps/website/content/docs/chat/guides/error-handling.mdx @@ -55,7 +55,7 @@ if (err) { ``` -The Retry button calls `agent().retry()` for you. For non-retryable failures (e.g. `auth`), no button is shown — retrying the same bad credential would just fail again. +The Retry button calls the bound Agent's `retry()` method for you. For non-retryable failures (e.g. `auth`), no button is shown — retrying the same bad credential would just fail again. ## Custom error UI diff --git a/apps/website/content/docs/choosing-an-adapter/index.mdx b/apps/website/content/docs/choosing-an-adapter/index.mdx index ad19215c1..dae8ac329 100644 --- a/apps/website/content/docs/choosing-an-adapter/index.mdx +++ b/apps/website/content/docs/choosing-an-adapter/index.mdx @@ -16,7 +16,7 @@ Threadplane ships two adapters that connect a backend agent runtime to `` ## Code comparison -The two adapters share the same public surface — `provideAgent`, `injectAgent`, `AgentConfig`. Swapping is a one-line import change. +The two adapters share the same public surface — `provideAgent`, `injectAgent`, and an adapter-specific `AgentConfig`. Component code stays the same; the provider import and config shape change. ### LangGraph adapter @@ -92,3 +92,9 @@ The `Agent` contract from `@threadplane/chat` is intentionally runtime-neutral. - `@threadplane/ag-ui` consumes the AG-UI event protocol. Any backend that emits AG-UI events plugs in, regardless of what graph engine sits behind it. Both adapters are MIT-licensed and live in the same monorepo. + +## Next steps + +- [LangGraph quickstart](/docs/langgraph/getting-started/quickstart) if your backend is LangGraph Platform and you want native streaming, checkpoints, threads, interrupts, and history APIs. +- [AG-UI quickstart](/docs/ag-ui/getting-started/quickstart) if your backend emits the AG-UI protocol from CrewAI, Mastra, CopilotKit runtime, or another compatible runtime. +- [Chat quickstart](/docs/chat/getting-started/quickstart) if you want to start from the drop-in `` surface and wire the adapter afterward. diff --git a/apps/website/content/docs/langgraph/api/api-docs.json b/apps/website/content/docs/langgraph/api/api-docs.json index ab7622bd1..d8c23adfb 100644 --- a/apps/website/content/docs/langgraph/api/api-docs.json +++ b/apps/website/content/docs/langgraph/api/api-docs.json @@ -2,7 +2,7 @@ { "name": "AgentLifecycleRegistry", "kind": "class", - "description": "Optional registry that collects per-instance agent lifecycles within\nan Angular injection context. External instrumentation packages\n(e.g. cockpit-telemetry) provide this token and read from it.\n\n`@threadplane/langgraph` does NOT provide this itself — `agent()` writes to\nthe registry only when an external consumer has provided it.", + "description": "Optional registry that collects per-instance agent lifecycles within\nan Angular injection context. External instrumentation packages\n(e.g. cockpit-telemetry) provide this token and read from it.\n\n`@threadplane/langgraph` does NOT provide this itself — the configured agent\ninstance writes to the registry only when an external consumer has provided it.", "params": [], "examples": [], "properties": [ diff --git a/apps/website/content/prompts/getting-started.md b/apps/website/content/prompts/getting-started.md index 23ba06f3c..77b8ad6a5 100644 --- a/apps/website/content/prompts/getting-started.md +++ b/apps/website/content/prompts/getting-started.md @@ -1,12 +1,12 @@ Add angular to my Angular 20+ application. -Install: npm install @threadplane/langgraph@latest +Install: npm install @threadplane/chat @threadplane/langgraph 1. In app.config.ts, add provideAgent({ apiUrl: 'http://localhost:2024', assistantId: 'chat' }) to the providers array. Import it from '@threadplane/langgraph'. -2. Create a ChatComponent that calls injectAgent() in the constructor or as a field initializer. injectAgent() MUST be called inside an Angular injection context — constructor or field initializer is correct; ngOnInit is not. +2. Create a component that imports ChatComponent from '@threadplane/chat' and calls injectAgent() in the constructor or as a field initializer. injectAgent() MUST be called inside an Angular injection context — constructor or field initializer is correct; ngOnInit is not. -3. The component template should loop over chat.messages() using @for and render each message's content. Add an input field and a button that calls chat.submit({ message: inputValue }). +3. The component template can start with . For custom layouts, loop over chat.messages() with @for and call chat.submit({ message: inputValue }) from your input. 4. In app.config.ts provideAgent call, the apiUrl should point to the LangGraph server. For local dev this is http://localhost:2024. For production use the LangGraph Platform URL from environment.ts. diff --git a/apps/website/content/prompts/testing.md b/apps/website/content/prompts/testing.md index 1e87508e9..2c9a3910a 100644 --- a/apps/website/content/prompts/testing.md +++ b/apps/website/content/prompts/testing.md @@ -7,6 +7,8 @@ const transport = new MockAgentTransport(); providers: [provideAgent({ transport, assistantId: 'test', apiUrl: '' })] const chat = injectAgent(); +In Angular tests, put the provider in TestBed.configureTestingModule({ providers: [...] }) or the component's providers array, then read chat with injectAgent() from an Angular injection context. + To emit a streaming response: transport.emit([ { type: 'values', values: { messages: [] } }, diff --git a/apps/website/content/prompts/thread-persistence.md b/apps/website/content/prompts/thread-persistence.md index c76cce173..816a6faa5 100644 --- a/apps/website/content/prompts/thread-persistence.md +++ b/apps/website/content/prompts/thread-persistence.md @@ -1,13 +1,13 @@ Add thread persistence to my Angular component that uses angular, so conversations survive page refresh. -1. On component init, read the stored thread ID: const storedId = localStorage.getItem('chat-thread-id'). +1. Put thread persistence in an injectable service so provider config can read it from Angular DI. -2. Create a signal: threadId = signal(storedId). +2. In that service, create a signal from storage: threadId = signal(localStorage.getItem('chat-thread-id')). -3. Pass it to provideAgent: provideAgent({ assistantId: 'chat', threadId: this.threadId, onThreadId: (id) => { this.threadId.set(id); localStorage.setItem('chat-thread-id', id); } }). +3. Configure the agent with a provider factory: provideAgent(() => { const memory = inject(ThreadMemory); return { assistantId: 'chat', threadId: memory.threadId, onThreadId: (id) => memory.remember(id) }; }). 4. The onThreadId callback fires once when the server creates a new thread. After that, the same thread ID is reused and the full conversation history is restored from the LangGraph server. -5. To start a new conversation, call this.threadId.set(null) — this causes the injected agent to create a fresh thread on the next submit. +5. To start a new conversation, call memory.threadId.set(null) and clear storage — this causes the injected agent to create a fresh thread on the next submit. No changes to the template are needed. diff --git a/apps/website/scripts/generate-whitepaper.ts b/apps/website/scripts/generate-whitepaper.ts index ad8e3a25c..2d9562e00 100644 --- a/apps/website/scripts/generate-whitepaper.ts +++ b/apps/website/scripts/generate-whitepaper.ts @@ -85,9 +85,9 @@ Context: Angular teams building AI agent applications must wire streaming transp Cover: - Why streaming state is hard in Angular (zone.js, change detection, timing) -- The Angular signals approach: how agent() exposes messages() as Signal +- The Angular signals approach: how injectAgent() exposes messages() as Signal - How isLoading() lets developers drive loading UI without polling -- Code example: minimal agent() setup (TypeScript snippet, 8-12 lines) +- Code example: minimal provideAgent() plus injectAgent() setup (TypeScript snippet, 8-12 lines) - Production checklist item: "Are your message signals OnPush-compatible?" Tone: Direct, technical, peer-to-peer. No fluff. Audience is senior Angular engineers.`, @@ -106,7 +106,7 @@ Cover: - The threadId signal and onThreadId callback pattern - How to persist threadId to localStorage and restore on mount - Thread list UI and switching between conversations -- Code example: provideAgent() plus agent({ threadId }) with thread persistence (8-12 lines) +- Code example: provideAgent() with a threadId signal plus injectAgent() in the component (8-12 lines) - Production checklist item: "Does your agent UI resume threads correctly after a browser refresh?" Tone: Direct, technical, peer-to-peer. No fluff. Audience is senior Angular engineers.`, @@ -142,7 +142,7 @@ Context: Production agents that take consequential actions — sending emails, d Cover: - The LangGraph interrupt() and Command.RESUME pattern - Why polling and custom websocket approaches are brittle -- The interrupt() signal in agent() and how it maps to approval state +- The interrupt() signal returned by injectAgent() and how it maps to approval state - headless and prebuilt - The three approval actions: approve, edit, cancel — and how each maps to a resume command - Code example: interrupt signal binding (8-12 lines) @@ -221,20 +221,20 @@ Tone: Direct, technical, peer-to-peer. No fluff. Audience is senior Angular engi }, { id: 'agent-api', - title: 'The agent() API', + title: 'The injectAgent() API', prompt: `Write a 400-600 word chapter for an engineering white paper titled "The Enterprise Guide to Agent UI in Angular". -Chapter topic: The agent() API +Chapter topic: The injectAgent() API -Context: @threadplane/langgraph exposes an Angular signals-based API for connecting LangGraph agents to Angular components. The core primitive is agent() — a function that returns reactive signals wired directly to the agent stream, with no manual subscription management, no zone-patching, and no token accumulation logic. +Context: @threadplane/langgraph exposes an Angular signals-based API for connecting LangGraph agents to Angular components. The core primitive is injectAgent() — an Angular DI helper that returns reactive signals wired directly to the configured agent stream, with no manual subscription management, no zone-patching, and no token accumulation logic. Cover: -- How agent() returns a LangGraphAgent with typed signals: messages(), isLoading(), error(), interrupt(), and langGraph* raw signals +- How injectAgent() returns a LangGraphAgent with typed signals: messages(), isLoading(), error(), interrupt(), and langGraph* raw signals - The provideAgent() provider and how it configures the agent endpoint and stream transport - Why the Angular signals design works with OnPush change detection out of the box - How to bind agent state directly in Angular templates without async pipe or manual subscriptions -- Code example: minimal agent() setup with template binding (10-14 lines) -- The contrast: what the equivalent hand-rolled code looks like vs. agent() in 3 lines +- Code example: minimal provideAgent() setup with injectAgent() template binding (10-14 lines) +- The contrast: what the equivalent hand-rolled code looks like vs. injectAgent() in 3 lines Tone: Direct, technical, peer-to-peer. No fluff. Audience is senior Angular engineers.`, }, diff --git a/apps/website/src/app/ag-ui/page.tsx b/apps/website/src/app/ag-ui/page.tsx index a1ad7cede..b0b2d2aab 100644 --- a/apps/website/src/app/ag-ui/page.tsx +++ b/apps/website/src/app/ag-ui/page.tsx @@ -51,7 +51,7 @@ export default async function AgUiPage() { maxWidth: 680, }} > - Build an Angular agent UI on any AG-UI-compatible runtime — LangGraph, CrewAI, Mastra, Microsoft Agent Framework, AG2, Pydantic AI, AWS Strands, CopilotKit. Same primitives, same chat surface, same testing story. + Build an Angular agent UI on any AG-UI-compatible runtime — CrewAI, Mastra, Microsoft Agent Framework, AG2, Pydantic AI, AWS Strands, CopilotKit, or LangGraph fronted by AG-UI. Same Agent contract and chat surface; runtime-specific history and checkpoint behavior stays with the backend.

@@ -111,10 +111,10 @@ export default async function AgUiPage() { id="primitives" eyebrow="Same primitives" headline="Drop-in for everything @threadplane/chat ships." - body="provideAgent registers an AG-UI client and exposes the same Agent contract that @threadplane/langgraph provides. Every chat primitive — durable threads, interrupts, subagents, generative UI, citations — works against any AG-UI runtime." + body="provideAgent registers an AG-UI client and exposes the same Agent contract that @threadplane/langgraph provides. Chat rendering, status, tool calls, generative UI, and citations use the same Angular primitives; durable checkpointed threads and history depend on the backend protocol, so use @threadplane/langgraph when you need the native LangGraph thread API." bullets={[ 'provideAgent + injectAgent — same names across adapters', - 'Identical Agent contract: messages() / status() / interrupt() / reload()', + 'Shared Agent contract: messages() / status() / reload()', 'Same A2UI surface, themes, and citations rendering', 'MockAgentTransport works the same way for tests', ]} diff --git a/apps/website/src/app/docs/page.tsx b/apps/website/src/app/docs/page.tsx index ee3369d43..049bcf737 100644 --- a/apps/website/src/app/docs/page.tsx +++ b/apps/website/src/app/docs/page.tsx @@ -30,7 +30,7 @@ const BACKENDS: Backend[] = [ { title: 'LangGraph', blurb: 'For LangChain & LangGraph backends.', - install: 'npm i @threadplane/langgraph', + install: 'npm i @threadplane/chat @threadplane/langgraph', href: '/docs/langgraph/getting-started/quickstart', logoSrc: '/logos/langgraph.svg', attribution: 'LangChain', @@ -38,7 +38,7 @@ const BACKENDS: Backend[] = [ { title: 'AG-UI', blurb: 'For CrewAI, Mastra, Pydantic AI, Strands, and more.', - install: 'npm i @threadplane/ag-ui', + install: 'npm i @threadplane/chat @threadplane/ag-ui @ag-ui/client @ag-ui/core', href: '/docs/ag-ui/getting-started/quickstart', logoSrc: '/logos/runtimes/copilotkit.svg', attribution: 'AG-UI · CopilotKit', @@ -347,8 +347,9 @@ export default function DocsLandingPage() { maxWidth: '52ch', }} > - A suite of MIT-licensed libraries for streaming agent interfaces. - Pick your backend to get started. + Streaming agent interfaces with runtime adapters, a shared Agent contract, + and a drop-in chat surface. Most packages are MIT; @threadplane/chat is + dual-licensed for noncommercial/evaluation and commercial production use.

@@ -374,7 +375,7 @@ export default function DocsLandingPage() { {b.install} - Quickstart → + Adapter quickstart → ))} @@ -384,6 +385,10 @@ export default function DocsLandingPage() { Choosing an adapter → + {' '}Want the drop-in UI first?{' '} + + Chat quickstart → +

diff --git a/apps/website/src/app/llms.txt/route.ts b/apps/website/src/app/llms.txt/route.ts index 92c2ccea2..958d255c9 100644 --- a/apps/website/src/app/llms.txt/route.ts +++ b/apps/website/src/app/llms.txt/route.ts @@ -26,14 +26,17 @@ function buildLlmsTxt(): string { '- @threadplane/ag-ui — adapter for any AG-UI-compatible backend (CrewAI, Mastra, Microsoft AF, AG2, Pydantic AI, AWS Strands, CopilotKit runtime)', '- @threadplane/render — generative UI runtime (Vercel json-render + Google A2UI)', '- @threadplane/a2ui — protocol types, JSONL parser, dynamic value resolver, and pointer helpers for A2UI streams', + '- @threadplane/middleware — backend LangGraph helpers for browser-executed client tools', '- @threadplane/licensing — offline license verification and non-blocking package check helpers', '- @threadplane/telemetry — browser, Node, and shared telemetry helpers with privacy controls', '', '## Install', '# LangGraph backend:', 'npm install @threadplane/chat @threadplane/langgraph', + '# LangGraph backend with browser client tools:', + 'npm install @threadplane/chat @threadplane/langgraph @threadplane/middleware', '# AG-UI backend:', - 'npm install @threadplane/chat @threadplane/ag-ui', + 'npm install @threadplane/chat @threadplane/ag-ui @ag-ui/client @ag-ui/core', '', '## Key API (symmetric across adapters)', '- provideAgent(config) — wires the adapter into Angular DI. Same name across @threadplane/langgraph and @threadplane/ag-ui.', @@ -59,7 +62,7 @@ function buildLlmsTxt(): string { '// Template: ', '', '## License', - 'MIT — free for any use, commercial or noncommercial.', + 'Most packages are MIT. @threadplane/chat is free for noncommercial/evaluation use and commercially licensed for production use.', '', '## Full reference', 'https://threadplane.ai/llms-full.txt', diff --git a/apps/website/src/app/page.tsx b/apps/website/src/app/page.tsx index daa0f5cf2..1537e9f4a 100644 --- a/apps/website/src/app/page.tsx +++ b/apps/website/src/app/page.tsx @@ -43,7 +43,7 @@ export default async function HomePage() { headline="Build the Angular UI layer for production agents." body={ <> - provideAgent + agent() give you headless chat, durable threads, interrupts, tool progress, and generative UI. LangGraph and AG-UI adapters share the contract, so teams can swap runtimes without rewriting the Angular surface. + provideAgent + injectAgent() give you headless chat, durable threads, interrupts, tool progress, and generative UI. LangGraph and AG-UI adapters share the contract, so teams can swap runtimes without rewriting the Angular surface. } bullets={[ @@ -53,9 +53,9 @@ export default async function HomePage() { 'One Angular UI layer, swappable runtimes', ]} supportingCards={[ - { title: 'provideAgent', description: 'Wire the agent into your app.config.ts.' }, - { title: 'AgUiAdapter', description: 'Any AG-UI compliant backend.' }, - { title: 'LangGraphAdapter', description: 'Native LangGraph streaming.' }, + { title: 'provideAgent', description: 'Wire the agent into Angular DI.' }, + { title: '@threadplane/ag-ui', description: 'Any AG-UI compliant backend.' }, + { title: '@threadplane/langgraph', description: 'Native LangGraph streaming.' }, ]} cta={{ label: 'Read the streaming guide', href: '/docs/langgraph/guides/streaming' }} visual={ @@ -108,12 +108,12 @@ export default async function HomePage() { id="ship" eyebrow="Ship" headline="Patterns built for production, not demos." - body="Error boundaries, observability hooks, fallback strategies — the stuff that turns a demo into a real app. MIT-licensed, so the code is yours forever." + body="Error boundaries, observability hooks, fallback strategies — the stuff that turns a demo into a real app. Most packages are MIT; the drop-in chat package is commercially licensed for production use." bullets={[ 'error() / status() / reload() signals', 'Readiness gate + per-component fallback', 'Thread persistence patterns', - 'MIT licensed — own it forever', + 'Clear package licensing', ]} supportingCards={[ { title: 'error/status/reload', description: 'Boundary signals for every agent.' }, diff --git a/apps/website/src/components/docs/DocsSearch.tsx b/apps/website/src/components/docs/DocsSearch.tsx index 608b8786a..618869b60 100644 --- a/apps/website/src/components/docs/DocsSearch.tsx +++ b/apps/website/src/components/docs/DocsSearch.tsx @@ -1,28 +1,39 @@ 'use client'; import { useState, useEffect, useRef, useCallback } from 'react'; import { useRouter } from 'next/navigation'; -import { docsConfig, type LibraryId } from '../../lib/docs-config'; +import { docsConfig, specialDocsPages, type LibraryId } from '../../lib/docs-config'; import { tokens } from '@threadplane/design-tokens'; import { analyticsEvents } from '../../lib/analytics/events'; import { track } from '../../lib/analytics/client'; interface SearchablePage { title: string; - slug: string; - section: string; - library: LibraryId; + description?: string; + href: string; + slug?: string; + section?: string; + library?: LibraryId; libraryTitle: string; } -const allSearchablePages: SearchablePage[] = docsConfig.flatMap((lib) => - lib.sections.flatMap((s) => - s.pages.map((p) => ({ - ...p, - library: lib.id, - libraryTitle: lib.title, - })) - ) -); +const allSearchablePages: SearchablePage[] = [ + ...specialDocsPages.map((page) => ({ + title: page.title, + description: page.description, + href: page.path, + libraryTitle: 'Start here', + })), + ...docsConfig.flatMap((lib) => + lib.sections.flatMap((s) => + s.pages.map((p) => ({ + ...p, + href: `/docs/${lib.id}/${p.section}/${p.slug}`, + library: lib.id, + libraryTitle: lib.title, + })) + ) + ), +]; export function DocsSearch({ library }: { library?: LibraryId }) { const [open, setOpen] = useState(false); @@ -34,11 +45,12 @@ export function DocsSearch({ library }: { library?: LibraryId }) { const results = query.length > 0 ? allSearchablePages.filter((p) => p.title.toLowerCase().includes(query.toLowerCase()) || - p.slug.toLowerCase().includes(query.toLowerCase()) || - p.section.toLowerCase().includes(query.toLowerCase()) || + p.description?.toLowerCase().includes(query.toLowerCase()) || + p.slug?.toLowerCase().includes(query.toLowerCase()) || + p.section?.toLowerCase().includes(query.toLowerCase()) || p.libraryTitle.toLowerCase().includes(query.toLowerCase()) ).slice(0, 8) - : allSearchablePages.filter((p) => !library || p.library === library).slice(0, 6); + : allSearchablePages.filter((p) => !library || !p.library || p.library === library).slice(0, 6); const handleKeyDown = useCallback((e: KeyboardEvent) => { if ((e.metaKey || e.ctrlKey) && e.key === 'k') { @@ -64,12 +76,12 @@ export function DocsSearch({ library }: { library?: LibraryId }) { const navigate = (page: SearchablePage) => { track(analyticsEvents.docsSearchResultClick, { surface: 'docs', - destination_url: `/docs/${page.library}/${page.section}/${page.slug}`, + destination_url: page.href, library: page.library === 'langgraph' || page.library === 'render' || page.library === 'chat' ? page.library : 'unknown', query_length: query.length, result_count: results.length, }); - router.push(`/docs/${page.library}/${page.section}/${page.slug}`); + router.push(page.href); setOpen(false); }; @@ -125,7 +137,7 @@ export function DocsSearch({ library }: { library?: LibraryId }) {
{results.map((page, i) => (
+ + {libConfig?.demoUrl && ( diff --git a/apps/website/src/components/landing/Differentiator.tsx b/apps/website/src/components/landing/Differentiator.tsx index 3c0f37d4b..4404e7267 100644 --- a/apps/website/src/components/landing/Differentiator.tsx +++ b/apps/website/src/components/landing/Differentiator.tsx @@ -59,9 +59,9 @@ const PRODUCTION_ROWS: ProductionRow[] = [ primitive: 'event hooks, opt-in only', }, { - need: 'MIT + self-hosted', + need: 'Open adapters + self-hosted', description: 'Own the primitives long-term, no vendor lock-in.', - primitive: 'MIT-licensed, no runtime SaaS dependency', + primitive: 'MIT adapters, no runtime SaaS dependency', }, ]; diff --git a/apps/website/src/components/landing/Hero.spec.tsx b/apps/website/src/components/landing/Hero.spec.tsx index b0bd4739b..5cbdc4bfb 100644 --- a/apps/website/src/components/landing/Hero.spec.tsx +++ b/apps/website/src/components/landing/Hero.spec.tsx @@ -60,9 +60,9 @@ describe('Hero', () => { it('primary CTA copies the install command and fires cta_id=hero_install', async () => { const { Hero } = await import('./Hero'); render(); - const btn = screen.getByRole('button', { name: /Install @threadplane\/chat/i }); + const btn = screen.getByRole('button', { name: /Install LangGraph starter/i }); fireEvent.click(btn); - expect(writeTextMock).toHaveBeenCalledWith('npm install @threadplane/chat'); + expect(writeTextMock).toHaveBeenCalledWith('npm install @threadplane/chat @threadplane/langgraph'); expect(trackMock).toHaveBeenCalledWith('marketing:cta_click', expect.objectContaining({ cta_id: 'hero_install', track: 'developer', diff --git a/apps/website/src/components/landing/Hero.tsx b/apps/website/src/components/landing/Hero.tsx index e326cf6cd..2d21342a3 100644 --- a/apps/website/src/components/landing/Hero.tsx +++ b/apps/website/src/components/landing/Hero.tsx @@ -12,7 +12,7 @@ import { track } from '../../lib/analytics/client'; import { analyticsEvents } from '../../lib/analytics/events'; import { HERO_SUBHEAD, POSITIONING_PROOF_POINTS } from '../../lib/positioning'; -const INSTALL_COMMAND = 'npm install @threadplane/chat'; +const INSTALL_COMMAND = 'npm install @threadplane/chat @threadplane/langgraph'; const COPY_FEEDBACK_MS = 1500; function PrimaryInstallButton() { @@ -35,7 +35,7 @@ function PrimaryInstallButton() { return ( ); } @@ -77,7 +77,7 @@ export function Hero() { {/* Left column */}
- Threadplane · MIT framework + Threadplane · Angular agent UI

= 2 ? pathParts[1] : ''; const activeSection = isDocsPage && pathParts.length >= 3 ? pathParts[2] : ''; const activeSlug = isDocsPage && pathParts.length >= 4 ? pathParts[3] : ''; + const initialMobileLibrary = docsConfig.find((lib) => lib.id === activeLibrary)?.id ?? 'langgraph'; const [mobileTab, setMobileTab] = useState<'site' | 'docs'>(isDocsPage ? 'docs' : 'site'); @@ -100,9 +101,14 @@ export function Nav() { } return () => { document.body.style.overflow = ''; }; }, [open]); - const [mobileLibrary, setMobileLibrary] = useState(activeLibrary || 'langgraph'); + const [mobileLibrary, setMobileLibrary] = useState(initialMobileLibrary); const [openSections, setOpenSections] = useState>(() => new Set(activeSection ? [activeSection] : [])); + useEffect(() => { + const nextLibrary = docsConfig.find((lib) => lib.id === activeLibrary)?.id; + if (nextLibrary) setMobileLibrary(nextLibrary); + }, [activeLibrary]); + const toggleSection = (id: string) => { setOpenSections(prev => { const next = new Set(prev); @@ -257,12 +263,44 @@ export function Nav() { {/* Library sub-tabs — only when Docs tab active */} {isDocsPage && mobileTab === 'docs' && ( -
- {docsConfig.map(lib => ( - - ))} +
+
+ {specialDocsPages.map((page) => { + const isActive = pathname === page.path; + return ( + { + trackCtaClick({ + surface: 'mobile_nav', + destination_url: page.path, + cta_id: 'mobile_nav_docs_page', + cta_text: page.title, + library: 'unknown', + }); + setOpen(false); + }} + style={{ + display: 'block', padding: '12px 14px', borderRadius: 8, + fontSize: 16, lineHeight: '24px', minHeight: 44, + color: isActive ? tokens.colors.accent : tokens.colors.textSecondary, + background: isActive ? tokens.colors.accentSurface : 'transparent', + textDecoration: 'none', fontFamily: 'Inter, sans-serif', fontWeight: 600, + }} + > + {page.title} + + ); + })} +
+
+ {docsConfig.map(lib => ( + + ))} +
)} @@ -271,7 +309,12 @@ export function Nav() {
{currentLib.demoUrl && ( { trackExternalLinkClick(currentLib.demoUrl!, { surface: 'mobile_nav', cta_id: `mobile_nav_docs_demo_${currentLib.id}`, cta_text: currentLib.demoLabel ?? 'Live demo' }); setOpen(false); }} + onClick={() => { + const demoUrl = currentLib.demoUrl; + if (!demoUrl) return; + trackExternalLinkClick(demoUrl, { surface: 'mobile_nav', cta_id: `mobile_nav_docs_demo_${currentLib.id}`, cta_text: currentLib.demoLabel ?? 'Live demo' }); + setOpen(false); + }} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '12px 14px', borderRadius: 8, minHeight: 44, color: tokens.colors.accent, background: tokens.colors.accentSurface, textDecoration: 'none', fontFamily: 'Inter, sans-serif', fontWeight: 600 }}> {currentLib.demoLabel ?? 'Live demo'} diff --git a/apps/website/src/lib/docs-config.ts b/apps/website/src/lib/docs-config.ts index 4de1314ae..a299e3d3d 100644 --- a/apps/website/src/lib/docs-config.ts +++ b/apps/website/src/lib/docs-config.ts @@ -35,12 +35,16 @@ export interface DocsLibrary { export interface SpecialDocsPage { path: string; contentPath: string; + title: string; + description: string; } export const specialDocsPages: SpecialDocsPage[] = [ { path: '/docs/choosing-an-adapter', contentPath: 'choosing-an-adapter/index.mdx', + title: 'Choosing an adapter', + description: 'Decide between @threadplane/langgraph and @threadplane/ag-ui for your Angular agent UI.', }, ]; diff --git a/apps/website/src/lib/positioning.ts b/apps/website/src/lib/positioning.ts index 8da15720f..096fdf2e3 100644 --- a/apps/website/src/lib/positioning.ts +++ b/apps/website/src/lib/positioning.ts @@ -9,7 +9,7 @@ export interface PositioningProofPoint { } export const POSITIONING_PROOF_POINTS: readonly PositioningProofPoint[] = [ - { label: 'LangGraph + AG-UI', href: '/docs/langgraph/concepts/langgraph-basics' }, + { label: 'LangGraph + AG-UI', href: '/docs/choosing-an-adapter' }, { label: 'Durable threads', href: '/docs/langgraph/guides/persistence' }, { label: 'Interrupts', href: '/docs/langgraph/guides/interrupts' }, { label: 'Subagents', href: '/docs/langgraph/guides/subgraphs' }, diff --git a/apps/website/src/lib/site-metadata.ts b/apps/website/src/lib/site-metadata.ts index 6fe7d5a4f..6a5744005 100644 --- a/apps/website/src/lib/site-metadata.ts +++ b/apps/website/src/lib/site-metadata.ts @@ -62,7 +62,7 @@ export function createPageMetadata({ } export function getSitemapRoutes(): string[] { - const staticRoutes = ['/', '/angular', '/render', '/chat', '/pricing', '/solutions', '/pilot-to-prod', '/docs', '/blog', '/contact']; + const staticRoutes = ['/', '/langgraph', '/render', '/chat', '/ag-ui', '/pricing', '/solutions', '/pilot-to-prod', '/docs', '/blog', '/contact']; const solutionRoutes = getAllSolutionSlugs().map((slug) => `/solutions/${slug}`); const docsRoutes = docsConfig.flatMap((library) => library.sections.flatMap((section) => diff --git a/libs/chat/src/lib/compositions/chat/chat.component.ts b/libs/chat/src/lib/compositions/chat/chat.component.ts index 7ca5777a6..99265d293 100644 --- a/libs/chat/src/lib/compositions/chat/chat.component.ts +++ b/libs/chat/src/lib/compositions/chat/chat.component.ts @@ -570,8 +570,8 @@ export class ChatComponent { * * Matches the same `streaming + current` condition the bubble uses * to enable `.chat-message__caret`: - * `agent().isLoading() && i === agent().messages().length - 1` - * `i === agent().messages().length - 1` + * `this.agent().isLoading() && i === this.agent().messages().length - 1` + * `i === this.agent().messages().length - 1` * * Restricted to assistant role because the caret only renders on * assistant bubbles (`:host([data-role="assistant"][data-current=... diff --git a/libs/langgraph/src/lib/agent-lifecycle-registry.ts b/libs/langgraph/src/lib/agent-lifecycle-registry.ts index 41c7e5d28..313d06b88 100644 --- a/libs/langgraph/src/lib/agent-lifecycle-registry.ts +++ b/libs/langgraph/src/lib/agent-lifecycle-registry.ts @@ -7,8 +7,8 @@ import type { AgentLifecycle } from './lifecycle'; * an Angular injection context. External instrumentation packages * (e.g. cockpit-telemetry) provide this token and read from it. * - * `@threadplane/langgraph` does NOT provide this itself — `agent()` writes to - * the registry only when an external consumer has provided it. + * `@threadplane/langgraph` does NOT provide this itself — the configured agent + * instance writes to the registry only when an external consumer has provided it. */ @Injectable() export class AgentLifecycleRegistry {