anima-core 1.0.1 → 1.1.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.
Files changed (83) hide show
  1. checksums.yaml +4 -4
  2. data/.gitattributes +1 -0
  3. data/.reek.yml +61 -0
  4. data/README.md +202 -116
  5. data/anima-core.gemspec +4 -1
  6. data/app/channels/session_channel.rb +44 -10
  7. data/app/decorators/agent_message_decorator.rb +6 -0
  8. data/app/decorators/event_decorator.rb +41 -7
  9. data/app/decorators/tool_call_decorator.rb +66 -5
  10. data/app/decorators/tool_decorator.rb +57 -0
  11. data/app/decorators/tool_response_decorator.rb +35 -5
  12. data/app/decorators/user_message_decorator.rb +6 -0
  13. data/app/decorators/web_get_tool_decorator.rb +102 -0
  14. data/app/jobs/agent_request_job.rb +95 -20
  15. data/app/jobs/mneme_job.rb +51 -0
  16. data/app/jobs/passive_recall_job.rb +29 -0
  17. data/app/models/concerns/event/broadcasting.rb +18 -0
  18. data/app/models/event.rb +10 -0
  19. data/app/models/goal.rb +27 -0
  20. data/app/models/goal_pinned_event.rb +11 -0
  21. data/app/models/pinned_event.rb +41 -0
  22. data/app/models/session.rb +335 -6
  23. data/app/models/snapshot.rb +76 -0
  24. data/config/initializers/event_subscribers.rb +14 -3
  25. data/config/initializers/fts5_schema_dump.rb +21 -0
  26. data/db/migrate/20260316094817_add_interrupt_requested_to_sessions.rb +5 -0
  27. data/db/migrate/20260321080000_create_mneme_schema.rb +32 -0
  28. data/db/migrate/20260321120000_create_pinned_events.rb +27 -0
  29. data/db/migrate/20260321140000_create_events_fts_index.rb +77 -0
  30. data/db/migrate/20260321140100_add_recalled_event_ids_to_sessions.rb +10 -0
  31. data/lib/agent_loop.rb +67 -18
  32. data/lib/analytical_brain/runner.rb +159 -84
  33. data/lib/analytical_brain/tools/assign_nickname.rb +76 -0
  34. data/lib/analytical_brain/tools/finish_goal.rb +6 -1
  35. data/lib/anima/cli.rb +34 -1
  36. data/lib/anima/config_migrator.rb +205 -0
  37. data/lib/anima/installer.rb +13 -130
  38. data/lib/anima/settings.rb +42 -1
  39. data/lib/anima/version.rb +1 -1
  40. data/lib/events/bounce_back.rb +37 -0
  41. data/lib/events/subscribers/agent_dispatcher.rb +29 -0
  42. data/lib/events/subscribers/persister.rb +17 -0
  43. data/lib/events/subscribers/subagent_message_router.rb +102 -0
  44. data/lib/events/subscribers/transient_broadcaster.rb +36 -0
  45. data/lib/llm/client.rb +99 -14
  46. data/lib/mneme/compressed_viewport.rb +200 -0
  47. data/lib/mneme/l2_runner.rb +138 -0
  48. data/lib/mneme/passive_recall.rb +69 -0
  49. data/lib/mneme/runner.rb +254 -0
  50. data/lib/mneme/search.rb +150 -0
  51. data/lib/mneme/tools/attach_events_to_goals.rb +107 -0
  52. data/lib/mneme/tools/everything_ok.rb +24 -0
  53. data/lib/mneme/tools/save_snapshot.rb +68 -0
  54. data/lib/mneme.rb +29 -0
  55. data/lib/providers/anthropic.rb +57 -13
  56. data/lib/shell_session.rb +188 -59
  57. data/lib/tasks/fts5.rake +6 -0
  58. data/lib/tools/remember.rb +179 -0
  59. data/lib/tools/spawn_specialist.rb +21 -9
  60. data/lib/tools/spawn_subagent.rb +22 -11
  61. data/lib/tools/subagent_prompts.rb +20 -3
  62. data/lib/tools/think.rb +57 -0
  63. data/lib/tools/web_get.rb +15 -6
  64. data/lib/tui/app.rb +230 -127
  65. data/lib/tui/cable_client.rb +8 -0
  66. data/lib/tui/decorators/base_decorator.rb +165 -0
  67. data/lib/tui/decorators/bash_decorator.rb +20 -0
  68. data/lib/tui/decorators/edit_decorator.rb +19 -0
  69. data/lib/tui/decorators/read_decorator.rb +24 -0
  70. data/lib/tui/decorators/think_decorator.rb +36 -0
  71. data/lib/tui/decorators/web_get_decorator.rb +19 -0
  72. data/lib/tui/decorators/write_decorator.rb +19 -0
  73. data/lib/tui/flash.rb +139 -0
  74. data/lib/tui/formatting.rb +28 -0
  75. data/lib/tui/height_map.rb +93 -0
  76. data/lib/tui/message_store.rb +25 -1
  77. data/lib/tui/performance_logger.rb +90 -0
  78. data/lib/tui/screens/chat.rb +374 -109
  79. data/templates/config.toml +156 -0
  80. metadata +87 -4
  81. data/CHANGELOG.md +0 -79
  82. data/Gemfile +0 -17
  83. data/lib/tools/return_result.rb +0 -81
data/lib/tui/app.rb CHANGED
@@ -4,6 +4,7 @@ require "time"
4
4
  require_relative "cable_client"
5
5
  require_relative "input_buffer"
6
6
  require_relative "message_store"
7
+ require_relative "performance_logger"
7
8
  require_relative "screens/chat"
8
9
 
9
10
  module TUI
@@ -12,6 +13,7 @@ module TUI
12
13
 
13
14
  COMMAND_KEYS = {
14
15
  "a" => :anthropic_token,
16
+ "h" => :toggle_hud,
15
17
  "n" => :new_session,
16
18
  "s" => :session_picker,
17
19
  "v" => :view_mode,
@@ -21,7 +23,8 @@ module TUI
21
23
  MENU_LABELS = (COMMAND_KEYS.map { |key, action| "[#{key}] #{action.to_s.tr("_", " ").capitalize}" } +
22
24
  ["[\u2191] Scroll chat", "[\u2193] Return to input"]).freeze
23
25
 
24
- SIDEBAR_WIDTH = 28
26
+ # HUD occupies 1/3 of screen width, clamped to a usable minimum.
27
+ HUD_MIN_WIDTH = 24
25
28
 
26
29
  # Picker entry prefix width: "[N]" (3) + marker (1) + space (1) = 5
27
30
  PICKER_PREFIX_WIDTH = 5
@@ -77,14 +80,20 @@ module TUI
77
80
 
78
81
  attr_reader :current_screen, :command_mode, :session_picker_active,
79
82
  :view_mode_picker_active
83
+ # @return [Boolean] true when the HUD info panel is visible
84
+ attr_reader :hud_visible
80
85
  # @return [Boolean] true when the token setup popup overlay is visible
81
86
  attr_reader :token_setup_active
82
87
  # @return [Boolean] true when graceful shutdown has been requested via signal
83
88
  attr_reader :shutdown_requested
89
+ # @return [TUI::PerformanceLogger] frame timing logger (no-op when debug is off)
90
+ attr_reader :perf_logger
84
91
 
85
92
  # @param cable_client [TUI::CableClient] WebSocket client connected to the brain
86
- def initialize(cable_client:)
93
+ # @param debug [Boolean] enable performance logging to log/tui_performance.log
94
+ def initialize(cable_client:, debug: false)
87
95
  @cable_client = cable_client
96
+ @perf_logger = PerformanceLogger.new(enabled: debug)
88
97
  @current_screen = :chat
89
98
  @command_mode = false
90
99
  @session_picker_active = false
@@ -92,17 +101,19 @@ module TUI
92
101
  @session_picker_page = 0
93
102
  @session_picker_mode = :root
94
103
  @session_picker_parent_id = nil
104
+ @hud_visible = true
95
105
  @view_mode_picker_active = false
96
106
  @view_mode_picker_index = 0
97
107
  @token_setup_active = false
98
108
  @token_input_buffer = InputBuffer.new
99
109
  @token_setup_error = nil
110
+ @token_setup_warning = nil
100
111
  @token_setup_status = :idle
101
112
  @shutdown_requested = false
102
113
  @previous_signal_handlers = {}
103
114
  @watchdog_thread = nil
104
115
  @screens = {
105
- chat: Screens::Chat.new(cable_client: cable_client)
116
+ chat: Screens::Chat.new(cable_client: cable_client, perf_logger: @perf_logger)
106
117
  }
107
118
  end
108
119
 
@@ -130,20 +141,31 @@ module TUI
130
141
  private
131
142
 
132
143
  def render(frame, tui)
133
- content_area, sidebar = tui.split(
134
- frame.area,
135
- direction: :horizontal,
136
- constraints: [
137
- tui.constraint_fill(1),
138
- tui.constraint_length(SIDEBAR_WIDTH)
139
- ]
140
- )
144
+ @perf_logger.start_frame
145
+
146
+ @screens[:chat].hud_hint = !@hud_visible
147
+
148
+ if @hud_visible
149
+ hud_width = [frame.area.width / 3, HUD_MIN_WIDTH].max
150
+ content_area, sidebar = tui.split(
151
+ frame.area,
152
+ direction: :horizontal,
153
+ constraints: [
154
+ tui.constraint_fill(1),
155
+ tui.constraint_length(hud_width)
156
+ ]
157
+ )
141
158
 
142
- @screens[@current_screen].render(frame, content_area, tui)
143
- render_sidebar(frame, sidebar, tui)
159
+ @perf_logger.measure(:chat_render) { @screens[@current_screen].render(frame, content_area, tui) }
160
+ @perf_logger.measure(:sidebar) { render_sidebar(frame, sidebar, tui) }
161
+ else
162
+ @perf_logger.measure(:chat_render) { @screens[@current_screen].render(frame, frame.area, tui) }
163
+ end
144
164
 
145
165
  check_token_setup_signals
146
166
  render_token_setup_popup(frame, frame.area, tui) if @token_setup_active
167
+
168
+ @perf_logger.end_frame
147
169
  end
148
170
 
149
171
  def render_sidebar(frame, area, tui)
@@ -171,10 +193,73 @@ module TUI
171
193
  frame.render_widget(menu, area)
172
194
  end
173
195
 
196
+ # HUD status icons for goal progress and sub-agent activity.
197
+ GOAL_ICON_ACTIVE = "\u25CF" # ●
198
+ GOAL_ICON_IN_PROGRESS = "\u25D0" # ◐
199
+ GOAL_ICON_COMPLETED = "\u2713" # ✓
200
+ CHILD_ICON_RUNNING = "\u25CF" # ●
201
+ CHILD_ICON_IDLE = "\u25CC" # ◌
202
+
174
203
  def render_info(frame, area, tui)
175
204
  session = @screens[:chat].session_info
176
- view_mode = @screens[:chat].view_mode
177
205
 
206
+ # Split into main content area and bottom status bar
207
+ main_area, status_area = tui.split(
208
+ area,
209
+ direction: :vertical,
210
+ constraints: [
211
+ tui.constraint_fill(1),
212
+ tui.constraint_length(3)
213
+ ]
214
+ )
215
+
216
+ render_hud_content(frame, main_area, tui, session)
217
+ render_hud_status_bar(frame, status_area, tui)
218
+ end
219
+
220
+ # Renders the main HUD content: session name, goals, skills,
221
+ # workflow, and sub-agents.
222
+ def render_hud_content(frame, area, tui, session)
223
+ session_label = session[:name] || "##{session[:id]}"
224
+
225
+ lines = [
226
+ tui.line(spans: [
227
+ tui.span(content: "\u{1F4CB} ", style: tui.style(fg: "dark_gray")),
228
+ tui.span(content: session_label, style: tui.style(fg: "cyan", modifiers: [:bold]))
229
+ ]),
230
+ hud_goals_section(tui, session),
231
+ hud_skills_line(tui, session),
232
+ hud_workflow_line(tui, session),
233
+ hud_children_section(tui, session),
234
+ interaction_state_line(tui)
235
+ ].flatten.compact
236
+
237
+ content = tui.paragraph(
238
+ text: lines,
239
+ wrap: true,
240
+ block: tui.block(
241
+ borders: [:left, :top, :right],
242
+ border_type: :rounded,
243
+ border_style: {fg: "white"}
244
+ )
245
+ )
246
+ frame.render_widget(content, area)
247
+ end
248
+
249
+ # Renders the bottom status bar: connection state and model name.
250
+ def render_hud_status_bar(frame, area, tui)
251
+ cable_status = @cable_client.status
252
+ style = STATUS_STYLES.fetch(cable_status, STATUS_STYLES[:disconnected])
253
+
254
+ status_label = if cable_status == :reconnecting
255
+ attempt = @cable_client.reconnect_attempt
256
+ max = CableClient::MAX_RECONNECT_ATTEMPTS
257
+ "#{style[:label]} (#{attempt}/#{max})"
258
+ else
259
+ style[:label]
260
+ end
261
+
262
+ view_mode = @screens[:chat].view_mode
178
263
  mode_label = view_mode.capitalize
179
264
  mode_color = case view_mode
180
265
  when "verbose" then "yellow"
@@ -182,110 +267,133 @@ module TUI
182
267
  else "cyan"
183
268
  end
184
269
 
185
- session_label = session[:name] || "##{session[:id]}"
186
-
187
- lines = [
188
- tui.line(spans: [
189
- tui.span(content: "Anima v#{Anima::VERSION}", style: tui.style(fg: "white"))
190
- ]),
191
- tui.line(spans: [tui.span(content: "")]),
192
- if session[:name]
193
- tui.line(spans: [
194
- tui.span(content: session_label, style: tui.style(fg: "cyan", modifiers: [:bold]))
195
- ])
196
- else
270
+ bar = tui.paragraph(
271
+ text: [
197
272
  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]))
273
+ tui.span(content: status_label, style: tui.style(fg: style[:color], modifiers: [:bold])),
274
+ tui.span(content: " \u2502 ", style: tui.style(fg: "dark_gray")),
275
+ tui.span(content: mode_label, style: tui.style(fg: mode_color, modifiers: [:bold]))
200
276
  ])
201
- end,
202
- tui.line(spans: [
203
- tui.span(content: "Messages ", style: tui.style(fg: "dark_gray")),
204
- tui.span(content: session[:message_count].to_s, style: tui.style(fg: "cyan"))
205
- ]),
206
- active_skills_line(tui, session),
207
- active_workflow_line(tui, session),
208
- goals_line(tui, session),
209
- tui.line(spans: [tui.span(content: "")]),
210
- tui.line(spans: [
211
- tui.span(content: "Mode ", style: tui.style(fg: "dark_gray")),
212
- tui.span(content: mode_label, style: tui.style(fg: mode_color, modifiers: [:bold]))
213
- ]),
214
- interaction_state_line(tui),
215
- tui.line(spans: [tui.span(content: "")]),
216
- connection_status_line(tui),
217
- tui.line(spans: [tui.span(content: "")]),
218
- tui.line(spans: [
219
- tui.span(content: "Ctrl+a", style: tui.style(fg: "cyan", modifiers: [:bold])),
220
- tui.span(content: " command mode", style: tui.style(fg: "dark_gray"))
221
- ])
222
- ].compact
223
-
224
- info = tui.paragraph(
225
- text: lines,
277
+ ],
226
278
  block: tui.block(
227
- title: "Info",
228
- borders: [:all],
279
+ borders: [:left, :bottom, :right],
229
280
  border_type: :rounded,
230
281
  border_style: {fg: "white"}
231
282
  )
232
283
  )
233
- frame.render_widget(info, area)
284
+ frame.render_widget(bar, area)
285
+ end
286
+
287
+ # Builds goal lines with status icons and descriptions.
288
+ # Root goals show as individual lines. A root goal with some completed
289
+ # sub-goals shows the ◐ (in-progress) icon.
290
+ #
291
+ # @return [Array<RatatuiRuby::Widgets::Line>, nil]
292
+ def hud_goals_section(tui, session)
293
+ goal_list = session[:goals]
294
+ return if goal_list.nil? || goal_list.empty?
295
+
296
+ lines = [
297
+ tui.line(spans: [tui.span(content: "")]),
298
+ tui.line(spans: [
299
+ tui.span(content: "\u{1F3AF} Goals", style: tui.style(fg: "dark_gray"))
300
+ ])
301
+ ]
302
+
303
+ goal_list.each do |goal|
304
+ icon, color = goal_icon_and_color(goal)
305
+ lines << tui.line(spans: [
306
+ tui.span(content: " #{icon} ", style: tui.style(fg: color)),
307
+ tui.span(content: goal["description"].to_s, style: tui.style(fg: "white"))
308
+ ])
309
+ end
310
+
311
+ lines
234
312
  end
235
313
 
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)
314
+ # Returns the status icon and color for a goal.
315
+ # Active goals with some completed sub-goals show as in-progress (◐).
316
+ #
317
+ # @param goal [Hash] goal data with "status" and optional "sub_goals" keys
318
+ # @return [Array(String, String)] icon and color pair
319
+ def goal_icon_and_color(goal)
320
+ if goal["status"] == "completed"
321
+ [GOAL_ICON_COMPLETED, "green"]
322
+ elsif goal["sub_goals"]&.any? { |sg| sg["status"] == "completed" }
323
+ [GOAL_ICON_IN_PROGRESS, "yellow"]
324
+ else
325
+ [GOAL_ICON_ACTIVE, "cyan"]
326
+ end
327
+ end
328
+
329
+ # Builds the skills line with brain emoji.
330
+ # @return [Array<RatatuiRuby::Widgets::Line>, nil]
331
+ def hud_skills_line(tui, session)
242
332
  skills = session[:active_skills]
243
333
  return if skills.nil? || skills.empty?
244
334
 
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
- ])
335
+ [
336
+ tui.line(spans: [tui.span(content: "")]),
337
+ tui.line(spans: [
338
+ tui.span(content: "\u{1F9E0} ", style: tui.style(fg: "dark_gray")),
339
+ tui.span(content: skills.join(", "), style: tui.style(fg: "yellow"))
340
+ ])
341
+ ]
250
342
  end
251
343
 
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)
344
+ # Builds the workflow line with scroll emoji.
345
+ # @return [Array<RatatuiRuby::Widgets::Line>, nil]
346
+ def hud_workflow_line(tui, session)
258
347
  workflow = session[:active_workflow]
259
348
  return if workflow.nil? || workflow.empty?
260
349
 
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
- ])
350
+ [
351
+ tui.line(spans: [tui.span(content: "")]),
352
+ tui.line(spans: [
353
+ tui.span(content: "\u{1F4DC} ", style: tui.style(fg: "dark_gray")),
354
+ tui.span(content: workflow, style: tui.style(fg: "magenta"))
355
+ ])
356
+ ]
265
357
  end
266
358
 
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?
359
+ # Builds the sub-agents section with activity indicators.
360
+ # @return [Array<RatatuiRuby::Widgets::Line>, nil]
361
+ def hud_children_section(tui, session)
362
+ children = session[:children]
363
+ return if children.nil? || children.empty?
276
364
 
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
- ])
365
+ lines = [
366
+ tui.line(spans: [tui.span(content: "")]),
367
+ tui.line(spans: [
368
+ tui.span(content: "\u{1F465} Sub-agents", style: tui.style(fg: "dark_gray"))
369
+ ])
370
+ ]
371
+
372
+ children.each do |child|
373
+ icon, color = child_icon_and_color(child)
374
+ name = child["name"] || "sub-agent"
375
+ lines << tui.line(spans: [
376
+ tui.span(content: " #{icon} ", style: tui.style(fg: color)),
377
+ tui.span(content: "@#{name}", style: tui.style(fg: "white"))
378
+ ])
379
+ end
380
+
381
+ lines
382
+ end
383
+
384
+ # Returns the activity icon and color for a child session.
385
+ #
386
+ # @param child [Hash] child session data with "processing" key
387
+ # @return [Array(String, String)] icon and color pair
388
+ def child_icon_and_color(child)
389
+ if child["processing"]
390
+ [CHILD_ICON_RUNNING, "yellow"]
391
+ else
392
+ [CHILD_ICON_IDLE, "green"]
393
+ end
285
394
  end
286
395
 
287
- # Builds the interaction state line for the info panel.
288
- # Shows "Scrolling" when chat pane is focused, or "Thinking..." during LLM processing.
396
+ # Shows "Scrolling" when chat pane is focused, "Thinking..." during LLM processing.
289
397
  def interaction_state_line(tui)
290
398
  if @screens[:chat].chat_focused
291
399
  tui.line(spans: [
@@ -295,33 +403,9 @@ module TUI
295
403
  tui.line(spans: [
296
404
  tui.span(content: "Thinking...", style: tui.style(fg: "magenta", modifiers: [:bold]))
297
405
  ])
298
- else
299
- tui.line(spans: [tui.span(content: "")])
300
406
  end
301
407
  end
302
408
 
303
- # Builds the connection status line for the info panel.
304
- # Shows a single emoji for the normal (subscribed) state; adds descriptive
305
- # text only when something requires attention.
306
- # @param tui [RatatuiRuby] TUI rendering context
307
- # @return [RatatuiRuby::Widgets::Line] styled status line with emoji indicator
308
- def connection_status_line(tui)
309
- cable_status = @cable_client.status
310
- style = STATUS_STYLES.fetch(cable_status, STATUS_STYLES[:disconnected])
311
-
312
- label = if cable_status == :reconnecting
313
- attempt = @cable_client.reconnect_attempt
314
- max = CableClient::MAX_RECONNECT_ATTEMPTS
315
- "#{style[:label]} (#{attempt}/#{max})"
316
- else
317
- style[:label]
318
- end
319
-
320
- tui.line(spans: [
321
- tui.span(content: label, style: tui.style(fg: style[:color], modifiers: [:bold]))
322
- ])
323
- end
324
-
325
409
  def chat_loading?
326
410
  @screens[:chat].loading?
327
411
  end
@@ -365,6 +449,9 @@ module TUI
365
449
  when :anthropic_token
366
450
  activate_token_setup
367
451
  nil
452
+ when :toggle_hud
453
+ @hud_visible = !@hud_visible
454
+ nil
368
455
  when :new_session
369
456
  @screens[:chat].new_session
370
457
  @current_screen = :chat
@@ -391,9 +478,15 @@ module TUI
391
478
  return nil
392
479
  end
393
480
 
481
+ # Escape key priority: unfocus chat > interrupt tools > clear input > parent session
394
482
  if event.esc?
395
- if @screens[:chat].chat_focused
396
- @screens[:chat].unfocus_chat
483
+ chat = @screens[:chat]
484
+ if chat.chat_focused
485
+ chat.unfocus_chat
486
+ elsif chat.loading? && chat.input.empty?
487
+ chat.interrupt_execution
488
+ elsif !chat.input.empty?
489
+ chat.clear_input
397
490
  else
398
491
  return_to_parent_session
399
492
  end
@@ -885,13 +978,14 @@ module TUI
885
978
  # -- Token setup popup -----------------------------------------------
886
979
 
887
980
  # Opens the token setup popup and resets all input state.
888
- # Can be triggered manually via Ctrl+a > a or automatically when the
981
+ # Can be triggered manually via C-a a or automatically when the
889
982
  # brain broadcasts authentication_required.
890
983
  # @return [void]
891
984
  def activate_token_setup
892
985
  @token_setup_active = true
893
986
  @token_input_buffer.clear
894
987
  @token_setup_error = nil
988
+ @token_setup_warning = nil
895
989
  @token_setup_status = :idle
896
990
  end
897
991
 
@@ -901,6 +995,7 @@ module TUI
901
995
  @token_setup_active = false
902
996
  @token_input_buffer.clear
903
997
  @token_setup_error = nil
998
+ @token_setup_warning = nil
904
999
  @token_setup_status = :idle
905
1000
  end
906
1001
 
@@ -927,9 +1022,11 @@ module TUI
927
1022
  if result[:success]
928
1023
  @token_setup_status = :success
929
1024
  @token_setup_error = nil
1025
+ @token_setup_warning = result[:warning]
930
1026
  else
931
1027
  @token_setup_status = :error
932
1028
  @token_setup_error = result[:message]
1029
+ @token_setup_warning = nil
933
1030
  end
934
1031
  end
935
1032
 
@@ -1081,9 +1178,15 @@ module TUI
1081
1178
  end
1082
1179
 
1083
1180
  if @token_setup_status == :success
1084
- lines << tui.line(spans: [
1085
- tui.span(content: "Token saved and validated!", style: tui.style(fg: "green", modifiers: [:bold]))
1086
- ])
1181
+ lines << if @token_setup_warning
1182
+ tui.line(spans: [
1183
+ tui.span(content: "Token saved (API unavailable, validation skipped)", style: tui.style(fg: "yellow", modifiers: [:bold]))
1184
+ ])
1185
+ else
1186
+ tui.line(spans: [
1187
+ tui.span(content: "Token saved and validated!", style: tui.style(fg: "green", modifiers: [:bold]))
1188
+ ])
1189
+ end
1087
1190
  lines << tui.line(spans: [tui.span(content: "")])
1088
1191
  end
1089
1192
 
@@ -1104,7 +1207,7 @@ module TUI
1104
1207
  def token_status_display
1105
1208
  case @token_setup_status
1106
1209
  when :success
1107
- ["Valid", "green"]
1210
+ @token_setup_warning ? ["Saved (unverified)", "yellow"] : ["Valid", "green"]
1108
1211
  when :validating
1109
1212
  ["Validating...", "yellow"]
1110
1213
  when :error
@@ -134,6 +134,14 @@ module TUI
134
134
  send_action("recall_pending", {"event_id" => event_id})
135
135
  end
136
136
 
137
+ # Requests interruption of the current tool execution. The server sets
138
+ # an interrupt flag that the LLM client checks between tool calls.
139
+ #
140
+ # @return [void]
141
+ def interrupt
142
+ send_action("interrupt_execution", {})
143
+ end
144
+
137
145
  # Sends an Anthropic subscription token to the brain for validation and storage.
138
146
  # The token flows directly from TUI input to encrypted credentials — never
139
147
  # enters the LLM context window.