close
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
53a1554
docs(plan): ai search optimization plan for threadplane.ai
blove Aug 20, 2026
2ddc788
feat(website): search console api service-account auth
blove Aug 20, 2026
deaa509
feat(website): typed search console api wrappers
blove Aug 20, 2026
93b6617
fix(website): cache search console access tokens
blove Aug 20, 2026
3ddf41b
feat(website): search console snapshot puller
blove Aug 20, 2026
3d3f3d2
fix(website): keep partial inspection results and fail loudly on a ba…
blove Aug 20, 2026
5852de3
feat(website): search console analysis report
blove Aug 20, 2026
c7cd907
test(website): cover analysis boundaries and harden the gsc report
blove Aug 20, 2026
68f1eec
docs(plan): correct website test command (no nx test target exists)
blove Aug 20, 2026
2c435fc
test(website): fix stale positioning proof-point assertion
blove Aug 20, 2026
87a7979
feat(website): emit honest lastmod in sitemap
blove Aug 20, 2026
160f84e
fix(website): detect shallow clones through the common git dir
blove Aug 20, 2026
207e49a
refactor(website): extract sitemap dates and close date-fabrication p…
blove Aug 20, 2026
532b8b1
feat(website): article metadata + canonical brand spelling
blove Aug 20, 2026
7c88eae
refactor(website): single-source the blog modified time
blove Aug 20, 2026
65aa7c7
feat(website): schema.org json-ld builders
blove Aug 20, 2026
4df555f
test(website): make json-ld round-trip assertions real
blove Aug 20, 2026
f47e15b
feat(website): mount json-ld on layout, blog, and docs
blove Aug 20, 2026
651842f
test(website): pin breadcrumb and description invariants at their rea…
blove Aug 20, 2026
b793d8d
fix(website): stop the per-post OpenGraph image route 500ing
blove Aug 20, 2026
da7a09e
perf(website): prerender per-post og cards
blove Aug 20, 2026
c6aa8fe
fix(website): keep anchor glyphs out of heading text
blove Aug 20, 2026
35f573d
test(website): guard the css-generated heading anchor
blove Aug 20, 2026
baa5ddb
feat(website): add /about page carrying a Person entity
blove Aug 20, 2026
f733f12
feat(website): unify the author entity across blog and about
blove Aug 20, 2026
2a46113
docs(blog): question-form section headings in the 2026-05/06 posts
blove Aug 20, 2026
3339d6b
docs(website): record the no-scaled-content rule for solutions pages
blove Aug 20, 2026
b95a201
feat(website): track ai crawler and ai referral traffic
blove Aug 20, 2026
3413824
fix(website): bound ai-traffic event volume and cover the capture pay…
blove Aug 20, 2026
c6aa2b9
revert(website): keep ai-traffic middleware on the edge runtime
blove Aug 20, 2026
9f532fd
docs(blog): architecture diagrams for the tutorial posts
blove Aug 20, 2026
1e1c44f
docs(gtm): ai search measurement runbook
blove Aug 20, 2026
5ffe40d
fix(blog): size diagrams for the real prose column
blove Aug 20, 2026
be35710
docs(gtm): scope the dedup claim to crawler events
blove Aug 20, 2026
29857e1
chore: ignore local service-account keys directory
blove Aug 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,6 @@ libs/licensing/src/lib/license-public-key.generated.ts
examples/ag-ui/angular/src/environments/generated-keys.local.ts
# Chat example generated API keys (injected from .env at build time)
examples/chat/angular/src/environments/generated-keys.local.ts

# Local service-account keys (GSC, etc). Never commit these.
keys/
1 change: 1 addition & 0 deletions apps/website/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.gsc/
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Most AI chat features still ship without streaming — they buffer the full resp
- Wire a real LangGraph backend to the UI without writing any transport code.
- Cover the three production patterns that matter once the scaffold works: errors, threads, and generative UI.

## Why streaming matters
## Why does streaming matter?

A user reads at roughly 200 to 300 words per minute; a modern model produces tokens faster than that. If you stream, the user starts reading before the model has finished. If you buffer, every response feels like a page load with no progress indicator.

Expand Down Expand Up @@ -160,7 +160,7 @@ The slot pattern is intentional: the chat doesn't set your welcome copy, pick yo

Theming is a separate concern. The chat reads from CSS custom properties — `--chat-bg`, `--chat-fg`, `--chat-accent`, and a few dozen more. If you already use a design system, map your tokens onto theirs in a single stylesheet and the chat picks them up.

## What's happening under the hood
## What's happening under the hood?

Let's peek at the contract. The adapter exposes a small surface, the chat consumes it, and everything else is implementation detail.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ Three boxes. Two seams.

**The wire.** Server-Sent Events. Plain HTTP, no WebSocket gymnastics, no custom binary framing. Your firewall, load balancer, and reverse proxy already know what to do with it.

**The Angular side.** This is what ThreadPlane provides. `@threadplane/ag-ui` is the adapter. It consumes the AG-UI event stream and exposes a runtime-neutral `Agent` contract built from signals. `@threadplane/chat` is the UI. It reads from that contract and renders. The two are decoupled on purpose. We'll get to why.
**The Angular side.** This is what Threadplane provides. `@threadplane/ag-ui` is the adapter. It consumes the AG-UI event stream and exposes a runtime-neutral `Agent` contract built from signals. `@threadplane/chat` is the UI. It reads from that contract and renders. The two are decoupled on purpose. We'll get to why.

## Let's wire it up

Expand Down Expand Up @@ -152,7 +152,7 @@ No `EventSource`. No reducer. No manual subscribe-and-render plumbing. No store.

Spin up your agent backend, point `url` at it, and the chat just works.

## How AG-UI events become signals
## How do AG-UI events become signals?

The AG-UI protocol has seventeen event types, grouped into five families:

Expand All @@ -164,7 +164,7 @@ The AG-UI protocol has seventeen event types, grouped into five families:

The families each do specific work. Lifecycle answers "is something happening?" Text messages are the streaming triad familiar from chat UIs. Tool calls are deliberately incremental so you can render the *intent* before the arguments are fully formed. State sync uses RFC 6902 JSON Patch so the wire stays small even when the agent's state is large.

ThreadPlane's `@threadplane/ag-ui` runs each event through a small reducer that updates a handful of signals on the `Agent` contract:
Threadplane's `@threadplane/ag-ui` runs each event through a small reducer that updates a handful of signals on the `Agent` contract:

- `messages()`: `Message[]`, the chat history. `TEXT_MESSAGE_CONTENT` appends a delta to the in-flight assistant message.
- `status()`: `'idle' | 'running' | 'error' | 'paused'`. Driven by the `RUN_*` events.
Expand Down Expand Up @@ -267,7 +267,7 @@ How you scope threads — per project, per task, per user session — is a produ

If you want a starting point, `@threadplane/chat` exposes a `<chat-sidebar>` primitive that handles the layout without locking you into a persistence model.

## Swap the backend without changing the UI
## Can you swap the backend without changing the UI?

This is the part that pays off the protocol bet.

Expand Down Expand Up @@ -300,6 +300,6 @@ Each of those is its own post. The point here is just that the protocol-to-signa

## Conclusion

AG-UI standardizes the wire between the agent and the UI: it's small enough to hold in your head, and the event model maps onto Angular signals cleanly. With ThreadPlane (`@threadplane/ag-ui` and `@threadplane/chat` on npm), the wiring is three lines — a provider, an inject, and a `<chat>` — which leaves the interesting work (tool cards, interrupt flows, generative UI, your design system) as the part you spend the day on.
AG-UI standardizes the wire between the agent and the UI: it's small enough to hold in your head, and the event model maps onto Angular signals cleanly. With Threadplane (`@threadplane/ag-ui` and `@threadplane/chat` on npm), the wiring is three lines — a provider, an inject, and a `<chat>` — which leaves the interesting work (tool cards, interrupt flows, generative UI, your design system) as the part you spend the day on.

The adapters are MIT; `@threadplane/chat` is source-available with a free non-commercial tier. If you're building this inside an enterprise Angular app (design system, multi-tenant, regulated), [talk to us](/contact?source=blog_ag_ui_pillar&track=enterprise).
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Everything below is running code from the cockpit example at `cockpit/langgraph/
- Render the approval dialog in Angular with the `<chat-approval-card>` composition.
- Resume, reject, or edit-then-resume — with a distinct path for each.

## When to use an interrupt
## When should you use an interrupt?

Most tool calls don't need approval. Reads, searches, and lookups can run unattended. Reach for an interrupt when a tool does something the operator wouldn't want to undo by hand: moves money, sends a customer-facing message, deletes a record, or triggers a deploy.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ That's the whole client-side delta. The rest of the file — the template bindin

`<chat-approval-card>` reads `agent.interrupt()` (a `Signal<AgentInterrupt | undefined>`), and `submit({ resume })` is part of the runtime-neutral `Agent` contract declared in `@threadplane/chat`. Both adapters populate the signal and forward the resume; the chat surface above doesn't see the wire format.

## When to use an interrupt
## When should you use an interrupt?

Most tool calls don't need approval. Reads, searches, and lookups can run unattended. Reach for an interrupt when a tool does something the operator wouldn't want to undo by hand: moves money, sends a customer-facing message, deletes a record, or triggers a deploy.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ Let the adapter own accumulation, deduplication, and lifecycle transitions.
Let the component read the result.
The [Signals guide](/docs/langgraph/concepts/angular-signals) shows the boundary in practice.

![LangGraph stream chunks and AG-UI SSE events both enter a runtime adapter that owns accumulation, deduplication and lifecycle transitions, and the adapter publishes one Agent contract of Angular signals - messages, status, toolCalls, state, error and interrupt - that chat components and an approved component registry read, with user intent travelling back through the same contract.](/blog/diagrams/agent-contract-boundary.svg)

The tradeoff is that normalization can hide useful runtime detail.
Keep an explicit event escape hatch for information that isn't durable UI state, but don't publish messages or tool calls through two competing sources.
Two sources of truth create timing bugs that are difficult to reproduce and even harder to explain to a user.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ A reading list. The user asks the assistant to save something; the assistant cal

Three tools, three different shapes, and the server implements none of them.

![The Angular chat component declares its action, view and ask client tools; the catalog travels to the FastAPI /agent endpoint where bind_client_tools binds it to the model for that run, the graph ends its turn and streams AG-UI events back over SSE, the @threadplane/ag-ui adapter reduces them into Angular signals, and the tool result the browser produces starts the next run.](/blog/diagrams/ag-ui-event-flow.svg)

## How do we get an AG-UI endpoint running?

Install the integration:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ Two Angular pieces, and they read from different places.

That split is the thing to hold onto. The agent knows about one conversation. The thread adapter knows about all of them.

![chat-sidenav renders every conversation from LangGraphThreadsAdapter while chat renders the active one from the @threadplane/langgraph agent; selecting a row sets the ACTIVE_THREAD signal, the agent adapter watches that signal and switches conversations, and onThreadId writes a newly created thread id back into it.](/blog/diagrams/langgraph-threads-and-runs.svg)

## How do we get a LangGraph server running?

Let's do the backend first, because the Angular side has nothing to bind to without it.
Expand Down
2 changes: 1 addition & 1 deletion apps/website/e2e/blog.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ test.describe('Blog landing page', () => {

// Brand eyebrow + H1
await expect(page.getByText('Blog', { exact: true }).first()).toBeVisible();
await expect(page.getByRole('heading', { level: 1, name: /Articles from ThreadPlane/i })).toBeVisible();
await expect(page.getByRole('heading', { level: 1, name: /Articles from Threadplane/i })).toBeVisible();

// Filter row contains the "All" chip in active state
await expect(page.getByText('All', { exact: true })).toBeVisible();
Expand Down
18 changes: 18 additions & 0 deletions apps/website/e2e/docs.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,24 @@ test.describe('Docs slug page', () => {
expect(id?.length).toBeGreaterThan(0);
});

test('heading permalinks carry no glyph in the text, only a CSS ::before', async ({ page }) => {
await page.goto(route);
const h2 = page.locator('article h2').first();
await expect(h2).toBeVisible();

// The `#` must never be a text node: extracted heading text feeds search
// snippets, the page outline, and anything summarizing the DOM.
expect((await h2.textContent())?.trim()).not.toContain('#');

// ...which means the visible affordance hangs entirely on one CSS rule
// (`.docs-prose h2 .heading-anchor::before` in global.css). jsdom cannot
// resolve pseudo-element content, so this is the only place it is guarded.
const anchor = h2.locator('a.heading-anchor');
await expect(anchor).toHaveCount(1);
const glyph = await anchor.evaluate((el) => getComputedStyle(el, '::before').content);
expect(glyph).toBe('"#"');
});

test('breadcrumb renders exactly once', async ({ page }) => {
await page.goto('/docs/langgraph/getting-started/introduction');
await expect(page.locator('nav[aria-label="Breadcrumb"]')).toHaveCount(1);
Expand Down
4 changes: 2 additions & 2 deletions apps/website/e2e/website.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,15 +237,15 @@ test('docs pages render canonical and social metadata', async ({ page }) => {
);
await expect(page.locator('meta[property="og:title"]')).toHaveAttribute(
'content',
'Streaming - LangGraph Docs - Threadplane',
'Streaming LangGraph Docs Threadplane',
);
await expect(page.locator('meta[property="og:url"]')).toHaveAttribute(
'content',
'https://threadplane.ai/docs/langgraph/guides/streaming',
);
await expect(page.locator('meta[name="twitter:title"]')).toHaveAttribute(
'content',
'Streaming - LangGraph Docs - Threadplane',
'Streaming LangGraph Docs Threadplane',
);
});

Expand Down
85 changes: 85 additions & 0 deletions apps/website/public/blog/diagrams/ag-ui-event-flow.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading