anima-core 0.2.0 → 0.2.1
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/CHANGELOG.md +19 -0
- data/README.md +14 -2
- data/anima-core.gemspec +1 -0
- data/app/channels/session_channel.rb +98 -6
- data/app/decorators/agent_message_decorator.rb +24 -0
- data/app/decorators/application_decorator.rb +6 -0
- data/app/decorators/event_decorator.rb +173 -0
- data/app/decorators/system_message_decorator.rb +21 -0
- data/app/decorators/tool_call_decorator.rb +48 -0
- data/app/decorators/tool_response_decorator.rb +37 -0
- data/app/decorators/user_message_decorator.rb +24 -0
- data/app/models/event.rb +17 -0
- data/app/models/session.rb +23 -11
- data/config/application.rb +1 -0
- data/db/cable_schema.rb +14 -2
- data/db/migrate/20260312170000_add_view_mode_to_sessions.rb +7 -0
- data/lib/anima/version.rb +1 -1
- data/lib/events/subscribers/action_cable_bridge.rb +27 -3
- data/lib/tui/app.rb +150 -44
- data/lib/tui/cable_client.rb +10 -0
- data/lib/tui/input_buffer.rb +181 -0
- data/lib/tui/message_store.rb +87 -14
- data/lib/tui/screens/chat.rb +428 -60
- metadata +24 -3
- data/lib/tui/screens/anthropic.rb +0 -25
- data/lib/tui/screens/settings.rb +0 -52
data/lib/tui/message_store.rb
CHANGED
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module TUI
|
|
4
|
-
# Thread-safe in-memory store for chat
|
|
4
|
+
# Thread-safe in-memory store for chat entries displayed in the TUI.
|
|
5
5
|
# Replaces {Events::Subscribers::MessageCollector} in the WebSocket-based
|
|
6
6
|
# TUI, with no dependency on Rails or the Events module.
|
|
7
7
|
#
|
|
8
|
-
# Accepts Action Cable event payloads and
|
|
8
|
+
# Accepts Action Cable event payloads and stores typed entries:
|
|
9
|
+
# - `{type: :rendered, data:, event_type:}` for events with structured decorator output
|
|
10
|
+
# - `{type: :message, role:, content:}` for user/agent messages (fallback)
|
|
11
|
+
# - `{type: :tool_counter, calls:, responses:}` for tool activity
|
|
12
|
+
#
|
|
13
|
+
# Structured data takes priority when available. Events with nil
|
|
14
|
+
# rendered content fall back to existing behavior: tool events aggregate
|
|
15
|
+
# into counters, messages store role and content.
|
|
16
|
+
#
|
|
17
|
+
# Tool counters aggregate per agent turn: a new counter starts when a
|
|
18
|
+
# tool_call arrives after a message entry. Consecutive tool events
|
|
19
|
+
# increment the same counter until the next message breaks the chain.
|
|
9
20
|
class MessageStore
|
|
10
|
-
|
|
21
|
+
MESSAGE_TYPES = %w[user_message agent_message].freeze
|
|
11
22
|
|
|
12
23
|
ROLE_MAP = {
|
|
13
24
|
"user_message" => "user",
|
|
@@ -15,35 +26,97 @@ module TUI
|
|
|
15
26
|
}.freeze
|
|
16
27
|
|
|
17
28
|
def initialize
|
|
18
|
-
@
|
|
29
|
+
@entries = []
|
|
19
30
|
@mutex = Mutex.new
|
|
20
31
|
end
|
|
21
32
|
|
|
22
|
-
# @return [Array<Hash>] thread-safe copy of
|
|
33
|
+
# @return [Array<Hash>] thread-safe copy of stored entries
|
|
23
34
|
def messages
|
|
24
|
-
@mutex.synchronize { @
|
|
35
|
+
@mutex.synchronize { @entries.dup }
|
|
25
36
|
end
|
|
26
37
|
|
|
27
38
|
# Processes a raw event payload from the WebSocket channel.
|
|
28
|
-
#
|
|
39
|
+
# Uses structured decorator data when available; falls back to
|
|
40
|
+
# role/content extraction for messages and tool counter aggregation.
|
|
29
41
|
#
|
|
30
|
-
# @param event_data [Hash] Action Cable event payload with "type"
|
|
31
|
-
#
|
|
42
|
+
# @param event_data [Hash] Action Cable event payload with "type", "content",
|
|
43
|
+
# and optionally "rendered" (hash of mode => lines)
|
|
44
|
+
# @return [Boolean] true if the event type was recognized and handled
|
|
32
45
|
def process_event(event_data)
|
|
33
|
-
|
|
34
|
-
|
|
46
|
+
rendered = extract_rendered(event_data)
|
|
47
|
+
|
|
48
|
+
if rendered
|
|
49
|
+
record_rendered(rendered, event_type: event_data["type"])
|
|
50
|
+
else
|
|
51
|
+
case event_data["type"]
|
|
52
|
+
when "tool_call" then record_tool_call
|
|
53
|
+
when "tool_response" then record_tool_response
|
|
54
|
+
when *MESSAGE_TYPES then record_message(event_data)
|
|
55
|
+
else false
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Removes all entries. Called on view mode change and session switch
|
|
61
|
+
# to prepare for re-decorated viewport events from the server.
|
|
62
|
+
# @return [void]
|
|
63
|
+
def clear
|
|
64
|
+
@mutex.synchronize { @entries = [] }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
# Extracts the first non-nil structured data hash from the rendered payload.
|
|
70
|
+
# The "rendered" hash is keyed by view mode — the server includes only the
|
|
71
|
+
# session's current mode, so there is always at most one entry.
|
|
72
|
+
# (e.g. {"basic" => {"role" => "user", ...}} or {"basic" => nil} for hidden events)
|
|
73
|
+
#
|
|
74
|
+
# @return [Hash, nil] structured event data, or nil if not present
|
|
75
|
+
def extract_rendered(event_data)
|
|
76
|
+
event_data.dig("rendered")&.values&.compact&.first
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def record_rendered(data, event_type: nil)
|
|
80
|
+
@mutex.synchronize do
|
|
81
|
+
@entries << {type: :rendered, data: data, event_type: event_type}
|
|
82
|
+
end
|
|
83
|
+
true
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def record_tool_call
|
|
87
|
+
@mutex.synchronize do
|
|
88
|
+
current = current_tool_counter
|
|
89
|
+
if current
|
|
90
|
+
current[:calls] += 1
|
|
91
|
+
else
|
|
92
|
+
@entries << {type: :tool_counter, calls: 1, responses: 0}
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
true
|
|
96
|
+
end
|
|
35
97
|
|
|
98
|
+
def record_tool_response
|
|
99
|
+
@mutex.synchronize do
|
|
100
|
+
current = current_tool_counter
|
|
101
|
+
current[:responses] += 1 if current
|
|
102
|
+
end
|
|
103
|
+
true
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def record_message(event_data)
|
|
36
107
|
content = event_data["content"]
|
|
37
108
|
return false if content.nil?
|
|
38
109
|
|
|
39
110
|
@mutex.synchronize do
|
|
40
|
-
@
|
|
111
|
+
@entries << {type: :message, role: ROLE_MAP.fetch(event_data["type"]), content: content}
|
|
41
112
|
end
|
|
42
113
|
true
|
|
43
114
|
end
|
|
44
115
|
|
|
45
|
-
|
|
46
|
-
|
|
116
|
+
# @return [Hash, nil] the last entry if it is a tool counter
|
|
117
|
+
def current_tool_counter
|
|
118
|
+
last = @entries.last
|
|
119
|
+
last if last&.dig(:type) == :tool_counter
|
|
47
120
|
end
|
|
48
121
|
end
|
|
49
122
|
end
|