Class: TUI::Screens::Chat

Inherits:
Object
  • Object
show all
Includes:
Formatting
Defined in:
lib/tui/screens/chat.rb

Constant Summary collapse

MIN_INPUT_HEIGHT =
3
PRINTABLE_CHAR =
/\A[[:print:]]\z/
ROLE_USER =
"user"
ROLE_ASSISTANT =
"assistant"
SCROLL_STEP =
1
MOUSE_SCROLL_STEP =
2
TOOL_ICON =
"\u{1F527}"
CHECKMARK =
"\u2713"
VIEWPORT_BACK_BUFFER =

Viewport virtualization tuning

3
VIEWPORT_OVERFLOW_MULTIPLIER =

entries before scroll target for upward scroll margin

2
VIEWPORT_BOTTOM_THRESHOLD =

build this many viewports worth of lines

10
ROLE_STYLES =

Background-highlighted styles for conversation roles. Dark tinted backgrounds make user/assistant messages easy to scan. 22 = dark green (#005f00), 17 = dark navy (#00005f) in 256-color.

{
  "user" => {fg: "white", bg: 22, modifiers: [:bold]},
  "assistant" => {fg: "white", bg: 17, modifiers: [:bold]}
}.freeze
VIEW_MODES =

Intentionally duplicated from Session::VIEW_MODES to keep the TUI independent of Rails. Must stay in sync when adding new modes.

%w[basic verbose debug].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Formatting

#format_ns_timestamp, #format_token_label, #token_count_color

Constructor Details

#initialize(cable_client:, message_store: nil, perf_logger: nil) ⇒ Chat

Returns a new instance of Chat.

Parameters:



65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/tui/screens/chat.rb', line 65

def initialize(cable_client:, message_store: nil, perf_logger: nil)
  @cable_client = cable_client
  @message_store = message_store || MessageStore.new
  @perf_logger = perf_logger || PerformanceLogger.new(enabled: false)
  @input_buffer = InputBuffer.new
  @flash = Flash.new
  @session_state = "idle"
  @session_loading = false
  @spinner = BrailleSpinner.new
  @scroll_offset = 0
  @auto_scroll = true
  @visible_height = 0
  @max_scroll = 0
  @input_scroll_offset = 0
  @view_mode = "basic"
  @session_info = {id: cable_client.session_id || 0, agent_name: "Anima", message_count: 0, active_skills: [], active_workflow: nil, goals: [], children: []}
  @sessions_list = nil
  @parent_session_id = nil
  @authentication_required = false
  @token_save_result = nil
  @chat_focused = false
  @input_history = []
  @history_index = nil
  @saved_input = nil
  # Viewport virtualization: only renders messages visible in the scroll
  # window. Heights are estimated for all entries (cheap string math),
  # but Line objects are only built for the visible range + buffer.
  @height_map = HeightMap.new
  @height_map_version = -1
  @height_map_width = nil
  @height_map_loading = nil
  @viewport = viewport_cache_empty
end

Instance Attribute Details

#authentication_requiredObject (readonly)

Returns the value of attribute authentication_required.



57
58
59
# File 'lib/tui/screens/chat.rb', line 57

def authentication_required
  @authentication_required
end

#chat_focusedObject (readonly)

Returns the value of attribute chat_focused.



57
58
59
# File 'lib/tui/screens/chat.rb', line 57

def chat_focused
  @chat_focused
end

#hud_hintObject

Returns the value of attribute hud_hint.



60
61
62
# File 'lib/tui/screens/chat.rb', line 60

def hud_hint
  @hud_hint
end

#message_storeObject (readonly)

Returns the value of attribute message_store.



57
58
59
# File 'lib/tui/screens/chat.rb', line 57

def message_store
  @message_store
end

#parent_session_idObject (readonly)

Returns the value of attribute parent_session_id.



57
58
59
# File 'lib/tui/screens/chat.rb', line 57

def parent_session_id
  @parent_session_id
end

#scroll_offsetObject (readonly)

Returns the value of attribute scroll_offset.



57
58
59
# File 'lib/tui/screens/chat.rb', line 57

def scroll_offset
  @scroll_offset
end

#session_infoObject (readonly)

Returns the value of attribute session_info.



57
58
59
# File 'lib/tui/screens/chat.rb', line 57

def session_info
  @session_info
end

#session_loadingObject (readonly)

Returns the value of attribute session_loading.



57
58
59
# File 'lib/tui/screens/chat.rb', line 57

attr_reader :message_store, :scroll_offset, :session_info, :view_mode, :sessions_list,
:authentication_required, :token_save_result, :parent_session_id,
:chat_focused, :session_state, :spinner, :session_loading

#session_stateObject (readonly)

Returns the value of attribute session_state.



57
58
59
# File 'lib/tui/screens/chat.rb', line 57

def session_state
  @session_state
end

#sessions_listObject (readonly)

Returns the value of attribute sessions_list.



57
58
59
# File 'lib/tui/screens/chat.rb', line 57

def sessions_list
  @sessions_list
end

#spinnerObject (readonly)

Returns the value of attribute spinner.



57
58
59
# File 'lib/tui/screens/chat.rb', line 57

def spinner
  @spinner
end

#token_save_resultObject (readonly)

Returns the value of attribute token_save_result.



57
58
59
# File 'lib/tui/screens/chat.rb', line 57

def token_save_result
  @token_save_result
end

#view_modeObject (readonly)

Returns the value of attribute view_mode.



57
58
59
# File 'lib/tui/screens/chat.rb', line 57

def view_mode
  @view_mode
end

Instance Method Details

#clear_authentication_requiredvoid

This method returns an undefined value.

Clears the authentication_required flag after the App has consumed it.



231
232
233
# File 'lib/tui/screens/chat.rb', line 231

def clear_authentication_required
  @authentication_required = false
end

#clear_inputvoid

This method returns an undefined value.

Clears the input buffer. Used when Escape is pressed with non-empty input.



225
226
227
# File 'lib/tui/screens/chat.rb', line 225

def clear_input
  @input_buffer.clear
end

#consume_token_save_resultHash?

Returns and clears the token save result for one-shot consumption by the App.

Returns:

  • (Hash, nil)

    true or false, message: “…”, or nil



237
238
239
240
241
# File 'lib/tui/screens/chat.rb', line 237

def consume_token_save_result
  result = @token_save_result
  @token_save_result = nil
  result
end

#cursor_posInteger

Returns current cursor position (delegates to InputBuffer).

Returns:

  • (Integer)

    current cursor position (delegates to InputBuffer)



109
110
111
# File 'lib/tui/screens/chat.rb', line 109

def cursor_pos
  @input_buffer.cursor_pos
end

#finalizeObject



243
244
# File 'lib/tui/screens/chat.rb', line 243

def finalize
end

#focus_chatvoid

This method returns an undefined value.

Switches focus to the chat pane for keyboard scrolling.



256
257
258
# File 'lib/tui/screens/chat.rb', line 256

def focus_chat
  @chat_focused = true
end

#handle_event(event) ⇒ Object

Dispatches keyboard, mouse, and paste events. Supports two focus modes: input mode (default) where arrows navigate the input buffer with bash-style history overflow, and chat-focused mode where arrows scroll the chat pane.

Page Up/Down and mouse scroll always control the chat pane regardless of focus mode.



140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
# File 'lib/tui/screens/chat.rb', line 140

def handle_event(event)
  return handle_mouse_event(event) if event.mouse?
  return handle_paste_event(event) if event.paste?
  return handle_scroll_key(event) if event.page_up? || event.page_down?

  # Dismiss flash on any keypress (flash auto-expires too)
  @flash.dismiss! if @flash.any?

  return handle_chat_focused_event(event) if @chat_focused

  if event.up?
    return true if @input_buffer.move_up
    return true if @input_buffer.text.empty? && recall_pending_message
    return navigate_history_back
  end

  if event.down?
    return true if @input_buffer.move_down
    return navigate_history_forward
  end

  if event.enter?
    submit_message
    true
  elsif event.backspace?
    reset_history_browsing
    @input_buffer.backspace
    true
  elsif event.delete?
    reset_history_browsing
    @input_buffer.delete
    true
  elsif event.left?
    @input_buffer.move_left
  elsif event.right?
    @input_buffer.move_right
  elsif event.home?
    @input_buffer.move_home
  elsif event.end?
    @input_buffer.move_end
  elsif printable_char?(event) && !@input_buffer.full?
    reset_history_browsing
    @input_buffer.insert(event.code)
  else
    false
  end
end

#inputString

Returns current input text (delegates to InputBuffer).

Returns:

  • (String)

    current input text (delegates to InputBuffer)



104
105
106
# File 'lib/tui/screens/chat.rb', line 104

def input
  @input_buffer.text
end

#interrupt_executionvoid

This method returns an undefined value.

Sends an interrupt request to the server to stop the current tool chain. Called when Escape is pressed with empty input during active processing.



218
219
220
# File 'lib/tui/screens/chat.rb', line 218

def interrupt_execution
  @cable_client.interrupt
end

#loading?Boolean

Whether the session is actively processing (any state other than idle). Used by the App’s HUD and scroll calculations.

Returns:

  • (Boolean)


250
251
252
# File 'lib/tui/screens/chat.rb', line 250

def loading?
  @session_state != "idle"
end

#messagesObject



99
100
101
# File 'lib/tui/screens/chat.rb', line 99

def messages
  @message_store.messages
end

#new_sessionObject

Creates a new session through the WebSocket protocol. The brain creates the session, switches the channel stream, and sends a session_changed signal followed by (empty) history. The client-side state reset happens when session_changed is received.



192
193
194
# File 'lib/tui/screens/chat.rb', line 192

def new_session
  @cable_client.create_session
end

#render(frame, area, tui) ⇒ Object



113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/tui/screens/chat.rb', line 113

def render(frame, area, tui)
  process_incoming_messages

  input_height = calculate_input_height(tui, area.width, area.height)

  chat_area, input_area = tui.split(
    area,
    direction: :vertical,
    constraints: [
      tui.constraint_fill(1),
      tui.constraint_length(input_height)
    ]
  )

  render_messages(frame, chat_area, tui)
  render_flash(frame, chat_area, tui)

  render_input(frame, input_area, tui)
end

#spinner_colorString

Color name for the spinner and HUD label based on session state. Follows the two-channel design: color = status (green = working, red = stopping). The braille animation pattern communicates type.

Returns:

  • (String)


283
284
285
286
287
288
289
# File 'lib/tui/screens/chat.rb', line 283

def spinner_color
  case @session_state
  when "llm_generating", "tool_executing" then "green"
  when "interrupting" then "red"
  else "dark_gray"
  end
end

#spinner_labelString

Short label describing the current session state for HUD display.

Returns:

  • (String)


269
270
271
272
273
274
275
276
# File 'lib/tui/screens/chat.rb', line 269

def spinner_label
  case @session_state
  when "llm_generating" then "Thinking..."
  when "tool_executing" then "Executing..."
  when "interrupting" then "Stopping..."
  else "Working..."
  end
end

#switch_session(session_id) ⇒ Object

Switches to an existing session through the WebSocket protocol. The brain switches the channel stream and sends a session_changed signal followed by chat history.

Parameters:

  • session_id (Integer)

    target session to switch to



201
202
203
# File 'lib/tui/screens/chat.rb', line 201

def switch_session(session_id)
  @cable_client.switch_session(session_id)
end

#switch_view_mode(mode) ⇒ Object

Sends an explicit view mode switch command to the server. The server broadcasts the mode change and re-transmits the viewport decorated in the new mode to all connected clients.

Parameters:

  • mode (String)

    target view mode (“basic”, “verbose”, or “debug”)



210
211
212
# File 'lib/tui/screens/chat.rb', line 210

def switch_view_mode(mode)
  @cable_client.change_view_mode(mode)
end

#unfocus_chatvoid

This method returns an undefined value.

Returns focus from the chat pane to the input field.



262
263
264
# File 'lib/tui/screens/chat.rb', line 262

def unfocus_chat
  @chat_focused = false
end