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/braille_spinner.rb
CHANGED
|
@@ -23,7 +23,7 @@ module TUI
|
|
|
23
23
|
#
|
|
24
24
|
# @example Basic usage
|
|
25
25
|
# spinner = BrailleSpinner.new
|
|
26
|
-
# spinner.state = "
|
|
26
|
+
# spinner.state = "awaiting"
|
|
27
27
|
# char = spinner.tick # => "⠋" (braille pattern)
|
|
28
28
|
class BrailleSpinner
|
|
29
29
|
# Clockwise traversal of the 8 dots in the braille grid.
|
|
@@ -73,8 +73,8 @@ module TUI
|
|
|
73
73
|
# Ticks per frame for each state — controls animation speed.
|
|
74
74
|
# Higher = slower. At ~15fps render loop: 2 = ~7.5fps, 4 = ~3.75fps.
|
|
75
75
|
SPEED = {
|
|
76
|
-
"
|
|
77
|
-
"
|
|
76
|
+
"awaiting" => 2,
|
|
77
|
+
"executing" => 1,
|
|
78
78
|
"interrupting" => 1
|
|
79
79
|
}.freeze
|
|
80
80
|
|
|
@@ -90,8 +90,8 @@ module TUI
|
|
|
90
90
|
# Updates the session state driving the animation.
|
|
91
91
|
# Resets frame position on state change for a clean transition.
|
|
92
92
|
#
|
|
93
|
-
# @param new_state [String] one of "idle", "
|
|
94
|
-
# "
|
|
93
|
+
# @param new_state [String] one of "idle", "awaiting",
|
|
94
|
+
# "executing", "interrupting"
|
|
95
95
|
def state=(new_state)
|
|
96
96
|
if @state != new_state
|
|
97
97
|
@frame_index = 0
|
|
@@ -143,8 +143,8 @@ module TUI
|
|
|
143
143
|
|
|
144
144
|
def frames_for_state
|
|
145
145
|
case @state
|
|
146
|
-
when "
|
|
147
|
-
when "
|
|
146
|
+
when "awaiting" then SNAKE_TRAIL_FRAMES
|
|
147
|
+
when "executing" then TOOL_FRAMES
|
|
148
148
|
when "interrupting" then INTERRUPT_FRAMES
|
|
149
149
|
end
|
|
150
150
|
end
|
data/lib/tui/cable_client.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "websocket-client-simple"
|
|
4
4
|
require "json"
|
|
5
|
+
require_relative "settings"
|
|
5
6
|
|
|
6
7
|
module TUI
|
|
7
8
|
# Action Cable WebSocket client for connecting the TUI to the brain server.
|
|
@@ -23,14 +24,6 @@ module TUI
|
|
|
23
24
|
# messages = client.drain_messages
|
|
24
25
|
# client.disconnect
|
|
25
26
|
class CableClient
|
|
26
|
-
DISCONNECT_TIMEOUT = 2 # seconds to wait for WebSocket thread to finish
|
|
27
|
-
POLL_INTERVAL = 0.1 # seconds between connection status checks
|
|
28
|
-
CONNECTION_TIMEOUT = 10 # seconds to wait for the connecting state to advance
|
|
29
|
-
MAX_RECONNECT_ATTEMPTS = 10
|
|
30
|
-
BACKOFF_BASE = 1.0 # initial backoff delay in seconds
|
|
31
|
-
BACKOFF_CAP = 30.0 # maximum backoff delay
|
|
32
|
-
PING_STALE_THRESHOLD = 6.0 # seconds without ping before connection is stale
|
|
33
|
-
|
|
34
27
|
# Message types queued for the TUI render loop via @message_queue
|
|
35
28
|
MSG_TYPE_CONNECTION = "connection"
|
|
36
29
|
|
|
@@ -180,7 +173,7 @@ module TUI
|
|
|
180
173
|
@status = :disconnected
|
|
181
174
|
end
|
|
182
175
|
@ws&.close
|
|
183
|
-
@ws_thread&.join(
|
|
176
|
+
@ws_thread&.join(Settings.connection_disconnect_timeout)
|
|
184
177
|
end
|
|
185
178
|
|
|
186
179
|
private
|
|
@@ -260,13 +253,13 @@ module TUI
|
|
|
260
253
|
loop do
|
|
261
254
|
break if @status == :disconnected
|
|
262
255
|
|
|
263
|
-
if @status == :connecting && (Time.now - connection_start) >
|
|
256
|
+
if @status == :connecting && (Time.now - connection_start) > Settings.connection_timeout
|
|
264
257
|
on_disconnected
|
|
265
258
|
break
|
|
266
259
|
end
|
|
267
260
|
|
|
268
261
|
check_stale_connection
|
|
269
|
-
sleep
|
|
262
|
+
sleep Settings.connection_poll_interval
|
|
270
263
|
end
|
|
271
264
|
end
|
|
272
265
|
|
|
@@ -276,7 +269,7 @@ module TUI
|
|
|
276
269
|
def check_stale_connection
|
|
277
270
|
stale = @mutex.synchronize do
|
|
278
271
|
next false unless @last_ping_at && @status == :subscribed
|
|
279
|
-
(Time.now - @last_ping_at) >=
|
|
272
|
+
(Time.now - @last_ping_at) >= Settings.connection_ping_stale_threshold
|
|
280
273
|
end
|
|
281
274
|
|
|
282
275
|
on_disconnected if stale
|
|
@@ -291,12 +284,12 @@ module TUI
|
|
|
291
284
|
@reconnect_attempt
|
|
292
285
|
end
|
|
293
286
|
|
|
294
|
-
if attempt >
|
|
287
|
+
if attempt > Settings.connection_max_reconnect_attempts
|
|
295
288
|
@mutex.synchronize { @status = :disconnected }
|
|
296
289
|
@message_queue << {
|
|
297
290
|
"type" => MSG_TYPE_CONNECTION,
|
|
298
291
|
"status" => STATUS_FAILED,
|
|
299
|
-
"message" => "Reconnection failed after #{
|
|
292
|
+
"message" => "Reconnection failed after #{Settings.connection_max_reconnect_attempts} attempts"
|
|
300
293
|
}
|
|
301
294
|
return false
|
|
302
295
|
end
|
|
@@ -307,7 +300,7 @@ module TUI
|
|
|
307
300
|
"type" => MSG_TYPE_CONNECTION,
|
|
308
301
|
"status" => STATUS_RECONNECTING,
|
|
309
302
|
"attempt" => attempt,
|
|
310
|
-
"max_attempts" =>
|
|
303
|
+
"max_attempts" => Settings.connection_max_reconnect_attempts,
|
|
311
304
|
"delay" => delay.round(1)
|
|
312
305
|
}
|
|
313
306
|
|
|
@@ -321,7 +314,7 @@ module TUI
|
|
|
321
314
|
# @param attempt [Integer] current attempt number (1-based)
|
|
322
315
|
# @return [Float] delay in seconds
|
|
323
316
|
def backoff_delay(attempt)
|
|
324
|
-
max_delay = [
|
|
317
|
+
max_delay = [Settings.connection_backoff_cap, Settings.connection_backoff_base * (2**(attempt - 1))].min
|
|
325
318
|
rand(0.0..max_delay)
|
|
326
319
|
end
|
|
327
320
|
|
|
@@ -19,6 +19,26 @@ module TUI
|
|
|
19
19
|
# @example Render a tool call
|
|
20
20
|
# decorator = TUI::Decorators::BaseDecorator.for(data)
|
|
21
21
|
# lines = decorator.render(tui)
|
|
22
|
+
# Shared rendering for file-related tool decorators (read, write, edit).
|
|
23
|
+
# Extracts the file path from input and displays it in the header line,
|
|
24
|
+
# making the target file immediately visible — like bash shows its command.
|
|
25
|
+
module FileCallBehavior
|
|
26
|
+
def render_call(tui)
|
|
27
|
+
style = tui.style(fg: effective_color)
|
|
28
|
+
input_lines = data["input"].to_s.split("\n", -1)
|
|
29
|
+
path_line = input_lines.first.to_s
|
|
30
|
+
|
|
31
|
+
header = build_call_header
|
|
32
|
+
header = "#{header} #{path_line}" unless path_line.empty?
|
|
33
|
+
lines = [tui.line(spans: [tui.span(content: header, style: style)])]
|
|
34
|
+
|
|
35
|
+
input_lines.drop(1).each do |line|
|
|
36
|
+
lines << tui.line(spans: [tui.span(content: preserve_indentation(" #{line}"), style: style)])
|
|
37
|
+
end
|
|
38
|
+
lines
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
22
42
|
class BaseDecorator
|
|
23
43
|
include Formatting
|
|
24
44
|
|
|
@@ -61,11 +81,11 @@ module TUI
|
|
|
61
81
|
# @param tui [RatatuiRuby] TUI rendering API
|
|
62
82
|
# @return [Array<RatatuiRuby::Widgets::Line>]
|
|
63
83
|
def render_call(tui)
|
|
64
|
-
style = tui.style(fg:
|
|
84
|
+
style = tui.style(fg: effective_color)
|
|
65
85
|
header = build_call_header
|
|
66
86
|
lines = [tui.line(spans: [tui.span(content: header, style: style)])]
|
|
67
87
|
data["input"].to_s.split("\n", -1).each do |line|
|
|
68
|
-
lines << tui.line(spans: [tui.span(content: " #{line}", style: style)])
|
|
88
|
+
lines << tui.line(spans: [tui.span(content: preserve_indentation(" #{line}"), style: style)])
|
|
69
89
|
end
|
|
70
90
|
lines
|
|
71
91
|
end
|
|
@@ -81,7 +101,7 @@ module TUI
|
|
|
81
101
|
indicator = (data["success"] == false) ? ERROR_ICON : CHECKMARK
|
|
82
102
|
tool_id = data["tool_use_id"]
|
|
83
103
|
tokens = data["tokens"]
|
|
84
|
-
style = tui.style(fg:
|
|
104
|
+
style = tui.style(fg: effective_response_color)
|
|
85
105
|
|
|
86
106
|
meta_parts = []
|
|
87
107
|
meta_parts << "[#{tool_id}]" if tool_id
|
|
@@ -97,7 +117,7 @@ module TUI
|
|
|
97
117
|
first_line_spans << tui.span(content: content_lines.first.to_s, style: style)
|
|
98
118
|
|
|
99
119
|
lines = [tui.line(spans: first_line_spans)]
|
|
100
|
-
content_lines.drop(1).each { |line| lines << tui.line(spans: [tui.span(content: " #{line}", style: style)]) }
|
|
120
|
+
content_lines.drop(1).each { |line| lines << tui.line(spans: [tui.span(content: preserve_indentation(" #{line}"), style: style)]) }
|
|
101
121
|
lines
|
|
102
122
|
end
|
|
103
123
|
|
|
@@ -120,13 +140,34 @@ module TUI
|
|
|
120
140
|
# visually distinct from conversation messages (user/assistant/thought).
|
|
121
141
|
# @return [String]
|
|
122
142
|
def color
|
|
123
|
-
|
|
143
|
+
Settings.theme_color_accent
|
|
124
144
|
end
|
|
125
145
|
|
|
126
146
|
# Color for tool response content. Subclasses override for tool-specific colors.
|
|
127
147
|
# @return [String]
|
|
128
148
|
def response_color
|
|
129
|
-
|
|
149
|
+
Settings.theme_color_text
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# @return [Boolean] true when the underlying message is still in the
|
|
153
|
+
# pending mailbox (not yet promoted to a {Message}). Drives muted
|
|
154
|
+
# styling so in-flight pipeline content reads as distinct from
|
|
155
|
+
# committed conversation.
|
|
156
|
+
def pending?
|
|
157
|
+
data["status"] == "pending"
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Tool-call color, dimmed to the muted theme color while pending so
|
|
161
|
+
# subclass overrides of {#color} don't have to know about pending state.
|
|
162
|
+
# @return [String]
|
|
163
|
+
def effective_color
|
|
164
|
+
pending? ? Settings.theme_color_muted : color
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Tool-response color, dimmed to the muted theme color while pending.
|
|
168
|
+
# @return [String]
|
|
169
|
+
def effective_response_color
|
|
170
|
+
pending? ? Settings.theme_color_muted : response_color
|
|
130
171
|
end
|
|
131
172
|
|
|
132
173
|
private
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
module TUI
|
|
4
4
|
module Decorators
|
|
5
5
|
# Renders edit_file tool calls and responses.
|
|
6
|
-
# Calls show the file path
|
|
6
|
+
# Calls show the file path in the header line for immediate visibility.
|
|
7
7
|
# Responses use the CRUD Update color (light_yellow) to flag modifications.
|
|
8
8
|
class EditDecorator < BaseDecorator
|
|
9
|
+
include FileCallBehavior
|
|
10
|
+
|
|
9
11
|
ICON = "\u270F\uFE0F" # pencil
|
|
10
12
|
|
|
11
13
|
def icon
|
|
@@ -13,7 +15,7 @@ module TUI
|
|
|
13
15
|
end
|
|
14
16
|
|
|
15
17
|
def response_color
|
|
16
|
-
|
|
18
|
+
Settings.theme_tool_update_color
|
|
17
19
|
end
|
|
18
20
|
end
|
|
19
21
|
end
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
module TUI
|
|
4
4
|
module Decorators
|
|
5
5
|
# Renders read_file tool calls and responses.
|
|
6
|
-
# Calls show the file path
|
|
6
|
+
# Calls show the file path in the header line for immediate visibility.
|
|
7
7
|
# Responses use the CRUD Read color (light_blue) for informational content.
|
|
8
8
|
class ReadDecorator < BaseDecorator
|
|
9
|
+
include FileCallBehavior
|
|
10
|
+
|
|
9
11
|
ICON = "\u{1F4C4}" # page facing up
|
|
10
12
|
|
|
11
13
|
def icon
|
|
@@ -13,7 +15,7 @@ module TUI
|
|
|
13
15
|
end
|
|
14
16
|
|
|
15
17
|
def response_color
|
|
16
|
-
|
|
18
|
+
Settings.theme_tool_read_color
|
|
17
19
|
end
|
|
18
20
|
end
|
|
19
21
|
end
|
|
@@ -17,7 +17,7 @@ module TUI
|
|
|
17
17
|
# @param tui [RatatuiRuby] TUI rendering API
|
|
18
18
|
# @return [Array<RatatuiRuby::Widgets::Line>]
|
|
19
19
|
def render_think(tui)
|
|
20
|
-
style = tui.style(fg:
|
|
20
|
+
style = tui.style(fg: Settings.theme_color_muted)
|
|
21
21
|
ts = data["timestamp"]
|
|
22
22
|
|
|
23
23
|
meta = []
|
|
@@ -26,7 +26,7 @@ module TUI
|
|
|
26
26
|
|
|
27
27
|
content_lines = data["content"].to_s.split("\n", -1)
|
|
28
28
|
lines = [tui.line(spans: [tui.span(content: "#{header} #{content_lines.first}", style: style)])]
|
|
29
|
-
content_lines.drop(1).each { |line| lines << tui.line(spans: [tui.span(content: " #{line}", style: style)]) }
|
|
29
|
+
content_lines.drop(1).each { |line| lines << tui.line(spans: [tui.span(content: preserve_indentation(" #{line}"), style: style)]) }
|
|
30
30
|
lines
|
|
31
31
|
end
|
|
32
32
|
end
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
module TUI
|
|
4
4
|
module Decorators
|
|
5
5
|
# Renders write_file tool calls and responses.
|
|
6
|
-
# Calls show the file path
|
|
6
|
+
# Calls show the file path in the header line for immediate visibility.
|
|
7
7
|
# Responses use the CRUD Create color (light_green) to signal new content.
|
|
8
8
|
class WriteDecorator < BaseDecorator
|
|
9
|
+
include FileCallBehavior
|
|
10
|
+
|
|
9
11
|
ICON = "\u{1F4DD}" # memo
|
|
10
12
|
|
|
11
13
|
def icon
|
|
@@ -13,7 +15,7 @@ module TUI
|
|
|
13
15
|
end
|
|
14
16
|
|
|
15
17
|
def response_color
|
|
16
|
-
|
|
18
|
+
Settings.theme_tool_create_color
|
|
17
19
|
end
|
|
18
20
|
end
|
|
19
21
|
end
|
data/lib/tui/flash.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "settings"
|
|
4
|
+
|
|
3
5
|
module TUI
|
|
4
6
|
# Ephemeral notification system for the TUI, modeled after Rails flash
|
|
5
7
|
# messages. Notifications render as a colored bar at the top of the
|
|
@@ -22,18 +24,19 @@ module TUI
|
|
|
22
24
|
# @example Dismissing
|
|
23
25
|
# flash.dismiss!
|
|
24
26
|
class Flash
|
|
25
|
-
AUTO_DISMISS_SECONDS = 20.0
|
|
26
|
-
|
|
27
|
-
# Flash area occupies at most 1/3 of the chat pane height.
|
|
28
|
-
MAX_HEIGHT_FRACTION = 3
|
|
29
|
-
|
|
30
27
|
Entry = Struct.new(:message, :level, :created_at, keyword_init: true)
|
|
31
28
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
29
|
+
LEVEL_ICONS = {error: " \u2718 ", warning: " \u26A0 ", info: " \u2139 "}.freeze
|
|
30
|
+
|
|
31
|
+
# Builds level styles from current theme settings.
|
|
32
|
+
# Called per-render so hot-reloaded theme changes take effect immediately.
|
|
33
|
+
def self.level_styles
|
|
34
|
+
{
|
|
35
|
+
error: {fg: Settings.theme_flash_error_fg, bg: Settings.theme_flash_error_bg},
|
|
36
|
+
warning: {fg: Settings.theme_flash_warning_fg, bg: Settings.theme_flash_warning_bg},
|
|
37
|
+
info: {fg: Settings.theme_flash_info_fg, bg: Settings.theme_flash_info_bg}
|
|
38
|
+
}
|
|
39
|
+
end
|
|
37
40
|
|
|
38
41
|
def initialize
|
|
39
42
|
@entries = []
|
|
@@ -81,7 +84,7 @@ module TUI
|
|
|
81
84
|
expire!
|
|
82
85
|
return 0 if @entries.empty?
|
|
83
86
|
|
|
84
|
-
height = [@entries.size, area.height /
|
|
87
|
+
height = [@entries.size, area.height / Settings.flash_max_height_fraction].min
|
|
85
88
|
|
|
86
89
|
flash_area, _ = tui.split(
|
|
87
90
|
area,
|
|
@@ -110,7 +113,7 @@ module TUI
|
|
|
110
113
|
|
|
111
114
|
def expire!
|
|
112
115
|
now = monotonic_now
|
|
113
|
-
@entries.reject! { |entry| now - entry.created_at >
|
|
116
|
+
@entries.reject! { |entry| now - entry.created_at > Settings.flash_auto_dismiss_seconds }
|
|
114
117
|
end
|
|
115
118
|
|
|
116
119
|
def row_rect(area, index, tui)
|
|
@@ -120,10 +123,12 @@ module TUI
|
|
|
120
123
|
end
|
|
121
124
|
|
|
122
125
|
def render_entry(frame, area, entry, tui)
|
|
123
|
-
|
|
126
|
+
styles = self.class.level_styles
|
|
127
|
+
config = styles.fetch(entry.level, styles[:info])
|
|
128
|
+
icon = LEVEL_ICONS.fetch(entry.level, LEVEL_ICONS[:info])
|
|
124
129
|
style = tui.style(fg: config[:fg], bg: config[:bg], modifiers: [:bold])
|
|
125
130
|
|
|
126
|
-
text = "#{
|
|
131
|
+
text = "#{icon}#{entry.message} "
|
|
127
132
|
# Pad to full width so background color fills the entire row
|
|
128
133
|
padded = text.ljust(area.width)
|
|
129
134
|
|
data/lib/tui/formatting.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "settings"
|
|
4
|
+
|
|
3
5
|
module TUI
|
|
4
6
|
# Shared formatting helpers for timestamps and token counts.
|
|
5
7
|
# Used by both the Chat screen and client-side decorators
|
|
@@ -21,21 +23,21 @@ module TUI
|
|
|
21
23
|
# responses jump out immediately in debug mode.
|
|
22
24
|
#
|
|
23
25
|
# Thresholds (empirically tuned from real agent sessions):
|
|
24
|
-
# < 1k →
|
|
25
|
-
# < 3k →
|
|
26
|
-
# < 10k →
|
|
26
|
+
# < 1k → muted (routine, ignorable)
|
|
27
|
+
# < 3k → text (normal)
|
|
28
|
+
# < 10k → warning (notable)
|
|
27
29
|
# < 20k → 208/orange (expensive)
|
|
28
|
-
# ≥ 20k →
|
|
30
|
+
# ≥ 20k → error (alarm — likely runaway)
|
|
29
31
|
#
|
|
30
32
|
# @param tokens [Integer] token count
|
|
31
33
|
# @return [String, Integer] named color or 256-color index
|
|
32
34
|
def token_count_color(tokens)
|
|
33
|
-
return
|
|
34
|
-
return
|
|
35
|
-
return
|
|
36
|
-
return
|
|
35
|
+
return Settings.theme_color_muted if tokens < 1_000
|
|
36
|
+
return Settings.theme_color_text if tokens < 3_000
|
|
37
|
+
return Settings.theme_color_warning if tokens < 10_000
|
|
38
|
+
return Settings.theme_color_expensive if tokens < 20_000
|
|
37
39
|
|
|
38
|
-
|
|
40
|
+
Settings.theme_color_error
|
|
39
41
|
end
|
|
40
42
|
|
|
41
43
|
# Converts nanosecond-precision timestamp to human-readable HH:MM:SS.
|
|
@@ -46,5 +48,14 @@ module TUI
|
|
|
46
48
|
|
|
47
49
|
Time.at(ns / 1_000_000_000.0).strftime("%H:%M:%S")
|
|
48
50
|
end
|
|
51
|
+
|
|
52
|
+
# Replaces leading ASCII spaces with non-breaking spaces (\u00a0).
|
|
53
|
+
# Ratatui's Paragraph widget with wrap:true trims regular leading
|
|
54
|
+
# spaces; NBSP preserves visual indentation in wrapped text.
|
|
55
|
+
# @param text [String] text that may contain leading spaces
|
|
56
|
+
# @return [String] text with leading spaces replaced by NBSP
|
|
57
|
+
def preserve_indentation(text)
|
|
58
|
+
text.gsub(/^( +)/) { "\u00a0" * _1.length }
|
|
59
|
+
end
|
|
49
60
|
end
|
|
50
61
|
end
|
data/lib/tui/input_buffer.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "settings"
|
|
4
|
+
|
|
3
5
|
module TUI
|
|
4
6
|
# Manages editable text with cursor position tracking.
|
|
5
7
|
# Supports multiline input with newline insertion, cursor navigation
|
|
@@ -7,8 +9,6 @@ module TUI
|
|
|
7
9
|
#
|
|
8
10
|
# Pure logic object with no rendering or framework dependencies.
|
|
9
11
|
class InputBuffer
|
|
10
|
-
MAX_LENGTH = 10_000
|
|
11
|
-
|
|
12
12
|
attr_reader :text, :cursor_pos
|
|
13
13
|
|
|
14
14
|
def initialize
|
|
@@ -36,9 +36,9 @@ module TUI
|
|
|
36
36
|
@text.include?("\n")
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
-
# @return [Boolean] whether the buffer has reached
|
|
39
|
+
# @return [Boolean] whether the buffer has reached Settings.input_max_length
|
|
40
40
|
def full?
|
|
41
|
-
@text.length >=
|
|
41
|
+
@text.length >= Settings.input_max_length
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
# Ensures cursor stays within valid bounds after external state changes.
|
|
@@ -48,9 +48,9 @@ module TUI
|
|
|
48
48
|
end
|
|
49
49
|
|
|
50
50
|
# @param char [String] character(s) to insert at cursor
|
|
51
|
-
# @return [Boolean] true if inserted, false if result would exceed
|
|
51
|
+
# @return [Boolean] true if inserted, false if result would exceed Settings.input_max_length
|
|
52
52
|
def insert(char)
|
|
53
|
-
return false if @text.length + char.length >
|
|
53
|
+
return false if @text.length + char.length > Settings.input_max_length
|
|
54
54
|
|
|
55
55
|
@text = "#{@text[0...@cursor_pos]}#{char}#{@text[@cursor_pos..]}"
|
|
56
56
|
@cursor_pos += char.length
|