Module: RubyCoded::Chat::Renderer::RichText

Includes:
RichTextInline
Included in:
RubyCoded::Chat::Renderer
Defined in:
lib/ruby_coded/chat/renderer/rich_text.rb

Overview

Helpers for building and serializing rich text using ratatui text lines/spans.

Constant Summary collapse

BASE_TEXT_STYLES =
{
  system: { fg: :dark_gray, modifiers: [:italic] },
  user_label: { fg: :cyan, modifiers: [:bold] },
  tool_call: { fg: :yellow, modifiers: [:bold] },
  tool_pending: { fg: :magenta, modifiers: [:bold] },
  tool_result: { fg: :green },
  thinking_title: { fg: :blue, modifiers: [:bold] }
}.freeze

Instance Method Summary collapse

Methods included from RichTextInline

#append_bold_span!, #append_code_span!, #append_italic_span!, #append_styled_span!, #inline_spans, #next_inline_marker

Instance Method Details

#base_text_style(role) ⇒ Object



78
79
80
# File 'lib/ruby_coded/chat/renderer/rich_text.rb', line 78

def base_text_style(role)
  @tui.style(**BASE_TEXT_STYLES.fetch(role, { modifiers: [] }))
end

#code_block_styleObject



86
87
88
# File 'lib/ruby_coded/chat/renderer/rich_text.rb', line 86

def code_block_style
  @tui.style(fg: :green, bg: :black)
end

#code_language_line(language) ⇒ Object



57
58
59
# File 'lib/ruby_coded/chat/renderer/rich_text.rb', line 57

def code_language_line(language)
  @tui.line(spans: [@tui.span(content: "[#{language}]", style: @tui.style(fg: :yellow, modifiers: [:bold]))])
end

#inline_code_styleObject



82
83
84
# File 'lib/ruby_coded/chat/renderer/rich_text.rb', line 82

def inline_code_style
  @tui.style(fg: :yellow, bg: :dark_gray)
end

#merge_style(style, foreground: nil, background: nil, modifiers: []) ⇒ Object



61
62
63
64
65
66
67
# File 'lib/ruby_coded/chat/renderer/rich_text.rb', line 61

def merge_style(style, foreground: nil, background: nil, modifiers: [])
  @tui.style(
    fg: foreground || style&.fg,
    bg: background || style&.bg,
    modifiers: ((style&.modifiers || []) + modifiers).uniq
  )
end

#parse_rich_line(raw_line, state, role) ⇒ Object



34
35
36
37
38
# File 'lib/ruby_coded/chat/renderer/rich_text.rb', line 34

def parse_rich_line(raw_line, state, role)
  return toggle_code_fence(raw_line, state) if raw_line.start_with?("```")

  state[:lines] << rich_line_for(raw_line, state[:in_code_block], role)
end

#parse_rich_text(text, role: :assistant) ⇒ Object



28
29
30
31
32
# File 'lib/ruby_coded/chat/renderer/rich_text.rb', line 28

def parse_rich_text(text, role: :assistant)
  state = { lines: [], in_code_block: false }
  text.split("\n", -1).each { |raw_line| parse_rich_line(raw_line, state, role) }
  state[:lines]
end

#rich_line_for(raw_line, in_code_block, role) ⇒ Object



51
52
53
54
55
# File 'lib/ruby_coded/chat/renderer/rich_text.rb', line 51

def rich_line_for(raw_line, in_code_block, role)
  return @tui.line(spans: inline_spans(raw_line, role: role)) unless in_code_block

  @tui.line(spans: [@tui.span(content: raw_line, style: code_block_style)])
end

#rich_line_plain(line) ⇒ Object



22
23
24
25
26
# File 'lib/ruby_coded/chat/renderer/rich_text.rb', line 22

def rich_line_plain(line)
  return line.to_s unless line.respond_to?(:spans)

  Array(line.spans).map { |span| span.respond_to?(:content) ? span.content.to_s : span.to_s }.join
end

#rich_text_lines(text, role: :assistant) ⇒ Object



12
13
14
15
16
# File 'lib/ruby_coded/chat/renderer/rich_text.rb', line 12

def rich_text_lines(text, role: :assistant)
  return [] if text.nil? || text.empty?

  parse_rich_text(text, role: role)
end

#rich_text_plain(lines) ⇒ Object



18
19
20
# File 'lib/ruby_coded/chat/renderer/rich_text.rb', line 18

def rich_text_plain(lines)
  Array(lines).map { |line| rich_line_plain(line) }.join("\n")
end

#toggle_code_fence(raw_line, state) ⇒ Object



40
41
42
43
44
45
46
47
48
49
# File 'lib/ruby_coded/chat/renderer/rich_text.rb', line 40

def toggle_code_fence(raw_line, state)
  if state[:in_code_block]
    state[:in_code_block] = false
    return
  end

  state[:in_code_block] = true
  language = raw_line.delete_prefix("```").strip
  state[:lines] << code_language_line(language) unless language.empty?
end