anima-core 1.3.0 → 1.5.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/.reek.yml +23 -26
- data/README.md +118 -104
- data/agents/thoughts-analyzer.md +12 -7
- data/anima-core.gemspec +1 -0
- data/app/channels/session_channel.rb +38 -58
- data/app/decorators/agent_message_decorator.rb +7 -2
- data/app/decorators/message_decorator.rb +31 -100
- data/app/decorators/pending_from_melete_decorator.rb +36 -0
- data/app/decorators/pending_from_melete_goal_decorator.rb +13 -0
- data/app/decorators/pending_from_melete_skill_decorator.rb +19 -0
- data/app/decorators/pending_from_melete_workflow_decorator.rb +13 -0
- data/app/decorators/pending_from_mneme_decorator.rb +44 -0
- data/app/decorators/pending_message_decorator.rb +94 -0
- data/app/decorators/pending_subagent_decorator.rb +46 -0
- data/app/decorators/pending_tool_response_decorator.rb +51 -0
- data/app/decorators/pending_user_message_decorator.rb +22 -0
- data/app/decorators/system_message_decorator.rb +5 -0
- data/app/decorators/tool_call_decorator.rb +16 -5
- data/app/decorators/tool_response_decorator.rb +2 -2
- data/app/decorators/user_message_decorator.rb +7 -2
- data/app/jobs/count_tokens_job.rb +23 -0
- data/app/jobs/drain_job.rb +169 -0
- data/app/jobs/melete_enrichment_job/goal_change_listener.rb +52 -0
- data/app/jobs/melete_enrichment_job.rb +48 -0
- data/app/jobs/mneme_enrichment_job.rb +46 -0
- data/app/jobs/tool_execution_job.rb +87 -0
- data/app/models/concerns/token_estimation.rb +54 -0
- data/app/models/goal.rb +23 -11
- data/app/models/message.rb +46 -48
- data/app/models/pending_message.rb +407 -12
- data/app/models/pinned_message.rb +8 -3
- data/app/models/session.rb +660 -566
- data/app/models/snapshot.rb +11 -21
- data/bin/inspect-cassette +157 -0
- data/bin/release +212 -0
- data/bin/with-llms +20 -0
- data/config/application.rb +1 -0
- data/config/database.yml +1 -0
- data/config/initializers/event_subscribers.rb +71 -4
- data/config/initializers/inflections.rb +3 -1
- data/db/cable_structure.sql +9 -0
- data/db/migrate/20260330120000_add_source_to_pending_messages.rb +8 -0
- data/db/migrate/20260401180000_add_api_metrics_to_messages.rb +7 -0
- data/db/migrate/20260401210935_remove_recalled_message_ids_from_sessions.rb +5 -0
- data/db/migrate/20260403080031_add_initial_cwd_to_sessions.rb +5 -0
- data/db/migrate/20260407170803_remove_viewport_message_ids_from_sessions.rb +5 -0
- data/db/migrate/20260407180400_remove_mneme_snapshot_pointer_columns_from_sessions.rb +6 -0
- data/db/migrate/20260411120553_add_token_count_to_pinned_messages.rb +5 -0
- data/db/migrate/20260411172926_remove_active_skills_and_workflow_from_sessions.rb +6 -0
- data/db/migrate/20260412110625_replace_processing_with_aasm_state.rb +6 -0
- data/db/migrate/20260418150323_add_kind_and_message_type_to_pending_messages.rb +6 -0
- data/db/migrate/20260419120000_add_drain_fields_to_pending_messages.rb +7 -0
- data/db/migrate/20260419130000_drop_pending_messages_kind_default.rb +5 -0
- data/db/migrate/20260419140000_add_drain_indexes_to_pending_messages.rb +8 -0
- data/db/migrate/20260420100000_add_hud_visibility_to_sessions.rb +15 -0
- data/db/queue_structure.sql +61 -0
- data/db/structure.sql +133 -0
- data/lib/agents/registry.rb +1 -1
- data/lib/anima/cli.rb +41 -13
- data/lib/anima/installer.rb +13 -0
- data/lib/anima/settings.rb +16 -36
- data/lib/anima/version.rb +1 -1
- data/lib/events/authentication_required.rb +24 -0
- data/lib/events/bounce_back.rb +4 -4
- data/lib/events/eviction_completed.rb +28 -0
- data/lib/events/goal_created.rb +28 -0
- data/lib/events/goal_updated.rb +32 -0
- data/lib/events/llm_responded.rb +35 -0
- data/lib/events/message_created.rb +27 -0
- data/lib/events/message_updated.rb +25 -0
- data/lib/events/session_state_changed.rb +30 -0
- data/lib/events/skill_activated.rb +28 -0
- data/lib/events/start_melete.rb +36 -0
- data/lib/events/start_mneme.rb +33 -0
- data/lib/events/start_processing.rb +32 -0
- data/lib/events/subagent_evicted.rb +31 -0
- data/lib/events/subscribers/active_state_broadcaster.rb +27 -0
- data/lib/events/subscribers/authentication_broadcaster.rb +34 -0
- data/lib/events/subscribers/drain_kickoff.rb +20 -0
- data/lib/events/subscribers/eviction_broadcaster.rb +26 -0
- data/lib/events/subscribers/llm_response_handler.rb +111 -0
- data/lib/events/subscribers/melete_kickoff.rb +24 -0
- data/lib/events/subscribers/message_broadcaster.rb +34 -0
- data/lib/events/subscribers/mneme_kickoff.rb +24 -0
- data/lib/events/subscribers/mneme_scheduler.rb +21 -0
- data/lib/events/subscribers/persister.rb +8 -9
- data/lib/events/subscribers/session_state_broadcaster.rb +33 -0
- data/lib/events/subscribers/subagent_message_router.rb +28 -34
- data/lib/events/subscribers/subagent_visibility_broadcaster.rb +33 -0
- data/lib/events/subscribers/tool_response_creator.rb +33 -0
- data/lib/events/subscribers/transient_broadcaster.rb +1 -1
- data/lib/events/tool_executed.rb +34 -0
- data/lib/events/workflow_activated.rb +27 -0
- data/lib/llm/client.rb +46 -199
- data/lib/mcp/client_manager.rb +41 -46
- data/lib/mcp/stdio_transport.rb +9 -5
- data/lib/{analytical_brain → melete}/runner.rb +73 -68
- data/lib/{analytical_brain → melete}/tools/activate_skill.rb +3 -3
- data/lib/{analytical_brain → melete}/tools/assign_nickname.rb +3 -3
- data/lib/{analytical_brain → melete}/tools/everything_is_ready.rb +2 -2
- data/lib/{analytical_brain → melete}/tools/finish_goal.rb +6 -3
- data/lib/melete/tools/goal_messaging.rb +29 -0
- data/lib/{analytical_brain → melete}/tools/read_workflow.rb +4 -4
- data/lib/{analytical_brain → melete}/tools/rename_session.rb +3 -3
- data/lib/{analytical_brain → melete}/tools/set_goal.rb +6 -2
- data/lib/{analytical_brain → melete}/tools/update_goal.rb +9 -5
- data/lib/{analytical_brain.rb → melete.rb} +6 -3
- data/lib/mneme/base_runner.rb +121 -0
- data/lib/mneme/l2_runner.rb +14 -20
- data/lib/mneme/recall_runner.rb +132 -0
- data/lib/mneme/runner.rb +123 -165
- data/lib/mneme/search.rb +104 -62
- data/lib/mneme/tools/nothing_to_surface.rb +25 -0
- data/lib/mneme/tools/save_snapshot.rb +2 -10
- data/lib/mneme/tools/surface_memory.rb +89 -0
- data/lib/mneme.rb +11 -5
- data/lib/providers/anthropic.rb +112 -7
- data/lib/shell_session.rb +290 -432
- data/lib/skills/definition.rb +2 -2
- data/lib/skills/registry.rb +1 -1
- data/lib/tools/base.rb +16 -1
- data/lib/tools/bash.rb +25 -55
- data/lib/tools/edit.rb +2 -0
- data/lib/tools/mark_goal_completed.rb +4 -5
- data/lib/tools/read.rb +2 -0
- data/lib/tools/registry.rb +85 -4
- data/lib/tools/response_truncator.rb +1 -1
- data/lib/tools/{recall.rb → search_messages.rb} +19 -21
- data/lib/tools/spawn_specialist.rb +22 -14
- data/lib/tools/spawn_subagent.rb +30 -20
- data/lib/tools/subagent_prompts.rb +17 -19
- data/lib/tools/think.rb +1 -1
- data/lib/tools/{remember.rb → view_messages.rb} +10 -10
- data/lib/tools/write.rb +2 -0
- data/lib/tui/app.rb +393 -149
- data/lib/tui/braille_spinner.rb +7 -7
- data/lib/tui/cable_client.rb +9 -16
- data/lib/tui/decorators/base_decorator.rb +47 -6
- data/lib/tui/decorators/bash_decorator.rb +1 -1
- data/lib/tui/decorators/edit_decorator.rb +4 -2
- data/lib/tui/decorators/read_decorator.rb +4 -2
- data/lib/tui/decorators/think_decorator.rb +2 -2
- data/lib/tui/decorators/web_get_decorator.rb +1 -1
- data/lib/tui/decorators/write_decorator.rb +4 -2
- data/lib/tui/flash.rb +19 -14
- data/lib/tui/formatting.rb +20 -9
- data/lib/tui/input_buffer.rb +6 -6
- data/lib/tui/message_store.rb +165 -28
- data/lib/tui/performance_logger.rb +2 -3
- data/lib/tui/screens/chat.rb +149 -79
- data/lib/tui/settings.rb +93 -0
- data/lib/workflows/definition.rb +3 -3
- data/lib/workflows/registry.rb +1 -1
- data/skills/github.md +38 -0
- data/templates/config.toml +16 -32
- data/templates/tui.toml +209 -0
- data/workflows/review_pr.md +18 -14
- metadata +98 -29
- data/app/jobs/agent_request_job.rb +0 -199
- data/app/jobs/analytical_brain_job.rb +0 -33
- data/app/jobs/count_message_tokens_job.rb +0 -39
- data/app/jobs/passive_recall_job.rb +0 -29
- data/app/models/concerns/message/broadcasting.rb +0 -85
- data/config/initializers/fts5_schema_dump.rb +0 -21
- data/lib/agent_loop.rb +0 -186
- data/lib/analytical_brain/tools/deactivate_skill.rb +0 -39
- data/lib/analytical_brain/tools/deactivate_workflow.rb +0 -34
- data/lib/environment_probe.rb +0 -232
- data/lib/events/agent_message.rb +0 -11
- data/lib/events/subscribers/message_collector.rb +0 -64
- data/lib/events/tool_call.rb +0 -31
- data/lib/events/tool_response.rb +0 -33
- data/lib/mneme/compressed_viewport.rb +0 -200
- data/lib/mneme/passive_recall.rb +0 -69
data/lib/tui/app.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "time"
|
|
4
|
+
require_relative "settings"
|
|
4
5
|
require_relative "cable_client"
|
|
5
6
|
require_relative "input_buffer"
|
|
6
7
|
require_relative "message_store"
|
|
@@ -21,10 +22,7 @@ module TUI
|
|
|
21
22
|
}.freeze
|
|
22
23
|
|
|
23
24
|
MENU_LABELS = (COMMAND_KEYS.map { |key, action| "[#{key}] #{action.to_s.tr("_", " ").capitalize}" } +
|
|
24
|
-
["[\u2191] Scroll chat", "[\u2193] Return to input", "[\u2192]
|
|
25
|
-
|
|
26
|
-
# HUD occupies 1/3 of screen width, clamped to a usable minimum.
|
|
27
|
-
HUD_MIN_WIDTH = 24
|
|
25
|
+
["[\u2191] Scroll chat", "[\u2193] Return to input", "[\u2192] Sub-agents / HUD"]).freeze
|
|
28
26
|
|
|
29
27
|
# Picker entry prefix width: "[N]" (3) + marker (1) + space (1) = 5
|
|
30
28
|
PICKER_PREFIX_WIDTH = 5
|
|
@@ -38,28 +36,22 @@ module TUI
|
|
|
38
36
|
|
|
39
37
|
# Connection status emoji indicators for the info panel.
|
|
40
38
|
# Subscribed (normal state) shows only the emoji; other states add text.
|
|
41
|
-
|
|
42
|
-
disconnected:
|
|
43
|
-
connecting:
|
|
44
|
-
connected:
|
|
45
|
-
subscribed:
|
|
46
|
-
reconnecting:
|
|
39
|
+
STATUS_LABELS = {
|
|
40
|
+
disconnected: "🔴 Disconnected",
|
|
41
|
+
connecting: "🟡 Connecting",
|
|
42
|
+
connected: "🟡 Connecting",
|
|
43
|
+
subscribed: "🟢",
|
|
44
|
+
reconnecting: "🟡 Reconnecting"
|
|
47
45
|
}.freeze
|
|
48
46
|
|
|
49
|
-
#
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
# Token setup popup dimensions. Height accommodates: status line, blank,
|
|
59
|
-
# 2 instruction lines, blank, "Token:" label, input line, blank,
|
|
60
|
-
# error/success line, blank, hint line, plus top/bottom borders.
|
|
61
|
-
POPUP_HEIGHT = 14
|
|
62
|
-
POPUP_MIN_WIDTH = 44
|
|
47
|
+
# Maps connection status to semantic theme color.
|
|
48
|
+
STATUS_COLORS = {
|
|
49
|
+
disconnected: :theme_color_error,
|
|
50
|
+
connecting: :theme_color_warning,
|
|
51
|
+
connected: :theme_color_warning,
|
|
52
|
+
subscribed: :theme_color_success,
|
|
53
|
+
reconnecting: :theme_color_warning
|
|
54
|
+
}.freeze
|
|
63
55
|
|
|
64
56
|
# Matches a single printable Unicode character (no control codes).
|
|
65
57
|
PRINTABLE_CHAR = /\A[[:print:]]\z/
|
|
@@ -67,27 +59,18 @@ module TUI
|
|
|
67
59
|
# Signals that trigger graceful shutdown when received from the OS.
|
|
68
60
|
SHUTDOWN_SIGNALS = %w[HUP TERM INT].freeze
|
|
69
61
|
|
|
70
|
-
# How often the watchdog thread checks if the controlling terminal is alive.
|
|
71
|
-
# @see #terminal_watchdog_loop
|
|
72
|
-
TERMINAL_CHECK_INTERVAL = 0.5
|
|
73
|
-
|
|
74
62
|
# Unix controlling terminal device path.
|
|
75
63
|
# @see #terminal_watchdog_loop
|
|
76
64
|
CONTROLLING_TERMINAL = "/dev/tty"
|
|
77
65
|
|
|
78
|
-
# Grace period for watchdog thread to exit before force-killing it.
|
|
79
|
-
WATCHDOG_SHUTDOWN_TIMEOUT = 1
|
|
80
|
-
|
|
81
|
-
# HUD scroll step sizes (lines per event).
|
|
82
|
-
HUD_SCROLL_STEP = 1
|
|
83
|
-
HUD_MOUSE_SCROLL_STEP = 2
|
|
84
|
-
|
|
85
66
|
attr_reader :current_screen, :command_mode, :session_picker_active,
|
|
86
67
|
:view_mode_picker_active
|
|
87
68
|
# @return [Boolean] true when the HUD info panel is visible
|
|
88
69
|
attr_reader :hud_visible
|
|
89
70
|
# @return [Boolean] true when the HUD pane has keyboard focus for scrolling
|
|
90
71
|
attr_reader :hud_focused
|
|
72
|
+
# @return [Integer, nil] index of selected child in HUD navigation, nil when inactive
|
|
73
|
+
attr_reader :hud_child_index
|
|
91
74
|
# @return [Boolean] true when the token setup popup overlay is visible
|
|
92
75
|
attr_reader :token_setup_active
|
|
93
76
|
# @return [Boolean] true when graceful shutdown has been requested via signal
|
|
@@ -109,6 +92,7 @@ module TUI
|
|
|
109
92
|
@session_picker_parent_id = nil
|
|
110
93
|
@hud_visible = true
|
|
111
94
|
@hud_focused = false
|
|
95
|
+
@hud_child_index = nil
|
|
112
96
|
@hud_scroll_offset = 0
|
|
113
97
|
@hud_max_scroll = 0
|
|
114
98
|
@hud_visible_height = 0
|
|
@@ -157,7 +141,7 @@ module TUI
|
|
|
157
141
|
@screens[:chat].hud_hint = !@hud_visible
|
|
158
142
|
|
|
159
143
|
if @hud_visible
|
|
160
|
-
hud_width = [frame.area.width / 3,
|
|
144
|
+
hud_width = [frame.area.width / 3, Settings.hud_min_width].max
|
|
161
145
|
content_area, sidebar = tui.split(
|
|
162
146
|
frame.area,
|
|
163
147
|
direction: :horizontal,
|
|
@@ -198,7 +182,7 @@ module TUI
|
|
|
198
182
|
title: "Command",
|
|
199
183
|
borders: [:all],
|
|
200
184
|
border_type: :rounded,
|
|
201
|
-
border_style: {fg:
|
|
185
|
+
border_style: {fg: Settings.theme_border_focused}
|
|
202
186
|
)
|
|
203
187
|
)
|
|
204
188
|
frame.render_widget(menu, area)
|
|
@@ -218,18 +202,34 @@ module TUI
|
|
|
218
202
|
|
|
219
203
|
def render_info(frame, area, tui)
|
|
220
204
|
session = @screens[:chat].session_info
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
205
|
+
stats = @screens[:chat].message_store.token_economy
|
|
206
|
+
has_metrics = stats[:call_count] > 0 || stats[:rate_limits]
|
|
207
|
+
|
|
208
|
+
# Split into main content, optional token economy panel, and status bar
|
|
209
|
+
if has_metrics
|
|
210
|
+
economy_height = token_economy_line_count(stats) + 1 # +1 for top border
|
|
211
|
+
main_area, economy_area, status_area = tui.split(
|
|
212
|
+
area,
|
|
213
|
+
direction: :vertical,
|
|
214
|
+
constraints: [
|
|
215
|
+
tui.constraint_fill(1),
|
|
216
|
+
tui.constraint_length(economy_height),
|
|
217
|
+
tui.constraint_length(2)
|
|
218
|
+
]
|
|
219
|
+
)
|
|
220
|
+
render_hud_content(frame, main_area, tui, session)
|
|
221
|
+
render_token_economy_panel(frame, economy_area, tui, stats)
|
|
222
|
+
else
|
|
223
|
+
main_area, status_area = tui.split(
|
|
224
|
+
area,
|
|
225
|
+
direction: :vertical,
|
|
226
|
+
constraints: [
|
|
227
|
+
tui.constraint_fill(1),
|
|
228
|
+
tui.constraint_length(2)
|
|
229
|
+
]
|
|
230
|
+
)
|
|
231
|
+
render_hud_content(frame, main_area, tui, session)
|
|
232
|
+
end
|
|
233
233
|
render_hud_status_bar(frame, status_area, tui)
|
|
234
234
|
end
|
|
235
235
|
|
|
@@ -242,8 +242,8 @@ module TUI
|
|
|
242
242
|
|
|
243
243
|
lines = [
|
|
244
244
|
tui.line(spans: [
|
|
245
|
-
tui.span(content: "\u{1F4CB} ", style: tui.style(fg:
|
|
246
|
-
tui.span(content: session_label, style: tui.style(fg:
|
|
245
|
+
tui.span(content: "\u{1F4CB} ", style: tui.style(fg: Settings.theme_color_muted)),
|
|
246
|
+
tui.span(content: session_label, style: tui.style(fg: Settings.theme_color_info, modifiers: [:bold]))
|
|
247
247
|
]),
|
|
248
248
|
hud_goals_section(tui, session),
|
|
249
249
|
hud_skills_line(tui, session),
|
|
@@ -258,7 +258,7 @@ module TUI
|
|
|
258
258
|
@hud_max_scroll = [total_height - @hud_visible_height, 0].max
|
|
259
259
|
@hud_scroll_offset = @hud_scroll_offset.clamp(0, @hud_max_scroll)
|
|
260
260
|
|
|
261
|
-
border_color = @hud_focused ?
|
|
261
|
+
border_color = @hud_focused ? Settings.theme_border_focused : Settings.theme_border_normal
|
|
262
262
|
|
|
263
263
|
content = tui.paragraph(
|
|
264
264
|
text: lines,
|
|
@@ -283,9 +283,9 @@ module TUI
|
|
|
283
283
|
content_length: @hud_max_scroll,
|
|
284
284
|
position: @hud_scroll_offset,
|
|
285
285
|
orientation: :vertical_right,
|
|
286
|
-
thumb_style: {fg:
|
|
286
|
+
thumb_style: {fg: Settings.theme_scrollbar_thumb},
|
|
287
287
|
track_symbol: "\u2502",
|
|
288
|
-
track_style: {fg:
|
|
288
|
+
track_style: {fg: Settings.theme_scrollbar_track}
|
|
289
289
|
)
|
|
290
290
|
frame.render_widget(scrollbar, area)
|
|
291
291
|
end
|
|
@@ -293,36 +293,38 @@ module TUI
|
|
|
293
293
|
# Renders the bottom status bar: connection state and model name.
|
|
294
294
|
def render_hud_status_bar(frame, area, tui)
|
|
295
295
|
cable_status = @cable_client.status
|
|
296
|
-
|
|
296
|
+
label = STATUS_LABELS.fetch(cable_status, STATUS_LABELS[:disconnected])
|
|
297
|
+
status_color = Settings.public_send(STATUS_COLORS.fetch(cable_status, :theme_color_error))
|
|
297
298
|
|
|
298
299
|
status_label = if cable_status == :reconnecting
|
|
299
300
|
attempt = @cable_client.reconnect_attempt
|
|
300
|
-
max =
|
|
301
|
-
"#{
|
|
301
|
+
max = Settings.connection_max_reconnect_attempts
|
|
302
|
+
"#{label} (#{attempt}/#{max})"
|
|
302
303
|
else
|
|
303
|
-
|
|
304
|
+
label
|
|
304
305
|
end
|
|
305
306
|
|
|
306
307
|
view_mode = @screens[:chat].view_mode
|
|
307
308
|
mode_label = view_mode.capitalize
|
|
308
309
|
mode_color = case view_mode
|
|
309
|
-
when "verbose" then
|
|
310
|
-
when "debug" then
|
|
311
|
-
else
|
|
310
|
+
when "verbose" then Settings.theme_color_warning
|
|
311
|
+
when "debug" then Settings.theme_color_accent
|
|
312
|
+
else Settings.theme_color_info
|
|
312
313
|
end
|
|
313
314
|
|
|
314
315
|
bar = tui.paragraph(
|
|
315
316
|
text: [
|
|
316
317
|
tui.line(spans: [
|
|
317
|
-
tui.span(content:
|
|
318
|
-
tui.span(content:
|
|
318
|
+
tui.span(content: " ", style: tui.style(fg: Settings.theme_color_muted)),
|
|
319
|
+
tui.span(content: status_label, style: tui.style(fg: status_color, modifiers: [:bold])),
|
|
320
|
+
tui.span(content: " ", style: tui.style(fg: Settings.theme_color_muted)),
|
|
319
321
|
tui.span(content: mode_label, style: tui.style(fg: mode_color, modifiers: [:bold]))
|
|
320
322
|
])
|
|
321
323
|
],
|
|
322
324
|
block: tui.block(
|
|
323
325
|
borders: [:left, :bottom, :right],
|
|
324
326
|
border_type: :rounded,
|
|
325
|
-
border_style: {fg:
|
|
327
|
+
border_style: {fg: Settings.theme_border_normal}
|
|
326
328
|
)
|
|
327
329
|
)
|
|
328
330
|
frame.render_widget(bar, area)
|
|
@@ -340,7 +342,7 @@ module TUI
|
|
|
340
342
|
lines = [
|
|
341
343
|
tui.line(spans: [tui.span(content: "")]),
|
|
342
344
|
tui.line(spans: [
|
|
343
|
-
tui.span(content: "\u{1F3AF} Goals", style: tui.style(fg:
|
|
345
|
+
tui.span(content: "\u{1F3AF} Goals", style: tui.style(fg: Settings.theme_color_muted))
|
|
344
346
|
])
|
|
345
347
|
]
|
|
346
348
|
|
|
@@ -348,7 +350,7 @@ module TUI
|
|
|
348
350
|
icon, color = goal_icon_and_color(goal)
|
|
349
351
|
lines << tui.line(spans: [
|
|
350
352
|
tui.span(content: " #{icon} ", style: tui.style(fg: color)),
|
|
351
|
-
tui.span(content: goal["description"].to_s, style: tui.style(fg:
|
|
353
|
+
tui.span(content: goal["description"].to_s, style: tui.style(fg: Settings.theme_color_text))
|
|
352
354
|
])
|
|
353
355
|
end
|
|
354
356
|
|
|
@@ -362,11 +364,11 @@ module TUI
|
|
|
362
364
|
# @return [Array(String, String)] icon and color pair
|
|
363
365
|
def goal_icon_and_color(goal)
|
|
364
366
|
if goal["status"] == "completed"
|
|
365
|
-
[GOAL_ICON_COMPLETED,
|
|
367
|
+
[GOAL_ICON_COMPLETED, Settings.theme_color_success]
|
|
366
368
|
elsif goal["sub_goals"]&.any? { |sg| sg["status"] == "completed" }
|
|
367
|
-
[GOAL_ICON_IN_PROGRESS,
|
|
369
|
+
[GOAL_ICON_IN_PROGRESS, Settings.theme_color_warning]
|
|
368
370
|
else
|
|
369
|
-
[GOAL_ICON_ACTIVE,
|
|
371
|
+
[GOAL_ICON_ACTIVE, Settings.theme_color_info]
|
|
370
372
|
end
|
|
371
373
|
end
|
|
372
374
|
|
|
@@ -379,8 +381,8 @@ module TUI
|
|
|
379
381
|
[
|
|
380
382
|
tui.line(spans: [tui.span(content: "")]),
|
|
381
383
|
tui.line(spans: [
|
|
382
|
-
tui.span(content: "\u{1F9E0} ", style: tui.style(fg:
|
|
383
|
-
tui.span(content: skills.join(", "), style: tui.style(fg:
|
|
384
|
+
tui.span(content: "\u{1F9E0} ", style: tui.style(fg: Settings.theme_color_muted)),
|
|
385
|
+
tui.span(content: skills.join(", "), style: tui.style(fg: Settings.theme_color_warning))
|
|
384
386
|
])
|
|
385
387
|
]
|
|
386
388
|
end
|
|
@@ -394,32 +396,44 @@ module TUI
|
|
|
394
396
|
[
|
|
395
397
|
tui.line(spans: [tui.span(content: "")]),
|
|
396
398
|
tui.line(spans: [
|
|
397
|
-
tui.span(content: "\u{1F4DC} ", style: tui.style(fg:
|
|
398
|
-
tui.span(content: workflow, style: tui.style(fg:
|
|
399
|
+
tui.span(content: "\u{1F4DC} ", style: tui.style(fg: Settings.theme_color_muted)),
|
|
400
|
+
tui.span(content: workflow, style: tui.style(fg: Settings.theme_color_accent))
|
|
399
401
|
])
|
|
400
402
|
]
|
|
401
403
|
end
|
|
402
404
|
|
|
403
405
|
# Builds the sub-agents section with activity indicators.
|
|
406
|
+
# Highlights the selected child when HUD child navigation is active.
|
|
404
407
|
# @return [Array<RatatuiRuby::Widgets::Line>, nil]
|
|
405
408
|
def hud_children_section(tui, session)
|
|
406
409
|
children = session[:children]
|
|
407
410
|
return if children.nil? || children.empty?
|
|
408
411
|
|
|
412
|
+
header_label = @hud_child_index ? "Sub-agents [\u2190 back]" : "Sub-agents"
|
|
413
|
+
|
|
409
414
|
lines = [
|
|
410
415
|
tui.line(spans: [tui.span(content: "")]),
|
|
411
416
|
tui.line(spans: [
|
|
412
|
-
tui.span(content: "\u{1F465}
|
|
417
|
+
tui.span(content: "\u{1F465} #{header_label}", style: tui.style(fg: Settings.theme_color_muted))
|
|
413
418
|
])
|
|
414
419
|
]
|
|
415
420
|
|
|
416
|
-
children.
|
|
421
|
+
children.each_with_index do |child, idx|
|
|
417
422
|
icon, color = child_icon_and_color(child)
|
|
418
423
|
name = child["name"] || "sub-agent"
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
424
|
+
selected = idx == @hud_child_index
|
|
425
|
+
|
|
426
|
+
lines << if selected
|
|
427
|
+
tui.line(spans: [
|
|
428
|
+
tui.span(content: "\u25B8 #{icon} ", style: tui.style(fg: Settings.theme_color_info)),
|
|
429
|
+
tui.span(content: "@#{name}", style: tui.style(fg: Settings.theme_color_info, modifiers: [:bold]))
|
|
430
|
+
])
|
|
431
|
+
else
|
|
432
|
+
tui.line(spans: [
|
|
433
|
+
tui.span(content: " #{icon} ", style: tui.style(fg: color)),
|
|
434
|
+
tui.span(content: "@#{name}", style: tui.style(fg: Settings.theme_color_text))
|
|
435
|
+
])
|
|
436
|
+
end
|
|
423
437
|
end
|
|
424
438
|
|
|
425
439
|
lines
|
|
@@ -434,27 +448,212 @@ module TUI
|
|
|
434
448
|
# @return [Array(String, String)] icon and color pair
|
|
435
449
|
def child_icon_and_color(child)
|
|
436
450
|
case child["session_state"]
|
|
437
|
-
when "
|
|
438
|
-
[CHILD_ICON_GENERATING,
|
|
439
|
-
when "
|
|
440
|
-
[CHILD_ICON_TOOL_EXECUTING,
|
|
451
|
+
when "awaiting"
|
|
452
|
+
[CHILD_ICON_GENERATING, Settings.theme_color_success]
|
|
453
|
+
when "executing"
|
|
454
|
+
[CHILD_ICON_TOOL_EXECUTING, Settings.theme_color_success]
|
|
441
455
|
when "interrupting"
|
|
442
|
-
[CHILD_ICON_INTERRUPTING,
|
|
456
|
+
[CHILD_ICON_INTERRUPTING, Settings.theme_color_error]
|
|
457
|
+
else
|
|
458
|
+
[CHILD_ICON_IDLE, Settings.theme_color_muted]
|
|
459
|
+
end
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
# Renders the Token Economy panel as a fixed section between HUD content
|
|
463
|
+
# and the status bar.
|
|
464
|
+
def render_token_economy_panel(frame, area, tui, stats)
|
|
465
|
+
lines = []
|
|
466
|
+
|
|
467
|
+
rate_limits = stats[:rate_limits]
|
|
468
|
+
lines.concat(build_rate_limit_lines(tui, rate_limits)) if rate_limits
|
|
469
|
+
lines.concat(build_cache_metrics_lines(tui, stats)) if stats[:call_count] > 0
|
|
470
|
+
|
|
471
|
+
panel = tui.paragraph(
|
|
472
|
+
text: lines,
|
|
473
|
+
block: tui.block(
|
|
474
|
+
title: " \u{1F4CA} Token Economy ",
|
|
475
|
+
borders: [:left, :top, :right],
|
|
476
|
+
border_type: :rounded,
|
|
477
|
+
border_style: {fg: Settings.theme_border_normal}
|
|
478
|
+
)
|
|
479
|
+
)
|
|
480
|
+
frame.render_widget(panel, area)
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
# Returns the number of content lines the token economy panel will render.
|
|
484
|
+
def token_economy_line_count(stats)
|
|
485
|
+
count = 0
|
|
486
|
+
if stats[:rate_limits]
|
|
487
|
+
count += 1 if stats[:rate_limits]["5h_utilization"]
|
|
488
|
+
count += 1 if stats[:rate_limits]["7d_utilization"]
|
|
489
|
+
end
|
|
490
|
+
if stats[:call_count] > 0
|
|
491
|
+
count += 2 # cache hit bar + saved
|
|
492
|
+
count += 1 if stats[:cache_history]&.size.to_i > 1 # sparkline
|
|
493
|
+
end
|
|
494
|
+
count
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
# Builds rate limit display lines with progress bars and pacing.
|
|
498
|
+
# Compact aligned labels: 5h, 7d (same 2-char width).
|
|
499
|
+
# @return [Array<RatatuiRuby::Widgets::Line>]
|
|
500
|
+
def build_rate_limit_lines(tui, rate_limits)
|
|
501
|
+
lines = []
|
|
502
|
+
|
|
503
|
+
# 5-hour window
|
|
504
|
+
util_5h = rate_limits["5h_utilization"]
|
|
505
|
+
if util_5h
|
|
506
|
+
pct = (util_5h * 100).round
|
|
507
|
+
bar = build_progress_bar(pct, Settings.theme_progress_bar_width)
|
|
508
|
+
color = rate_limit_color(pct)
|
|
509
|
+
reset_5h = rate_limits["5h_reset"]
|
|
510
|
+
reset_label = reset_5h ? " #{format_reset_time(reset_5h)}" : ""
|
|
511
|
+
|
|
512
|
+
lines << tui.line(spans: [
|
|
513
|
+
tui.span(content: " 5h ", style: tui.style(fg: color, modifiers: [:bold])),
|
|
514
|
+
tui.span(content: bar, style: tui.style(fg: color)),
|
|
515
|
+
tui.span(content: " #{pct.to_s.rjust(2)}%", style: tui.style(fg: color)),
|
|
516
|
+
tui.span(content: reset_label, style: tui.style(fg: Settings.theme_color_muted))
|
|
517
|
+
])
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
# 7-day window
|
|
521
|
+
util_7d = rate_limits["7d_utilization"]
|
|
522
|
+
if util_7d
|
|
523
|
+
pct = (util_7d * 100).round
|
|
524
|
+
bar = build_progress_bar(pct, Settings.theme_progress_bar_width)
|
|
525
|
+
color = rate_limit_color(pct)
|
|
526
|
+
|
|
527
|
+
lines << tui.line(spans: [
|
|
528
|
+
tui.span(content: " 7d ", style: tui.style(fg: color, modifiers: [:bold])),
|
|
529
|
+
tui.span(content: bar, style: tui.style(fg: color)),
|
|
530
|
+
tui.span(content: " #{pct.to_s.rjust(2)}%", style: tui.style(fg: color))
|
|
531
|
+
])
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
lines
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
# Builds cache efficiency display lines with sparkline graph.
|
|
538
|
+
# ⚡ for hit rate bar, braille sparkline for per-call history, 💾 for saved.
|
|
539
|
+
# @return [Array<RatatuiRuby::Widgets::Line>]
|
|
540
|
+
def build_cache_metrics_lines(tui, stats)
|
|
541
|
+
hit_rate = (stats[:cache_hit_rate] * 100).round
|
|
542
|
+
color = cache_hit_color(hit_rate)
|
|
543
|
+
bar = build_progress_bar(hit_rate, Settings.theme_progress_bar_width)
|
|
544
|
+
|
|
545
|
+
total_cached = stats[:cache_read_input_tokens]
|
|
546
|
+
saved_label = format_token_count(total_cached)
|
|
547
|
+
|
|
548
|
+
lines = [
|
|
549
|
+
tui.line(spans: [
|
|
550
|
+
tui.span(content: " \u{26A1} ", style: tui.style(fg: color)),
|
|
551
|
+
tui.span(content: bar, style: tui.style(fg: color)),
|
|
552
|
+
tui.span(content: " #{hit_rate.to_s.rjust(2)}%", style: tui.style(fg: color))
|
|
553
|
+
]),
|
|
554
|
+
tui.line(spans: [
|
|
555
|
+
tui.span(content: " \u{1F4BE} ", style: tui.style(fg: Settings.theme_color_success)),
|
|
556
|
+
tui.span(content: saved_label, style: tui.style(fg: Settings.theme_color_success))
|
|
557
|
+
])
|
|
558
|
+
]
|
|
559
|
+
|
|
560
|
+
# Braille sparkline: per-call cache hit history (below icons)
|
|
561
|
+
history = stats[:cache_history]
|
|
562
|
+
if history && history.size > 1
|
|
563
|
+
sparkline = build_braille_sparkline(history)
|
|
564
|
+
color = cache_hit_color(hit_rate)
|
|
565
|
+
lines << tui.line(spans: [
|
|
566
|
+
tui.span(content: " ", style: tui.style(fg: Settings.theme_color_muted)),
|
|
567
|
+
tui.span(content: sparkline, style: tui.style(fg: color))
|
|
568
|
+
])
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
lines
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
# Builds a braille sparkline from a series of values (0.0-1.0).
|
|
575
|
+
# Each braille character encodes two adjacent data points using
|
|
576
|
+
# the left and right dot columns (4 dots each = 5 levels: 0-4).
|
|
577
|
+
#
|
|
578
|
+
# @param values [Array<Float>] data points between 0.0 and 1.0
|
|
579
|
+
# @return [String] braille sparkline string
|
|
580
|
+
def build_braille_sparkline(values)
|
|
581
|
+
# Braille dot positions: left column (dots 1,2,3,7), right column (dots 4,5,6,8)
|
|
582
|
+
# Numbered per Unicode spec: bit 0=dot1, bit 1=dot2, bit 2=dot3,
|
|
583
|
+
# bit 3=dot4, bit 4=dot5, bit 5=dot6, bit 6=dot7, bit 7=dot8
|
|
584
|
+
left_dots = [0x01, 0x02, 0x04, 0x40] # dots 1,2,3,7 (bottom to top)
|
|
585
|
+
right_dots = [0x08, 0x10, 0x20, 0x80] # dots 4,5,6,8 (bottom to top)
|
|
586
|
+
|
|
587
|
+
chars = []
|
|
588
|
+
values.each_slice(2) do |pair|
|
|
589
|
+
code = 0x2800 # braille base
|
|
590
|
+
# Left column: first value
|
|
591
|
+
fill_left = (pair[0].clamp(0.0, 1.0) * 4).round
|
|
592
|
+
fill_left.times { |i| code |= left_dots[i] }
|
|
593
|
+
# Right column: second value (or repeat first if odd)
|
|
594
|
+
right_val = pair[1] || pair[0]
|
|
595
|
+
fill_right = (right_val.clamp(0.0, 1.0) * 4).round
|
|
596
|
+
fill_right.times { |i| code |= right_dots[i] }
|
|
597
|
+
chars << code.chr(Encoding::UTF_8)
|
|
598
|
+
end
|
|
599
|
+
chars.join
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
# Builds a Unicode progress bar.
|
|
603
|
+
# @param pct [Integer] percentage (0-100)
|
|
604
|
+
# @param width [Integer] bar width in characters
|
|
605
|
+
# @return [String] progress bar string
|
|
606
|
+
def build_progress_bar(pct, width)
|
|
607
|
+
filled = (pct.clamp(0, 100) * width / 100.0).round
|
|
608
|
+
empty = width - filled
|
|
609
|
+
("\u2593" * filled) + ("\u2591" * empty)
|
|
610
|
+
end
|
|
611
|
+
|
|
612
|
+
# Color for rate limit percentage: green below warning, yellow below critical, red above.
|
|
613
|
+
def rate_limit_color(pct)
|
|
614
|
+
return Settings.theme_color_error if pct >= Settings.theme_rate_limit_critical
|
|
615
|
+
return Settings.theme_color_warning if pct >= Settings.theme_rate_limit_warning
|
|
616
|
+
Settings.theme_color_success
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
# Color for cache hit rate: green above good, yellow above low, red below.
|
|
620
|
+
def cache_hit_color(pct)
|
|
621
|
+
return Settings.theme_color_success if pct >= Settings.theme_cache_hit_good
|
|
622
|
+
return Settings.theme_color_warning if pct >= Settings.theme_cache_hit_low
|
|
623
|
+
Settings.theme_color_error
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
# Formats a reset timestamp as time remaining (e.g., "2h15m").
|
|
627
|
+
def format_reset_time(epoch_seconds)
|
|
628
|
+
diff = epoch_seconds - Time.now.to_i
|
|
629
|
+
return "soon" if diff <= 0
|
|
630
|
+
|
|
631
|
+
hours = diff / 3600
|
|
632
|
+
minutes = (diff % 3600) / 60
|
|
633
|
+
if hours > 0
|
|
634
|
+
"\u279E#{hours}h#{minutes.to_s.rjust(2, "0")}m"
|
|
443
635
|
else
|
|
444
|
-
|
|
636
|
+
"\u279E#{minutes}m"
|
|
445
637
|
end
|
|
446
638
|
end
|
|
447
639
|
|
|
640
|
+
# Formats a token count for display (e.g., "47.2K").
|
|
641
|
+
def format_token_count(count)
|
|
642
|
+
return "0" if count == 0
|
|
643
|
+
return count.to_s if count < 1000
|
|
644
|
+
"#{(count / 1000.0).round(1)}K tokens"
|
|
645
|
+
end
|
|
646
|
+
|
|
448
647
|
# Shows focus mode when a pane is focused, or the braille spinner
|
|
449
648
|
# during active processing.
|
|
450
649
|
def interaction_state_line(tui)
|
|
451
650
|
if @hud_focused
|
|
452
651
|
tui.line(spans: [
|
|
453
|
-
tui.span(content: "HUD Scroll", style: tui.style(fg:
|
|
652
|
+
tui.span(content: "HUD Scroll", style: tui.style(fg: Settings.theme_color_warning, modifiers: [:bold]))
|
|
454
653
|
])
|
|
455
654
|
elsif @screens[:chat].chat_focused
|
|
456
655
|
tui.line(spans: [
|
|
457
|
-
tui.span(content: "Scrolling", style: tui.style(fg:
|
|
656
|
+
tui.span(content: "Scrolling", style: tui.style(fg: Settings.theme_color_warning, modifiers: [:bold]))
|
|
458
657
|
])
|
|
459
658
|
elsif chat_loading?
|
|
460
659
|
chat = @screens[:chat]
|
|
@@ -472,20 +671,34 @@ module TUI
|
|
|
472
671
|
@screens[:chat].loading?
|
|
473
672
|
end
|
|
474
673
|
|
|
475
|
-
# Switches keyboard focus to the HUD pane
|
|
476
|
-
#
|
|
674
|
+
# Switches keyboard focus to the HUD pane.
|
|
675
|
+
# When children exist, enters child navigation mode with the first
|
|
676
|
+
# child selected; otherwise enters plain scroll mode.
|
|
477
677
|
#
|
|
478
678
|
# @return [void]
|
|
479
679
|
def focus_hud
|
|
480
680
|
@screens[:chat].unfocus_chat if @screens[:chat].chat_focused
|
|
481
681
|
@hud_focused = true
|
|
682
|
+
children = hud_children
|
|
683
|
+
if children.any?
|
|
684
|
+
@hud_child_index = 0
|
|
685
|
+
@hud_scroll_offset = @hud_max_scroll
|
|
686
|
+
end
|
|
482
687
|
end
|
|
483
688
|
|
|
484
|
-
# Returns keyboard focus from the HUD pane.
|
|
689
|
+
# Returns keyboard focus from the HUD pane and exits child navigation.
|
|
485
690
|
#
|
|
486
691
|
# @return [void]
|
|
487
692
|
def unfocus_hud
|
|
488
693
|
@hud_focused = false
|
|
694
|
+
@hud_child_index = nil
|
|
695
|
+
end
|
|
696
|
+
|
|
697
|
+
# Returns the current session's child sessions (sub-agents).
|
|
698
|
+
#
|
|
699
|
+
# @return [Array<Hash>] child session hashes, empty when none
|
|
700
|
+
def hud_children
|
|
701
|
+
@screens[:chat].session_info[:children] || []
|
|
489
702
|
end
|
|
490
703
|
|
|
491
704
|
# Scrolls the HUD viewport up, clamping at the top.
|
|
@@ -606,7 +819,10 @@ module TUI
|
|
|
606
819
|
end
|
|
607
820
|
|
|
608
821
|
# Handles keyboard events when the HUD pane has focus.
|
|
609
|
-
#
|
|
822
|
+
# When a child is selected (child navigation mode), arrow keys move
|
|
823
|
+
# between sub-agents and Enter switches to the selected session.
|
|
824
|
+
# Otherwise, arrow keys and Page Up/Down scroll the HUD content.
|
|
825
|
+
# Escape and Ctrl+A always exit HUD focus.
|
|
610
826
|
def handle_hud_focused_event(event)
|
|
611
827
|
return nil if event.none?
|
|
612
828
|
return :quit if event.ctrl_c?
|
|
@@ -624,10 +840,40 @@ module TUI
|
|
|
624
840
|
return nil
|
|
625
841
|
end
|
|
626
842
|
|
|
843
|
+
if @hud_child_index
|
|
844
|
+
handle_hud_child_navigation(event)
|
|
845
|
+
else
|
|
846
|
+
handle_hud_scroll(event)
|
|
847
|
+
end
|
|
848
|
+
nil
|
|
849
|
+
end
|
|
850
|
+
|
|
851
|
+
# Navigates between sub-agent entries in the HUD.
|
|
852
|
+
# Up/Down move selection, Enter switches to the selected session,
|
|
853
|
+
# Left exits child navigation back to scroll mode.
|
|
854
|
+
def handle_hud_child_navigation(event)
|
|
855
|
+
children = hud_children
|
|
856
|
+
return if children.empty?
|
|
857
|
+
|
|
627
858
|
if event.up?
|
|
628
|
-
|
|
859
|
+
@hud_child_index = [@hud_child_index - 1, 0].max
|
|
629
860
|
elsif event.down?
|
|
630
|
-
|
|
861
|
+
@hud_child_index = [@hud_child_index + 1, children.size - 1].min
|
|
862
|
+
elsif event.left?
|
|
863
|
+
@hud_child_index = nil
|
|
864
|
+
elsif event.enter?
|
|
865
|
+
child = children[@hud_child_index]
|
|
866
|
+
@screens[:chat].switch_session(child["id"]) if child
|
|
867
|
+
unfocus_hud
|
|
868
|
+
end
|
|
869
|
+
end
|
|
870
|
+
|
|
871
|
+
# Scrolls the HUD viewport with arrow/page keys.
|
|
872
|
+
def handle_hud_scroll(event)
|
|
873
|
+
if event.up?
|
|
874
|
+
scroll_hud_up(Settings.hud_scroll_step)
|
|
875
|
+
elsif event.down?
|
|
876
|
+
scroll_hud_down(Settings.hud_scroll_step)
|
|
631
877
|
elsif event.page_up?
|
|
632
878
|
scroll_hud_up(@hud_visible_height)
|
|
633
879
|
elsif event.page_down?
|
|
@@ -637,7 +883,6 @@ module TUI
|
|
|
637
883
|
elsif event.end?
|
|
638
884
|
scroll_hud_down(@hud_max_scroll)
|
|
639
885
|
end
|
|
640
|
-
nil
|
|
641
886
|
end
|
|
642
887
|
|
|
643
888
|
# Routes mouse scroll events to the HUD when the cursor is over the HUD area.
|
|
@@ -648,9 +893,9 @@ module TUI
|
|
|
648
893
|
return false unless event.scroll_up? || event.scroll_down?
|
|
649
894
|
|
|
650
895
|
if event.scroll_up?
|
|
651
|
-
scroll_hud_up(
|
|
896
|
+
scroll_hud_up(Settings.hud_mouse_scroll_step)
|
|
652
897
|
else
|
|
653
|
-
scroll_hud_down(
|
|
898
|
+
scroll_hud_down(Settings.hud_mouse_scroll_step)
|
|
654
899
|
end
|
|
655
900
|
true
|
|
656
901
|
end
|
|
@@ -749,12 +994,10 @@ module TUI
|
|
|
749
994
|
CHILD_STATUS_DONE = "\u2713" # ✓
|
|
750
995
|
CHILDREN_ARROW = "\u25B8" # ▸ shown next to sessions with children
|
|
751
996
|
UNNAMED_SUBAGENT_LABEL = "sub-agent"
|
|
752
|
-
SESSION_PICKER_PAGE_SIZE = 9
|
|
753
|
-
SESSION_PICKER_FETCH_LIMIT = 50
|
|
754
997
|
BACK_ARROW = "\u2190" # ←
|
|
755
998
|
|
|
756
999
|
# Requests the session list from the brain and opens the picker overlay.
|
|
757
|
-
# Fetches up to
|
|
1000
|
+
# Fetches up to Settings.session_picker_fetch_limit sessions for client-side pagination.
|
|
758
1001
|
# @return [void]
|
|
759
1002
|
def activate_session_picker
|
|
760
1003
|
@session_picker_active = true
|
|
@@ -762,7 +1005,7 @@ module TUI
|
|
|
762
1005
|
@session_picker_page = 0
|
|
763
1006
|
@session_picker_mode = :root
|
|
764
1007
|
@session_picker_parent_id = nil
|
|
765
|
-
@cable_client.list_sessions(limit:
|
|
1008
|
+
@cable_client.list_sessions(limit: Settings.session_picker_fetch_limit)
|
|
766
1009
|
end
|
|
767
1010
|
|
|
768
1011
|
# Dispatches keyboard events while the session picker overlay is open.
|
|
@@ -826,8 +1069,8 @@ module TUI
|
|
|
826
1069
|
# @return [Array<Hash>] visible items for the current page
|
|
827
1070
|
def session_picker_visible_items
|
|
828
1071
|
all = session_picker_all_items_for_mode
|
|
829
|
-
start = @session_picker_page *
|
|
830
|
-
page = all[start,
|
|
1072
|
+
start = @session_picker_page * Settings.session_picker_page_size
|
|
1073
|
+
page = all[start, Settings.session_picker_page_size] || []
|
|
831
1074
|
|
|
832
1075
|
page.map do |item|
|
|
833
1076
|
case @session_picker_mode
|
|
@@ -842,13 +1085,13 @@ module TUI
|
|
|
842
1085
|
# @return [Boolean] true when more items exist beyond the current page
|
|
843
1086
|
def session_picker_has_more?
|
|
844
1087
|
total = session_picker_all_items_for_mode.size
|
|
845
|
-
((@session_picker_page + 1) *
|
|
1088
|
+
((@session_picker_page + 1) * Settings.session_picker_page_size) < total
|
|
846
1089
|
end
|
|
847
1090
|
|
|
848
1091
|
# @return [Integer] number of items beyond the current page
|
|
849
1092
|
def session_picker_remaining_count
|
|
850
1093
|
total = session_picker_all_items_for_mode.size
|
|
851
|
-
[total - ((@session_picker_page + 1) *
|
|
1094
|
+
[total - ((@session_picker_page + 1) * Settings.session_picker_page_size), 0].max
|
|
852
1095
|
end
|
|
853
1096
|
|
|
854
1097
|
# Handles Escape in the session picker. In children mode, returns to root.
|
|
@@ -925,7 +1168,7 @@ module TUI
|
|
|
925
1168
|
|
|
926
1169
|
if sessions.nil?
|
|
927
1170
|
lines = [tui.line(spans: [
|
|
928
|
-
tui.span(content: "Loading...", style: tui.style(fg:
|
|
1171
|
+
tui.span(content: "Loading...", style: tui.style(fg: Settings.theme_color_warning))
|
|
929
1172
|
])]
|
|
930
1173
|
else
|
|
931
1174
|
visible = session_picker_visible_items
|
|
@@ -943,7 +1186,7 @@ module TUI
|
|
|
943
1186
|
|
|
944
1187
|
if lines.empty?
|
|
945
1188
|
lines = [tui.line(spans: [
|
|
946
|
-
tui.span(content: "No sessions", style: tui.style(fg:
|
|
1189
|
+
tui.span(content: "No sessions", style: tui.style(fg: Settings.theme_color_muted))
|
|
947
1190
|
])]
|
|
948
1191
|
end
|
|
949
1192
|
end
|
|
@@ -954,7 +1197,7 @@ module TUI
|
|
|
954
1197
|
title: session_picker_title,
|
|
955
1198
|
borders: [:all],
|
|
956
1199
|
border_type: :rounded,
|
|
957
|
-
border_style: {fg:
|
|
1200
|
+
border_style: {fg: Settings.theme_color_info}
|
|
958
1201
|
)
|
|
959
1202
|
)
|
|
960
1203
|
frame.render_widget(picker, area)
|
|
@@ -995,11 +1238,11 @@ module TUI
|
|
|
995
1238
|
label = "#{prefix}#{marker}#{arrow}#{display_name} #{count}#{child_info} #{time}"
|
|
996
1239
|
|
|
997
1240
|
style = if selected
|
|
998
|
-
tui.style(fg:
|
|
1241
|
+
tui.style(fg: Settings.theme_highlight_fg, bg: Settings.theme_highlight_bg)
|
|
999
1242
|
elsif is_current
|
|
1000
|
-
tui.style(fg:
|
|
1243
|
+
tui.style(fg: Settings.theme_color_info, modifiers: [:bold])
|
|
1001
1244
|
else
|
|
1002
|
-
tui.style(fg:
|
|
1245
|
+
tui.style(fg: Settings.theme_color_text)
|
|
1003
1246
|
end
|
|
1004
1247
|
|
|
1005
1248
|
[tui.line(spans: [tui.span(content: label, style: style)])]
|
|
@@ -1019,16 +1262,17 @@ module TUI
|
|
|
1019
1262
|
hotkey = picker_hotkey(idx)
|
|
1020
1263
|
prefix = hotkey ? "[#{hotkey}]" : " "
|
|
1021
1264
|
marker = is_current ? "*" : " "
|
|
1022
|
-
|
|
1023
|
-
|
|
1265
|
+
active = child["session_state"] != "idle"
|
|
1266
|
+
status = active ? CHILD_STATUS_RUNNING : CHILD_STATUS_DONE
|
|
1267
|
+
status_color = active ? Settings.theme_color_warning : Settings.theme_color_success
|
|
1024
1268
|
display_name = child["name"] || UNNAMED_SUBAGENT_LABEL
|
|
1025
1269
|
|
|
1026
1270
|
label = "#{prefix}#{marker}#{status} #{display_name}"
|
|
1027
1271
|
|
|
1028
1272
|
style = if selected
|
|
1029
|
-
tui.style(fg:
|
|
1273
|
+
tui.style(fg: Settings.theme_highlight_fg, bg: Settings.theme_highlight_bg)
|
|
1030
1274
|
elsif is_current
|
|
1031
|
-
tui.style(fg:
|
|
1275
|
+
tui.style(fg: Settings.theme_color_info, modifiers: [:bold])
|
|
1032
1276
|
else
|
|
1033
1277
|
tui.style(fg: status_color)
|
|
1034
1278
|
end
|
|
@@ -1043,7 +1287,7 @@ module TUI
|
|
|
1043
1287
|
def format_load_more_entry(tui)
|
|
1044
1288
|
remaining = session_picker_remaining_count
|
|
1045
1289
|
label = "[0] Load more (#{remaining})"
|
|
1046
|
-
[tui.line(spans: [tui.span(content: label, style: tui.style(fg:
|
|
1290
|
+
[tui.line(spans: [tui.span(content: label, style: tui.style(fg: Settings.theme_color_muted))])]
|
|
1047
1291
|
end
|
|
1048
1292
|
|
|
1049
1293
|
# -- View mode picker ----------------------------------------------
|
|
@@ -1100,7 +1344,7 @@ module TUI
|
|
|
1100
1344
|
title: "View Mode",
|
|
1101
1345
|
borders: [:all],
|
|
1102
1346
|
border_type: :rounded,
|
|
1103
|
-
border_style: {fg:
|
|
1347
|
+
border_style: {fg: Settings.theme_color_info}
|
|
1104
1348
|
)
|
|
1105
1349
|
)
|
|
1106
1350
|
frame.render_widget(picker, area)
|
|
@@ -1122,17 +1366,17 @@ module TUI
|
|
|
1122
1366
|
prefix = hotkey ? "[#{hotkey}]" : " "
|
|
1123
1367
|
marker = is_current ? "*" : " "
|
|
1124
1368
|
|
|
1125
|
-
selected_style = tui.style(fg:
|
|
1369
|
+
selected_style = tui.style(fg: Settings.theme_highlight_fg, bg: Settings.theme_highlight_bg)
|
|
1126
1370
|
|
|
1127
1371
|
name_style = if selected
|
|
1128
1372
|
selected_style
|
|
1129
1373
|
elsif is_current
|
|
1130
|
-
tui.style(fg:
|
|
1374
|
+
tui.style(fg: Settings.theme_color_info, modifiers: [:bold])
|
|
1131
1375
|
else
|
|
1132
|
-
tui.style(fg:
|
|
1376
|
+
tui.style(fg: Settings.theme_color_text)
|
|
1133
1377
|
end
|
|
1134
1378
|
|
|
1135
|
-
desc_style = selected ? selected_style : tui.style(fg:
|
|
1379
|
+
desc_style = selected ? selected_style : tui.style(fg: Settings.theme_color_muted)
|
|
1136
1380
|
|
|
1137
1381
|
[
|
|
1138
1382
|
tui.line(spans: [tui.span(content: "#{prefix}#{marker}#{mode.capitalize}", style: name_style)]),
|
|
@@ -1277,9 +1521,9 @@ module TUI
|
|
|
1277
1521
|
frame.render_widget(tui.clear, popup_area)
|
|
1278
1522
|
|
|
1279
1523
|
border_color = case @token_setup_status
|
|
1280
|
-
when :success then
|
|
1281
|
-
when :error then
|
|
1282
|
-
else
|
|
1524
|
+
when :success then Settings.theme_color_success
|
|
1525
|
+
when :error then Settings.theme_color_error
|
|
1526
|
+
else Settings.theme_color_warning
|
|
1283
1527
|
end
|
|
1284
1528
|
|
|
1285
1529
|
lines = build_token_setup_lines(tui)
|
|
@@ -1308,36 +1552,36 @@ module TUI
|
|
|
1308
1552
|
# Status
|
|
1309
1553
|
status_text, status_color = token_status_display
|
|
1310
1554
|
lines << tui.line(spans: [
|
|
1311
|
-
tui.span(content: "Status: ", style: tui.style(fg:
|
|
1555
|
+
tui.span(content: "Status: ", style: tui.style(fg: Settings.theme_color_muted)),
|
|
1312
1556
|
tui.span(content: status_text, style: tui.style(fg: status_color, modifiers: [:bold]))
|
|
1313
1557
|
])
|
|
1314
1558
|
lines << tui.line(spans: [tui.span(content: "")])
|
|
1315
1559
|
|
|
1316
1560
|
# Instructions
|
|
1317
1561
|
lines << tui.line(spans: [
|
|
1318
|
-
tui.span(content: "Run ", style: tui.style(fg:
|
|
1319
|
-
tui.span(content: "claude setup-token", style: tui.style(fg:
|
|
1320
|
-
tui.span(content: " to get", style: tui.style(fg:
|
|
1562
|
+
tui.span(content: "Run ", style: tui.style(fg: Settings.theme_color_text)),
|
|
1563
|
+
tui.span(content: "claude setup-token", style: tui.style(fg: Settings.theme_color_info, modifiers: [:bold])),
|
|
1564
|
+
tui.span(content: " to get", style: tui.style(fg: Settings.theme_color_text))
|
|
1321
1565
|
])
|
|
1322
1566
|
lines << tui.line(spans: [
|
|
1323
|
-
tui.span(content: "your token, then paste it here.", style: tui.style(fg:
|
|
1567
|
+
tui.span(content: "your token, then paste it here.", style: tui.style(fg: Settings.theme_color_text))
|
|
1324
1568
|
])
|
|
1325
1569
|
lines << tui.line(spans: [tui.span(content: "")])
|
|
1326
1570
|
|
|
1327
1571
|
# Token input
|
|
1328
1572
|
masked = mask_token(@token_input_buffer.text)
|
|
1329
1573
|
lines << tui.line(spans: [
|
|
1330
|
-
tui.span(content: "Token:", style: tui.style(fg:
|
|
1574
|
+
tui.span(content: "Token:", style: tui.style(fg: Settings.theme_color_text, modifiers: [:bold]))
|
|
1331
1575
|
])
|
|
1332
1576
|
lines << tui.line(spans: [
|
|
1333
|
-
tui.span(content: "> #{masked}", style: tui.style(fg:
|
|
1577
|
+
tui.span(content: "> #{masked}", style: tui.style(fg: Settings.theme_color_text))
|
|
1334
1578
|
])
|
|
1335
1579
|
lines << tui.line(spans: [tui.span(content: "")])
|
|
1336
1580
|
|
|
1337
1581
|
# Error or success message
|
|
1338
1582
|
if @token_setup_error
|
|
1339
1583
|
lines << tui.line(spans: [
|
|
1340
|
-
tui.span(content: @token_setup_error, style: tui.style(fg:
|
|
1584
|
+
tui.span(content: @token_setup_error, style: tui.style(fg: Settings.theme_color_error))
|
|
1341
1585
|
])
|
|
1342
1586
|
lines << tui.line(spans: [tui.span(content: "")])
|
|
1343
1587
|
end
|
|
@@ -1345,11 +1589,11 @@ module TUI
|
|
|
1345
1589
|
if @token_setup_status == :success
|
|
1346
1590
|
lines << if @token_setup_warning
|
|
1347
1591
|
tui.line(spans: [
|
|
1348
|
-
tui.span(content: "Token saved (API unavailable, validation skipped)", style: tui.style(fg:
|
|
1592
|
+
tui.span(content: "Token saved (API unavailable, validation skipped)", style: tui.style(fg: Settings.theme_color_warning, modifiers: [:bold]))
|
|
1349
1593
|
])
|
|
1350
1594
|
else
|
|
1351
1595
|
tui.line(spans: [
|
|
1352
|
-
tui.span(content: "Token saved and validated!", style: tui.style(fg:
|
|
1596
|
+
tui.span(content: "Token saved and validated!", style: tui.style(fg: Settings.theme_color_success, modifiers: [:bold]))
|
|
1353
1597
|
])
|
|
1354
1598
|
end
|
|
1355
1599
|
lines << tui.line(spans: [tui.span(content: "")])
|
|
@@ -1362,7 +1606,7 @@ module TUI
|
|
|
1362
1606
|
else "[Enter] Save [Esc] Cancel"
|
|
1363
1607
|
end
|
|
1364
1608
|
lines << tui.line(spans: [
|
|
1365
|
-
tui.span(content: hint, style: tui.style(fg:
|
|
1609
|
+
tui.span(content: hint, style: tui.style(fg: Settings.theme_color_muted))
|
|
1366
1610
|
])
|
|
1367
1611
|
|
|
1368
1612
|
lines
|
|
@@ -1372,31 +1616,31 @@ module TUI
|
|
|
1372
1616
|
def token_status_display
|
|
1373
1617
|
case @token_setup_status
|
|
1374
1618
|
when :success
|
|
1375
|
-
@token_setup_warning ? ["Saved (unverified)",
|
|
1619
|
+
@token_setup_warning ? ["Saved (unverified)", Settings.theme_color_warning] : ["Valid", Settings.theme_color_success]
|
|
1376
1620
|
when :validating
|
|
1377
|
-
["Validating...",
|
|
1621
|
+
["Validating...", Settings.theme_color_warning]
|
|
1378
1622
|
when :error
|
|
1379
|
-
["Invalid",
|
|
1623
|
+
["Invalid", Settings.theme_color_error]
|
|
1380
1624
|
else
|
|
1381
1625
|
if @token_input_buffer.text.empty?
|
|
1382
|
-
["Not configured",
|
|
1626
|
+
["Not configured", Settings.theme_color_muted]
|
|
1383
1627
|
else
|
|
1384
|
-
["Ready to save",
|
|
1628
|
+
["Ready to save", Settings.theme_color_info]
|
|
1385
1629
|
end
|
|
1386
1630
|
end
|
|
1387
1631
|
end
|
|
1388
1632
|
|
|
1389
|
-
# Masks an Anthropic token for display: shows the first
|
|
1633
|
+
# Masks an Anthropic token for display: shows the first Settings.token_dialog_mask_visible
|
|
1390
1634
|
# characters (the prefix) and replaces the rest with stars.
|
|
1391
1635
|
#
|
|
1392
1636
|
# @param token [String] raw token text
|
|
1393
1637
|
# @return [String] masked display text
|
|
1394
1638
|
def mask_token(token)
|
|
1395
1639
|
return "" if token.empty?
|
|
1396
|
-
return token if token.length <=
|
|
1640
|
+
return token if token.length <= Settings.token_dialog_mask_visible
|
|
1397
1641
|
|
|
1398
|
-
visible = token[0...
|
|
1399
|
-
hidden_count = [token.length -
|
|
1642
|
+
visible = token[0...Settings.token_dialog_mask_visible]
|
|
1643
|
+
hidden_count = [token.length - Settings.token_dialog_mask_visible, Settings.token_dialog_mask_stars].min
|
|
1400
1644
|
"#{visible}#{"*" * hidden_count}..."
|
|
1401
1645
|
end
|
|
1402
1646
|
|
|
@@ -1434,7 +1678,7 @@ module TUI
|
|
|
1434
1678
|
# @param area [RatatuiRuby::Rect] full terminal area
|
|
1435
1679
|
# @return [RatatuiRuby::Rect] centered popup area
|
|
1436
1680
|
def centered_popup_area(tui, area)
|
|
1437
|
-
popup_height = [
|
|
1681
|
+
popup_height = [Settings.token_dialog_popup_height, area.height - 2].min
|
|
1438
1682
|
v_margin = [(area.height - popup_height) / 2, 0].max
|
|
1439
1683
|
|
|
1440
1684
|
_, center_v, _ = tui.split(
|
|
@@ -1447,7 +1691,7 @@ module TUI
|
|
|
1447
1691
|
]
|
|
1448
1692
|
)
|
|
1449
1693
|
|
|
1450
|
-
popup_width = (area.width * 60 / 100).clamp(
|
|
1694
|
+
popup_width = (area.width * 60 / 100).clamp(Settings.token_dialog_popup_min_width, area.width - 2)
|
|
1451
1695
|
h_margin = [(area.width - popup_width) / 2, 0].max
|
|
1452
1696
|
|
|
1453
1697
|
_, center, _ = tui.split(
|
|
@@ -1527,18 +1771,18 @@ module TUI
|
|
|
1527
1771
|
def stop_terminal_watchdog
|
|
1528
1772
|
return unless @watchdog_thread
|
|
1529
1773
|
|
|
1530
|
-
@watchdog_thread.join(
|
|
1774
|
+
@watchdog_thread.join(Settings.terminal_shutdown_timeout)
|
|
1531
1775
|
@watchdog_thread.kill if @watchdog_thread.alive?
|
|
1532
1776
|
@watchdog_thread = nil
|
|
1533
1777
|
end
|
|
1534
1778
|
|
|
1535
|
-
# Opens {CONTROLLING_TERMINAL} every
|
|
1779
|
+
# Opens {CONTROLLING_TERMINAL} every +terminal.check_interval+ seconds.
|
|
1536
1780
|
# File.open (not File.stat) is required because stat only checks the
|
|
1537
1781
|
# filesystem entry which always exists; open actually probes the device.
|
|
1538
1782
|
# When the terminal disappears, calls {#handle_terminal_loss}.
|
|
1539
1783
|
# Exits silently in non-TTY environments (CI, test suites).
|
|
1540
1784
|
# @see CONTROLLING_TERMINAL
|
|
1541
|
-
# @see
|
|
1785
|
+
# @see TUI::Settings.terminal_check_interval
|
|
1542
1786
|
# @return [void]
|
|
1543
1787
|
def terminal_watchdog_loop
|
|
1544
1788
|
# Empty block triggers open syscall to probe the device, then immediately closes the FD.
|
|
@@ -1551,7 +1795,7 @@ module TUI
|
|
|
1551
1795
|
rescue Errno::ENXIO, Errno::EIO, Errno::ENOENT
|
|
1552
1796
|
handle_terminal_loss
|
|
1553
1797
|
end
|
|
1554
|
-
sleep
|
|
1798
|
+
sleep Settings.terminal_check_interval
|
|
1555
1799
|
end
|
|
1556
1800
|
rescue SystemCallError
|
|
1557
1801
|
# No controlling terminal — nothing to watch (ENXIO, EIO, ENOENT, EACCES, etc.)
|