From fa3a4a944d94d01f0b525203be1d53e1d0de4f37 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 5 May 2026 11:12:04 -0700 Subject: [PATCH 1/5] fix(chat): dispatch table rows through component, not children MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MarkdownTableComponent template used which walked the row's children (cells) directly under /, skipping the wrapper entirely. Result: bare cells appeared under thead/tbody and the row component (with its IS_HEADER_ROW DI plumbing) was never instantiated, so cells couldn't tell whether to render or . Caught by live browser smoke testing in ~/tmp/ngaf — Chrome inspection showed the parser produced 3 row nodes but the rendered DOM had zero elements. Fixed by importing MarkdownTableRowComponent and dispatching each row through it directly. Synchronized version bump to 0.0.24 across all 16 @ngaf libs. --- libs/a2ui/package.json | 2 +- libs/ag-ui/package.json | 2 +- libs/chat/package.json | 2 +- .../views/markdown-table.component.spec.ts | 25 +++++++++++++++++++ .../views/markdown-table.component.ts | 8 +++--- libs/cockpit-docs/package.json | 2 +- libs/cockpit-registry/package.json | 2 +- libs/cockpit-shell/package.json | 2 +- libs/cockpit-testing/package.json | 2 +- libs/cockpit-ui/package.json | 2 +- libs/db/package.json | 2 +- libs/design-tokens/package.json | 2 +- libs/example-layouts/package.json | 2 +- libs/langgraph/package.json | 2 +- libs/licensing/package.json | 2 +- libs/partial-json/package.json | 2 +- libs/render/package.json | 2 +- libs/ui-react/package.json | 2 +- 18 files changed, 45 insertions(+), 20 deletions(-) diff --git a/libs/a2ui/package.json b/libs/a2ui/package.json index 5638a5e8b..d47134a0a 100644 --- a/libs/a2ui/package.json +++ b/libs/a2ui/package.json @@ -1,6 +1,6 @@ { "name": "@ngaf/a2ui", - "version": "0.0.23", + "version": "0.0.24", "license": "MIT", "repository": { "type": "git", diff --git a/libs/ag-ui/package.json b/libs/ag-ui/package.json index 4ca6733d8..dd44dde0a 100644 --- a/libs/ag-ui/package.json +++ b/libs/ag-ui/package.json @@ -1,6 +1,6 @@ { "name": "@ngaf/ag-ui", - "version": "0.0.23", + "version": "0.0.24", "peerDependencies": { "@ngaf/chat": "*", "@ngaf/licensing": "*", diff --git a/libs/chat/package.json b/libs/chat/package.json index 5a0f4a5de..6d0670ef4 100644 --- a/libs/chat/package.json +++ b/libs/chat/package.json @@ -1,6 +1,6 @@ { "name": "@ngaf/chat", - "version": "0.0.23", + "version": "0.0.24", "exports": { ".": { "types": "./index.d.ts", diff --git a/libs/chat/src/lib/markdown/views/markdown-table.component.spec.ts b/libs/chat/src/lib/markdown/views/markdown-table.component.spec.ts index edcfb3ff8..932b1fb90 100644 --- a/libs/chat/src/lib/markdown/views/markdown-table.component.spec.ts +++ b/libs/chat/src/lib/markdown/views/markdown-table.component.spec.ts @@ -61,4 +61,29 @@ describe('MarkdownTableComponent', () => { expect(fixture.nativeElement.querySelector('thead')).toBeTruthy(); expect(fixture.nativeElement.querySelector('tbody')).toBeTruthy(); }); + + it('dispatches each row through chat-md-table-row component', () => { + // Regression: prior impl used which + // walked row.children (cells) directly and skipped the row wrapper. Cells + // appeared bare under /, no elements + // existed. Live browser smoke caught this; the test below pins the fix. + const fixture = TestBed.createComponent(HostComponent); + fixture.componentInstance.node.set(makeTableNode({ + alignments: [null, null], + children: [ + { id: 2, type: 'table-row', status: 'complete', parent: null, index: 0, + isHeader: true, children: [] } as never, + { id: 3, type: 'table-row', status: 'complete', parent: null, index: 1, + isHeader: false, children: [] } as never, + { id: 4, type: 'table-row', status: 'complete', parent: null, index: 2, + isHeader: false, children: [] } as never, + ], + })); + fixture.detectChanges(); + const rows = fixture.nativeElement.querySelectorAll('chat-md-table-row'); + expect(rows.length).toBe(3); + // Header row goes in ; body rows in . + expect(fixture.nativeElement.querySelectorAll('thead chat-md-table-row').length).toBe(1); + expect(fixture.nativeElement.querySelectorAll('tbody chat-md-table-row').length).toBe(2); + }); }); diff --git a/libs/chat/src/lib/markdown/views/markdown-table.component.ts b/libs/chat/src/lib/markdown/views/markdown-table.component.ts index 71b661cf7..96f3182ae 100644 --- a/libs/chat/src/lib/markdown/views/markdown-table.component.ts +++ b/libs/chat/src/lib/markdown/views/markdown-table.component.ts @@ -2,23 +2,23 @@ // SPDX-License-Identifier: MIT import { Component, ChangeDetectionStrategy, input, computed } from '@angular/core'; import type { MarkdownTableNode, MarkdownTableRowNode } from '@cacheplane/partial-markdown'; -import { MarkdownChildrenComponent } from '../markdown-children.component'; +import { MarkdownTableRowComponent } from './markdown-table-row.component'; @Component({ selector: 'chat-md-table', standalone: true, - imports: [MarkdownChildrenComponent], + imports: [MarkdownTableRowComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: ` @if (headerRow(); as row) { - + } @for (row of bodyRows(); track $any(row)) { - + }
diff --git a/libs/cockpit-docs/package.json b/libs/cockpit-docs/package.json index 0c47a0bbf..bd79fe6c5 100644 --- a/libs/cockpit-docs/package.json +++ b/libs/cockpit-docs/package.json @@ -1,6 +1,6 @@ { "name": "@ngaf/cockpit-docs", - "version": "0.0.23", + "version": "0.0.24", "license": "MIT", "repository": { "type": "git", diff --git a/libs/cockpit-registry/package.json b/libs/cockpit-registry/package.json index 9485f1a3b..09c65328c 100644 --- a/libs/cockpit-registry/package.json +++ b/libs/cockpit-registry/package.json @@ -1,6 +1,6 @@ { "name": "@ngaf/cockpit-registry", - "version": "0.0.23", + "version": "0.0.24", "license": "MIT", "repository": { "type": "git", diff --git a/libs/cockpit-shell/package.json b/libs/cockpit-shell/package.json index 6ce830433..a2e588291 100644 --- a/libs/cockpit-shell/package.json +++ b/libs/cockpit-shell/package.json @@ -1,6 +1,6 @@ { "name": "@ngaf/cockpit-shell", - "version": "0.0.23", + "version": "0.0.24", "license": "MIT", "repository": { "type": "git", diff --git a/libs/cockpit-testing/package.json b/libs/cockpit-testing/package.json index 7262b7a65..e1ef6b44d 100644 --- a/libs/cockpit-testing/package.json +++ b/libs/cockpit-testing/package.json @@ -1,6 +1,6 @@ { "name": "@ngaf/cockpit-testing", - "version": "0.0.23", + "version": "0.0.24", "license": "MIT", "repository": { "type": "git", diff --git a/libs/cockpit-ui/package.json b/libs/cockpit-ui/package.json index bcfa9c4cf..3e46e4b6a 100644 --- a/libs/cockpit-ui/package.json +++ b/libs/cockpit-ui/package.json @@ -1,6 +1,6 @@ { "name": "@ngaf/cockpit-ui", - "version": "0.0.23", + "version": "0.0.24", "license": "MIT", "repository": { "type": "git", diff --git a/libs/db/package.json b/libs/db/package.json index aee320e67..a14f756e5 100644 --- a/libs/db/package.json +++ b/libs/db/package.json @@ -1,6 +1,6 @@ { "name": "@ngaf/db", - "version": "0.0.23", + "version": "0.0.24", "license": "MIT", "repository": { "type": "git", diff --git a/libs/design-tokens/package.json b/libs/design-tokens/package.json index e099fb72c..5adea4849 100644 --- a/libs/design-tokens/package.json +++ b/libs/design-tokens/package.json @@ -1,6 +1,6 @@ { "name": "@ngaf/design-tokens", - "version": "0.0.23", + "version": "0.0.24", "license": "MIT", "repository": { "type": "git", diff --git a/libs/example-layouts/package.json b/libs/example-layouts/package.json index 36d93472f..4bf750cda 100644 --- a/libs/example-layouts/package.json +++ b/libs/example-layouts/package.json @@ -1,6 +1,6 @@ { "name": "@ngaf/example-layouts", - "version": "0.0.23", + "version": "0.0.24", "peerDependencies": { "@angular/core": "^20.0.0 || ^21.0.0", "@angular/common": "^20.0.0 || ^21.0.0" diff --git a/libs/langgraph/package.json b/libs/langgraph/package.json index 24f8432fc..baf5ea4cf 100644 --- a/libs/langgraph/package.json +++ b/libs/langgraph/package.json @@ -1,6 +1,6 @@ { "name": "@ngaf/langgraph", - "version": "0.0.23", + "version": "0.0.24", "peerDependencies": { "@ngaf/chat": "*", "@ngaf/licensing": "*", diff --git a/libs/licensing/package.json b/libs/licensing/package.json index e7c4a2922..b8f803e83 100644 --- a/libs/licensing/package.json +++ b/libs/licensing/package.json @@ -1,6 +1,6 @@ { "name": "@ngaf/licensing", - "version": "0.0.23", + "version": "0.0.24", "license": "MIT", "repository": { "type": "git", diff --git a/libs/partial-json/package.json b/libs/partial-json/package.json index 84d51e1bd..a3285d050 100644 --- a/libs/partial-json/package.json +++ b/libs/partial-json/package.json @@ -1,6 +1,6 @@ { "name": "@ngaf/partial-json", - "version": "0.0.23", + "version": "0.0.24", "deprecated": "Replaced by @cacheplane/partial-json. No further versions will be published from this package.", "license": "MIT", "repository": { diff --git a/libs/render/package.json b/libs/render/package.json index b79ca2cc7..08f2fbcb7 100644 --- a/libs/render/package.json +++ b/libs/render/package.json @@ -1,6 +1,6 @@ { "name": "@ngaf/render", - "version": "0.0.23", + "version": "0.0.24", "peerDependencies": { "@angular/core": "^20.0.0 || ^21.0.0", "@angular/common": "^20.0.0 || ^21.0.0", diff --git a/libs/ui-react/package.json b/libs/ui-react/package.json index 77f006787..3667e703e 100644 --- a/libs/ui-react/package.json +++ b/libs/ui-react/package.json @@ -1,6 +1,6 @@ { "name": "@ngaf/ui-react", - "version": "0.0.23", + "version": "0.0.24", "license": "MIT", "repository": { "type": "git", From a9455a1e673b5f47c62ac17443a471a4864b606a Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 5 May 2026 11:19:36 -0700 Subject: [PATCH 2/5] fix(chat): citation marker without URL renders span, not broken anchor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Live Chrome smoke caught: when a Pandoc citation def has a bare URL (no brackets), mdDefToCitation returns Citation { url: undefined }. Prior template used [href]="r.citation.url ?? null" which Angular serialized as href="" — a broken anchor that renders as a clickable link that goes nowhere. Fix: split the resolved branch into "with URL" (renders ) and "without URL" (renders ). The span shows the bracket marker but is non-interactive, matching the unresolved fallback's contract. Also switched from [href] property binding to [attr.href] for explicit attribute removal when null. Regression test pins both branches. --- ...kdown-citation-reference.component.spec.ts | 18 ++++++++++++++++++ .../markdown-citation-reference.component.ts | 19 +++++++++++++------ 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/libs/chat/src/lib/markdown/views/markdown-citation-reference.component.spec.ts b/libs/chat/src/lib/markdown/views/markdown-citation-reference.component.spec.ts index c1da58ac3..e10c9282f 100644 --- a/libs/chat/src/lib/markdown/views/markdown-citation-reference.component.spec.ts +++ b/libs/chat/src/lib/markdown/views/markdown-citation-reference.component.spec.ts @@ -47,4 +47,22 @@ describe('MarkdownCitationReferenceComponent', () => { expect(a.getAttribute('href')).toBe('https://example.com'); expect(a.textContent).toContain('1'); }); + + it('renders (not ) when citation has no URL — bug #197 regression', () => { + // Live Chrome smoke caught: a Pandoc def with bare URL (no brackets) + // produces a Citation with url === undefined. Prior template rendered + // which is a broken link. Fix renders . + const fixture = TestBed.createComponent(HostComponent); + const svc = fixture.debugElement.injector.get(CitationsResolverService); + svc.message.set({ + id: 'm1', role: 'assistant', content: 'x', + citations: [{ id: 'src1', index: 1, title: 'Source title only, no URL' }], + }); + fixture.componentInstance.node.set(makeNode('src1', 1, true)); + fixture.detectChanges(); + expect(fixture.nativeElement.querySelector('a.chat-citation-marker')).toBeNull(); + const span = fixture.nativeElement.querySelector('span.chat-citation-marker--no-url'); + expect(span).toBeTruthy(); + expect(span.textContent).toContain('1'); + }); }); diff --git a/libs/chat/src/lib/markdown/views/markdown-citation-reference.component.ts b/libs/chat/src/lib/markdown/views/markdown-citation-reference.component.ts index 8e11969cd..429510378 100644 --- a/libs/chat/src/lib/markdown/views/markdown-citation-reference.component.ts +++ b/libs/chat/src/lib/markdown/views/markdown-citation-reference.component.ts @@ -10,12 +10,19 @@ import { CitationsResolverService } from '../citations-resolver.service'; changeDetection: ChangeDetectionStrategy.OnPush, template: ` @if (resolved(); as r) { - - [{{ node().index }}] - + @if (r.citation.url; as href) { + + [{{ node().index }}] + + } @else { + + [{{ node().index }}] + + } } @else { From c60bb2707f8718cfd6c04de319a7a06b8261a94e Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 5 May 2026 11:21:22 -0700 Subject: [PATCH 3/5] fix(chat): chat-citations panel surfaces markdown-sidecar citations too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Live Chrome smoke caught: when citations come from Pandoc-formatted [^id]: defs in message content (no provider metadata in additional_kwargs.citations), inline markers resolved correctly via the markdown sidecar but the sources panel under the message never rendered — defeating the click-to-source affordance. Fix: ChatCitationsComponent now optionally injects CitationsResolverService and merges markdown-sidecar defs (via mdDefToCitation) into its citations list, with Message.citations taking precedence by id. Behavior matches the same precedence as inline-marker resolution. Two new regression tests pin both behaviors: markdown-only citations appear in the panel; Message.citations wins on id overlap. --- .../chat-citations.component.spec.ts | 58 +++++++++++++++++++ .../chat-citations.component.ts | 30 +++++++++- 2 files changed, 86 insertions(+), 2 deletions(-) diff --git a/libs/chat/src/lib/primitives/chat-citations/chat-citations.component.spec.ts b/libs/chat/src/lib/primitives/chat-citations/chat-citations.component.spec.ts index 90c53a361..73d512c6c 100644 --- a/libs/chat/src/lib/primitives/chat-citations/chat-citations.component.spec.ts +++ b/libs/chat/src/lib/primitives/chat-citations/chat-citations.component.spec.ts @@ -64,4 +64,62 @@ describe('ChatCitationsComponent', () => { expect(fixture.nativeElement.querySelector('.custom-card')?.textContent.trim()).toBe('Custom'); expect(fixture.nativeElement.querySelector('.chat-citations-card')).toBeNull(); }); + + it('merges markdown sidecar citations when resolver is available — bug #197 regression', async () => { + // Live Chrome smoke caught: when citations come from Pandoc-formatted + // [^id]: defs in content (no provider metadata), inline markers resolved + // correctly via the markdown sidecar but the sources panel never rendered. + const { CitationsResolverService } = await import('../../markdown/citations-resolver.service'); + @Component({ + standalone: true, + imports: [ChatCitationsComponent], + providers: [CitationsResolverService], + template: ``, + }) + class ResolverHost { + message: Message = msg(undefined); // no provider citations + } + const fixture = TestBed.createComponent(ResolverHost); + const resolver = fixture.debugElement.injector.get(CitationsResolverService); + resolver.markdownDefs.set(new Map([ + ['src1', { + id: 'src1', index: 1, status: 'complete', + children: [ + { id: 1, type: 'text', status: 'complete', parent: null, index: 0, text: 'Wikipedia ' }, + { id: 2, type: 'autolink', status: 'complete', parent: null, index: 1, + url: 'https://en.wikipedia.org/wiki/Coral_reef', + text: 'https://en.wikipedia.org/wiki/Coral_reef' }, + ], + } as never], + ])); + fixture.detectChanges(); + const cards = fixture.nativeElement.querySelectorAll('.chat-citations-card'); + expect(cards.length).toBe(1); + expect(fixture.nativeElement.textContent).toContain('Wikipedia'); + }); + + it('Message.citations takes precedence over markdown sidecar when ids overlap', async () => { + const { CitationsResolverService } = await import('../../markdown/citations-resolver.service'); + @Component({ + standalone: true, + imports: [ChatCitationsComponent], + providers: [CitationsResolverService], + template: ``, + }) + class PrecedenceHost { + message: Message = msg([{ id: 'src1', index: 1, title: 'From message' }]); + } + const fixture = TestBed.createComponent(PrecedenceHost); + const resolver = fixture.debugElement.injector.get(CitationsResolverService); + resolver.markdownDefs.set(new Map([ + ['src1', { id: 'src1', index: 1, status: 'complete', + children: [{ id: 1, type: 'text', status: 'complete', parent: null, index: 0, text: 'From markdown' }], + } as never], + ])); + fixture.detectChanges(); + const cards = fixture.nativeElement.querySelectorAll('.chat-citations-card'); + expect(cards.length).toBe(1); + expect(fixture.nativeElement.textContent).toContain('From message'); + expect(fixture.nativeElement.textContent).not.toContain('From markdown'); + }); }); diff --git a/libs/chat/src/lib/primitives/chat-citations/chat-citations.component.ts b/libs/chat/src/lib/primitives/chat-citations/chat-citations.component.ts index dd00753f8..a135cf39e 100644 --- a/libs/chat/src/lib/primitives/chat-citations/chat-citations.component.ts +++ b/libs/chat/src/lib/primitives/chat-citations/chat-citations.component.ts @@ -8,6 +8,7 @@ import { NgTemplateOutlet } from '@angular/common'; import type { Message } from '../../agent/message'; import type { Citation } from '../../agent/citation'; import { ChatCitationsCardComponent } from './chat-citations-card.component'; +import { CitationsResolverService, mdDefToCitation } from '../../markdown/citations-resolver.service'; /** * ContentChild template directive for custom citation card rendering. @@ -48,8 +49,33 @@ export class ChatCitationsComponent { @ContentChild(ChatCitationCardTemplateDirective) cardTpl: ChatCitationCardTemplateDirective | null = null; + /** + * Optional resolver — present when chat-citations is rendered inside a + * chat-message that provides CitationsResolverService (the standard + * placement). When absent, the panel reads only Message.citations. + */ + private readonly resolver = inject(CitationsResolverService, { optional: true }); + + /** + * Combined citation list: + * 1. Message.citations (provider-populated, takes precedence by id) + * 2. Markdown sidecar defs (Pandoc-formatted [^id]: lines), merged in + * for any id not already present. + * + * Sorted by index ascending. This guarantees the sources panel surfaces + * citations whether they come from message metadata, content syntax, or + * both — matching the same precedence as inline-marker resolution. + */ protected readonly citations = computed(() => { - const list = this.message().citations ?? []; - return [...list].sort((a, b) => a.index - b.index); + const fromMessage = this.message().citations ?? []; + const seenIds = new Set(fromMessage.map(c => c.id)); + const fromMarkdown: Citation[] = []; + const mdDefs = this.resolver?.markdownDefs(); + if (mdDefs) { + for (const def of mdDefs.values()) { + if (!seenIds.has(def.id)) fromMarkdown.push(mdDefToCitation(def)); + } + } + return [...fromMessage, ...fromMarkdown].sort((a, b) => a.index - b.index); }); } From fb6bb208605c071b837ea01e61a56c34e28f6730 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 5 May 2026 11:22:28 -0700 Subject: [PATCH 4/5] fix(chat): task-list checkbox aligns inline with first-paragraph text Live Chrome smoke caught: checkbox rendered on a separate line above the task text because >

is block-level, pushing the content below the input. Now uses flexbox on the .chat-md-list-item--task container to align checkbox + content baseline, and collapses the inner

margin so tight tasks read as a single line. Multi-paragraph task items (sub-lists, additional paragraphs) still flow correctly because flex-wrap is enabled and only direct children flex. --- .../src/lib/styles/chat-markdown.styles.ts | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/libs/chat/src/lib/styles/chat-markdown.styles.ts b/libs/chat/src/lib/styles/chat-markdown.styles.ts index efd16183c..fc74db43f 100644 --- a/libs/chat/src/lib/styles/chat-markdown.styles.ts +++ b/libs/chat/src/lib/styles/chat-markdown.styles.ts @@ -114,9 +114,30 @@ export const CHAT_MARKDOWN_STYLES = ` chat-streaming-md chat-md-table { display: contents; } chat-streaming-md chat-md-table-row { display: contents; } chat-streaming-md chat-md-table-cell { display: contents; } - /* Task-list items */ - chat-streaming-md li.chat-md-list-item--task { list-style: none; margin-left: -1.25rem; } - chat-streaming-md li.chat-md-list-item--task > input[type="checkbox"] { margin-right: 0.5rem; vertical-align: middle; } + /* Task-list items: checkbox + first paragraph render inline; subsequent + blocks (sub-lists, multi-paragraph items) flow normally below. */ + chat-streaming-md li.chat-md-list-item--task { + list-style: none; + margin-left: -1.25rem; + display: flex; + flex-wrap: wrap; + align-items: baseline; + gap: 0.5rem; + } + chat-streaming-md li.chat-md-list-item--task > input[type="checkbox"] { + margin: 0; + flex: 0 0 auto; + transform: translateY(2px); + } + /* The chat-md-children wrapper around list-item content takes remaining width */ + chat-streaming-md li.chat-md-list-item--task > chat-md-children { + flex: 1 1 auto; + min-width: 0; + } + /* Tight task items: collapse the wrapping

margins so the line aligns. */ + chat-streaming-md li.chat-md-list-item--task chat-md-paragraph > p { + margin: 0; + } /* Media */ chat-streaming-md img { max-width: 100%; height: auto; border-radius: 6px; } From 90e3b8688dc68b017a867b57dcd955afc6559bb1 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 5 May 2026 11:24:49 -0700 Subject: [PATCH 5/5] fix(chat): only collapse first-paragraph margin in task items Live Chrome smoke caught: prior fix collapsed margin on ALL paragraphs inside task items, so multi-paragraph content ran together with no visible break (e.g. "write reportCoral reefs are diverse..." running into the next block when the model emitted prose immediately after a task item without a blank line). Refined: only the FIRST paragraph of a task item collapses its margin (so it aligns inline with the checkbox). Subsequent blocks keep normal vertical spacing. --- libs/chat/src/lib/styles/chat-markdown.styles.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/libs/chat/src/lib/styles/chat-markdown.styles.ts b/libs/chat/src/lib/styles/chat-markdown.styles.ts index fc74db43f..db75d5416 100644 --- a/libs/chat/src/lib/styles/chat-markdown.styles.ts +++ b/libs/chat/src/lib/styles/chat-markdown.styles.ts @@ -134,8 +134,10 @@ export const CHAT_MARKDOWN_STYLES = ` flex: 1 1 auto; min-width: 0; } - /* Tight task items: collapse the wrapping

margins so the line aligns. */ - chat-streaming-md li.chat-md-list-item--task chat-md-paragraph > p { + /* Tight task items: only the FIRST paragraph aligns inline with the + checkbox (margin collapsed). Subsequent paragraphs/blocks keep their + normal vertical spacing so multi-block items render readably. */ + chat-streaming-md li.chat-md-list-item--task > chat-md-children > chat-md-paragraph:first-child > p { margin: 0; }