Skip to content

D-13: DialogueMarkdownView — hybrid block visitor + AttributedString inline #263

@kirich1409

Description

@kirich1409

Description

Build the markdown renderer for Dialogue — hybrid approach: custom MarkupVisitor over apple/swift-markdown AST for block elements (headings, paragraphs, code fences, lists, blockquotes, thematic breaks), Apple's AttributedString(markdown:, options: .inlineOnlyPreservingWhitespace) for inline inside paragraphs.

Spec: Epic #250 §4 (apple/swift-markdown as AST provider); architecture plan in docs/architecture/dialogue.md §4.

Scope

File layout

MacApp/Packages/AgentChatUI/Sources/AgentChatUI/Markdown/

  • DialogueMarkdownView.swift — public view.
  • MarkdownBlockVisitor.swiftMarkupVisitor with Result = RenderedBlock.
  • RenderedBlock.swift — enum of block kinds, each variant carries rendered data (AttributedString for inline, metadata for special blocks).
  • MarkdownStreamingBuffer.swift — throttle / coalesce layer for streaming input.
  • MarkdownBlockCache.swiftLRUCache<UInt64, RenderedBlock> keyed by hash(blockSource).

Public API

```swift
public struct DialogueMarkdownView: View {
public init(
source: String,
streaming: Bool = false,
cache: MarkdownBlockCache? = nil
)
}
```

  • source — full markdown content. For streaming, the view splits it into stableSource (up to last \n\n) and tail (unfinished).
  • streaming = true — use tail-fast-path (render tail as plain Text, re-parse only stable on boundary changes).
  • cache — optional external cache (shared across many DialogueMarkdownView instances, e.g., one per message).

Block subset (MVP)

Supported blocks:

  • Document — VStack (DS.Spacing.md between blocks)
  • Heading (1–6) — DS.Text.{title, headline, body} with level-specific weight
  • ParagraphText(AttributedString(markdown: paragraph.format(), options: .inlineOnlyPreservingWhitespace))
  • CodeBlock (fenced) — BlockCodeContainer (D-3) wrapping monospaced Text
  • UnorderedList / OrderedListForEach with bullet / number + indented content
  • ListItem — paragraph children; task-list checkbox (SF Symbol, read-only)
  • BlockQuote — left border + indent
  • ThematicBreakDivider
  • Text / inline — delegated to AttributedString

Not supported in MVP (render as plain text with warning):

  • Table (v1.1)
  • Image (v1.1)
  • HTMLBlock / InlineHTML (render as plain text)
  • Math / LaTeX

Streaming strategy

MarkdownStreamingBuffer:

  • Input: stream of String (growing source).
  • Debounce: Task.sleep(for: .milliseconds(16)) cancellable on each new input. When debounce fires, split source at last \n\n.
  • Stable block source → parse with swift-markdown (hashed, cache lookup before parse).
  • Tail → plain Text at bottom of rendered list.

Swift 6 concurrency

  • Document, Markup types not explicitly Sendable — parse on @MainActor, never cross actor boundary with AST types; pass only RenderedBlock / AttributedString (which are Sendable).

Acceptance Criteria

  • DialogueMarkdownView renders all MVP block types correctly — verified via snapshot tests for 10 representative markdown samples (mix of paragraphs + code + lists + headings + blockquote).
  • Streaming mode: feed source growing token-by-token, no visible flicker on 2000-token typical answer. Stable blocks cached.
  • Cache hit rate ≥ 80% on second render of same content.
  • Text selection works across paragraphs (.textSelection(.enabled)).
  • Link tap handled via @Environment(\.openURL).
  • All DS tokens used — no hardcoded padding / color / radius.
  • Swift 6 strict concurrency clean; Document / Markup kept behind @MainActor.
  • Snapshot tests in 4 appearances (light / dark / HCR / reduceTransparency).
  • Benchmark: render 10KB markdown in < 50ms (sanity, not hard SLA).

Relationships

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions