Skip to content

fix(chat+langgraph): streaming complex-content + GFM markdown + model picker (chat 0.0.18, langgraph 0.0.10)#191

Merged
blove merged 7 commits into
mainfrom
claude/chat-streaming-and-markdown-fixes
May 4, 2026
Merged

fix(chat+langgraph): streaming complex-content + GFM markdown + model picker (chat 0.0.18, langgraph 0.0.10)#191
blove merged 7 commits into
mainfrom
claude/chat-streaming-and-markdown-fixes

Conversation

@blove
Copy link
Copy Markdown
Contributor

@blove blove commented May 3, 2026

Summary

Stacks on top of #189 (chat 0.0.17). Four commits, two version bumps.

fix(langgraph) — stream complex-content correctly + dedupe assistant bubbles

OpenAI gpt-5/o-series Responses API streams typed content blocks ([{type:'text',text:'…',index}]) and emits two parallel views of the same assistant message: per-event AIMessageChunks on messages-tuple and the canonical ai on values-sync. The bridge wasn't equipped for either — chunks JSON-dumped into the bubble and the two paths each spawned their own bubble that never collapsed.

Fixes in stream-manager.bridge.ts:

  • extractText walks complex-content arrays, pulls text / output_text blocks
  • accumulateContent merges chunk content into the prior slot (handles superset / prefix / pure-delta cases); always returns string for stable downstream prefix-matching
  • normalizeMessageType collapses AIMessage / AIMessageChunk / ai / assistant to ai so dedupe matches across paths
  • AIMessageChunk fallback in mergeMessages: when an AIMessageChunk arrives without id-match or content-prefix-match, accumulate into the trailing AI message (Responses API emits per-event ids, not message ids)
  • Drop empty-AI placeholders from state.messages before the values-sync merge — keeping them creates a phantom slot that competes with the chunk stream
  • collapseAdjacentAi post-pass merges adjacent AI messages where one's text is a prefix/equal of the other

Also drops an obsolete hand-rolled rawMessages throttle in agent.fn.ts. Bumps @ngaf/langgraph 0.0.9 → 0.0.10.

fix(chat) — render markdown with full GFM coverage + visible CommonMark styles

Two related rendering bugs converged: chat showed raw JSON arrays, then after fixing that, plain unstyled paragraphs with no heading sizes, bullets, or table borders.

  • messageContent shared util: was JSON-stringifying complex-content arrays. Now extracts text / output_text blocks the same way the langgraph adapter does.
  • chat-streaming-md: switched to ViewEncapsulation.None and wired in CHAT_MARKDOWN_STYLES. The component renders sanitized HTML via innerHTML, so emulated encapsulation was silently skipping every selector below :host.
  • CHAT_MARKDOWN_STYLES: re-scoped from :host to chat-streaming-md element selectors, expanded to full CommonMark + GFM (h1–h6 hierarchy, strong / em / del / mark / sub / sup, links with hover, bullet / ordered / nested lists with visible markers, GFM task-list checkboxes, inline code / fenced pre, blockquote, hr, GFM table (bordered + header bg), img (max-width)).
  • marked enabled with gfm: true, breaks: true.

Bumps @ngaf/chat 0.0.17 → 0.0.18.

feat(chat) — first-class model picker on <chat> + welcome suggestions as pills

  • New [modelOptions] + [(selectedModel)] + [modelPickerPlaceholder] inputs project a <chat-select chatInputModelSelect> into the input pill on both welcome and conversation views — no manual slot wiring for the common case.
  • Welcome suggestions restyled from full-width stacked rows with dividers to centered floating pills (border-radius: 9999px, surface bg, flex-wrap: wrap, equal gap). Matches the input pill aesthetic.

chore — gpt-4o-mini → gpt-5-mini sweep

Pure model-name swap across cockpit/** demo graphs/docs and docs/superpowers/{plans,specs}/**. Reasoning models stream visibly with the langgraph adapter's reasoning.effort='minimal' default.

Test plan

  • Smoke against LangGraph + OpenAI gpt-5-mini: pick suggestion, single bubble streams visibly, markdown bullets/headings/code formatted
  • Markdown sampler renders headings, bold/italic/strike, links, blockquote, hr, lists (incl. task), GFM tables, fenced code
  • No left-flash on optimistic user message after suggestion click
  • CI green

🤖 Generated with Claude Code

blove and others added 4 commits May 3, 2026 09:54
…ubbles

OpenAI's gpt-5/o-series Responses API streams `BaseMessage.content` as
arrays of typed blocks (`[{type:'text',text:'…',index:n}, …]`) rather
than accumulated strings, and emits two parallel views of the same
assistant message: per-event `AIMessageChunk`s on `messages-tuple` and
the canonical `ai` on `values`-sync. The bridge wasn't equipped for
either — chunks JSON-dumped into the bubble and the two paths each
spawned their own bubble that never collapsed.

Bridge fixes (`stream-manager.bridge.ts`):
- `extractText` walks complex-content arrays and pulls visible text
  blocks (`text` / `output_text`), skipping reasoning / tool_use /
  image blocks.
- `accumulateContent` merges incoming-chunk content into the prior
  slot's accumulated text. Handles the three cases: incoming is a
  strict superset (final-id swap), existing is a strict superset
  (chunk arrives after canonical), or pure delta append. Always
  returns string so downstream `findContentMatch` can prefix-compare
  cleanly without `JSON.stringify`-ing the array.
- `normalizeMessageType` collapses `AIMessage` / `AIMessageChunk` /
  `ai` / `assistant` to `ai` so `findContentMatch` and
  `sameRoleAndContent` correctly match across the values-sync and
  messages-tuple paths.
- `mergeMessages` gains an AIMessageChunk fallback: when an
  AIMessageChunk arrives without an id-match or content-prefix match,
  accumulate into the trailing AI message. The OpenAI Responses API
  emits per-chunk *event* ids, not message ids, so consecutive chunks
  would otherwise each create a fresh bubble.
- Empty-content AI placeholders are dropped from `state.messages`
  before the values-sync merge — keeping them creates a phantom slot
  that competes with the chunk-streamed AIMessageChunk.
- `collapseAdjacentAi` post-pass collapses adjacent AI messages where
  one's text is a prefix/equal of the other, keeping the older slot's
  id for stable track-by-id.

Also dropped an obsolete hand-rolled rawMessages throttle in
`agent.fn.ts` — `messages$` already emits at the bridge boundary and
extra signal-side throttling collapsed visible token streaming.

Bumps @ngaf/langgraph 0.0.9 → 0.0.10.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…k styles

Two related rendering bugs converged into a chat that showed raw JSON
arrays and then, after fixing that, plain unstyled paragraphs with no
heading sizes, bullets, or table borders.

`messageContent` shared util:
- Was JSON-stringifying complex-content arrays, dumping
  `[{"type":"text",…}]` into the chat bubble for OpenAI gpt-5 / o-series
  output. Now extracts visible `text` / `output_text` blocks the same
  way the langgraph adapter does and joins them; everything else
  (reasoning, tool_use, images) is skipped.

`chat-streaming-md` component:
- Switched to `ViewEncapsulation.None`. The component renders markdown
  by assigning sanitized HTML to `innerHTML`, so the resulting `<ul>`,
  `<p>`, `<table>`, etc. nodes never carry the `_ngcontent-…`
  attribute that emulated encapsulation requires. Without this the
  exported `CHAT_MARKDOWN_STYLES` were silently skipped for every
  selector below `:host`.
- Wired `CHAT_MARKDOWN_STYLES` into the component (it was exported but
  never applied anywhere).

`CHAT_MARKDOWN_STYLES`:
- Re-scoped from `:host` to `chat-streaming-md` element selectors so
  the rules stay locally meaningful under `ViewEncapsulation.None`.
- Expanded coverage to the full CommonMark + GFM surface: heading
  hierarchy (h1–h6 with size scale), `strong` / `b`, `em` / `i`,
  `del` / `s`, `mark`, `sub` / `sup`, `a` (with hover), bullet /
  ordered / nested lists with visible `disc` / `circle` / `decimal`
  markers, GFM task-list checkboxes, inline `code` / fenced `pre`,
  `blockquote`, `hr`, GFM `table` (bordered, header background), and
  `img` (max-width).

`marked` options:
- Enabled `gfm: true, breaks: true` so single newlines render as
  `<br>` (matching common chat UX) and tables / strikethrough / task
  lists / autolinks are honored.

Bumps @ngaf/chat 0.0.17 → 0.0.18.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…estions as pills

Two UX improvements driven by smoke-testing the chat-select primitive in
real apps.

`<chat>` composition:
- New `[modelOptions]`, `[(selectedModel)]`, `[modelPickerPlaceholder]`
  inputs that, when populated, render a `<chat-select chatInputModelSelect>`
  inside the chat-input pill on both the welcome screen and the
  conversation footer. Consumers no longer have to project the slot
  themselves for the common "model picker" use case — they just pass
  options + a model signal.
- Slot projection still works in conversation mode for any consumer
  that needs custom chat-input children (an inner `ngProjectAs`
  bridges the outer `[chatInputModelSelect]` content through).

Welcome suggestions:
- Restyled from full-width stacked rows separated by dividers to
  centered floating pills (`border-radius: 9999px`, surface
  background, equal gap). Matches the chat-input pill aesthetic.
- Container becomes `flex-wrap` + `justify-content: center` + `gap: 8px`
  so suggestions reflow naturally on narrow viewports.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…5-mini

Default model in every demo graph and prose example is now gpt-5-mini.
Reasoning models (gpt-5/o-series) stream visibly out of the box at
`reasoning.effort='minimal'`, which the langgraph adapter sets by
default — no developer-facing config needed. Older non-reasoning
gpt-4o-mini references in graphs and docs were stale.

No code path changed; pure model-name swap across cockpit examples
and docs/superpowers plan / spec files.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 3, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cacheplane Ready Ready Preview, Comment May 4, 2026 3:31am

Request Review

blove and others added 3 commits May 3, 2026 19:39
Design spec for the next chat phase: surface model reasoning content
as a first-class collapsible "Thinking… / Thought for Ns" pill above
the assistant response, and turn tool-call rendering into a first-
class extension surface via a chatToolCallTemplate directive while
keeping a polished default that auto-collapses completed cards and
groups sequential same-name calls.

Lands one new primitive (<chat-reasoning>), one new directive
(chatToolCallTemplate, including a "*" wildcard catch-all), augments
two existing primitives (<chat-tool-calls>, <chat-tool-call-card>),
and adds two new optional fields (Message.reasoning,
Message.reasoningDurationMs) populated by both adapters from
provider-agnostic sources: LangGraph complex-content reasoning blocks
and AG-UI REASONING_MESSAGE_* events.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The prior assertion checked that JSON-stringified output contained the
literal string 'text' (the JSON key from {type:'text',...}). After the
0.0.18 fix to extract visible text instead of dumping JSON, the
assertion is wrong by construction. Replaced with two assertions:
single-block text extraction, and multi-block concatenation that skips
reasoning blocks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Eleven phases, ~70 tasks. TDD throughout for new primitives + the
reasoning conformance fixture. Each task is self-contained with full
context, exact paths, complete code, and explicit verification
commands. Subagent-friendly per superpowers:subagent-driven-development.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@blove blove merged commit cccd844 into main May 4, 2026
14 checks passed
@blove blove deleted the claude/chat-streaming-and-markdown-fixes branch May 7, 2026 16:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant