anima-core 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitattributes +1 -0
- data/.reek.yml +61 -0
- data/README.md +202 -116
- data/anima-core.gemspec +4 -1
- data/app/channels/session_channel.rb +44 -10
- data/app/decorators/agent_message_decorator.rb +6 -0
- data/app/decorators/event_decorator.rb +41 -7
- data/app/decorators/tool_call_decorator.rb +66 -5
- data/app/decorators/tool_decorator.rb +57 -0
- data/app/decorators/tool_response_decorator.rb +35 -5
- data/app/decorators/user_message_decorator.rb +6 -0
- data/app/decorators/web_get_tool_decorator.rb +102 -0
- data/app/jobs/agent_request_job.rb +95 -20
- data/app/jobs/mneme_job.rb +51 -0
- data/app/jobs/passive_recall_job.rb +29 -0
- data/app/models/concerns/event/broadcasting.rb +18 -0
- data/app/models/event.rb +10 -0
- data/app/models/goal.rb +27 -0
- data/app/models/goal_pinned_event.rb +11 -0
- data/app/models/pinned_event.rb +41 -0
- data/app/models/session.rb +335 -6
- data/app/models/snapshot.rb +76 -0
- data/config/initializers/event_subscribers.rb +14 -3
- data/config/initializers/fts5_schema_dump.rb +21 -0
- data/db/migrate/20260316094817_add_interrupt_requested_to_sessions.rb +5 -0
- data/db/migrate/20260321080000_create_mneme_schema.rb +32 -0
- data/db/migrate/20260321120000_create_pinned_events.rb +27 -0
- data/db/migrate/20260321140000_create_events_fts_index.rb +77 -0
- data/db/migrate/20260321140100_add_recalled_event_ids_to_sessions.rb +10 -0
- data/lib/agent_loop.rb +67 -18
- data/lib/analytical_brain/runner.rb +159 -84
- data/lib/analytical_brain/tools/assign_nickname.rb +76 -0
- data/lib/analytical_brain/tools/finish_goal.rb +6 -1
- data/lib/anima/cli.rb +34 -1
- data/lib/anima/config_migrator.rb +205 -0
- data/lib/anima/installer.rb +13 -130
- data/lib/anima/settings.rb +42 -1
- data/lib/anima/version.rb +1 -1
- data/lib/events/bounce_back.rb +37 -0
- data/lib/events/subscribers/agent_dispatcher.rb +29 -0
- data/lib/events/subscribers/persister.rb +17 -0
- data/lib/events/subscribers/subagent_message_router.rb +102 -0
- data/lib/events/subscribers/transient_broadcaster.rb +36 -0
- data/lib/llm/client.rb +99 -14
- data/lib/mneme/compressed_viewport.rb +200 -0
- data/lib/mneme/l2_runner.rb +138 -0
- data/lib/mneme/passive_recall.rb +69 -0
- data/lib/mneme/runner.rb +254 -0
- data/lib/mneme/search.rb +150 -0
- data/lib/mneme/tools/attach_events_to_goals.rb +107 -0
- data/lib/mneme/tools/everything_ok.rb +24 -0
- data/lib/mneme/tools/save_snapshot.rb +68 -0
- data/lib/mneme.rb +29 -0
- data/lib/providers/anthropic.rb +57 -13
- data/lib/shell_session.rb +188 -59
- data/lib/tasks/fts5.rake +6 -0
- data/lib/tools/remember.rb +179 -0
- data/lib/tools/spawn_specialist.rb +21 -9
- data/lib/tools/spawn_subagent.rb +22 -11
- data/lib/tools/subagent_prompts.rb +20 -3
- data/lib/tools/think.rb +57 -0
- data/lib/tools/web_get.rb +15 -6
- data/lib/tui/app.rb +230 -127
- data/lib/tui/cable_client.rb +8 -0
- data/lib/tui/decorators/base_decorator.rb +165 -0
- data/lib/tui/decorators/bash_decorator.rb +20 -0
- data/lib/tui/decorators/edit_decorator.rb +19 -0
- data/lib/tui/decorators/read_decorator.rb +24 -0
- data/lib/tui/decorators/think_decorator.rb +36 -0
- data/lib/tui/decorators/web_get_decorator.rb +19 -0
- data/lib/tui/decorators/write_decorator.rb +19 -0
- data/lib/tui/flash.rb +139 -0
- data/lib/tui/formatting.rb +28 -0
- data/lib/tui/height_map.rb +93 -0
- data/lib/tui/message_store.rb +25 -1
- data/lib/tui/performance_logger.rb +90 -0
- data/lib/tui/screens/chat.rb +374 -109
- data/templates/config.toml +156 -0
- metadata +87 -4
- data/CHANGELOG.md +0 -79
- data/Gemfile +0 -17
- data/lib/tools/return_result.rb +0 -81
data/lib/tui/app.rb
CHANGED
|
@@ -4,6 +4,7 @@ require "time"
|
|
|
4
4
|
require_relative "cable_client"
|
|
5
5
|
require_relative "input_buffer"
|
|
6
6
|
require_relative "message_store"
|
|
7
|
+
require_relative "performance_logger"
|
|
7
8
|
require_relative "screens/chat"
|
|
8
9
|
|
|
9
10
|
module TUI
|
|
@@ -12,6 +13,7 @@ module TUI
|
|
|
12
13
|
|
|
13
14
|
COMMAND_KEYS = {
|
|
14
15
|
"a" => :anthropic_token,
|
|
16
|
+
"h" => :toggle_hud,
|
|
15
17
|
"n" => :new_session,
|
|
16
18
|
"s" => :session_picker,
|
|
17
19
|
"v" => :view_mode,
|
|
@@ -21,7 +23,8 @@ module TUI
|
|
|
21
23
|
MENU_LABELS = (COMMAND_KEYS.map { |key, action| "[#{key}] #{action.to_s.tr("_", " ").capitalize}" } +
|
|
22
24
|
["[\u2191] Scroll chat", "[\u2193] Return to input"]).freeze
|
|
23
25
|
|
|
24
|
-
|
|
26
|
+
# HUD occupies 1/3 of screen width, clamped to a usable minimum.
|
|
27
|
+
HUD_MIN_WIDTH = 24
|
|
25
28
|
|
|
26
29
|
# Picker entry prefix width: "[N]" (3) + marker (1) + space (1) = 5
|
|
27
30
|
PICKER_PREFIX_WIDTH = 5
|
|
@@ -77,14 +80,20 @@ module TUI
|
|
|
77
80
|
|
|
78
81
|
attr_reader :current_screen, :command_mode, :session_picker_active,
|
|
79
82
|
:view_mode_picker_active
|
|
83
|
+
# @return [Boolean] true when the HUD info panel is visible
|
|
84
|
+
attr_reader :hud_visible
|
|
80
85
|
# @return [Boolean] true when the token setup popup overlay is visible
|
|
81
86
|
attr_reader :token_setup_active
|
|
82
87
|
# @return [Boolean] true when graceful shutdown has been requested via signal
|
|
83
88
|
attr_reader :shutdown_requested
|
|
89
|
+
# @return [TUI::PerformanceLogger] frame timing logger (no-op when debug is off)
|
|
90
|
+
attr_reader :perf_logger
|
|
84
91
|
|
|
85
92
|
# @param cable_client [TUI::CableClient] WebSocket client connected to the brain
|
|
86
|
-
|
|
93
|
+
# @param debug [Boolean] enable performance logging to log/tui_performance.log
|
|
94
|
+
def initialize(cable_client:, debug: false)
|
|
87
95
|
@cable_client = cable_client
|
|
96
|
+
@perf_logger = PerformanceLogger.new(enabled: debug)
|
|
88
97
|
@current_screen = :chat
|
|
89
98
|
@command_mode = false
|
|
90
99
|
@session_picker_active = false
|
|
@@ -92,17 +101,19 @@ module TUI
|
|
|
92
101
|
@session_picker_page = 0
|
|
93
102
|
@session_picker_mode = :root
|
|
94
103
|
@session_picker_parent_id = nil
|
|
104
|
+
@hud_visible = true
|
|
95
105
|
@view_mode_picker_active = false
|
|
96
106
|
@view_mode_picker_index = 0
|
|
97
107
|
@token_setup_active = false
|
|
98
108
|
@token_input_buffer = InputBuffer.new
|
|
99
109
|
@token_setup_error = nil
|
|
110
|
+
@token_setup_warning = nil
|
|
100
111
|
@token_setup_status = :idle
|
|
101
112
|
@shutdown_requested = false
|
|
102
113
|
@previous_signal_handlers = {}
|
|
103
114
|
@watchdog_thread = nil
|
|
104
115
|
@screens = {
|
|
105
|
-
chat: Screens::Chat.new(cable_client: cable_client)
|
|
116
|
+
chat: Screens::Chat.new(cable_client: cable_client, perf_logger: @perf_logger)
|
|
106
117
|
}
|
|
107
118
|
end
|
|
108
119
|
|
|
@@ -130,20 +141,31 @@ module TUI
|
|
|
130
141
|
private
|
|
131
142
|
|
|
132
143
|
def render(frame, tui)
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
144
|
+
@perf_logger.start_frame
|
|
145
|
+
|
|
146
|
+
@screens[:chat].hud_hint = !@hud_visible
|
|
147
|
+
|
|
148
|
+
if @hud_visible
|
|
149
|
+
hud_width = [frame.area.width / 3, HUD_MIN_WIDTH].max
|
|
150
|
+
content_area, sidebar = tui.split(
|
|
151
|
+
frame.area,
|
|
152
|
+
direction: :horizontal,
|
|
153
|
+
constraints: [
|
|
154
|
+
tui.constraint_fill(1),
|
|
155
|
+
tui.constraint_length(hud_width)
|
|
156
|
+
]
|
|
157
|
+
)
|
|
141
158
|
|
|
142
|
-
|
|
143
|
-
|
|
159
|
+
@perf_logger.measure(:chat_render) { @screens[@current_screen].render(frame, content_area, tui) }
|
|
160
|
+
@perf_logger.measure(:sidebar) { render_sidebar(frame, sidebar, tui) }
|
|
161
|
+
else
|
|
162
|
+
@perf_logger.measure(:chat_render) { @screens[@current_screen].render(frame, frame.area, tui) }
|
|
163
|
+
end
|
|
144
164
|
|
|
145
165
|
check_token_setup_signals
|
|
146
166
|
render_token_setup_popup(frame, frame.area, tui) if @token_setup_active
|
|
167
|
+
|
|
168
|
+
@perf_logger.end_frame
|
|
147
169
|
end
|
|
148
170
|
|
|
149
171
|
def render_sidebar(frame, area, tui)
|
|
@@ -171,10 +193,73 @@ module TUI
|
|
|
171
193
|
frame.render_widget(menu, area)
|
|
172
194
|
end
|
|
173
195
|
|
|
196
|
+
# HUD status icons for goal progress and sub-agent activity.
|
|
197
|
+
GOAL_ICON_ACTIVE = "\u25CF" # ●
|
|
198
|
+
GOAL_ICON_IN_PROGRESS = "\u25D0" # ◐
|
|
199
|
+
GOAL_ICON_COMPLETED = "\u2713" # ✓
|
|
200
|
+
CHILD_ICON_RUNNING = "\u25CF" # ●
|
|
201
|
+
CHILD_ICON_IDLE = "\u25CC" # ◌
|
|
202
|
+
|
|
174
203
|
def render_info(frame, area, tui)
|
|
175
204
|
session = @screens[:chat].session_info
|
|
176
|
-
view_mode = @screens[:chat].view_mode
|
|
177
205
|
|
|
206
|
+
# Split into main content area and bottom status bar
|
|
207
|
+
main_area, status_area = tui.split(
|
|
208
|
+
area,
|
|
209
|
+
direction: :vertical,
|
|
210
|
+
constraints: [
|
|
211
|
+
tui.constraint_fill(1),
|
|
212
|
+
tui.constraint_length(3)
|
|
213
|
+
]
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
render_hud_content(frame, main_area, tui, session)
|
|
217
|
+
render_hud_status_bar(frame, status_area, tui)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Renders the main HUD content: session name, goals, skills,
|
|
221
|
+
# workflow, and sub-agents.
|
|
222
|
+
def render_hud_content(frame, area, tui, session)
|
|
223
|
+
session_label = session[:name] || "##{session[:id]}"
|
|
224
|
+
|
|
225
|
+
lines = [
|
|
226
|
+
tui.line(spans: [
|
|
227
|
+
tui.span(content: "\u{1F4CB} ", style: tui.style(fg: "dark_gray")),
|
|
228
|
+
tui.span(content: session_label, style: tui.style(fg: "cyan", modifiers: [:bold]))
|
|
229
|
+
]),
|
|
230
|
+
hud_goals_section(tui, session),
|
|
231
|
+
hud_skills_line(tui, session),
|
|
232
|
+
hud_workflow_line(tui, session),
|
|
233
|
+
hud_children_section(tui, session),
|
|
234
|
+
interaction_state_line(tui)
|
|
235
|
+
].flatten.compact
|
|
236
|
+
|
|
237
|
+
content = tui.paragraph(
|
|
238
|
+
text: lines,
|
|
239
|
+
wrap: true,
|
|
240
|
+
block: tui.block(
|
|
241
|
+
borders: [:left, :top, :right],
|
|
242
|
+
border_type: :rounded,
|
|
243
|
+
border_style: {fg: "white"}
|
|
244
|
+
)
|
|
245
|
+
)
|
|
246
|
+
frame.render_widget(content, area)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Renders the bottom status bar: connection state and model name.
|
|
250
|
+
def render_hud_status_bar(frame, area, tui)
|
|
251
|
+
cable_status = @cable_client.status
|
|
252
|
+
style = STATUS_STYLES.fetch(cable_status, STATUS_STYLES[:disconnected])
|
|
253
|
+
|
|
254
|
+
status_label = if cable_status == :reconnecting
|
|
255
|
+
attempt = @cable_client.reconnect_attempt
|
|
256
|
+
max = CableClient::MAX_RECONNECT_ATTEMPTS
|
|
257
|
+
"#{style[:label]} (#{attempt}/#{max})"
|
|
258
|
+
else
|
|
259
|
+
style[:label]
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
view_mode = @screens[:chat].view_mode
|
|
178
263
|
mode_label = view_mode.capitalize
|
|
179
264
|
mode_color = case view_mode
|
|
180
265
|
when "verbose" then "yellow"
|
|
@@ -182,110 +267,133 @@ module TUI
|
|
|
182
267
|
else "cyan"
|
|
183
268
|
end
|
|
184
269
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
lines = [
|
|
188
|
-
tui.line(spans: [
|
|
189
|
-
tui.span(content: "Anima v#{Anima::VERSION}", style: tui.style(fg: "white"))
|
|
190
|
-
]),
|
|
191
|
-
tui.line(spans: [tui.span(content: "")]),
|
|
192
|
-
if session[:name]
|
|
193
|
-
tui.line(spans: [
|
|
194
|
-
tui.span(content: session_label, style: tui.style(fg: "cyan", modifiers: [:bold]))
|
|
195
|
-
])
|
|
196
|
-
else
|
|
270
|
+
bar = tui.paragraph(
|
|
271
|
+
text: [
|
|
197
272
|
tui.line(spans: [
|
|
198
|
-
tui.span(content:
|
|
199
|
-
tui.span(content:
|
|
273
|
+
tui.span(content: status_label, style: tui.style(fg: style[:color], modifiers: [:bold])),
|
|
274
|
+
tui.span(content: " \u2502 ", style: tui.style(fg: "dark_gray")),
|
|
275
|
+
tui.span(content: mode_label, style: tui.style(fg: mode_color, modifiers: [:bold]))
|
|
200
276
|
])
|
|
201
|
-
|
|
202
|
-
tui.line(spans: [
|
|
203
|
-
tui.span(content: "Messages ", style: tui.style(fg: "dark_gray")),
|
|
204
|
-
tui.span(content: session[:message_count].to_s, style: tui.style(fg: "cyan"))
|
|
205
|
-
]),
|
|
206
|
-
active_skills_line(tui, session),
|
|
207
|
-
active_workflow_line(tui, session),
|
|
208
|
-
goals_line(tui, session),
|
|
209
|
-
tui.line(spans: [tui.span(content: "")]),
|
|
210
|
-
tui.line(spans: [
|
|
211
|
-
tui.span(content: "Mode ", style: tui.style(fg: "dark_gray")),
|
|
212
|
-
tui.span(content: mode_label, style: tui.style(fg: mode_color, modifiers: [:bold]))
|
|
213
|
-
]),
|
|
214
|
-
interaction_state_line(tui),
|
|
215
|
-
tui.line(spans: [tui.span(content: "")]),
|
|
216
|
-
connection_status_line(tui),
|
|
217
|
-
tui.line(spans: [tui.span(content: "")]),
|
|
218
|
-
tui.line(spans: [
|
|
219
|
-
tui.span(content: "Ctrl+a", style: tui.style(fg: "cyan", modifiers: [:bold])),
|
|
220
|
-
tui.span(content: " command mode", style: tui.style(fg: "dark_gray"))
|
|
221
|
-
])
|
|
222
|
-
].compact
|
|
223
|
-
|
|
224
|
-
info = tui.paragraph(
|
|
225
|
-
text: lines,
|
|
277
|
+
],
|
|
226
278
|
block: tui.block(
|
|
227
|
-
|
|
228
|
-
borders: [:all],
|
|
279
|
+
borders: [:left, :bottom, :right],
|
|
229
280
|
border_type: :rounded,
|
|
230
281
|
border_style: {fg: "white"}
|
|
231
282
|
)
|
|
232
283
|
)
|
|
233
|
-
frame.render_widget(
|
|
284
|
+
frame.render_widget(bar, area)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Builds goal lines with status icons and descriptions.
|
|
288
|
+
# Root goals show as individual lines. A root goal with some completed
|
|
289
|
+
# sub-goals shows the ◐ (in-progress) icon.
|
|
290
|
+
#
|
|
291
|
+
# @return [Array<RatatuiRuby::Widgets::Line>, nil]
|
|
292
|
+
def hud_goals_section(tui, session)
|
|
293
|
+
goal_list = session[:goals]
|
|
294
|
+
return if goal_list.nil? || goal_list.empty?
|
|
295
|
+
|
|
296
|
+
lines = [
|
|
297
|
+
tui.line(spans: [tui.span(content: "")]),
|
|
298
|
+
tui.line(spans: [
|
|
299
|
+
tui.span(content: "\u{1F3AF} Goals", style: tui.style(fg: "dark_gray"))
|
|
300
|
+
])
|
|
301
|
+
]
|
|
302
|
+
|
|
303
|
+
goal_list.each do |goal|
|
|
304
|
+
icon, color = goal_icon_and_color(goal)
|
|
305
|
+
lines << tui.line(spans: [
|
|
306
|
+
tui.span(content: " #{icon} ", style: tui.style(fg: color)),
|
|
307
|
+
tui.span(content: goal["description"].to_s, style: tui.style(fg: "white"))
|
|
308
|
+
])
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
lines
|
|
234
312
|
end
|
|
235
313
|
|
|
236
|
-
#
|
|
237
|
-
#
|
|
238
|
-
#
|
|
239
|
-
# @param
|
|
240
|
-
# @return [
|
|
241
|
-
def
|
|
314
|
+
# Returns the status icon and color for a goal.
|
|
315
|
+
# Active goals with some completed sub-goals show as in-progress (◐).
|
|
316
|
+
#
|
|
317
|
+
# @param goal [Hash] goal data with "status" and optional "sub_goals" keys
|
|
318
|
+
# @return [Array(String, String)] icon and color pair
|
|
319
|
+
def goal_icon_and_color(goal)
|
|
320
|
+
if goal["status"] == "completed"
|
|
321
|
+
[GOAL_ICON_COMPLETED, "green"]
|
|
322
|
+
elsif goal["sub_goals"]&.any? { |sg| sg["status"] == "completed" }
|
|
323
|
+
[GOAL_ICON_IN_PROGRESS, "yellow"]
|
|
324
|
+
else
|
|
325
|
+
[GOAL_ICON_ACTIVE, "cyan"]
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# Builds the skills line with brain emoji.
|
|
330
|
+
# @return [Array<RatatuiRuby::Widgets::Line>, nil]
|
|
331
|
+
def hud_skills_line(tui, session)
|
|
242
332
|
skills = session[:active_skills]
|
|
243
333
|
return if skills.nil? || skills.empty?
|
|
244
334
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
tui.
|
|
248
|
-
|
|
249
|
-
|
|
335
|
+
[
|
|
336
|
+
tui.line(spans: [tui.span(content: "")]),
|
|
337
|
+
tui.line(spans: [
|
|
338
|
+
tui.span(content: "\u{1F9E0} ", style: tui.style(fg: "dark_gray")),
|
|
339
|
+
tui.span(content: skills.join(", "), style: tui.style(fg: "yellow"))
|
|
340
|
+
])
|
|
341
|
+
]
|
|
250
342
|
end
|
|
251
343
|
|
|
252
|
-
# Builds the
|
|
253
|
-
#
|
|
254
|
-
|
|
255
|
-
# @param session [Hash] session info hash containing :active_workflow string
|
|
256
|
-
# @return [RatatuiRuby::Widgets::Line, nil] styled workflow line, or nil when empty
|
|
257
|
-
def active_workflow_line(tui, session)
|
|
344
|
+
# Builds the workflow line with scroll emoji.
|
|
345
|
+
# @return [Array<RatatuiRuby::Widgets::Line>, nil]
|
|
346
|
+
def hud_workflow_line(tui, session)
|
|
258
347
|
workflow = session[:active_workflow]
|
|
259
348
|
return if workflow.nil? || workflow.empty?
|
|
260
349
|
|
|
261
|
-
|
|
262
|
-
tui.
|
|
263
|
-
tui.
|
|
264
|
-
|
|
350
|
+
[
|
|
351
|
+
tui.line(spans: [tui.span(content: "")]),
|
|
352
|
+
tui.line(spans: [
|
|
353
|
+
tui.span(content: "\u{1F4DC} ", style: tui.style(fg: "dark_gray")),
|
|
354
|
+
tui.span(content: workflow, style: tui.style(fg: "magenta"))
|
|
355
|
+
])
|
|
356
|
+
]
|
|
265
357
|
end
|
|
266
358
|
|
|
267
|
-
# Builds the
|
|
268
|
-
#
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
# @return [RatatuiRuby::Widgets::Line, nil] styled goals line, or nil when empty
|
|
273
|
-
def goals_line(tui, session)
|
|
274
|
-
goal_list = session[:goals]
|
|
275
|
-
return if goal_list.nil? || goal_list.empty?
|
|
359
|
+
# Builds the sub-agents section with activity indicators.
|
|
360
|
+
# @return [Array<RatatuiRuby::Widgets::Line>, nil]
|
|
361
|
+
def hud_children_section(tui, session)
|
|
362
|
+
children = session[:children]
|
|
363
|
+
return if children.nil? || children.empty?
|
|
276
364
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
365
|
+
lines = [
|
|
366
|
+
tui.line(spans: [tui.span(content: "")]),
|
|
367
|
+
tui.line(spans: [
|
|
368
|
+
tui.span(content: "\u{1F465} Sub-agents", style: tui.style(fg: "dark_gray"))
|
|
369
|
+
])
|
|
370
|
+
]
|
|
371
|
+
|
|
372
|
+
children.each do |child|
|
|
373
|
+
icon, color = child_icon_and_color(child)
|
|
374
|
+
name = child["name"] || "sub-agent"
|
|
375
|
+
lines << tui.line(spans: [
|
|
376
|
+
tui.span(content: " #{icon} ", style: tui.style(fg: color)),
|
|
377
|
+
tui.span(content: "@#{name}", style: tui.style(fg: "white"))
|
|
378
|
+
])
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
lines
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
# Returns the activity icon and color for a child session.
|
|
385
|
+
#
|
|
386
|
+
# @param child [Hash] child session data with "processing" key
|
|
387
|
+
# @return [Array(String, String)] icon and color pair
|
|
388
|
+
def child_icon_and_color(child)
|
|
389
|
+
if child["processing"]
|
|
390
|
+
[CHILD_ICON_RUNNING, "yellow"]
|
|
391
|
+
else
|
|
392
|
+
[CHILD_ICON_IDLE, "green"]
|
|
393
|
+
end
|
|
285
394
|
end
|
|
286
395
|
|
|
287
|
-
#
|
|
288
|
-
# Shows "Scrolling" when chat pane is focused, or "Thinking..." during LLM processing.
|
|
396
|
+
# Shows "Scrolling" when chat pane is focused, "Thinking..." during LLM processing.
|
|
289
397
|
def interaction_state_line(tui)
|
|
290
398
|
if @screens[:chat].chat_focused
|
|
291
399
|
tui.line(spans: [
|
|
@@ -295,33 +403,9 @@ module TUI
|
|
|
295
403
|
tui.line(spans: [
|
|
296
404
|
tui.span(content: "Thinking...", style: tui.style(fg: "magenta", modifiers: [:bold]))
|
|
297
405
|
])
|
|
298
|
-
else
|
|
299
|
-
tui.line(spans: [tui.span(content: "")])
|
|
300
406
|
end
|
|
301
407
|
end
|
|
302
408
|
|
|
303
|
-
# Builds the connection status line for the info panel.
|
|
304
|
-
# Shows a single emoji for the normal (subscribed) state; adds descriptive
|
|
305
|
-
# text only when something requires attention.
|
|
306
|
-
# @param tui [RatatuiRuby] TUI rendering context
|
|
307
|
-
# @return [RatatuiRuby::Widgets::Line] styled status line with emoji indicator
|
|
308
|
-
def connection_status_line(tui)
|
|
309
|
-
cable_status = @cable_client.status
|
|
310
|
-
style = STATUS_STYLES.fetch(cable_status, STATUS_STYLES[:disconnected])
|
|
311
|
-
|
|
312
|
-
label = if cable_status == :reconnecting
|
|
313
|
-
attempt = @cable_client.reconnect_attempt
|
|
314
|
-
max = CableClient::MAX_RECONNECT_ATTEMPTS
|
|
315
|
-
"#{style[:label]} (#{attempt}/#{max})"
|
|
316
|
-
else
|
|
317
|
-
style[:label]
|
|
318
|
-
end
|
|
319
|
-
|
|
320
|
-
tui.line(spans: [
|
|
321
|
-
tui.span(content: label, style: tui.style(fg: style[:color], modifiers: [:bold]))
|
|
322
|
-
])
|
|
323
|
-
end
|
|
324
|
-
|
|
325
409
|
def chat_loading?
|
|
326
410
|
@screens[:chat].loading?
|
|
327
411
|
end
|
|
@@ -365,6 +449,9 @@ module TUI
|
|
|
365
449
|
when :anthropic_token
|
|
366
450
|
activate_token_setup
|
|
367
451
|
nil
|
|
452
|
+
when :toggle_hud
|
|
453
|
+
@hud_visible = !@hud_visible
|
|
454
|
+
nil
|
|
368
455
|
when :new_session
|
|
369
456
|
@screens[:chat].new_session
|
|
370
457
|
@current_screen = :chat
|
|
@@ -391,9 +478,15 @@ module TUI
|
|
|
391
478
|
return nil
|
|
392
479
|
end
|
|
393
480
|
|
|
481
|
+
# Escape key priority: unfocus chat > interrupt tools > clear input > parent session
|
|
394
482
|
if event.esc?
|
|
395
|
-
|
|
396
|
-
|
|
483
|
+
chat = @screens[:chat]
|
|
484
|
+
if chat.chat_focused
|
|
485
|
+
chat.unfocus_chat
|
|
486
|
+
elsif chat.loading? && chat.input.empty?
|
|
487
|
+
chat.interrupt_execution
|
|
488
|
+
elsif !chat.input.empty?
|
|
489
|
+
chat.clear_input
|
|
397
490
|
else
|
|
398
491
|
return_to_parent_session
|
|
399
492
|
end
|
|
@@ -885,13 +978,14 @@ module TUI
|
|
|
885
978
|
# -- Token setup popup -----------------------------------------------
|
|
886
979
|
|
|
887
980
|
# Opens the token setup popup and resets all input state.
|
|
888
|
-
# Can be triggered manually via
|
|
981
|
+
# Can be triggered manually via C-a → a or automatically when the
|
|
889
982
|
# brain broadcasts authentication_required.
|
|
890
983
|
# @return [void]
|
|
891
984
|
def activate_token_setup
|
|
892
985
|
@token_setup_active = true
|
|
893
986
|
@token_input_buffer.clear
|
|
894
987
|
@token_setup_error = nil
|
|
988
|
+
@token_setup_warning = nil
|
|
895
989
|
@token_setup_status = :idle
|
|
896
990
|
end
|
|
897
991
|
|
|
@@ -901,6 +995,7 @@ module TUI
|
|
|
901
995
|
@token_setup_active = false
|
|
902
996
|
@token_input_buffer.clear
|
|
903
997
|
@token_setup_error = nil
|
|
998
|
+
@token_setup_warning = nil
|
|
904
999
|
@token_setup_status = :idle
|
|
905
1000
|
end
|
|
906
1001
|
|
|
@@ -927,9 +1022,11 @@ module TUI
|
|
|
927
1022
|
if result[:success]
|
|
928
1023
|
@token_setup_status = :success
|
|
929
1024
|
@token_setup_error = nil
|
|
1025
|
+
@token_setup_warning = result[:warning]
|
|
930
1026
|
else
|
|
931
1027
|
@token_setup_status = :error
|
|
932
1028
|
@token_setup_error = result[:message]
|
|
1029
|
+
@token_setup_warning = nil
|
|
933
1030
|
end
|
|
934
1031
|
end
|
|
935
1032
|
|
|
@@ -1081,9 +1178,15 @@ module TUI
|
|
|
1081
1178
|
end
|
|
1082
1179
|
|
|
1083
1180
|
if @token_setup_status == :success
|
|
1084
|
-
lines <<
|
|
1085
|
-
tui.
|
|
1086
|
-
|
|
1181
|
+
lines << if @token_setup_warning
|
|
1182
|
+
tui.line(spans: [
|
|
1183
|
+
tui.span(content: "Token saved (API unavailable, validation skipped)", style: tui.style(fg: "yellow", modifiers: [:bold]))
|
|
1184
|
+
])
|
|
1185
|
+
else
|
|
1186
|
+
tui.line(spans: [
|
|
1187
|
+
tui.span(content: "Token saved and validated!", style: tui.style(fg: "green", modifiers: [:bold]))
|
|
1188
|
+
])
|
|
1189
|
+
end
|
|
1087
1190
|
lines << tui.line(spans: [tui.span(content: "")])
|
|
1088
1191
|
end
|
|
1089
1192
|
|
|
@@ -1104,7 +1207,7 @@ module TUI
|
|
|
1104
1207
|
def token_status_display
|
|
1105
1208
|
case @token_setup_status
|
|
1106
1209
|
when :success
|
|
1107
|
-
["Valid", "green"]
|
|
1210
|
+
@token_setup_warning ? ["Saved (unverified)", "yellow"] : ["Valid", "green"]
|
|
1108
1211
|
when :validating
|
|
1109
1212
|
["Validating...", "yellow"]
|
|
1110
1213
|
when :error
|
data/lib/tui/cable_client.rb
CHANGED
|
@@ -134,6 +134,14 @@ module TUI
|
|
|
134
134
|
send_action("recall_pending", {"event_id" => event_id})
|
|
135
135
|
end
|
|
136
136
|
|
|
137
|
+
# Requests interruption of the current tool execution. The server sets
|
|
138
|
+
# an interrupt flag that the LLM client checks between tool calls.
|
|
139
|
+
#
|
|
140
|
+
# @return [void]
|
|
141
|
+
def interrupt
|
|
142
|
+
send_action("interrupt_execution", {})
|
|
143
|
+
end
|
|
144
|
+
|
|
137
145
|
# Sends an Anthropic subscription token to the brain for validation and storage.
|
|
138
146
|
# The token flows directly from TUI input to encrypted credentials — never
|
|
139
147
|
# enters the LLM context window.
|