Module: Protege::Components::IntrospectionHelper

Included in:
ComponentsHelper
Defined in:
app/helpers/protege/components/introspection_helper.rb

Overview

View helpers for the thread introspection panel — the collapsible right sidebar in the console that surfaces the agent's reasoning and tool use for the open thread. Mirrors +LayoutHelper+'s sidebar builders: small composable pieces (panel shell, section, tool card, toggle) so the view stays declarative and the panel's markup lives in one place as it grows.

Constant Summary collapse

INTROSPECTION_PANEL_ID =

DOM id of the panel <aside> — the toggle shows/hides this element.

'thread-introspection'

Instance Method Summary collapse

Instance Method Details

#introspection_feed(thread) ⇒ ActiveSupport::SafeBuffer

The live activity feed container — the single scrollable target the IntrospectionBroadcaster appends reasoning tokens and tool-call cards into, interleaved chronologically. Live-only: the introspection-empty placeholder (see tailwind/application.css) shows until the next run streams in.

Marked data-turbo-permanent so the accumulated feed survives Turbo Drive navigations (the id is per-thread, so it is only preserved within the same thread). A hard page reload still starts it empty — permanence spans Drive visits only — keeping the feed live-only.

Parameters:

Returns:

  • (ActiveSupport::SafeBuffer)

    the feed container markup



60
61
62
63
64
65
# File 'app/helpers/protege/components/introspection_helper.rb', line 60

def introspection_feed(thread)
  tag.div(id:    dom_id(thread, :activity),
          class: 'introspection-empty text-xs font-mono leading-relaxed',
          style: 'color: var(--text-muted)',
          data:  { empty: 'Waiting for activity…', turbo_permanent: '' })
end

#introspection_panel { ... } ⇒ ActiveSupport::SafeBuffer

Render the panel shell: a full-height right <aside> (mirroring the left inbox aside) with a fixed header and a scrollable body that yields its content (the activity feed).

The user can resize its width by dragging the left-edge handle (the general resize_handle paired with the engine's self-contained resize.js), bounded by min/max and remembered across page loads. Collapsing (open/close) is handled by introspection_toggle in the thread header.

Yields:

  • the panel body (e.g. introspection_feed)

Returns:

  • (ActiveSupport::SafeBuffer)

    the panel markup



22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# File 'app/helpers/protege/components/introspection_helper.rb', line 22

def introspection_panel(&block)
  class_names = 'relative flex w-80 flex-shrink-0 flex-col overflow-hidden'

  data = {
    resize:      '',
    resize_edge: 'left',
    resize_min:  240,
    resize_max:  720,
    resize_key:  'protege:introspection-width'
  }

  tag.aside(id: INTROSPECTION_PANEL_ID, class: class_names, data:) do
    resize_handle(edge: :left) +
      introspection_header +
      tag.div(class: 'flex-1 overflow-y-auto px-4 py-3', &block)
  end
end

#introspection_reasoning(id:, content: nil) ⇒ ActiveSupport::SafeBuffer

A reasoning block — one paragraph of the model's streamed reasoning. The IntrospectionBroadcaster appends a fresh block after each tool call and streams that turn's tokens into it (by id), so reasoning is grouped into spaced, whitespace-preserving paragraphs rather than loose text nodes.

Parameters:

  • id (String)

    the block's DOM id (the target the run's tokens append into)

  • content (String, nil) (defaults to: nil)

    the block's initial token text

Returns:

  • (ActiveSupport::SafeBuffer)

    the reasoning block markup



74
75
76
# File 'app/helpers/protege/components/introspection_helper.rb', line 74

def introspection_reasoning(id:, content: nil)
  tag.div(content, id:, class: 'my-3 whitespace-pre-wrap')
end

#introspection_run_marker(id:, status: :processing, detail: nil) ⇒ ActiveSupport::SafeBuffer

A run-lifecycle marker in the activity feed: a pulsing "Processing…" line while the run is in flight, or a red error when it fails. Appended on run start; on success it is removed, on failure replaced in place (by its id) with the error — so each run reads as processing → reasoning/tool use, then repeats for the next reply.

Parameters:

  • id (String)

    the marker's DOM id (stable across the run so the outcome replaces it)

  • status (Symbol) (defaults to: :processing)

    :processing or :failed

  • detail (String, nil) (defaults to: nil)

    the error message, shown when :failed

Returns:

  • (ActiveSupport::SafeBuffer)

    the marker markup



110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'app/helpers/protege/components/introspection_helper.rb', line 110

def introspection_run_marker(id:, status: :processing, detail: nil)
  if status == :failed
    tag.div(id:, class: 'my-3 text-xs font-mono', style: 'color: #ef4444') do
      tag.div('✗ Run failed', class: 'font-medium') +
        introspection_card_line(detail, color: '#ef4444')
    end
  else
    tag.div(id:, class: 'my-3 flex items-center gap-2 text-xs animate-pulse',
            style: 'color: var(--text-faint)') do
      tag.span('') + tag.span('Processing…')
    end
  end
end

#introspection_toggleActiveSupport::SafeBuffer

The toggle button (placed in the thread header) that shows/hides the panel.

Returns:

  • (ActiveSupport::SafeBuffer)

    the toggle button markup



43
44
45
46
47
48
# File 'app/helpers/protege/components/introspection_helper.rb', line 43

def introspection_toggle
  button('Introspection',
         variant: :secondary,
         type:    'button',
         onclick: "document.getElementById('#{INTROSPECTION_PANEL_ID}').classList.toggle('hidden')")
end

#introspection_tool_card(name:, args: nil, status: :done, preview: nil, id: nil) ⇒ ActiveSupport::SafeBuffer

A single tool-call card: a status glyph, the tool name, an optional one-line args summary, and an optional response preview (done) or error (failed). Streamed into the panel as a :pending card the instant the model requests the call, then replaced in place with the outcome — so pass a stable id: (the card's DOM id) that the replace can target.

Parameters:

  • name (String)

    the tool name

  • args (String, nil) (defaults to: nil)

    a one-line argument summary

  • status (Symbol) (defaults to: :done)

    :pending, :done, or :failed

  • preview (String, nil) (defaults to: nil)

    a response preview or error string

  • id (String, nil) (defaults to: nil)

    the card's DOM id (so a pending card can be replaced by its outcome)

Returns:

  • (ActiveSupport::SafeBuffer)

    the card markup



89
90
91
92
93
94
95
96
97
98
99
# File 'app/helpers/protege/components/introspection_helper.rb', line 89

def introspection_tool_card(name:, args: nil, status: :done, preview: nil, id: nil)
  glyph, color, pulsing = tool_card_glyph(status)
  tag.div(id:, class: 'my-3 pl-2 text-xs font-mono', style: 'border-left: 2px solid var(--border)') do
    tag.div(class: 'flex items-center gap-2') do
      tag.span(glyph, class: pulsing ? 'animate-pulse' : '', style: "color: #{color}") +
        tag.span(name, class: 'font-medium', style: 'color: var(--text)')
    end +
      introspection_card_line(args, color: 'var(--text-muted)') +
      introspection_card_line(preview.present? ? "#{preview}" : nil, color: 'var(--text-faint)')
  end
end