anima-core 0.3.0 → 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 +4 -0
- data/README.md +219 -25
- 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 +76 -28
- data/app/jobs/agent_request_job.rb +24 -0
- data/app/jobs/analytical_brain_job.rb +33 -0
- data/app/jobs/count_event_tokens_job.rb +1 -1
- data/app/models/concerns/event/broadcasting.rb +20 -2
- data/app/models/event.rb +1 -1
- data/app/models/goal.rb +91 -0
- data/app/models/session.rb +347 -22
- data/config/application.rb +2 -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 -9
- 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 +4 -0
- 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/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 +8 -7
- 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 +3 -4
- data/lib/tools/mcp_tool.rb +114 -0
- data/lib/tools/read.rb +15 -16
- 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/tui/app.rb +332 -43
- data/lib/tui/message_store.rb +20 -0
- data/lib/tui/screens/chat.rb +207 -20
- 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 +284 -1
data/lib/tui/app.rb
CHANGED
|
@@ -18,7 +18,8 @@ module TUI
|
|
|
18
18
|
"q" => :quit
|
|
19
19
|
}.freeze
|
|
20
20
|
|
|
21
|
-
MENU_LABELS = COMMAND_KEYS.map { |key, action| "[#{key}] #{action.to_s.tr("_", " ").capitalize}" }
|
|
21
|
+
MENU_LABELS = (COMMAND_KEYS.map { |key, action| "[#{key}] #{action.to_s.tr("_", " ").capitalize}" } +
|
|
22
|
+
["[\u2191] Scroll chat", "[\u2193] Return to input"]).freeze
|
|
22
23
|
|
|
23
24
|
SIDEBAR_WIDTH = 28
|
|
24
25
|
|
|
@@ -88,6 +89,9 @@ module TUI
|
|
|
88
89
|
@command_mode = false
|
|
89
90
|
@session_picker_active = false
|
|
90
91
|
@session_picker_index = 0
|
|
92
|
+
@session_picker_page = 0
|
|
93
|
+
@session_picker_mode = :root
|
|
94
|
+
@session_picker_parent_id = nil
|
|
91
95
|
@view_mode_picker_active = false
|
|
92
96
|
@view_mode_picker_index = 0
|
|
93
97
|
@token_setup_active = false
|
|
@@ -178,19 +182,30 @@ module TUI
|
|
|
178
182
|
else "cyan"
|
|
179
183
|
end
|
|
180
184
|
|
|
185
|
+
session_label = session[:name] || "##{session[:id]}"
|
|
186
|
+
|
|
181
187
|
lines = [
|
|
182
188
|
tui.line(spans: [
|
|
183
189
|
tui.span(content: "Anima v#{Anima::VERSION}", style: tui.style(fg: "white"))
|
|
184
190
|
]),
|
|
185
191
|
tui.line(spans: [tui.span(content: "")]),
|
|
186
|
-
|
|
187
|
-
tui.
|
|
188
|
-
|
|
189
|
-
|
|
192
|
+
if session[:name]
|
|
193
|
+
tui.line(spans: [
|
|
194
|
+
tui.span(content: session_label, style: tui.style(fg: "cyan", modifiers: [:bold]))
|
|
195
|
+
])
|
|
196
|
+
else
|
|
197
|
+
tui.line(spans: [
|
|
198
|
+
tui.span(content: "Session ", style: tui.style(fg: "dark_gray")),
|
|
199
|
+
tui.span(content: session_label, style: tui.style(fg: "cyan", modifiers: [:bold]))
|
|
200
|
+
])
|
|
201
|
+
end,
|
|
190
202
|
tui.line(spans: [
|
|
191
203
|
tui.span(content: "Messages ", style: tui.style(fg: "dark_gray")),
|
|
192
204
|
tui.span(content: session[:message_count].to_s, style: tui.style(fg: "cyan"))
|
|
193
205
|
]),
|
|
206
|
+
active_skills_line(tui, session),
|
|
207
|
+
active_workflow_line(tui, session),
|
|
208
|
+
goals_line(tui, session),
|
|
194
209
|
tui.line(spans: [tui.span(content: "")]),
|
|
195
210
|
tui.line(spans: [
|
|
196
211
|
tui.span(content: "Mode ", style: tui.style(fg: "dark_gray")),
|
|
@@ -204,7 +219,7 @@ module TUI
|
|
|
204
219
|
tui.span(content: "Ctrl+a", style: tui.style(fg: "cyan", modifiers: [:bold])),
|
|
205
220
|
tui.span(content: " command mode", style: tui.style(fg: "dark_gray"))
|
|
206
221
|
])
|
|
207
|
-
]
|
|
222
|
+
].compact
|
|
208
223
|
|
|
209
224
|
info = tui.paragraph(
|
|
210
225
|
text: lines,
|
|
@@ -218,10 +233,65 @@ module TUI
|
|
|
218
233
|
frame.render_widget(info, area)
|
|
219
234
|
end
|
|
220
235
|
|
|
236
|
+
# Builds the active skills line for the info panel.
|
|
237
|
+
# Returns nil when no skills are active so the line is hidden entirely.
|
|
238
|
+
# @param tui [RatatuiRuby] TUI rendering context
|
|
239
|
+
# @param session [Hash] session info hash containing :active_skills array
|
|
240
|
+
# @return [RatatuiRuby::Widgets::Line, nil] styled skills line, or nil when empty
|
|
241
|
+
def active_skills_line(tui, session)
|
|
242
|
+
skills = session[:active_skills]
|
|
243
|
+
return if skills.nil? || skills.empty?
|
|
244
|
+
|
|
245
|
+
label = skills.join(", ")
|
|
246
|
+
tui.line(spans: [
|
|
247
|
+
tui.span(content: "\u{1F4DA} ", style: tui.style(fg: "dark_gray")),
|
|
248
|
+
tui.span(content: label, style: tui.style(fg: "yellow"))
|
|
249
|
+
])
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Builds the active workflow line for the info panel.
|
|
253
|
+
# Returns nil when no workflow is active so the line is hidden entirely.
|
|
254
|
+
# @param tui [RatatuiRuby] TUI rendering context
|
|
255
|
+
# @param session [Hash] session info hash containing :active_workflow string
|
|
256
|
+
# @return [RatatuiRuby::Widgets::Line, nil] styled workflow line, or nil when empty
|
|
257
|
+
def active_workflow_line(tui, session)
|
|
258
|
+
workflow = session[:active_workflow]
|
|
259
|
+
return if workflow.nil? || workflow.empty?
|
|
260
|
+
|
|
261
|
+
tui.line(spans: [
|
|
262
|
+
tui.span(content: "\u{1F504} ", style: tui.style(fg: "dark_gray")),
|
|
263
|
+
tui.span(content: workflow, style: tui.style(fg: "magenta"))
|
|
264
|
+
])
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# Builds the active goals line for the info panel.
|
|
268
|
+
# Returns nil when no goals exist so the line is hidden entirely.
|
|
269
|
+
# Shows root goal count with active/completed breakdown.
|
|
270
|
+
# @param tui [RatatuiRuby] TUI rendering context
|
|
271
|
+
# @param session [Hash] session info hash containing :goals array
|
|
272
|
+
# @return [RatatuiRuby::Widgets::Line, nil] styled goals line, or nil when empty
|
|
273
|
+
def goals_line(tui, session)
|
|
274
|
+
goal_list = session[:goals]
|
|
275
|
+
return if goal_list.nil? || goal_list.empty?
|
|
276
|
+
|
|
277
|
+
active = goal_list.count { |g| g["status"] == "active" }
|
|
278
|
+
completed = goal_list.count { |g| g["status"] == "completed" }
|
|
279
|
+
label = "#{active} active"
|
|
280
|
+
label += ", #{completed} done" if completed > 0
|
|
281
|
+
tui.line(spans: [
|
|
282
|
+
tui.span(content: "\u{1F3AF} ", style: tui.style(fg: "dark_gray")),
|
|
283
|
+
tui.span(content: label, style: tui.style(fg: "green"))
|
|
284
|
+
])
|
|
285
|
+
end
|
|
286
|
+
|
|
221
287
|
# Builds the interaction state line for the info panel.
|
|
222
|
-
# Shows "Thinking..." during LLM processing.
|
|
288
|
+
# Shows "Scrolling" when chat pane is focused, or "Thinking..." during LLM processing.
|
|
223
289
|
def interaction_state_line(tui)
|
|
224
|
-
if
|
|
290
|
+
if @screens[:chat].chat_focused
|
|
291
|
+
tui.line(spans: [
|
|
292
|
+
tui.span(content: "Scrolling", style: tui.style(fg: "yellow", modifiers: [:bold]))
|
|
293
|
+
])
|
|
294
|
+
elsif chat_loading?
|
|
225
295
|
tui.line(spans: [
|
|
226
296
|
tui.span(content: "Thinking...", style: tui.style(fg: "magenta", modifiers: [:bold]))
|
|
227
297
|
])
|
|
@@ -278,6 +348,16 @@ module TUI
|
|
|
278
348
|
|
|
279
349
|
return nil unless event.key?
|
|
280
350
|
|
|
351
|
+
if event.up?
|
|
352
|
+
@screens[:chat].focus_chat
|
|
353
|
+
return nil
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
if event.down?
|
|
357
|
+
@screens[:chat].unfocus_chat
|
|
358
|
+
return nil
|
|
359
|
+
end
|
|
360
|
+
|
|
281
361
|
action = COMMAND_KEYS[event.code]
|
|
282
362
|
case action
|
|
283
363
|
when :quit
|
|
@@ -311,10 +391,30 @@ module TUI
|
|
|
311
391
|
return nil
|
|
312
392
|
end
|
|
313
393
|
|
|
394
|
+
if event.esc?
|
|
395
|
+
if @screens[:chat].chat_focused
|
|
396
|
+
@screens[:chat].unfocus_chat
|
|
397
|
+
else
|
|
398
|
+
return_to_parent_session
|
|
399
|
+
end
|
|
400
|
+
return nil
|
|
401
|
+
end
|
|
402
|
+
|
|
314
403
|
delegate_to_screen(event)
|
|
315
404
|
nil
|
|
316
405
|
end
|
|
317
406
|
|
|
407
|
+
# Switches to the parent session when viewing a child (sub-agent) session.
|
|
408
|
+
# No-op if the current session is a root session.
|
|
409
|
+
#
|
|
410
|
+
# @return [void]
|
|
411
|
+
def return_to_parent_session
|
|
412
|
+
parent_id = @screens[:chat].parent_session_id
|
|
413
|
+
return unless parent_id
|
|
414
|
+
|
|
415
|
+
@screens[:chat].switch_session(parent_id)
|
|
416
|
+
end
|
|
417
|
+
|
|
318
418
|
# Forwards an event to the active screen for handling
|
|
319
419
|
def delegate_to_screen(event)
|
|
320
420
|
screen = @screens[@current_screen]
|
|
@@ -365,71 +465,197 @@ module TUI
|
|
|
365
465
|
end
|
|
366
466
|
|
|
367
467
|
# Maps digit key codes to picker list indices.
|
|
368
|
-
# Keys 1-9 map to indices 0-8
|
|
468
|
+
# Keys 1-9 map to indices 0-8. Key 0 is reserved for Load More in paginated pickers.
|
|
369
469
|
#
|
|
370
470
|
# @param code [String] the key code
|
|
371
471
|
# @return [Integer, nil] list index, or nil for non-digit keys
|
|
372
472
|
def hotkey_to_index(code)
|
|
373
473
|
return nil unless code.length == 1
|
|
374
474
|
|
|
375
|
-
|
|
376
|
-
when "1".."9" then code.to_i - 1
|
|
377
|
-
when "0" then 9
|
|
378
|
-
end
|
|
475
|
+
code.to_i - 1 if ("1".."9").cover?(code)
|
|
379
476
|
end
|
|
380
477
|
|
|
381
478
|
# Returns the hotkey character for a given picker list position.
|
|
382
|
-
# Positions 0-8 get keys "1"-"9"
|
|
479
|
+
# Positions 0-8 get keys "1"-"9". Positions beyond 8 get no hotkey.
|
|
383
480
|
#
|
|
384
481
|
# @param idx [Integer] zero-based list position
|
|
385
|
-
# @return [String, nil] hotkey character, or nil for positions beyond
|
|
482
|
+
# @return [String, nil] hotkey character, or nil for positions beyond 8
|
|
386
483
|
def picker_hotkey(idx)
|
|
387
|
-
|
|
388
|
-
return "0" if idx == 9
|
|
389
|
-
nil
|
|
484
|
+
(idx + 1).to_s if idx >= 0 && idx < 9
|
|
390
485
|
end
|
|
391
486
|
|
|
392
487
|
# -- Session picker ------------------------------------------------
|
|
393
488
|
|
|
489
|
+
# Status indicators for child session state.
|
|
490
|
+
CHILD_STATUS_RUNNING = "\u27F3" # ⟳
|
|
491
|
+
CHILD_STATUS_DONE = "\u2713" # ✓
|
|
492
|
+
CHILDREN_ARROW = "\u25B8" # ▸ shown next to sessions with children
|
|
493
|
+
UNNAMED_SUBAGENT_LABEL = "sub-agent"
|
|
494
|
+
SESSION_PICKER_PAGE_SIZE = 9
|
|
495
|
+
SESSION_PICKER_FETCH_LIMIT = 50
|
|
496
|
+
BACK_ARROW = "\u2190" # ←
|
|
497
|
+
|
|
394
498
|
# Requests the session list from the brain and opens the picker overlay.
|
|
499
|
+
# Fetches up to SESSION_PICKER_FETCH_LIMIT sessions for client-side pagination.
|
|
395
500
|
# @return [void]
|
|
396
501
|
def activate_session_picker
|
|
397
502
|
@session_picker_active = true
|
|
398
503
|
@session_picker_index = 0
|
|
399
|
-
@
|
|
504
|
+
@session_picker_page = 0
|
|
505
|
+
@session_picker_mode = :root
|
|
506
|
+
@session_picker_parent_id = nil
|
|
507
|
+
@cable_client.list_sessions(limit: SESSION_PICKER_FETCH_LIMIT)
|
|
400
508
|
end
|
|
401
509
|
|
|
402
510
|
# Dispatches keyboard events while the session picker overlay is open.
|
|
511
|
+
# Supports drill-down navigation: root sessions → children, with
|
|
512
|
+
# pagination via key 0 (Load More) at both levels.
|
|
403
513
|
#
|
|
404
514
|
# @param event [RatatuiRuby::Event] keyboard event
|
|
405
515
|
# @return [nil]
|
|
406
516
|
def handle_session_picker(event)
|
|
517
|
+
return nil unless event.key?
|
|
518
|
+
|
|
519
|
+
if event.esc?
|
|
520
|
+
handle_session_picker_escape
|
|
521
|
+
return nil
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
visible = session_picker_visible_items
|
|
525
|
+
return nil if visible.empty?
|
|
526
|
+
|
|
527
|
+
if event.up?
|
|
528
|
+
@session_picker_index = [@session_picker_index - 1, 0].max
|
|
529
|
+
elsif event.down?
|
|
530
|
+
@session_picker_index = [@session_picker_index + 1, visible.size - 1].min
|
|
531
|
+
elsif event.right?
|
|
532
|
+
drill_into_children(visible)
|
|
533
|
+
elsif event.left?
|
|
534
|
+
return_to_root_sessions
|
|
535
|
+
elsif event.enter?
|
|
536
|
+
select_session_picker_item(visible)
|
|
537
|
+
elsif event.code == "0" && session_picker_has_more?
|
|
538
|
+
load_more_sessions
|
|
539
|
+
else
|
|
540
|
+
idx = hotkey_to_index(event.code)
|
|
541
|
+
if idx && idx < visible.size
|
|
542
|
+
@session_picker_index = idx
|
|
543
|
+
select_session_picker_item(visible)
|
|
544
|
+
end
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
nil
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
# Returns the raw items for the current picker mode (root sessions or children).
|
|
551
|
+
#
|
|
552
|
+
# @return [Array<Hash>] session or child hashes from the sessions list
|
|
553
|
+
def session_picker_all_items_for_mode
|
|
407
554
|
sessions = @screens[:chat].sessions_list || []
|
|
408
|
-
result = navigate_picker(event, items: sessions, index_ivar: :@session_picker_index)
|
|
409
555
|
|
|
410
|
-
case
|
|
411
|
-
when :
|
|
556
|
+
case @session_picker_mode
|
|
557
|
+
when :root
|
|
558
|
+
sessions
|
|
559
|
+
when :children
|
|
560
|
+
parent = sessions.find { |s| s["id"] == @session_picker_parent_id }
|
|
561
|
+
parent&.dig("children") || []
|
|
562
|
+
end
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
# Returns the visible items for the current page of the current mode.
|
|
566
|
+
# Each item is a Hash with :type (:root or :child), :data, and :parent_id (for children).
|
|
567
|
+
#
|
|
568
|
+
# @return [Array<Hash>] visible items for the current page
|
|
569
|
+
def session_picker_visible_items
|
|
570
|
+
all = session_picker_all_items_for_mode
|
|
571
|
+
start = @session_picker_page * SESSION_PICKER_PAGE_SIZE
|
|
572
|
+
page = all[start, SESSION_PICKER_PAGE_SIZE] || []
|
|
573
|
+
|
|
574
|
+
page.map do |item|
|
|
575
|
+
case @session_picker_mode
|
|
576
|
+
when :root
|
|
577
|
+
{type: :root, data: item}
|
|
578
|
+
when :children
|
|
579
|
+
{type: :child, data: item, parent_id: @session_picker_parent_id}
|
|
580
|
+
end
|
|
581
|
+
end
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
# @return [Boolean] true when more items exist beyond the current page
|
|
585
|
+
def session_picker_has_more?
|
|
586
|
+
total = session_picker_all_items_for_mode.size
|
|
587
|
+
((@session_picker_page + 1) * SESSION_PICKER_PAGE_SIZE) < total
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
# @return [Integer] number of items beyond the current page
|
|
591
|
+
def session_picker_remaining_count
|
|
592
|
+
total = session_picker_all_items_for_mode.size
|
|
593
|
+
[total - ((@session_picker_page + 1) * SESSION_PICKER_PAGE_SIZE), 0].max
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
# Handles Escape in the session picker. In children mode, returns to root.
|
|
597
|
+
# In root mode, closes the picker.
|
|
598
|
+
# @return [void]
|
|
599
|
+
def handle_session_picker_escape
|
|
600
|
+
if @session_picker_mode == :children
|
|
601
|
+
return_to_root_sessions
|
|
602
|
+
else
|
|
412
603
|
@session_picker_active = false
|
|
413
|
-
when Hash
|
|
414
|
-
pick_session(result)
|
|
415
604
|
end
|
|
605
|
+
end
|
|
416
606
|
|
|
417
|
-
|
|
607
|
+
# Drills into the children of the selected root session.
|
|
608
|
+
# Only available in root mode on sessions with children.
|
|
609
|
+
#
|
|
610
|
+
# @param visible [Array<Hash>] current page items from {#session_picker_visible_items}
|
|
611
|
+
# @return [void]
|
|
612
|
+
def drill_into_children(visible)
|
|
613
|
+
return unless @session_picker_mode == :root
|
|
614
|
+
|
|
615
|
+
item = visible[@session_picker_index]
|
|
616
|
+
return unless item&.dig(:type) == :root
|
|
617
|
+
|
|
618
|
+
session = item[:data]
|
|
619
|
+
return unless session["children"]&.any?
|
|
620
|
+
|
|
621
|
+
@session_picker_mode = :children
|
|
622
|
+
@session_picker_parent_id = session["id"]
|
|
623
|
+
@session_picker_page = 0
|
|
624
|
+
@session_picker_index = 0
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
# Returns from children mode to root sessions view.
|
|
628
|
+
# @return [void]
|
|
629
|
+
def return_to_root_sessions
|
|
630
|
+
return unless @session_picker_mode == :children
|
|
631
|
+
|
|
632
|
+
@session_picker_mode = :root
|
|
633
|
+
@session_picker_parent_id = nil
|
|
634
|
+
@session_picker_page = 0
|
|
635
|
+
@session_picker_index = 0
|
|
636
|
+
end
|
|
637
|
+
|
|
638
|
+
# Advances to the next page of sessions in the current mode.
|
|
639
|
+
# @return [void]
|
|
640
|
+
def load_more_sessions
|
|
641
|
+
@session_picker_page += 1
|
|
642
|
+
@session_picker_index = 0
|
|
418
643
|
end
|
|
419
644
|
|
|
420
|
-
# Switches to the selected
|
|
645
|
+
# Switches to the session selected in the picker and closes the overlay.
|
|
421
646
|
#
|
|
422
|
-
# @param
|
|
647
|
+
# @param visible [Array<Hash>] current page items from {#session_picker_visible_items}
|
|
423
648
|
# @return [void]
|
|
424
|
-
def
|
|
425
|
-
|
|
649
|
+
def select_session_picker_item(visible)
|
|
650
|
+
item = visible[@session_picker_index]
|
|
651
|
+
return unless item
|
|
426
652
|
|
|
427
653
|
@session_picker_active = false
|
|
428
|
-
@screens[:chat].switch_session(
|
|
654
|
+
@screens[:chat].switch_session(item[:data]["id"])
|
|
429
655
|
end
|
|
430
656
|
|
|
431
657
|
# Renders the session picker overlay in the sidebar.
|
|
432
|
-
# Shows
|
|
658
|
+
# Shows paginated root sessions or children with drill-down navigation.
|
|
433
659
|
#
|
|
434
660
|
# @param frame [RatatuiRuby::Frame] terminal frame for widget rendering
|
|
435
661
|
# @param area [RatatuiRuby::Rect] sidebar area to render into
|
|
@@ -444,10 +670,19 @@ module TUI
|
|
|
444
670
|
tui.span(content: "Loading...", style: tui.style(fg: "yellow"))
|
|
445
671
|
])]
|
|
446
672
|
else
|
|
447
|
-
|
|
448
|
-
|
|
673
|
+
visible = session_picker_visible_items
|
|
674
|
+
@session_picker_index = @session_picker_index.clamp(0, [visible.size - 1, 0].max)
|
|
675
|
+
|
|
676
|
+
lines = visible.each_with_index.flat_map do |item, idx|
|
|
677
|
+
if item[:type] == :root
|
|
678
|
+
format_root_session_entry(tui, item[:data], idx, current_id)
|
|
679
|
+
else
|
|
680
|
+
format_child_session_entry(tui, item[:data], idx, current_id)
|
|
681
|
+
end
|
|
449
682
|
end
|
|
450
683
|
|
|
684
|
+
lines.concat(format_load_more_entry(tui)) if session_picker_has_more?
|
|
685
|
+
|
|
451
686
|
if lines.empty?
|
|
452
687
|
lines = [tui.line(spans: [
|
|
453
688
|
tui.span(content: "No sessions", style: tui.style(fg: "dark_gray"))
|
|
@@ -458,7 +693,7 @@ module TUI
|
|
|
458
693
|
picker = tui.paragraph(
|
|
459
694
|
text: lines,
|
|
460
695
|
block: tui.block(
|
|
461
|
-
title:
|
|
696
|
+
title: session_picker_title,
|
|
462
697
|
borders: [:all],
|
|
463
698
|
border_type: :rounded,
|
|
464
699
|
border_style: {fg: "cyan"}
|
|
@@ -467,26 +702,39 @@ module TUI
|
|
|
467
702
|
frame.render_widget(picker, area)
|
|
468
703
|
end
|
|
469
704
|
|
|
470
|
-
#
|
|
471
|
-
#
|
|
705
|
+
# Returns the picker title based on the current navigation mode.
|
|
706
|
+
#
|
|
707
|
+
# @return [String] "Sessions" for root mode, "← #N" for children mode
|
|
708
|
+
def session_picker_title
|
|
709
|
+
case @session_picker_mode
|
|
710
|
+
when :root then "Sessions"
|
|
711
|
+
when :children then "#{BACK_ARROW} ##{@session_picker_parent_id}"
|
|
712
|
+
end
|
|
713
|
+
end
|
|
714
|
+
|
|
715
|
+
# Formats a root session entry with drill-in arrow and child count.
|
|
472
716
|
#
|
|
473
717
|
# @param tui [RatatuiRuby] TUI rendering API
|
|
474
|
-
# @param session [Hash] session
|
|
475
|
-
# @param idx [Integer] position in the
|
|
476
|
-
# @param current_id [Integer] the active session
|
|
477
|
-
# @return [Array<RatatuiRuby::Widgets::Line>]
|
|
478
|
-
def
|
|
718
|
+
# @param session [Hash] serialized session from the brain
|
|
719
|
+
# @param idx [Integer] position in the current page
|
|
720
|
+
# @param current_id [Integer] ID of the currently active session
|
|
721
|
+
# @return [Array<RatatuiRuby::Widgets::Line>]
|
|
722
|
+
def format_root_session_entry(tui, session, idx, current_id)
|
|
479
723
|
selected = idx == @session_picker_index
|
|
480
724
|
is_current = session["id"] == current_id
|
|
725
|
+
children = session["children"] || []
|
|
481
726
|
|
|
482
727
|
hotkey = picker_hotkey(idx)
|
|
483
728
|
prefix = hotkey ? "[#{hotkey}]" : " "
|
|
484
729
|
marker = is_current ? "*" : " "
|
|
485
|
-
|
|
730
|
+
arrow = children.any? ? CHILDREN_ARROW : " "
|
|
731
|
+
|
|
732
|
+
display_name = session["name"] || "##{session["id"]}"
|
|
486
733
|
count = "#{session["message_count"]}msg"
|
|
487
734
|
time = format_relative_time(session["updated_at"])
|
|
735
|
+
child_info = children.any? ? " (#{children.size})" : ""
|
|
488
736
|
|
|
489
|
-
label = "#{prefix}#{marker}#{
|
|
737
|
+
label = "#{prefix}#{marker}#{arrow}#{display_name} #{count}#{child_info} #{time}"
|
|
490
738
|
|
|
491
739
|
style = if selected
|
|
492
740
|
tui.style(fg: "black", bg: "cyan")
|
|
@@ -499,6 +747,47 @@ module TUI
|
|
|
499
747
|
[tui.line(spans: [tui.span(content: label, style: style)])]
|
|
500
748
|
end
|
|
501
749
|
|
|
750
|
+
# Formats a child session entry with hotkey, status indicator, and agent name.
|
|
751
|
+
#
|
|
752
|
+
# @param tui [RatatuiRuby] TUI rendering API
|
|
753
|
+
# @param child [Hash] serialized child session from the brain
|
|
754
|
+
# @param idx [Integer] position in the current page
|
|
755
|
+
# @param current_id [Integer] ID of the currently active session
|
|
756
|
+
# @return [Array<RatatuiRuby::Widgets::Line>]
|
|
757
|
+
def format_child_session_entry(tui, child, idx, current_id)
|
|
758
|
+
selected = idx == @session_picker_index
|
|
759
|
+
is_current = child["id"] == current_id
|
|
760
|
+
|
|
761
|
+
hotkey = picker_hotkey(idx)
|
|
762
|
+
prefix = hotkey ? "[#{hotkey}]" : " "
|
|
763
|
+
marker = is_current ? "*" : " "
|
|
764
|
+
status = child["processing"] ? CHILD_STATUS_RUNNING : CHILD_STATUS_DONE
|
|
765
|
+
status_color = child["processing"] ? "yellow" : "green"
|
|
766
|
+
display_name = child["name"] || UNNAMED_SUBAGENT_LABEL
|
|
767
|
+
|
|
768
|
+
label = "#{prefix}#{marker}#{status} #{display_name}"
|
|
769
|
+
|
|
770
|
+
style = if selected
|
|
771
|
+
tui.style(fg: "black", bg: "cyan")
|
|
772
|
+
elsif is_current
|
|
773
|
+
tui.style(fg: "cyan", modifiers: [:bold])
|
|
774
|
+
else
|
|
775
|
+
tui.style(fg: status_color)
|
|
776
|
+
end
|
|
777
|
+
|
|
778
|
+
[tui.line(spans: [tui.span(content: label, style: style)])]
|
|
779
|
+
end
|
|
780
|
+
|
|
781
|
+
# Formats the "Load more" entry shown when additional pages exist.
|
|
782
|
+
#
|
|
783
|
+
# @param tui [RatatuiRuby] TUI rendering API
|
|
784
|
+
# @return [Array<RatatuiRuby::Widgets::Line>]
|
|
785
|
+
def format_load_more_entry(tui)
|
|
786
|
+
remaining = session_picker_remaining_count
|
|
787
|
+
label = "[0] Load more (#{remaining})"
|
|
788
|
+
[tui.line(spans: [tui.span(content: label, style: tui.style(fg: "dark_gray"))])]
|
|
789
|
+
end
|
|
790
|
+
|
|
502
791
|
# -- View mode picker ----------------------------------------------
|
|
503
792
|
|
|
504
793
|
# Opens the view mode picker overlay. Pre-selects the current mode.
|
data/lib/tui/message_store.rb
CHANGED
|
@@ -115,6 +115,26 @@ module TUI
|
|
|
115
115
|
end
|
|
116
116
|
end
|
|
117
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
|
|
136
|
+
end
|
|
137
|
+
|
|
118
138
|
private
|
|
119
139
|
|
|
120
140
|
# Replaces data on an existing entry matched by event ID.
|