anima-core 1.1.3 → 1.3.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 +10 -1
- data/README.md +36 -11
- data/agents/codebase-analyzer.md +2 -2
- data/agents/codebase-pattern-finder.md +2 -2
- data/agents/documentation-researcher.md +2 -2
- data/agents/thoughts-analyzer.md +2 -2
- data/agents/web-search-researcher.md +3 -3
- data/app/channels/session_channel.rb +83 -64
- data/app/decorators/agent_message_decorator.rb +2 -2
- data/app/decorators/{event_decorator.rb → message_decorator.rb} +40 -40
- data/app/decorators/system_message_decorator.rb +2 -2
- data/app/decorators/tool_call_decorator.rb +6 -6
- data/app/decorators/tool_decorator.rb +4 -4
- data/app/decorators/tool_response_decorator.rb +2 -2
- data/app/decorators/user_message_decorator.rb +5 -19
- data/app/decorators/web_get_tool_decorator.rb +41 -9
- data/app/jobs/agent_request_job.rb +33 -24
- data/app/jobs/count_message_tokens_job.rb +39 -0
- data/app/jobs/passive_recall_job.rb +4 -4
- data/app/models/concerns/{event → message}/broadcasting.rb +16 -16
- data/app/models/goal.rb +17 -4
- data/app/models/goal_pinned_message.rb +11 -0
- data/app/models/message.rb +127 -0
- data/app/models/pending_message.rb +43 -0
- data/app/models/pinned_message.rb +41 -0
- data/app/models/secret.rb +72 -0
- data/app/models/session.rb +385 -226
- data/app/models/snapshot.rb +25 -25
- data/config/environments/test.rb +5 -0
- data/config/initializers/time_nanoseconds.rb +11 -0
- data/db/migrate/20260326180000_rename_event_to_message.rb +172 -0
- data/db/migrate/20260328100000_create_secrets.rb +15 -0
- data/db/migrate/20260328152142_add_evicted_at_to_goals.rb +6 -0
- data/db/migrate/20260329120000_create_pending_messages.rb +11 -0
- data/lib/agent_loop.rb +14 -41
- data/lib/agents/definition.rb +1 -1
- data/lib/analytical_brain/runner.rb +40 -37
- data/lib/analytical_brain/tools/activate_skill.rb +5 -9
- data/lib/analytical_brain/tools/assign_nickname.rb +2 -4
- data/lib/analytical_brain/tools/deactivate_skill.rb +5 -9
- data/lib/analytical_brain/tools/everything_is_ready.rb +1 -2
- data/lib/analytical_brain/tools/finish_goal.rb +5 -8
- data/lib/analytical_brain/tools/read_workflow.rb +5 -9
- data/lib/analytical_brain/tools/rename_session.rb +3 -10
- data/lib/analytical_brain/tools/set_goal.rb +3 -7
- data/lib/analytical_brain/tools/update_goal.rb +3 -7
- data/lib/anima/cli/mcp/secrets.rb +4 -4
- data/lib/anima/cli/mcp.rb +4 -4
- data/lib/anima/installer.rb +7 -1
- data/lib/anima/settings.rb +46 -6
- data/lib/anima/version.rb +1 -1
- data/lib/anima.rb +1 -1
- data/lib/credential_store.rb +17 -66
- data/lib/events/base.rb +1 -1
- data/lib/events/bounce_back.rb +7 -7
- data/lib/events/subscribers/persister.rb +15 -22
- data/lib/events/subscribers/subagent_message_router.rb +20 -8
- data/lib/events/subscribers/transient_broadcaster.rb +2 -2
- data/lib/events/user_message.rb +2 -13
- data/lib/llm/client.rb +54 -20
- data/lib/mcp/config.rb +2 -2
- data/lib/mcp/secrets.rb +7 -8
- data/lib/mneme/compressed_viewport.rb +57 -57
- data/lib/mneme/l2_runner.rb +4 -4
- data/lib/mneme/passive_recall.rb +2 -2
- data/lib/mneme/runner.rb +57 -75
- data/lib/mneme/search.rb +38 -38
- data/lib/mneme/tools/attach_messages_to_goals.rb +103 -0
- data/lib/mneme/tools/everything_ok.rb +1 -3
- data/lib/mneme/tools/save_snapshot.rb +12 -16
- data/lib/shell_session.rb +54 -16
- data/lib/tools/base.rb +23 -0
- data/lib/tools/bash.rb +60 -16
- data/lib/tools/edit.rb +6 -8
- data/lib/tools/mark_goal_completed.rb +86 -0
- data/lib/tools/{request_feature.rb → open_issue.rb} +10 -13
- data/lib/tools/read.rb +6 -5
- data/lib/tools/recall.rb +98 -0
- data/lib/tools/registry.rb +37 -8
- data/lib/tools/remember.rb +46 -55
- data/lib/tools/response_truncator.rb +70 -0
- data/lib/tools/spawn_specialist.rb +15 -25
- data/lib/tools/spawn_subagent.rb +14 -22
- data/lib/tools/subagent_prompts.rb +42 -6
- data/lib/tools/think.rb +26 -10
- data/lib/tools/web_get.rb +23 -4
- data/lib/tools/write.rb +4 -4
- data/lib/tui/app.rb +178 -13
- data/lib/tui/braille_spinner.rb +152 -0
- data/lib/tui/cable_client.rb +4 -4
- data/lib/tui/decorators/base_decorator.rb +17 -8
- data/lib/tui/decorators/bash_decorator.rb +2 -2
- data/lib/tui/decorators/edit_decorator.rb +5 -4
- data/lib/tui/decorators/read_decorator.rb +4 -8
- data/lib/tui/decorators/think_decorator.rb +3 -5
- data/lib/tui/decorators/web_get_decorator.rb +4 -3
- data/lib/tui/decorators/write_decorator.rb +5 -4
- data/lib/tui/flash.rb +1 -1
- data/lib/tui/formatting.rb +22 -0
- data/lib/tui/message_store.rb +103 -59
- data/lib/tui/screens/chat.rb +293 -78
- data/skills/activerecord/SKILL.md +1 -1
- data/skills/dragonruby/SKILL.md +1 -1
- data/skills/draper-decorators/SKILL.md +1 -1
- data/skills/gh-issue.md +1 -1
- data/skills/mcp-server/SKILL.md +1 -1
- data/skills/ratatui-ruby/SKILL.md +1 -1
- data/skills/rspec/SKILL.md +1 -1
- data/templates/config.toml +42 -5
- data/templates/soul.md +7 -19
- data/workflows/create_handoff.md +1 -1
- data/workflows/create_note.md +1 -1
- data/workflows/create_plan.md +1 -1
- data/workflows/implement_plan.md +1 -1
- data/workflows/iterate_plan.md +1 -1
- data/workflows/research_codebase.md +1 -1
- data/workflows/resume_handoff.md +1 -1
- data/workflows/review_pr.md +78 -16
- data/workflows/thoughts_init.md +1 -1
- data/workflows/validate_plan.md +1 -1
- metadata +20 -9
- data/app/jobs/count_event_tokens_job.rb +0 -39
- data/app/models/event.rb +0 -129
- data/app/models/goal_pinned_event.rb +0 -11
- data/app/models/pinned_event.rb +0 -41
- data/lib/mneme/tools/attach_events_to_goals.rb +0 -107
data/lib/tools/write.rb
CHANGED
|
@@ -15,16 +15,16 @@ module Tools
|
|
|
15
15
|
# tool.execute("path" => "README.md", "content" => "# Title\n")
|
|
16
16
|
# # => "Wrote 9 bytes to /home/user/project/README.md"
|
|
17
17
|
class Write < Base
|
|
18
|
-
def self.tool_name = "
|
|
18
|
+
def self.tool_name = "write_file"
|
|
19
19
|
|
|
20
|
-
def self.description = "
|
|
20
|
+
def self.description = "Write file."
|
|
21
21
|
|
|
22
22
|
def self.input_schema
|
|
23
23
|
{
|
|
24
24
|
type: "object",
|
|
25
25
|
properties: {
|
|
26
|
-
path: {type: "string", description: "
|
|
27
|
-
content: {type: "string"
|
|
26
|
+
path: {type: "string", description: "Relative paths resolve against working directory. Creates intermediate directories."},
|
|
27
|
+
content: {type: "string"}
|
|
28
28
|
},
|
|
29
29
|
required: %w[path content]
|
|
30
30
|
}
|
data/lib/tui/app.rb
CHANGED
|
@@ -21,7 +21,7 @@ module TUI
|
|
|
21
21
|
}.freeze
|
|
22
22
|
|
|
23
23
|
MENU_LABELS = (COMMAND_KEYS.map { |key, action| "[#{key}] #{action.to_s.tr("_", " ").capitalize}" } +
|
|
24
|
-
["[\u2191] Scroll chat", "[\u2193] Return to input"]).freeze
|
|
24
|
+
["[\u2191] Scroll chat", "[\u2193] Return to input", "[\u2192] Scroll HUD"]).freeze
|
|
25
25
|
|
|
26
26
|
# HUD occupies 1/3 of screen width, clamped to a usable minimum.
|
|
27
27
|
HUD_MIN_WIDTH = 24
|
|
@@ -78,10 +78,16 @@ module TUI
|
|
|
78
78
|
# Grace period for watchdog thread to exit before force-killing it.
|
|
79
79
|
WATCHDOG_SHUTDOWN_TIMEOUT = 1
|
|
80
80
|
|
|
81
|
+
# HUD scroll step sizes (lines per event).
|
|
82
|
+
HUD_SCROLL_STEP = 1
|
|
83
|
+
HUD_MOUSE_SCROLL_STEP = 2
|
|
84
|
+
|
|
81
85
|
attr_reader :current_screen, :command_mode, :session_picker_active,
|
|
82
86
|
:view_mode_picker_active
|
|
83
87
|
# @return [Boolean] true when the HUD info panel is visible
|
|
84
88
|
attr_reader :hud_visible
|
|
89
|
+
# @return [Boolean] true when the HUD pane has keyboard focus for scrolling
|
|
90
|
+
attr_reader :hud_focused
|
|
85
91
|
# @return [Boolean] true when the token setup popup overlay is visible
|
|
86
92
|
attr_reader :token_setup_active
|
|
87
93
|
# @return [Boolean] true when graceful shutdown has been requested via signal
|
|
@@ -102,6 +108,11 @@ module TUI
|
|
|
102
108
|
@session_picker_mode = :root
|
|
103
109
|
@session_picker_parent_id = nil
|
|
104
110
|
@hud_visible = true
|
|
111
|
+
@hud_focused = false
|
|
112
|
+
@hud_scroll_offset = 0
|
|
113
|
+
@hud_max_scroll = 0
|
|
114
|
+
@hud_visible_height = 0
|
|
115
|
+
@hud_content_area = nil
|
|
105
116
|
@view_mode_picker_active = false
|
|
106
117
|
@view_mode_picker_index = 0
|
|
107
118
|
@token_setup_active = false
|
|
@@ -197,8 +208,13 @@ module TUI
|
|
|
197
208
|
GOAL_ICON_ACTIVE = "\u25CF" # ●
|
|
198
209
|
GOAL_ICON_IN_PROGRESS = "\u25D0" # ◐
|
|
199
210
|
GOAL_ICON_COMPLETED = "\u2713" # ✓
|
|
200
|
-
|
|
201
|
-
|
|
211
|
+
|
|
212
|
+
# Sub-agent state icons — form communicates type of work,
|
|
213
|
+
# color communicates status. Work independently.
|
|
214
|
+
CHILD_ICON_IDLE = "\u25CC" # ◌ hollow — nothing happening
|
|
215
|
+
CHILD_ICON_GENERATING = "\u25CF" # ● filled — LLM thinking
|
|
216
|
+
CHILD_ICON_TOOL_EXECUTING = "\u25C9" # ◉ dot-in-circle — tool running
|
|
217
|
+
CHILD_ICON_INTERRUPTING = "\u25CF" # ● filled — stopping
|
|
202
218
|
|
|
203
219
|
def render_info(frame, area, tui)
|
|
204
220
|
session = @screens[:chat].session_info
|
|
@@ -218,8 +234,10 @@ module TUI
|
|
|
218
234
|
end
|
|
219
235
|
|
|
220
236
|
# Renders the main HUD content: session name, goals, skills,
|
|
221
|
-
# workflow, and sub-agents.
|
|
237
|
+
# workflow, and sub-agents. Supports vertical scrolling with
|
|
238
|
+
# a scrollbar when content exceeds the visible area.
|
|
222
239
|
def render_hud_content(frame, area, tui, session)
|
|
240
|
+
@hud_content_area = area
|
|
223
241
|
session_label = session[:name] || "##{session[:id]}"
|
|
224
242
|
|
|
225
243
|
lines = [
|
|
@@ -234,16 +252,42 @@ module TUI
|
|
|
234
252
|
interaction_state_line(tui)
|
|
235
253
|
].flatten.compact
|
|
236
254
|
|
|
255
|
+
@hud_visible_height = [area.height - 2, 0].max
|
|
256
|
+
inner_width = [area.width - 2, 1].max
|
|
257
|
+
total_height = tui.paragraph(text: lines, wrap: true).line_count(inner_width)
|
|
258
|
+
@hud_max_scroll = [total_height - @hud_visible_height, 0].max
|
|
259
|
+
@hud_scroll_offset = @hud_scroll_offset.clamp(0, @hud_max_scroll)
|
|
260
|
+
|
|
261
|
+
border_color = @hud_focused ? "yellow" : "white"
|
|
262
|
+
|
|
237
263
|
content = tui.paragraph(
|
|
238
264
|
text: lines,
|
|
239
265
|
wrap: true,
|
|
266
|
+
scroll: [@hud_scroll_offset, 0],
|
|
240
267
|
block: tui.block(
|
|
241
268
|
borders: [:left, :top, :right],
|
|
242
269
|
border_type: :rounded,
|
|
243
|
-
border_style: {fg:
|
|
270
|
+
border_style: {fg: border_color}
|
|
244
271
|
)
|
|
245
272
|
)
|
|
246
273
|
frame.render_widget(content, area)
|
|
274
|
+
|
|
275
|
+
render_hud_scrollbar(frame, area, tui)
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# Renders a scrollbar on the right edge of the HUD when content overflows.
|
|
279
|
+
def render_hud_scrollbar(frame, area, tui)
|
|
280
|
+
return unless @hud_max_scroll > 0
|
|
281
|
+
|
|
282
|
+
scrollbar = tui.scrollbar(
|
|
283
|
+
content_length: @hud_max_scroll,
|
|
284
|
+
position: @hud_scroll_offset,
|
|
285
|
+
orientation: :vertical_right,
|
|
286
|
+
thumb_style: {fg: "cyan"},
|
|
287
|
+
track_symbol: "\u2502",
|
|
288
|
+
track_style: {fg: "dark_gray"}
|
|
289
|
+
)
|
|
290
|
+
frame.render_widget(scrollbar, area)
|
|
247
291
|
end
|
|
248
292
|
|
|
249
293
|
# Renders the bottom status bar: connection state and model name.
|
|
@@ -382,26 +426,44 @@ module TUI
|
|
|
382
426
|
end
|
|
383
427
|
|
|
384
428
|
# Returns the activity icon and color for a child session.
|
|
429
|
+
# Form communicates type of work, color communicates status —
|
|
430
|
+
# they work independently so even without color the shape tells
|
|
431
|
+
# the story.
|
|
385
432
|
#
|
|
386
|
-
# @param child [Hash] child session data with "
|
|
433
|
+
# @param child [Hash] child session data with "session_state" key
|
|
387
434
|
# @return [Array(String, String)] icon and color pair
|
|
388
435
|
def child_icon_and_color(child)
|
|
389
|
-
|
|
390
|
-
|
|
436
|
+
case child["session_state"]
|
|
437
|
+
when "llm_generating"
|
|
438
|
+
[CHILD_ICON_GENERATING, "green"]
|
|
439
|
+
when "tool_executing"
|
|
440
|
+
[CHILD_ICON_TOOL_EXECUTING, "green"]
|
|
441
|
+
when "interrupting"
|
|
442
|
+
[CHILD_ICON_INTERRUPTING, "red"]
|
|
391
443
|
else
|
|
392
|
-
[CHILD_ICON_IDLE, "
|
|
444
|
+
[CHILD_ICON_IDLE, "dark_gray"]
|
|
393
445
|
end
|
|
394
446
|
end
|
|
395
447
|
|
|
396
|
-
# Shows
|
|
448
|
+
# Shows focus mode when a pane is focused, or the braille spinner
|
|
449
|
+
# during active processing.
|
|
397
450
|
def interaction_state_line(tui)
|
|
398
|
-
if @
|
|
451
|
+
if @hud_focused
|
|
452
|
+
tui.line(spans: [
|
|
453
|
+
tui.span(content: "HUD Scroll", style: tui.style(fg: "yellow", modifiers: [:bold]))
|
|
454
|
+
])
|
|
455
|
+
elsif @screens[:chat].chat_focused
|
|
399
456
|
tui.line(spans: [
|
|
400
457
|
tui.span(content: "Scrolling", style: tui.style(fg: "yellow", modifiers: [:bold]))
|
|
401
458
|
])
|
|
402
459
|
elsif chat_loading?
|
|
460
|
+
chat = @screens[:chat]
|
|
461
|
+
char = chat.spinner.tick || "\u2800"
|
|
462
|
+
color = chat.spinner_color
|
|
463
|
+
label = chat.spinner_label
|
|
403
464
|
tui.line(spans: [
|
|
404
|
-
tui.span(content: "
|
|
465
|
+
tui.span(content: "#{char} ", style: tui.style(fg: color, modifiers: [:bold])),
|
|
466
|
+
tui.span(content: label, style: tui.style(fg: color))
|
|
405
467
|
])
|
|
406
468
|
end
|
|
407
469
|
end
|
|
@@ -410,6 +472,38 @@ module TUI
|
|
|
410
472
|
@screens[:chat].loading?
|
|
411
473
|
end
|
|
412
474
|
|
|
475
|
+
# Switches keyboard focus to the HUD pane for scrolling.
|
|
476
|
+
# Unfocuses the chat pane if it was focused.
|
|
477
|
+
#
|
|
478
|
+
# @return [void]
|
|
479
|
+
def focus_hud
|
|
480
|
+
@screens[:chat].unfocus_chat if @screens[:chat].chat_focused
|
|
481
|
+
@hud_focused = true
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
# Returns keyboard focus from the HUD pane.
|
|
485
|
+
#
|
|
486
|
+
# @return [void]
|
|
487
|
+
def unfocus_hud
|
|
488
|
+
@hud_focused = false
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
# Scrolls the HUD viewport up, clamping at the top.
|
|
492
|
+
#
|
|
493
|
+
# @param lines [Integer] number of lines to scroll
|
|
494
|
+
# @return [void]
|
|
495
|
+
def scroll_hud_up(lines)
|
|
496
|
+
@hud_scroll_offset = [@hud_scroll_offset - lines, 0].max
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
# Scrolls the HUD viewport down, clamping at max_scroll.
|
|
500
|
+
#
|
|
501
|
+
# @param lines [Integer] number of lines to scroll
|
|
502
|
+
# @return [void]
|
|
503
|
+
def scroll_hud_down(lines)
|
|
504
|
+
@hud_scroll_offset = [@hud_scroll_offset + lines, @hud_max_scroll].min
|
|
505
|
+
end
|
|
506
|
+
|
|
413
507
|
def handle_event(event)
|
|
414
508
|
return nil if event.none?
|
|
415
509
|
return :quit if event.ctrl_c?
|
|
@@ -422,6 +516,8 @@ module TUI
|
|
|
422
516
|
handle_view_mode_picker(event)
|
|
423
517
|
elsif @command_mode
|
|
424
518
|
handle_command_mode(event)
|
|
519
|
+
elsif @hud_focused
|
|
520
|
+
handle_hud_focused_event(event)
|
|
425
521
|
else
|
|
426
522
|
handle_normal_mode(event)
|
|
427
523
|
end
|
|
@@ -442,6 +538,11 @@ module TUI
|
|
|
442
538
|
return nil
|
|
443
539
|
end
|
|
444
540
|
|
|
541
|
+
if event.right? && @hud_visible
|
|
542
|
+
focus_hud
|
|
543
|
+
return nil
|
|
544
|
+
end
|
|
545
|
+
|
|
445
546
|
action = COMMAND_KEYS[event.code]
|
|
446
547
|
case action
|
|
447
548
|
when :quit
|
|
@@ -451,6 +552,7 @@ module TUI
|
|
|
451
552
|
nil
|
|
452
553
|
when :toggle_hud
|
|
453
554
|
@hud_visible = !@hud_visible
|
|
555
|
+
unfocus_hud if !@hud_visible
|
|
454
556
|
nil
|
|
455
557
|
when :new_session
|
|
456
558
|
@screens[:chat].new_session
|
|
@@ -466,7 +568,13 @@ module TUI
|
|
|
466
568
|
end
|
|
467
569
|
|
|
468
570
|
def handle_normal_mode(event)
|
|
469
|
-
if event.
|
|
571
|
+
if event.paste?
|
|
572
|
+
delegate_to_screen(event)
|
|
573
|
+
return nil
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
if event.mouse?
|
|
577
|
+
return nil if route_mouse_to_hud(event)
|
|
470
578
|
delegate_to_screen(event)
|
|
471
579
|
return nil
|
|
472
580
|
end
|
|
@@ -497,6 +605,63 @@ module TUI
|
|
|
497
605
|
nil
|
|
498
606
|
end
|
|
499
607
|
|
|
608
|
+
# Handles keyboard events when the HUD pane has focus.
|
|
609
|
+
# Arrow keys and Page Up/Down scroll the HUD; Escape and Ctrl+A exit.
|
|
610
|
+
def handle_hud_focused_event(event)
|
|
611
|
+
return nil if event.none?
|
|
612
|
+
return :quit if event.ctrl_c?
|
|
613
|
+
|
|
614
|
+
if event.mouse?
|
|
615
|
+
return nil if route_mouse_to_hud(event)
|
|
616
|
+
delegate_to_screen(event)
|
|
617
|
+
return nil
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
return nil unless event.key?
|
|
621
|
+
|
|
622
|
+
if event.esc? || ctrl_a?(event)
|
|
623
|
+
unfocus_hud
|
|
624
|
+
return nil
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
if event.up?
|
|
628
|
+
scroll_hud_up(HUD_SCROLL_STEP)
|
|
629
|
+
elsif event.down?
|
|
630
|
+
scroll_hud_down(HUD_SCROLL_STEP)
|
|
631
|
+
elsif event.page_up?
|
|
632
|
+
scroll_hud_up(@hud_visible_height)
|
|
633
|
+
elsif event.page_down?
|
|
634
|
+
scroll_hud_down(@hud_visible_height)
|
|
635
|
+
elsif event.home?
|
|
636
|
+
scroll_hud_up(@hud_max_scroll)
|
|
637
|
+
elsif event.end?
|
|
638
|
+
scroll_hud_down(@hud_max_scroll)
|
|
639
|
+
end
|
|
640
|
+
nil
|
|
641
|
+
end
|
|
642
|
+
|
|
643
|
+
# Routes mouse scroll events to the HUD when the cursor is over the HUD area.
|
|
644
|
+
# @return [Boolean] true if the event was handled by the HUD
|
|
645
|
+
def route_mouse_to_hud(event)
|
|
646
|
+
return false if !@hud_visible || !@hud_content_area
|
|
647
|
+
return false unless mouse_over_hud?(event)
|
|
648
|
+
return false unless event.scroll_up? || event.scroll_down?
|
|
649
|
+
|
|
650
|
+
if event.scroll_up?
|
|
651
|
+
scroll_hud_up(HUD_MOUSE_SCROLL_STEP)
|
|
652
|
+
else
|
|
653
|
+
scroll_hud_down(HUD_MOUSE_SCROLL_STEP)
|
|
654
|
+
end
|
|
655
|
+
true
|
|
656
|
+
end
|
|
657
|
+
|
|
658
|
+
# Checks whether a mouse event's coordinates fall within the HUD content area.
|
|
659
|
+
def mouse_over_hud?(event)
|
|
660
|
+
area = @hud_content_area
|
|
661
|
+
event.x >= area.x && event.x < area.x + area.width &&
|
|
662
|
+
event.y >= area.y && event.y < area.y + area.height
|
|
663
|
+
end
|
|
664
|
+
|
|
500
665
|
# Switches to the parent session when viewing a child (sub-agent) session.
|
|
501
666
|
# No-op if the current session is a root session.
|
|
502
667
|
#
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TUI
|
|
4
|
+
# Animated braille spinner that communicates session state through distinct
|
|
5
|
+
# visual patterns. Each state gets its own animation — a user watching long
|
|
6
|
+
# enough starts _feeling_ the difference between LLM thinking and tool
|
|
7
|
+
# execution without reading text.
|
|
8
|
+
#
|
|
9
|
+
# The 2x4 braille grid (U+2800-U+28FF) encodes 8 dots in a single character
|
|
10
|
+
# cell. Dot positions map to bit flags:
|
|
11
|
+
#
|
|
12
|
+
# ┌───┬───┐
|
|
13
|
+
# │ 0 │ 3 │ bit 0 = top-left, bit 3 = top-right
|
|
14
|
+
# │ 1 │ 4 │ bit 1 = mid-left, bit 4 = mid-right
|
|
15
|
+
# │ 2 │ 5 │ bit 2 = lower-left, bit 5 = lower-right
|
|
16
|
+
# │ 6 │ 7 │ bit 6 = bottom-left, bit 7 = bottom-right
|
|
17
|
+
# └───┴───┘
|
|
18
|
+
#
|
|
19
|
+
# During LLM generation, a snake weaves through the grid — organic,
|
|
20
|
+
# unpredictable movement like watching a campfire. During tool execution,
|
|
21
|
+
# a fast staccato pulse signals mechanical work. Interrupting decelerates
|
|
22
|
+
# to a freeze.
|
|
23
|
+
#
|
|
24
|
+
# @example Basic usage
|
|
25
|
+
# spinner = BrailleSpinner.new
|
|
26
|
+
# spinner.state = "llm_generating"
|
|
27
|
+
# char = spinner.tick # => "⠋" (braille pattern)
|
|
28
|
+
class BrailleSpinner
|
|
29
|
+
# Clockwise traversal of the 8 dots in the braille grid.
|
|
30
|
+
# Produces a smooth rotating animation — one dot lit at a time.
|
|
31
|
+
SNAKE_FRAMES = [
|
|
32
|
+
0x01, # ⠁ dot 0 (top-left)
|
|
33
|
+
0x02, # ⠂ dot 1 (mid-left)
|
|
34
|
+
0x04, # ⠄ dot 2 (lower-left)
|
|
35
|
+
0x40, # ⡀ dot 6 (bottom-left)
|
|
36
|
+
0x80, # ⢀ dot 7 (bottom-right)
|
|
37
|
+
0x20, # ⠠ dot 5 (lower-right)
|
|
38
|
+
0x10, # ⠐ dot 4 (mid-right)
|
|
39
|
+
0x08 # ⠈ dot 3 (top-right)
|
|
40
|
+
].freeze
|
|
41
|
+
|
|
42
|
+
# Snake animation: 3 consecutive dots form a growing/moving tail.
|
|
43
|
+
# Each frame is the OR of 3 adjacent positions in SNAKE_FRAMES,
|
|
44
|
+
# creating a worm-like creature circling the grid.
|
|
45
|
+
SNAKE_TRAIL_FRAMES = SNAKE_FRAMES.each_index.map { |idx|
|
|
46
|
+
SNAKE_FRAMES[idx] | SNAKE_FRAMES[(idx + 1) % 8] | SNAKE_FRAMES[(idx + 2) % 8]
|
|
47
|
+
}.freeze
|
|
48
|
+
|
|
49
|
+
# Tool execution: alternating dot patterns for a staccato pulse.
|
|
50
|
+
# Fast, mechanical, clearly different from the smooth snake.
|
|
51
|
+
TOOL_FRAMES = [
|
|
52
|
+
0x09, # ⠉ dots 0+3 (top row)
|
|
53
|
+
0x12, # ⠒ dots 1+4 (middle row)
|
|
54
|
+
0x24, # ⠤ dots 2+5 (lower row)
|
|
55
|
+
0xC0, # ⣀ dots 6+7 (bottom row)
|
|
56
|
+
0x24, # ⠤ dots 2+5 (lower row)
|
|
57
|
+
0x12 # ⠒ dots 1+4 (middle row)
|
|
58
|
+
].freeze
|
|
59
|
+
|
|
60
|
+
# Interrupting: rapid deceleration — full grid fading to empty.
|
|
61
|
+
INTERRUPT_FRAMES = [
|
|
62
|
+
0xFF, # ⣿ all dots
|
|
63
|
+
0xDB, # ⣛ most dots
|
|
64
|
+
0x49, # ⡉ sparse
|
|
65
|
+
0x00, # ⠀ empty
|
|
66
|
+
0x49, # ⡉ sparse
|
|
67
|
+
0xFF # ⣿ all dots
|
|
68
|
+
].freeze
|
|
69
|
+
|
|
70
|
+
# Braille Unicode block base codepoint.
|
|
71
|
+
BRAILLE_BASE = 0x2800
|
|
72
|
+
|
|
73
|
+
# Ticks per frame for each state — controls animation speed.
|
|
74
|
+
# Higher = slower. At ~15fps render loop: 2 = ~7.5fps, 4 = ~3.75fps.
|
|
75
|
+
SPEED = {
|
|
76
|
+
"llm_generating" => 2,
|
|
77
|
+
"tool_executing" => 1,
|
|
78
|
+
"interrupting" => 1
|
|
79
|
+
}.freeze
|
|
80
|
+
|
|
81
|
+
# @return [String] current session state
|
|
82
|
+
attr_reader :state
|
|
83
|
+
|
|
84
|
+
def initialize
|
|
85
|
+
@state = "idle"
|
|
86
|
+
@frame_index = 0
|
|
87
|
+
@tick_count = 0
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Updates the session state driving the animation.
|
|
91
|
+
# Resets frame position on state change for a clean transition.
|
|
92
|
+
#
|
|
93
|
+
# @param new_state [String] one of "idle", "llm_generating",
|
|
94
|
+
# "tool_executing", "interrupting"
|
|
95
|
+
def state=(new_state)
|
|
96
|
+
if @state != new_state
|
|
97
|
+
@frame_index = 0
|
|
98
|
+
@tick_count = 0
|
|
99
|
+
end
|
|
100
|
+
@state = new_state
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Advances the animation by one tick and returns the current
|
|
104
|
+
# braille character. Returns nil when idle (no animation).
|
|
105
|
+
#
|
|
106
|
+
# @return [String, nil] single braille character, or nil when idle
|
|
107
|
+
def tick
|
|
108
|
+
return nil if @state == "idle"
|
|
109
|
+
|
|
110
|
+
frames = frames_for_state
|
|
111
|
+
return nil unless frames
|
|
112
|
+
|
|
113
|
+
speed = SPEED.fetch(@state, 2)
|
|
114
|
+
@tick_count += 1
|
|
115
|
+
if @tick_count >= speed
|
|
116
|
+
@tick_count = 0
|
|
117
|
+
@frame_index = (@frame_index + 1) % frames.size
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
(BRAILLE_BASE + frames[@frame_index]).chr(Encoding::UTF_8)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Returns the current frame character without advancing.
|
|
124
|
+
#
|
|
125
|
+
# @return [String, nil] single braille character, or nil when idle
|
|
126
|
+
def current
|
|
127
|
+
return nil if @state == "idle"
|
|
128
|
+
|
|
129
|
+
frames = frames_for_state
|
|
130
|
+
return nil unless frames
|
|
131
|
+
|
|
132
|
+
(BRAILLE_BASE + frames[@frame_index]).chr(Encoding::UTF_8)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Whether the spinner is actively animating.
|
|
136
|
+
#
|
|
137
|
+
# @return [Boolean]
|
|
138
|
+
def active?
|
|
139
|
+
@state != "idle"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
private
|
|
143
|
+
|
|
144
|
+
def frames_for_state
|
|
145
|
+
case @state
|
|
146
|
+
when "llm_generating" then SNAKE_TRAIL_FRAMES
|
|
147
|
+
when "tool_executing" then TOOL_FRAMES
|
|
148
|
+
when "interrupting" then INTERRUPT_FRAMES
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
data/lib/tui/cable_client.rb
CHANGED
|
@@ -129,9 +129,9 @@ module TUI
|
|
|
129
129
|
# Requests the brain to recall (delete) a pending message so the user
|
|
130
130
|
# can edit it before the LLM sees it.
|
|
131
131
|
#
|
|
132
|
-
# @param
|
|
133
|
-
def recall_pending(
|
|
134
|
-
send_action("recall_pending", {"
|
|
132
|
+
# @param pending_message_id [Integer] database ID of the {PendingMessage}
|
|
133
|
+
def recall_pending(pending_message_id)
|
|
134
|
+
send_action("recall_pending", {"pending_message_id" => pending_message_id})
|
|
135
135
|
end
|
|
136
136
|
|
|
137
137
|
# Requests interruption of the current tool execution. The server sets
|
|
@@ -143,7 +143,7 @@ module TUI
|
|
|
143
143
|
end
|
|
144
144
|
|
|
145
145
|
# Sends an Anthropic subscription token to the brain for validation and storage.
|
|
146
|
-
# The token flows directly from TUI input to encrypted
|
|
146
|
+
# The token flows directly from TUI input to the encrypted secrets table — never
|
|
147
147
|
# enters the LLM context window.
|
|
148
148
|
#
|
|
149
149
|
# @param token [String] Anthropic subscription token (sk-ant-oat01-...)
|
|
@@ -71,6 +71,8 @@ module TUI
|
|
|
71
71
|
end
|
|
72
72
|
|
|
73
73
|
# Generic tool response rendering — success/failure indicator and content.
|
|
74
|
+
# Token counts get their own color-coded span so expensive responses
|
|
75
|
+
# visually jump out in debug mode.
|
|
74
76
|
# Subclasses override for tool-specific presentation.
|
|
75
77
|
#
|
|
76
78
|
# @param tui [RatatuiRuby] TUI rendering API
|
|
@@ -79,16 +81,22 @@ module TUI
|
|
|
79
81
|
indicator = (data["success"] == false) ? ERROR_ICON : CHECKMARK
|
|
80
82
|
tool_id = data["tool_use_id"]
|
|
81
83
|
tokens = data["tokens"]
|
|
84
|
+
style = tui.style(fg: response_color)
|
|
82
85
|
|
|
83
86
|
meta_parts = []
|
|
84
87
|
meta_parts << "[#{tool_id}]" if tool_id
|
|
85
88
|
meta_parts << indicator
|
|
86
|
-
meta_parts << format_token_label(tokens, data["estimated"]) if tokens
|
|
87
89
|
prefix = " #{RETURN_ARROW} #{meta_parts.join(" ")} "
|
|
88
90
|
|
|
89
91
|
content_lines = data["content"].to_s.split("\n", -1)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
+
first_line_spans = [tui.span(content: prefix, style: style)]
|
|
93
|
+
if tokens
|
|
94
|
+
tok_label = format_token_label(tokens, data["estimated"])
|
|
95
|
+
first_line_spans << tui.span(content: "#{tok_label} ", style: tui.style(fg: token_count_color(tokens)))
|
|
96
|
+
end
|
|
97
|
+
first_line_spans << tui.span(content: content_lines.first.to_s, style: style)
|
|
98
|
+
|
|
99
|
+
lines = [tui.line(spans: first_line_spans)]
|
|
92
100
|
content_lines.drop(1).each { |line| lines << tui.line(spans: [tui.span(content: " #{line}", style: style)]) }
|
|
93
101
|
lines
|
|
94
102
|
end
|
|
@@ -108,10 +116,11 @@ module TUI
|
|
|
108
116
|
ICON
|
|
109
117
|
end
|
|
110
118
|
|
|
111
|
-
#
|
|
119
|
+
# Unified color for all tool call headers. Keeps tool invocations
|
|
120
|
+
# visually distinct from conversation messages (user/assistant/thought).
|
|
112
121
|
# @return [String]
|
|
113
122
|
def color
|
|
114
|
-
"
|
|
123
|
+
"magenta"
|
|
115
124
|
end
|
|
116
125
|
|
|
117
126
|
# Color for tool response content. Subclasses override for tool-specific colors.
|
|
@@ -152,9 +161,9 @@ module TUI
|
|
|
152
161
|
case tool_name
|
|
153
162
|
when "bash" then BashDecorator
|
|
154
163
|
when "think" then ThinkDecorator
|
|
155
|
-
when "
|
|
156
|
-
when "
|
|
157
|
-
when "
|
|
164
|
+
when "read_file" then ReadDecorator
|
|
165
|
+
when "edit_file" then EditDecorator
|
|
166
|
+
when "write_file" then WriteDecorator
|
|
158
167
|
when "web_get" then WebGetDecorator
|
|
159
168
|
else self
|
|
160
169
|
end
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
module TUI
|
|
4
4
|
module Decorators
|
|
5
5
|
# Renders bash tool calls and responses.
|
|
6
|
-
# Calls show the shell command with a terminal icon.
|
|
7
|
-
# Responses use green for success, red for failure.
|
|
6
|
+
# Calls show the shell command with a terminal icon in the unified tool color.
|
|
7
|
+
# Responses use green for success, red for failure — immediate actionable feedback.
|
|
8
8
|
class BashDecorator < BaseDecorator
|
|
9
9
|
ICON = "\u{1F4BB}" # laptop / terminal
|
|
10
10
|
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
module TUI
|
|
4
4
|
module Decorators
|
|
5
|
-
# Renders
|
|
6
|
-
# Calls show the file path with a pencil icon.
|
|
5
|
+
# Renders edit_file tool calls and responses.
|
|
6
|
+
# Calls show the file path with a pencil icon in the unified tool color.
|
|
7
|
+
# Responses use the CRUD Update color (light_yellow) to flag modifications.
|
|
7
8
|
class EditDecorator < BaseDecorator
|
|
8
9
|
ICON = "\u270F\uFE0F" # pencil
|
|
9
10
|
|
|
@@ -11,8 +12,8 @@ module TUI
|
|
|
11
12
|
ICON
|
|
12
13
|
end
|
|
13
14
|
|
|
14
|
-
def
|
|
15
|
-
"
|
|
15
|
+
def response_color
|
|
16
|
+
"light_yellow"
|
|
16
17
|
end
|
|
17
18
|
end
|
|
18
19
|
end
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
module TUI
|
|
4
4
|
module Decorators
|
|
5
|
-
# Renders
|
|
6
|
-
# Calls show the file path with a page icon.
|
|
7
|
-
# Responses
|
|
5
|
+
# Renders read_file tool calls and responses.
|
|
6
|
+
# Calls show the file path with a page icon in the unified tool color.
|
|
7
|
+
# Responses use the CRUD Read color (light_blue) for informational content.
|
|
8
8
|
class ReadDecorator < BaseDecorator
|
|
9
9
|
ICON = "\u{1F4C4}" # page facing up
|
|
10
10
|
|
|
@@ -12,12 +12,8 @@ module TUI
|
|
|
12
12
|
ICON
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
def color
|
|
16
|
-
"cyan"
|
|
17
|
-
end
|
|
18
|
-
|
|
19
15
|
def response_color
|
|
20
|
-
"
|
|
16
|
+
"light_blue"
|
|
21
17
|
end
|
|
22
18
|
end
|
|
23
19
|
end
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
module TUI
|
|
4
4
|
module Decorators
|
|
5
5
|
# Renders think tool events — the agent's inner reasoning.
|
|
6
|
-
# "aloud" thoughts use
|
|
7
|
-
#
|
|
6
|
+
# Both "aloud" and "inner" thoughts use grey (dark_gray) to visually
|
|
7
|
+
# de-emphasize reasoning content vs. actual conversation output.
|
|
8
8
|
class ThinkDecorator < BaseDecorator
|
|
9
9
|
THOUGHT_BUBBLE = "\u{1F4AD}" # thought balloon
|
|
10
10
|
|
|
@@ -17,9 +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
|
-
|
|
21
|
-
fg = aloud ? "yellow" : "dark_gray"
|
|
22
|
-
style = tui.style(fg: fg)
|
|
20
|
+
style = tui.style(fg: "dark_gray")
|
|
23
21
|
ts = data["timestamp"]
|
|
24
22
|
|
|
25
23
|
meta = []
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
module TUI
|
|
4
4
|
module Decorators
|
|
5
5
|
# Renders web_get tool calls and responses.
|
|
6
|
-
# Calls show the URL with a globe icon.
|
|
6
|
+
# Calls show the URL with a globe icon in the unified tool color.
|
|
7
|
+
# Responses use the CRUD Read color (light_blue) for fetched content.
|
|
7
8
|
class WebGetDecorator < BaseDecorator
|
|
8
9
|
ICON = "\u{1F310}" # globe with meridians
|
|
9
10
|
|
|
@@ -11,8 +12,8 @@ module TUI
|
|
|
11
12
|
ICON
|
|
12
13
|
end
|
|
13
14
|
|
|
14
|
-
def
|
|
15
|
-
"
|
|
15
|
+
def response_color
|
|
16
|
+
"light_blue"
|
|
16
17
|
end
|
|
17
18
|
end
|
|
18
19
|
end
|