anima-core 0.2.1 → 1.0.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 +27 -1
- data/CHANGELOG.md +19 -0
- data/README.md +213 -43
- data/agents/codebase-analyzer.md +88 -0
- data/agents/codebase-pattern-finder.md +83 -0
- data/agents/documentation-researcher.md +59 -0
- data/agents/thoughts-analyzer.md +102 -0
- data/agents/web-search-researcher.md +71 -0
- data/anima-core.gemspec +3 -0
- data/app/channels/session_channel.rb +195 -45
- data/app/decorators/user_message_decorator.rb +16 -5
- data/app/jobs/agent_request_job.rb +55 -2
- data/app/jobs/analytical_brain_job.rb +33 -0
- data/app/jobs/count_event_tokens_job.rb +15 -4
- data/app/models/concerns/event/broadcasting.rb +81 -0
- data/app/models/event.rb +20 -1
- data/app/models/goal.rb +91 -0
- data/app/models/session.rb +366 -21
- data/config/application.rb +2 -0
- data/config/initializers/event_subscribers.rb +0 -1
- data/config/routes.rb +0 -6
- data/db/migrate/20260313010000_add_status_to_events.rb +8 -0
- data/db/migrate/20260313020000_add_processing_to_sessions.rb +7 -0
- data/db/migrate/20260314075248_add_subagent_support_to_sessions.rb +6 -0
- data/db/migrate/20260314112417_add_granted_tools_to_sessions.rb +5 -0
- data/db/migrate/20260314140000_add_name_to_sessions.rb +7 -0
- data/db/migrate/20260314150000_add_viewport_event_ids_to_sessions.rb +7 -0
- data/db/migrate/20260315100000_add_active_skills_to_sessions.rb +7 -0
- data/db/migrate/20260315140843_create_goals.rb +16 -0
- data/db/migrate/20260315144837_add_completed_at_to_goals.rb +5 -0
- data/db/migrate/20260315191105_add_active_workflow_to_sessions.rb +5 -0
- data/lib/agent_loop.rb +65 -6
- data/lib/agents/definition.rb +116 -0
- data/lib/agents/registry.rb +106 -0
- data/lib/analytical_brain/runner.rb +276 -0
- data/lib/analytical_brain/tools/activate_skill.rb +52 -0
- data/lib/analytical_brain/tools/deactivate_skill.rb +43 -0
- data/lib/analytical_brain/tools/deactivate_workflow.rb +34 -0
- data/lib/analytical_brain/tools/everything_is_ready.rb +28 -0
- data/lib/analytical_brain/tools/finish_goal.rb +62 -0
- data/lib/analytical_brain/tools/read_workflow.rb +58 -0
- data/lib/analytical_brain/tools/rename_session.rb +63 -0
- data/lib/analytical_brain/tools/set_goal.rb +60 -0
- data/lib/analytical_brain/tools/update_goal.rb +60 -0
- data/lib/analytical_brain.rb +23 -0
- data/lib/anima/cli/mcp/secrets.rb +76 -0
- data/lib/anima/cli/mcp.rb +197 -0
- data/lib/anima/cli.rb +5 -40
- data/lib/anima/installer.rb +168 -0
- data/lib/anima/settings.rb +226 -0
- data/lib/anima/version.rb +1 -1
- data/lib/anima.rb +9 -0
- data/lib/credential_store.rb +103 -0
- data/lib/environment_probe.rb +232 -0
- data/lib/events/subscribers/persister.rb +1 -0
- data/lib/events/user_message.rb +17 -0
- data/lib/llm/client.rb +29 -10
- data/lib/mcp/client_manager.rb +86 -0
- data/lib/mcp/config.rb +213 -0
- data/lib/mcp/health_check.rb +77 -0
- data/lib/mcp/secrets.rb +73 -0
- data/lib/mcp/stdio_transport.rb +206 -0
- data/lib/providers/anthropic.rb +11 -20
- data/lib/shell_session.rb +11 -10
- data/lib/skills/definition.rb +97 -0
- data/lib/skills/registry.rb +105 -0
- data/lib/tools/edit.rb +226 -0
- data/lib/tools/mcp_tool.rb +114 -0
- data/lib/tools/read.rb +151 -0
- data/lib/tools/registry.rb +14 -12
- data/lib/tools/request_feature.rb +121 -0
- data/lib/tools/return_result.rb +81 -0
- data/lib/tools/spawn_specialist.rb +109 -0
- data/lib/tools/spawn_subagent.rb +111 -0
- data/lib/tools/subagent_prompts.rb +12 -0
- data/lib/tools/web_get.rb +8 -9
- data/lib/tools/write.rb +86 -0
- data/lib/tui/app.rb +985 -26
- data/lib/tui/cable_client.rb +69 -31
- data/lib/tui/message_store.rb +103 -8
- data/lib/tui/screens/chat.rb +293 -45
- data/lib/workflows/definition.rb +97 -0
- data/lib/workflows/registry.rb +89 -0
- data/skills/activerecord/SKILL.md +255 -0
- data/skills/activerecord/examples/associations/association_extensions.rb +298 -0
- data/skills/activerecord/examples/associations/basic_associations.rb +118 -0
- data/skills/activerecord/examples/associations/counter_caches.rb +215 -0
- data/skills/activerecord/examples/associations/polymorphic_associations.rb +217 -0
- data/skills/activerecord/examples/associations/self_referential.rb +302 -0
- data/skills/activerecord/examples/associations/through_associations.rb +203 -0
- data/skills/activerecord/examples/basics/crud_operations.rb +209 -0
- data/skills/activerecord/examples/basics/dirty_tracking.rb +218 -0
- data/skills/activerecord/examples/basics/inheritance.rb +377 -0
- data/skills/activerecord/examples/basics/type_casting.rb +317 -0
- data/skills/activerecord/examples/callbacks/alternatives_to_callbacks.rb +447 -0
- data/skills/activerecord/examples/callbacks/conditional_callbacks.rb +353 -0
- data/skills/activerecord/examples/callbacks/lifecycle_callbacks.rb +280 -0
- data/skills/activerecord/examples/callbacks/transaction_callbacks.rb +340 -0
- data/skills/activerecord/examples/migrations/indexes_and_constraints.rb +337 -0
- data/skills/activerecord/examples/migrations/reversible_patterns.rb +403 -0
- data/skills/activerecord/examples/migrations/safe_patterns.rb +420 -0
- data/skills/activerecord/examples/migrations/schema_changes.rb +277 -0
- data/skills/activerecord/examples/querying/batch_processing.rb +226 -0
- data/skills/activerecord/examples/querying/eager_loading.rb +259 -0
- data/skills/activerecord/examples/querying/finder_methods.rb +170 -0
- data/skills/activerecord/examples/querying/optimization.rb +275 -0
- data/skills/activerecord/examples/querying/scopes.rb +260 -0
- data/skills/activerecord/examples/validations/built_in_validators.rb +277 -0
- data/skills/activerecord/examples/validations/conditional_validations.rb +288 -0
- data/skills/activerecord/examples/validations/custom_validators.rb +381 -0
- data/skills/activerecord/examples/validations/database_constraints.rb +432 -0
- data/skills/activerecord/examples/validations/validation_contexts.rb +367 -0
- data/skills/activerecord/references/associations.md +709 -0
- data/skills/activerecord/references/basics.md +622 -0
- data/skills/activerecord/references/callbacks.md +738 -0
- data/skills/activerecord/references/migrations.md +657 -0
- data/skills/activerecord/references/querying.md +655 -0
- data/skills/activerecord/references/validations.md +596 -0
- data/skills/dragonruby/SKILL.md +250 -0
- data/skills/dragonruby/examples/audio/audio_events.rb +55 -0
- data/skills/dragonruby/examples/audio/background_music.rb +29 -0
- data/skills/dragonruby/examples/audio/crossfade.rb +51 -0
- data/skills/dragonruby/examples/audio/music_controls.rb +51 -0
- data/skills/dragonruby/examples/audio/sound_effects.rb +30 -0
- data/skills/dragonruby/examples/core/coordinate_system.rb +27 -0
- data/skills/dragonruby/examples/core/hello_world.rb +24 -0
- data/skills/dragonruby/examples/core/labels.rb +22 -0
- data/skills/dragonruby/examples/core/sprites.rb +35 -0
- data/skills/dragonruby/examples/core/state_management.rb +29 -0
- data/skills/dragonruby/examples/distribution/background_pause.rb +42 -0
- data/skills/dragonruby/examples/distribution/build_workflow.sh +26 -0
- data/skills/dragonruby/examples/distribution/cvars_production.txt +16 -0
- data/skills/dragonruby/examples/distribution/game_metadata_hd.txt +23 -0
- data/skills/dragonruby/examples/distribution/game_metadata_minimal.txt +9 -0
- data/skills/dragonruby/examples/distribution/game_metadata_mobile.txt +31 -0
- data/skills/dragonruby/examples/distribution/platform_detection.rb +36 -0
- data/skills/dragonruby/examples/distribution/steam_metadata.txt +19 -0
- data/skills/dragonruby/examples/entities/collision_detection.rb +43 -0
- data/skills/dragonruby/examples/entities/entity_lifecycle.rb +68 -0
- data/skills/dragonruby/examples/entities/entity_storage.rb +38 -0
- data/skills/dragonruby/examples/entities/factory_methods.rb +45 -0
- data/skills/dragonruby/examples/entities/random_spawning.rb +50 -0
- data/skills/dragonruby/examples/game-logic/reset_patterns.rb +98 -0
- data/skills/dragonruby/examples/game-logic/save_load.rb +101 -0
- data/skills/dragonruby/examples/game-logic/scoring.rb +104 -0
- data/skills/dragonruby/examples/game-logic/state_transitions.rb +103 -0
- data/skills/dragonruby/examples/game-logic/timers.rb +87 -0
- data/skills/dragonruby/examples/input/action_triggers.rb +36 -0
- data/skills/dragonruby/examples/input/analog_movement.rb +28 -0
- data/skills/dragonruby/examples/input/controller_input.rb +28 -0
- data/skills/dragonruby/examples/input/directional_input.rb +24 -0
- data/skills/dragonruby/examples/input/keyboard_input.rb +28 -0
- data/skills/dragonruby/examples/input/mouse_click.rb +26 -0
- data/skills/dragonruby/examples/input/movement_with_bounds.rb +22 -0
- data/skills/dragonruby/examples/input/normalized_movement.rb +32 -0
- data/skills/dragonruby/examples/rendering/frame_animation.rb +32 -0
- data/skills/dragonruby/examples/rendering/labels.rb +32 -0
- data/skills/dragonruby/examples/rendering/layering.rb +51 -0
- data/skills/dragonruby/examples/rendering/solids.rb +61 -0
- data/skills/dragonruby/examples/rendering/sprites.rb +33 -0
- data/skills/dragonruby/examples/rendering/spritesheet_animation.rb +39 -0
- data/skills/dragonruby/examples/scenes/case_dispatch.rb +60 -0
- data/skills/dragonruby/examples/scenes/class_based.rb +150 -0
- data/skills/dragonruby/examples/scenes/pause_overlay.rb +100 -0
- data/skills/dragonruby/examples/scenes/safe_transitions.rb +68 -0
- data/skills/dragonruby/examples/scenes/scene_transitions.rb +98 -0
- data/skills/dragonruby/examples/scenes/send_dispatch.rb +88 -0
- data/skills/dragonruby/references/audio.md +396 -0
- data/skills/dragonruby/references/core.md +385 -0
- data/skills/dragonruby/references/distribution.md +434 -0
- data/skills/dragonruby/references/entities.md +516 -0
- data/skills/dragonruby/references/game-logic/persistence.md +386 -0
- data/skills/dragonruby/references/game-logic/state.md +389 -0
- data/skills/dragonruby/references/input.md +414 -0
- data/skills/dragonruby/references/rendering/animation.md +467 -0
- data/skills/dragonruby/references/rendering/primitives.md +403 -0
- data/skills/dragonruby/references/scenes.md +443 -0
- data/skills/draper-decorators/SKILL.md +344 -0
- data/skills/draper-decorators/examples/application_decorator.rb +61 -0
- data/skills/draper-decorators/examples/decorator_spec.rb +253 -0
- data/skills/draper-decorators/examples/model_decorator.rb +152 -0
- data/skills/draper-decorators/references/anti-patterns.md +640 -0
- data/skills/draper-decorators/references/patterns.md +507 -0
- data/skills/draper-decorators/references/testing.md +559 -0
- data/skills/gh-issue.md +182 -0
- data/skills/mcp-server/SKILL.md +177 -0
- data/skills/mcp-server/examples/dynamic_tools.rb +36 -0
- data/skills/mcp-server/examples/file_manager_tool.rb +85 -0
- data/skills/mcp-server/examples/http_client.rb +48 -0
- data/skills/mcp-server/examples/http_server.rb +97 -0
- data/skills/mcp-server/examples/rails_integration.rb +88 -0
- data/skills/mcp-server/examples/stdio_server.rb +108 -0
- data/skills/mcp-server/examples/streaming_client.rb +95 -0
- data/skills/mcp-server/references/gotchas.md +183 -0
- data/skills/mcp-server/references/prompts.md +98 -0
- data/skills/mcp-server/references/resources.md +53 -0
- data/skills/mcp-server/references/server.md +140 -0
- data/skills/mcp-server/references/tools.md +146 -0
- data/skills/mcp-server/references/transport.md +104 -0
- data/skills/ratatui-ruby/SKILL.md +315 -0
- data/skills/ratatui-ruby/references/core-concepts.md +340 -0
- data/skills/ratatui-ruby/references/events.md +387 -0
- data/skills/ratatui-ruby/references/frameworks.md +522 -0
- data/skills/ratatui-ruby/references/layout.md +423 -0
- data/skills/ratatui-ruby/references/styling.md +268 -0
- data/skills/ratatui-ruby/references/testing.md +433 -0
- data/skills/ratatui-ruby/references/widgets.md +532 -0
- data/skills/rspec/SKILL.md +340 -0
- data/skills/rspec/examples/core/basic_structure.rb +69 -0
- data/skills/rspec/examples/core/configuration.rb +126 -0
- data/skills/rspec/examples/core/hooks.rb +126 -0
- data/skills/rspec/examples/core/memoized_helpers.rb +139 -0
- data/skills/rspec/examples/core/metadata_filtering.rb +144 -0
- data/skills/rspec/examples/core/shared_examples.rb +145 -0
- data/skills/rspec/examples/factory_bot/associations.rb +314 -0
- data/skills/rspec/examples/factory_bot/build_strategies.rb +272 -0
- data/skills/rspec/examples/factory_bot/callbacks.rb +320 -0
- data/skills/rspec/examples/factory_bot/custom_construction.rb +328 -0
- data/skills/rspec/examples/factory_bot/factory_definition.rb +191 -0
- data/skills/rspec/examples/factory_bot/inheritance.rb +314 -0
- data/skills/rspec/examples/factory_bot/traits.rb +293 -0
- data/skills/rspec/examples/factory_bot/transients.rb +229 -0
- data/skills/rspec/examples/matchers/change.rb +115 -0
- data/skills/rspec/examples/matchers/collections.rb +154 -0
- data/skills/rspec/examples/matchers/comparisons.rb +79 -0
- data/skills/rspec/examples/matchers/composing.rb +155 -0
- data/skills/rspec/examples/matchers/custom_matchers.rb +197 -0
- data/skills/rspec/examples/matchers/equality.rb +58 -0
- data/skills/rspec/examples/matchers/errors.rb +136 -0
- data/skills/rspec/examples/matchers/output.rb +103 -0
- data/skills/rspec/examples/matchers/predicates.rb +87 -0
- data/skills/rspec/examples/matchers/truthiness.rb +101 -0
- data/skills/rspec/examples/matchers/types.rb +82 -0
- data/skills/rspec/examples/matchers/yield.rb +147 -0
- data/skills/rspec/examples/mocks/any_instance.rb +172 -0
- data/skills/rspec/examples/mocks/argument_matchers.rb +206 -0
- data/skills/rspec/examples/mocks/constants.rb +177 -0
- data/skills/rspec/examples/mocks/doubles.rb +139 -0
- data/skills/rspec/examples/mocks/expectations.rb +137 -0
- data/skills/rspec/examples/mocks/message_chains.rb +173 -0
- data/skills/rspec/examples/mocks/ordering.rb +144 -0
- data/skills/rspec/examples/mocks/receive_counts.rb +181 -0
- data/skills/rspec/examples/mocks/responses.rb +223 -0
- data/skills/rspec/examples/mocks/spies.rb +149 -0
- data/skills/rspec/examples/mocks/stubbing.rb +133 -0
- data/skills/rspec/examples/rails/channels.rb +250 -0
- data/skills/rspec/examples/rails/controller_specs.rb +302 -0
- data/skills/rspec/examples/rails/helper_specs.rb +245 -0
- data/skills/rspec/examples/rails/job_specs.rb +256 -0
- data/skills/rspec/examples/rails/mailer_specs.rb +228 -0
- data/skills/rspec/examples/rails/matchers.rb +374 -0
- data/skills/rspec/examples/rails/model_specs.rb +193 -0
- data/skills/rspec/examples/rails/request_specs.rb +275 -0
- data/skills/rspec/examples/rails/routing_specs.rb +276 -0
- data/skills/rspec/examples/rails/system_specs.rb +294 -0
- data/skills/rspec/examples/rails/transactions.rb +254 -0
- data/skills/rspec/examples/rails/view_specs.rb +252 -0
- data/skills/rspec/references/core.md +816 -0
- data/skills/rspec/references/factory_bot.md +641 -0
- data/skills/rspec/references/matchers.md +516 -0
- data/skills/rspec/references/mocks.md +381 -0
- data/skills/rspec/references/rails.md +528 -0
- data/templates/soul.md +40 -0
- data/workflows/commit.md +45 -0
- data/workflows/create_handoff.md +98 -0
- data/workflows/create_note.md +82 -0
- data/workflows/create_plan.md +457 -0
- data/workflows/decompose_ticket.md +109 -0
- data/workflows/feature.md +91 -0
- data/workflows/implement_plan.md +87 -0
- data/workflows/iterate_plan.md +247 -0
- data/workflows/research_codebase.md +210 -0
- data/workflows/resume_handoff.md +217 -0
- data/workflows/review_pr.md +320 -0
- data/workflows/thoughts_init.md +71 -0
- data/workflows/validate_plan.md +166 -0
- metadata +290 -3
- data/app/controllers/api/sessions_controller.rb +0 -25
- data/lib/events/subscribers/action_cable_bridge.rb +0 -59
data/lib/tui/cable_client.rb
CHANGED
|
@@ -17,7 +17,7 @@ module TUI
|
|
|
17
17
|
# heartbeat monitoring.
|
|
18
18
|
#
|
|
19
19
|
# @example
|
|
20
|
-
# client = TUI::CableClient.new(host: "localhost:42134"
|
|
20
|
+
# client = TUI::CableClient.new(host: "localhost:42134")
|
|
21
21
|
# client.connect
|
|
22
22
|
# client.speak("Hello!")
|
|
23
23
|
# messages = client.drain_messages
|
|
@@ -31,23 +31,39 @@ module TUI
|
|
|
31
31
|
BACKOFF_CAP = 30.0 # maximum backoff delay
|
|
32
32
|
PING_STALE_THRESHOLD = 6.0 # seconds without ping before connection is stale
|
|
33
33
|
|
|
34
|
+
# Message types queued for the TUI render loop via @message_queue
|
|
35
|
+
MSG_TYPE_CONNECTION = "connection"
|
|
36
|
+
|
|
37
|
+
# Connection status values sent as MSG_TYPE_CONNECTION messages.
|
|
38
|
+
# These are message-level concepts for the TUI — distinct from the
|
|
39
|
+
# internal @status state machine (:disconnected, :connecting, etc.).
|
|
40
|
+
STATUS_SUBSCRIBING = "subscribing"
|
|
41
|
+
STATUS_SUBSCRIBED = "subscribed"
|
|
42
|
+
STATUS_REJECTED = "rejected"
|
|
43
|
+
STATUS_DISCONNECTED = "disconnected"
|
|
44
|
+
STATUS_RECONNECTING = "reconnecting"
|
|
45
|
+
STATUS_FAILED = "failed"
|
|
46
|
+
|
|
34
47
|
# @return [String] brain server host:port
|
|
35
48
|
attr_reader :host
|
|
36
49
|
|
|
37
50
|
# @return [Integer] current session ID
|
|
38
51
|
attr_reader :session_id
|
|
39
52
|
|
|
40
|
-
# @return [Symbol] connection status (:disconnected, :connecting, :connected, :subscribed, :reconnecting)
|
|
53
|
+
# @return [Symbol] connection status (:disconnected, :connecting, :connected, :subscribed, :reconnecting).
|
|
54
|
+
# Note: the "subscribing" concept exists only as a message-level status
|
|
55
|
+
# (see {STATUS_SUBSCRIBING}) queued for the TUI, not as an internal state.
|
|
41
56
|
attr_reader :status
|
|
42
57
|
|
|
43
58
|
# @return [Integer] current reconnection attempt (0 when connected)
|
|
44
59
|
attr_reader :reconnect_attempt
|
|
45
60
|
|
|
46
61
|
# @param host [String] brain server address (e.g. "localhost:42134")
|
|
47
|
-
# @param session_id [Integer] session to subscribe to
|
|
48
|
-
def initialize(host:, session_id:)
|
|
62
|
+
# @param session_id [Integer, nil] session to subscribe to (nil for server-side resolution)
|
|
63
|
+
def initialize(host:, session_id: nil)
|
|
49
64
|
@host = host
|
|
50
65
|
@session_id = session_id
|
|
66
|
+
@subscribed_session_id = session_id
|
|
51
67
|
@status = :disconnected
|
|
52
68
|
@message_queue = Thread::Queue.new
|
|
53
69
|
@mutex = Mutex.new
|
|
@@ -110,6 +126,23 @@ module TUI
|
|
|
110
126
|
send_action("change_view_mode", {"view_mode" => mode})
|
|
111
127
|
end
|
|
112
128
|
|
|
129
|
+
# Requests the brain to recall (delete) a pending message so the user
|
|
130
|
+
# can edit it before the LLM sees it.
|
|
131
|
+
#
|
|
132
|
+
# @param event_id [Integer] database ID of the pending user_message event
|
|
133
|
+
def recall_pending(event_id)
|
|
134
|
+
send_action("recall_pending", {"event_id" => event_id})
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Sends an Anthropic subscription token to the brain for validation and storage.
|
|
138
|
+
# The token flows directly from TUI input to encrypted credentials — never
|
|
139
|
+
# enters the LLM context window.
|
|
140
|
+
#
|
|
141
|
+
# @param token [String] Anthropic subscription token (sk-ant-oat01-...)
|
|
142
|
+
def save_token(token)
|
|
143
|
+
send_action("save_token", {"token" => token})
|
|
144
|
+
end
|
|
145
|
+
|
|
113
146
|
# Updates the local session ID reference after a server-side session switch.
|
|
114
147
|
#
|
|
115
148
|
# @param new_id [Integer] the new session ID
|
|
@@ -131,17 +164,6 @@ module TUI
|
|
|
131
164
|
messages
|
|
132
165
|
end
|
|
133
166
|
|
|
134
|
-
# Unsubscribes from the current session and subscribes to a new one.
|
|
135
|
-
#
|
|
136
|
-
# @deprecated Use {#create_session} or {#switch_session} instead.
|
|
137
|
-
# The server now handles stream switching via the session protocol.
|
|
138
|
-
# @param new_session_id [Integer] session to switch to
|
|
139
|
-
def resubscribe(new_session_id)
|
|
140
|
-
unsubscribe_current
|
|
141
|
-
@mutex.synchronize { @session_id = new_session_id }
|
|
142
|
-
subscribe
|
|
143
|
-
end
|
|
144
|
-
|
|
145
167
|
# Closes the WebSocket connection and cleans up the background thread.
|
|
146
168
|
# Prevents automatic reconnection.
|
|
147
169
|
def disconnect
|
|
@@ -264,8 +286,8 @@ module TUI
|
|
|
264
286
|
if attempt > MAX_RECONNECT_ATTEMPTS
|
|
265
287
|
@mutex.synchronize { @status = :disconnected }
|
|
266
288
|
@message_queue << {
|
|
267
|
-
"type" =>
|
|
268
|
-
"status" =>
|
|
289
|
+
"type" => MSG_TYPE_CONNECTION,
|
|
290
|
+
"status" => STATUS_FAILED,
|
|
269
291
|
"message" => "Reconnection failed after #{MAX_RECONNECT_ATTEMPTS} attempts"
|
|
270
292
|
}
|
|
271
293
|
return false
|
|
@@ -274,8 +296,8 @@ module TUI
|
|
|
274
296
|
delay = backoff_delay(attempt)
|
|
275
297
|
@mutex.synchronize { @status = :reconnecting }
|
|
276
298
|
@message_queue << {
|
|
277
|
-
"type" =>
|
|
278
|
-
"status" =>
|
|
299
|
+
"type" => MSG_TYPE_CONNECTION,
|
|
300
|
+
"status" => STATUS_RECONNECTING,
|
|
279
301
|
"attempt" => attempt,
|
|
280
302
|
"max_attempts" => MAX_RECONNECT_ATTEMPTS,
|
|
281
303
|
"delay" => delay.round(1)
|
|
@@ -324,17 +346,17 @@ module TUI
|
|
|
324
346
|
@status = :subscribed
|
|
325
347
|
@reconnect_attempt = 0
|
|
326
348
|
end
|
|
327
|
-
@message_queue << {"type" =>
|
|
349
|
+
@message_queue << {"type" => MSG_TYPE_CONNECTION, "status" => STATUS_SUBSCRIBED}
|
|
328
350
|
when "reject_subscription"
|
|
329
351
|
on_disconnected
|
|
330
|
-
@message_queue << {"type" =>
|
|
352
|
+
@message_queue << {"type" => MSG_TYPE_CONNECTION, "status" => STATUS_REJECTED}
|
|
331
353
|
when "disconnect"
|
|
332
354
|
if data["reconnect"] == false
|
|
333
355
|
@mutex.synchronize do
|
|
334
356
|
@intentional_disconnect = true
|
|
335
357
|
@status = :disconnected
|
|
336
358
|
end
|
|
337
|
-
@message_queue << {"type" =>
|
|
359
|
+
@message_queue << {"type" => MSG_TYPE_CONNECTION, "status" => STATUS_DISCONNECTED}
|
|
338
360
|
else
|
|
339
361
|
on_disconnected
|
|
340
362
|
end
|
|
@@ -353,30 +375,46 @@ module TUI
|
|
|
353
375
|
return if @status == :disconnected || @status == :reconnecting
|
|
354
376
|
@status = :disconnected
|
|
355
377
|
end
|
|
356
|
-
@message_queue << {"type" =>
|
|
378
|
+
@message_queue << {"type" => MSG_TYPE_CONNECTION, "status" => STATUS_DISCONNECTED}
|
|
357
379
|
end
|
|
358
380
|
|
|
381
|
+
# Captures the current session ID under mutex, queues a "subscribing"
|
|
382
|
+
# status for the TUI, then sends the Action Cable subscribe command.
|
|
383
|
+
#
|
|
384
|
+
# The "subscribing" message must be queued _before_ the subscribe command
|
|
385
|
+
# so the TUI clears stale state before history arrives. Action Cable
|
|
386
|
+
# transmits history (via +subscribed+ callback) before sending
|
|
387
|
+
# +confirm_subscription+, so the TUI would see history first, then
|
|
388
|
+
# the "subscribed" status.
|
|
389
|
+
#
|
|
390
|
+
# @see handle_protocol_message called on "welcome" to trigger this
|
|
359
391
|
def subscribe
|
|
360
|
-
|
|
392
|
+
sid = @mutex.synchronize do
|
|
393
|
+
@subscribed_session_id = @session_id || 0
|
|
394
|
+
end
|
|
395
|
+
@message_queue << {"type" => MSG_TYPE_CONNECTION, "status" => STATUS_SUBSCRIBING}
|
|
396
|
+
identifier = {channel: "SessionChannel", session_id: sid}.to_json
|
|
361
397
|
send_command("subscribe", identifier)
|
|
362
398
|
end
|
|
363
399
|
|
|
364
|
-
def unsubscribe_current
|
|
365
|
-
identifier = {channel: "SessionChannel", session_id: @session_id}.to_json
|
|
366
|
-
send_command("unsubscribe", identifier)
|
|
367
|
-
end
|
|
368
|
-
|
|
369
400
|
def send_action(action, data = {})
|
|
370
|
-
identifier = {channel: "SessionChannel", session_id: @session_id}.to_json
|
|
371
401
|
payload = data.merge("action" => action).to_json
|
|
372
402
|
|
|
373
403
|
@ws&.send({
|
|
374
404
|
command: "message",
|
|
375
|
-
identifier:
|
|
405
|
+
identifier: subscription_identifier,
|
|
376
406
|
data: payload
|
|
377
407
|
}.to_json)
|
|
378
408
|
end
|
|
379
409
|
|
|
410
|
+
# Returns the identifier matching the active ActionCable subscription.
|
|
411
|
+
# After session switches, @session_id changes but the subscription
|
|
412
|
+
# identifier must match the one used during subscribe.
|
|
413
|
+
def subscription_identifier
|
|
414
|
+
sid = @mutex.synchronize { @subscribed_session_id }
|
|
415
|
+
{channel: "SessionChannel", session_id: sid}.to_json
|
|
416
|
+
end
|
|
417
|
+
|
|
380
418
|
def send_command(command, identifier)
|
|
381
419
|
@ws&.send({
|
|
382
420
|
command: command,
|
data/lib/tui/message_store.rb
CHANGED
|
@@ -6,8 +6,8 @@ module TUI
|
|
|
6
6
|
# TUI, with no dependency on Rails or the Events module.
|
|
7
7
|
#
|
|
8
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)
|
|
9
|
+
# - `{type: :rendered, data:, event_type:, id:}` for events with structured decorator output
|
|
10
|
+
# - `{type: :message, role:, content:, id:}` for user/agent messages (fallback)
|
|
11
11
|
# - `{type: :tool_counter, calls:, responses:}` for tool activity
|
|
12
12
|
#
|
|
13
13
|
# Structured data takes priority when available. Events with nil
|
|
@@ -17,6 +17,9 @@ module TUI
|
|
|
17
17
|
# Tool counters aggregate per agent turn: a new counter starts when a
|
|
18
18
|
# tool_call arrives after a message entry. Consecutive tool events
|
|
19
19
|
# increment the same counter until the next message breaks the chain.
|
|
20
|
+
#
|
|
21
|
+
# When an event arrives with `"action" => "update"` and a known `"id"`,
|
|
22
|
+
# the existing entry is replaced in-place, preserving display order.
|
|
20
23
|
class MessageStore
|
|
21
24
|
MESSAGE_TYPES = %w[user_message agent_message].freeze
|
|
22
25
|
|
|
@@ -27,6 +30,7 @@ module TUI
|
|
|
27
30
|
|
|
28
31
|
def initialize
|
|
29
32
|
@entries = []
|
|
33
|
+
@entries_by_id = {}
|
|
30
34
|
@mutex = Mutex.new
|
|
31
35
|
end
|
|
32
36
|
|
|
@@ -39,14 +43,23 @@ module TUI
|
|
|
39
43
|
# Uses structured decorator data when available; falls back to
|
|
40
44
|
# role/content extraction for messages and tool counter aggregation.
|
|
41
45
|
#
|
|
46
|
+
# Events with `"action" => "update"` and a matching `"id"` replace
|
|
47
|
+
# the existing entry's data in-place rather than appending.
|
|
48
|
+
#
|
|
42
49
|
# @param event_data [Hash] Action Cable event payload with "type", "content",
|
|
43
|
-
# and optionally "rendered" (hash of mode => lines)
|
|
50
|
+
# and optionally "rendered" (hash of mode => lines), "id", "action"
|
|
44
51
|
# @return [Boolean] true if the event type was recognized and handled
|
|
45
52
|
def process_event(event_data)
|
|
53
|
+
event_id = event_data["id"]
|
|
54
|
+
|
|
55
|
+
if event_data["action"] == "update" && event_id
|
|
56
|
+
return update_existing(event_data, event_id)
|
|
57
|
+
end
|
|
58
|
+
|
|
46
59
|
rendered = extract_rendered(event_data)
|
|
47
60
|
|
|
48
61
|
if rendered
|
|
49
|
-
record_rendered(rendered, event_type: event_data["type"])
|
|
62
|
+
record_rendered(rendered, event_type: event_data["type"], id: event_id)
|
|
50
63
|
else
|
|
51
64
|
case event_data["type"]
|
|
52
65
|
when "tool_call" then record_tool_call
|
|
@@ -61,11 +74,87 @@ module TUI
|
|
|
61
74
|
# to prepare for re-decorated viewport events from the server.
|
|
62
75
|
# @return [void]
|
|
63
76
|
def clear
|
|
64
|
-
@mutex.synchronize
|
|
77
|
+
@mutex.synchronize do
|
|
78
|
+
@entries = []
|
|
79
|
+
@entries_by_id = {}
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Returns the last pending user message for recall editing.
|
|
84
|
+
# Walks entries backwards and returns the first pending user_message found.
|
|
85
|
+
#
|
|
86
|
+
# @return [Hash, nil] `{id: Integer, content: String}` or nil if none pending
|
|
87
|
+
def last_pending_user_message
|
|
88
|
+
@mutex.synchronize do
|
|
89
|
+
@entries.reverse_each do |entry|
|
|
90
|
+
next unless entry[:event_type] == "user_message"
|
|
91
|
+
|
|
92
|
+
if entry[:type] == :rendered && entry.dig(:data, "status") == "pending"
|
|
93
|
+
return {id: entry[:id], content: entry.dig(:data, "content")}
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Only check the most recent user message
|
|
97
|
+
break
|
|
98
|
+
end
|
|
99
|
+
nil
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Removes an entry by its event ID. Used when a pending message is
|
|
104
|
+
# recalled for editing or deleted by another client.
|
|
105
|
+
#
|
|
106
|
+
# @param event_id [Integer] database ID of the event to remove
|
|
107
|
+
# @return [Boolean] true if the entry was found and removed
|
|
108
|
+
def remove_by_id(event_id)
|
|
109
|
+
@mutex.synchronize do
|
|
110
|
+
entry = @entries_by_id.delete(event_id)
|
|
111
|
+
return false unless entry
|
|
112
|
+
|
|
113
|
+
@entries.delete(entry)
|
|
114
|
+
true
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Removes entries by their event IDs. Used when the brain reports
|
|
119
|
+
# that events have left the LLM's viewport (context window eviction).
|
|
120
|
+
# Acquires the mutex once for the entire batch.
|
|
121
|
+
#
|
|
122
|
+
# @param event_ids [Array<Integer>] database IDs of events to remove
|
|
123
|
+
# @return [Integer] count of entries actually removed
|
|
124
|
+
def remove_by_ids(event_ids)
|
|
125
|
+
@mutex.synchronize do
|
|
126
|
+
removed = 0
|
|
127
|
+
event_ids.each do |event_id|
|
|
128
|
+
entry = @entries_by_id.delete(event_id)
|
|
129
|
+
next unless entry
|
|
130
|
+
|
|
131
|
+
@entries.delete(entry)
|
|
132
|
+
removed += 1
|
|
133
|
+
end
|
|
134
|
+
removed
|
|
135
|
+
end
|
|
65
136
|
end
|
|
66
137
|
|
|
67
138
|
private
|
|
68
139
|
|
|
140
|
+
# Replaces data on an existing entry matched by event ID.
|
|
141
|
+
# Only updates rendered entries — tool counters and plain messages
|
|
142
|
+
# are not individually addressable by ID.
|
|
143
|
+
#
|
|
144
|
+
# @return [Boolean] true if the entry was found and updated
|
|
145
|
+
def update_existing(event_data, event_id)
|
|
146
|
+
rendered = extract_rendered(event_data)
|
|
147
|
+
return false unless rendered
|
|
148
|
+
|
|
149
|
+
@mutex.synchronize do
|
|
150
|
+
entry = @entries_by_id[event_id]
|
|
151
|
+
return false unless entry
|
|
152
|
+
|
|
153
|
+
entry[:data] = rendered
|
|
154
|
+
true
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
69
158
|
# Extracts the first non-nil structured data hash from the rendered payload.
|
|
70
159
|
# The "rendered" hash is keyed by view mode — the server includes only the
|
|
71
160
|
# session's current mode, so there is always at most one entry.
|
|
@@ -76,9 +165,11 @@ module TUI
|
|
|
76
165
|
event_data.dig("rendered")&.values&.compact&.first
|
|
77
166
|
end
|
|
78
167
|
|
|
79
|
-
def record_rendered(data, event_type: nil)
|
|
168
|
+
def record_rendered(data, event_type: nil, id: nil)
|
|
80
169
|
@mutex.synchronize do
|
|
81
|
-
|
|
170
|
+
entry = {type: :rendered, data: data, event_type: event_type, id: id}
|
|
171
|
+
@entries << entry
|
|
172
|
+
@entries_by_id[id] = entry if id
|
|
82
173
|
end
|
|
83
174
|
true
|
|
84
175
|
end
|
|
@@ -107,8 +198,12 @@ module TUI
|
|
|
107
198
|
content = event_data["content"]
|
|
108
199
|
return false if content.nil?
|
|
109
200
|
|
|
201
|
+
event_id = event_data["id"]
|
|
202
|
+
|
|
110
203
|
@mutex.synchronize do
|
|
111
|
-
|
|
204
|
+
entry = {type: :message, role: ROLE_MAP.fetch(event_data["type"]), content: content, id: event_id}
|
|
205
|
+
@entries << entry
|
|
206
|
+
@entries_by_id[event_id] = entry if event_id
|
|
112
207
|
end
|
|
113
208
|
true
|
|
114
209
|
end
|