The short version
- Strive's core product is the chat. The agent knows a customer's marketing context in and out, and replies in rich generative UI — charts, tables, forms, whole interactive surfaces.
- Nothing off the shelf. Data model, interface contract, streaming, validation layer — all built from scratch.
- One component, one streaming contract, one block vocabulary. Every agent team in the company writes against it instead of building their own UI.
The vocabulary I designed
- Blocks — the singular unit. Interactive controls, presentation elements, rich charts, tables. The model composes an answer out of them.
- Slots — a canvas beside the thread for things too big to inline: artifacts, editors, full agent interfaces that behave like standalone products. We shipped this before the industry settled on calling it a canvas or artifact panel.
- Contracts — every streamed block is Zod-validated at the boundary, so a confused model degrades gracefully instead of blanking the screen.
Worth calling out
- Block-by-block streaming — the UI assembles itself as the model thinks.
- A composer that does real work: file attachments, slash commands, skills, response modes, dictation.
- Multiple concurrent conversations with centralised abort control.
- Human-in-the-loop tools — the agent can stop and ask before it acts.
- Reasoning traces, agent presence, edit-and-resend, grounding citations with source favicons, a conversation minimap, next-step suggestions after every turn.
- Sidebar history with search, date bucketing, infinite scroll, rename and delete; in-thread pagination that loads older messages on scroll-to-top without losing scroll position.
The rebuild that fixed reliability
The first design asked the model for one big JSON payload holding every block. It worked until answers got complex — several charts, tables, interactive elements — and then it didn't: malformed JSON, truncated responses, silent retry loops.
BEFORE
LLM ─▶ one JSON blob ─▶ CONTENT_STREAM ─▶ parser ─▶ blocks
AFTER
LLM ─▶ individual tool calls ─▶ TOOL_STREAM_START/CONTENT
─▶ per-tool parsing ─▶ blocks
(plain text keeps streaming alongside)
- Each tool call parses on its own, so one bad block can't take the answer down with it.
- Smaller units mean the model stops trying to hold a whole document in its head — fewer truncations, fewer retries.
- Complexity became additive: richer answers just mean more tool calls, not a bigger blob.
Where it shows up
Mounted at the product's home route, embedded as a side panel in agent workspaces, and shipped as a sheet inside individual agent screens. Routines get created in it, reports get built in it, agent output gets reviewed and acted on in it.
Under the hood
- Core
- React 19, TypeScript, Vite 6
- State
- Zustand, TanStack Query, Context for live sessions
- Streaming
- Fetch + SSE, custom incremental JSON parser
- UI
- Tailwind, Radix, in-house
ui-core/ui-blocks, Storybook - Rendering
- streamdown + react-markdown, Recharts, TanStack Table
- Validation
- Zod schemas on every streamed block
- Models
- Claude and OpenAI, configurable per deployment
- Backend
- Fastify (Node)
- Platform
- Clerk auth, PostHog analytics
- Testing
- Vitest, Testing Library, MSW, Playwright E2E
- Design system
- Storybook, an internal doc site, and Figma, kept in sync
Chat is the easy part. The contract underneath it is the product.