anima-core 1.3.0 → 1.5.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 +23 -26
- data/README.md +118 -104
- data/agents/thoughts-analyzer.md +12 -7
- data/anima-core.gemspec +1 -0
- data/app/channels/session_channel.rb +38 -58
- data/app/decorators/agent_message_decorator.rb +7 -2
- data/app/decorators/message_decorator.rb +31 -100
- data/app/decorators/pending_from_melete_decorator.rb +36 -0
- data/app/decorators/pending_from_melete_goal_decorator.rb +13 -0
- data/app/decorators/pending_from_melete_skill_decorator.rb +19 -0
- data/app/decorators/pending_from_melete_workflow_decorator.rb +13 -0
- data/app/decorators/pending_from_mneme_decorator.rb +44 -0
- data/app/decorators/pending_message_decorator.rb +94 -0
- data/app/decorators/pending_subagent_decorator.rb +46 -0
- data/app/decorators/pending_tool_response_decorator.rb +51 -0
- data/app/decorators/pending_user_message_decorator.rb +22 -0
- data/app/decorators/system_message_decorator.rb +5 -0
- data/app/decorators/tool_call_decorator.rb +16 -5
- data/app/decorators/tool_response_decorator.rb +2 -2
- data/app/decorators/user_message_decorator.rb +7 -2
- data/app/jobs/count_tokens_job.rb +23 -0
- data/app/jobs/drain_job.rb +169 -0
- data/app/jobs/melete_enrichment_job/goal_change_listener.rb +52 -0
- data/app/jobs/melete_enrichment_job.rb +48 -0
- data/app/jobs/mneme_enrichment_job.rb +46 -0
- data/app/jobs/tool_execution_job.rb +87 -0
- data/app/models/concerns/token_estimation.rb +54 -0
- data/app/models/goal.rb +23 -11
- data/app/models/message.rb +46 -48
- data/app/models/pending_message.rb +407 -12
- data/app/models/pinned_message.rb +8 -3
- data/app/models/session.rb +660 -566
- data/app/models/snapshot.rb +11 -21
- data/bin/inspect-cassette +157 -0
- data/bin/release +212 -0
- data/bin/with-llms +20 -0
- data/config/application.rb +1 -0
- data/config/database.yml +1 -0
- data/config/initializers/event_subscribers.rb +71 -4
- data/config/initializers/inflections.rb +3 -1
- data/db/cable_structure.sql +9 -0
- data/db/migrate/20260330120000_add_source_to_pending_messages.rb +8 -0
- data/db/migrate/20260401180000_add_api_metrics_to_messages.rb +7 -0
- data/db/migrate/20260401210935_remove_recalled_message_ids_from_sessions.rb +5 -0
- data/db/migrate/20260403080031_add_initial_cwd_to_sessions.rb +5 -0
- data/db/migrate/20260407170803_remove_viewport_message_ids_from_sessions.rb +5 -0
- data/db/migrate/20260407180400_remove_mneme_snapshot_pointer_columns_from_sessions.rb +6 -0
- data/db/migrate/20260411120553_add_token_count_to_pinned_messages.rb +5 -0
- data/db/migrate/20260411172926_remove_active_skills_and_workflow_from_sessions.rb +6 -0
- data/db/migrate/20260412110625_replace_processing_with_aasm_state.rb +6 -0
- data/db/migrate/20260418150323_add_kind_and_message_type_to_pending_messages.rb +6 -0
- data/db/migrate/20260419120000_add_drain_fields_to_pending_messages.rb +7 -0
- data/db/migrate/20260419130000_drop_pending_messages_kind_default.rb +5 -0
- data/db/migrate/20260419140000_add_drain_indexes_to_pending_messages.rb +8 -0
- data/db/migrate/20260420100000_add_hud_visibility_to_sessions.rb +15 -0
- data/db/queue_structure.sql +61 -0
- data/db/structure.sql +133 -0
- data/lib/agents/registry.rb +1 -1
- data/lib/anima/cli.rb +41 -13
- data/lib/anima/installer.rb +13 -0
- data/lib/anima/settings.rb +16 -36
- data/lib/anima/version.rb +1 -1
- data/lib/events/authentication_required.rb +24 -0
- data/lib/events/bounce_back.rb +4 -4
- data/lib/events/eviction_completed.rb +28 -0
- data/lib/events/goal_created.rb +28 -0
- data/lib/events/goal_updated.rb +32 -0
- data/lib/events/llm_responded.rb +35 -0
- data/lib/events/message_created.rb +27 -0
- data/lib/events/message_updated.rb +25 -0
- data/lib/events/session_state_changed.rb +30 -0
- data/lib/events/skill_activated.rb +28 -0
- data/lib/events/start_melete.rb +36 -0
- data/lib/events/start_mneme.rb +33 -0
- data/lib/events/start_processing.rb +32 -0
- data/lib/events/subagent_evicted.rb +31 -0
- data/lib/events/subscribers/active_state_broadcaster.rb +27 -0
- data/lib/events/subscribers/authentication_broadcaster.rb +34 -0
- data/lib/events/subscribers/drain_kickoff.rb +20 -0
- data/lib/events/subscribers/eviction_broadcaster.rb +26 -0
- data/lib/events/subscribers/llm_response_handler.rb +111 -0
- data/lib/events/subscribers/melete_kickoff.rb +24 -0
- data/lib/events/subscribers/message_broadcaster.rb +34 -0
- data/lib/events/subscribers/mneme_kickoff.rb +24 -0
- data/lib/events/subscribers/mneme_scheduler.rb +21 -0
- data/lib/events/subscribers/persister.rb +8 -9
- data/lib/events/subscribers/session_state_broadcaster.rb +33 -0
- data/lib/events/subscribers/subagent_message_router.rb +28 -34
- data/lib/events/subscribers/subagent_visibility_broadcaster.rb +33 -0
- data/lib/events/subscribers/tool_response_creator.rb +33 -0
- data/lib/events/subscribers/transient_broadcaster.rb +1 -1
- data/lib/events/tool_executed.rb +34 -0
- data/lib/events/workflow_activated.rb +27 -0
- data/lib/llm/client.rb +46 -199
- data/lib/mcp/client_manager.rb +41 -46
- data/lib/mcp/stdio_transport.rb +9 -5
- data/lib/{analytical_brain → melete}/runner.rb +73 -68
- data/lib/{analytical_brain → melete}/tools/activate_skill.rb +3 -3
- data/lib/{analytical_brain → melete}/tools/assign_nickname.rb +3 -3
- data/lib/{analytical_brain → melete}/tools/everything_is_ready.rb +2 -2
- data/lib/{analytical_brain → melete}/tools/finish_goal.rb +6 -3
- data/lib/melete/tools/goal_messaging.rb +29 -0
- data/lib/{analytical_brain → melete}/tools/read_workflow.rb +4 -4
- data/lib/{analytical_brain → melete}/tools/rename_session.rb +3 -3
- data/lib/{analytical_brain → melete}/tools/set_goal.rb +6 -2
- data/lib/{analytical_brain → melete}/tools/update_goal.rb +9 -5
- data/lib/{analytical_brain.rb → melete.rb} +6 -3
- data/lib/mneme/base_runner.rb +121 -0
- data/lib/mneme/l2_runner.rb +14 -20
- data/lib/mneme/recall_runner.rb +132 -0
- data/lib/mneme/runner.rb +123 -165
- data/lib/mneme/search.rb +104 -62
- data/lib/mneme/tools/nothing_to_surface.rb +25 -0
- data/lib/mneme/tools/save_snapshot.rb +2 -10
- data/lib/mneme/tools/surface_memory.rb +89 -0
- data/lib/mneme.rb +11 -5
- data/lib/providers/anthropic.rb +112 -7
- data/lib/shell_session.rb +290 -432
- data/lib/skills/definition.rb +2 -2
- data/lib/skills/registry.rb +1 -1
- data/lib/tools/base.rb +16 -1
- data/lib/tools/bash.rb +25 -55
- data/lib/tools/edit.rb +2 -0
- data/lib/tools/mark_goal_completed.rb +4 -5
- data/lib/tools/read.rb +2 -0
- data/lib/tools/registry.rb +85 -4
- data/lib/tools/response_truncator.rb +1 -1
- data/lib/tools/{recall.rb → search_messages.rb} +19 -21
- data/lib/tools/spawn_specialist.rb +22 -14
- data/lib/tools/spawn_subagent.rb +30 -20
- data/lib/tools/subagent_prompts.rb +17 -19
- data/lib/tools/think.rb +1 -1
- data/lib/tools/{remember.rb → view_messages.rb} +10 -10
- data/lib/tools/write.rb +2 -0
- data/lib/tui/app.rb +393 -149
- data/lib/tui/braille_spinner.rb +7 -7
- data/lib/tui/cable_client.rb +9 -16
- data/lib/tui/decorators/base_decorator.rb +47 -6
- data/lib/tui/decorators/bash_decorator.rb +1 -1
- data/lib/tui/decorators/edit_decorator.rb +4 -2
- data/lib/tui/decorators/read_decorator.rb +4 -2
- data/lib/tui/decorators/think_decorator.rb +2 -2
- data/lib/tui/decorators/web_get_decorator.rb +1 -1
- data/lib/tui/decorators/write_decorator.rb +4 -2
- data/lib/tui/flash.rb +19 -14
- data/lib/tui/formatting.rb +20 -9
- data/lib/tui/input_buffer.rb +6 -6
- data/lib/tui/message_store.rb +165 -28
- data/lib/tui/performance_logger.rb +2 -3
- data/lib/tui/screens/chat.rb +149 -79
- data/lib/tui/settings.rb +93 -0
- data/lib/workflows/definition.rb +3 -3
- data/lib/workflows/registry.rb +1 -1
- data/skills/github.md +38 -0
- data/templates/config.toml +16 -32
- data/templates/tui.toml +209 -0
- data/workflows/review_pr.md +18 -14
- metadata +98 -29
- data/app/jobs/agent_request_job.rb +0 -199
- data/app/jobs/analytical_brain_job.rb +0 -33
- data/app/jobs/count_message_tokens_job.rb +0 -39
- data/app/jobs/passive_recall_job.rb +0 -29
- data/app/models/concerns/message/broadcasting.rb +0 -85
- data/config/initializers/fts5_schema_dump.rb +0 -21
- data/lib/agent_loop.rb +0 -186
- data/lib/analytical_brain/tools/deactivate_skill.rb +0 -39
- data/lib/analytical_brain/tools/deactivate_workflow.rb +0 -34
- data/lib/environment_probe.rb +0 -232
- data/lib/events/agent_message.rb +0 -11
- data/lib/events/subscribers/message_collector.rb +0 -64
- data/lib/events/tool_call.rb +0 -31
- data/lib/events/tool_response.rb +0 -33
- data/lib/mneme/compressed_viewport.rb +0 -200
- data/lib/mneme/passive_recall.rb +0 -69
data/lib/tui/screens/chat.rb
CHANGED
|
@@ -20,30 +20,23 @@ module TUI
|
|
|
20
20
|
class Chat
|
|
21
21
|
include Formatting
|
|
22
22
|
|
|
23
|
-
MIN_INPUT_HEIGHT = 3
|
|
24
23
|
PRINTABLE_CHAR = /\A[[:print:]]\z/
|
|
25
24
|
|
|
26
25
|
ROLE_USER = "user"
|
|
27
26
|
ROLE_ASSISTANT = "assistant"
|
|
28
27
|
|
|
29
|
-
SCROLL_STEP = 1
|
|
30
|
-
MOUSE_SCROLL_STEP = 2
|
|
31
|
-
|
|
32
28
|
TOOL_ICON = "\u{1F527}"
|
|
33
29
|
CHECKMARK = "\u2713"
|
|
34
30
|
|
|
35
|
-
# Viewport virtualization tuning
|
|
36
|
-
VIEWPORT_BACK_BUFFER = 3 # entries before scroll target for upward scroll margin
|
|
37
|
-
VIEWPORT_OVERFLOW_MULTIPLIER = 2 # build this many viewports worth of lines
|
|
38
|
-
VIEWPORT_BOTTOM_THRESHOLD = 10 # entries from end before we include all trailing
|
|
39
|
-
|
|
40
31
|
# Background-highlighted styles for conversation roles.
|
|
41
32
|
# Dark tinted backgrounds make user/assistant messages easy to scan.
|
|
42
|
-
#
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
33
|
+
# Colors configured via [theme] user_message_bg / assistant_message_bg.
|
|
34
|
+
def self.role_styles
|
|
35
|
+
{
|
|
36
|
+
"user" => {fg: Settings.theme_color_text, bg: Settings.theme_user_message_bg, modifiers: [:bold]},
|
|
37
|
+
"assistant" => {fg: Settings.theme_color_text, bg: Settings.theme_assistant_message_bg, modifiers: [:bold]}
|
|
38
|
+
}
|
|
39
|
+
end
|
|
47
40
|
|
|
48
41
|
# Intentionally duplicated from Session::VIEW_MODES to keep the TUI
|
|
49
42
|
# independent of Rails. Must stay in sync when adding new modes.
|
|
@@ -268,8 +261,8 @@ module TUI
|
|
|
268
261
|
# @return [String]
|
|
269
262
|
def spinner_label
|
|
270
263
|
case @session_state
|
|
271
|
-
when "
|
|
272
|
-
when "
|
|
264
|
+
when "awaiting" then "Thinking..."
|
|
265
|
+
when "executing" then "Executing..."
|
|
273
266
|
when "interrupting" then "Stopping..."
|
|
274
267
|
else "Working..."
|
|
275
268
|
end
|
|
@@ -282,9 +275,9 @@ module TUI
|
|
|
282
275
|
# @return [String]
|
|
283
276
|
def spinner_color
|
|
284
277
|
case @session_state
|
|
285
|
-
when "
|
|
286
|
-
when "interrupting" then
|
|
287
|
-
else
|
|
278
|
+
when "awaiting", "executing" then Settings.theme_color_success
|
|
279
|
+
when "interrupting" then Settings.theme_color_error
|
|
280
|
+
else Settings.theme_color_muted
|
|
288
281
|
end
|
|
289
282
|
end
|
|
290
283
|
|
|
@@ -322,10 +315,12 @@ module TUI
|
|
|
322
315
|
handle_goals_updated(msg)
|
|
323
316
|
when "children_updated"
|
|
324
317
|
handle_children_updated(msg)
|
|
318
|
+
when "subagent_evicted"
|
|
319
|
+
handle_subagent_evicted(msg)
|
|
325
320
|
when "sessions_list"
|
|
326
321
|
@sessions_list = msg["sessions"]
|
|
327
322
|
when "pending_message_created"
|
|
328
|
-
@message_store.add_pending(msg["pending_message_id"], msg
|
|
323
|
+
@message_store.add_pending(msg["pending_message_id"], msg) if msg["pending_message_id"]
|
|
329
324
|
when "pending_message_removed"
|
|
330
325
|
@message_store.remove_pending(msg["pending_message_id"]) if msg["pending_message_id"]
|
|
331
326
|
when "authentication_required"
|
|
@@ -363,20 +358,20 @@ module TUI
|
|
|
363
358
|
end
|
|
364
359
|
end
|
|
365
360
|
|
|
366
|
-
|
|
361
|
+
handle_eviction(msg) if msg["action"] == "eviction"
|
|
367
362
|
end
|
|
368
363
|
end
|
|
369
364
|
|
|
370
|
-
# Removes messages
|
|
371
|
-
#
|
|
372
|
-
#
|
|
365
|
+
# Removes messages above the eviction cutoff from the message store.
|
|
366
|
+
# Triggered by {Events::EvictionCompleted} when Mneme advances the
|
|
367
|
+
# boundary past an eviction zone.
|
|
373
368
|
#
|
|
374
|
-
# @param msg [Hash] incoming WebSocket message
|
|
375
|
-
def
|
|
376
|
-
|
|
377
|
-
return unless
|
|
369
|
+
# @param msg [Hash] incoming WebSocket message with "evict_above_id"
|
|
370
|
+
def handle_eviction(msg)
|
|
371
|
+
cutoff = msg["evict_above_id"]
|
|
372
|
+
return unless cutoff
|
|
378
373
|
|
|
379
|
-
@message_store.
|
|
374
|
+
@message_store.remove_above(cutoff)
|
|
380
375
|
end
|
|
381
376
|
|
|
382
377
|
# Renders flash messages as colored bars inside the chat frame,
|
|
@@ -456,7 +451,7 @@ module TUI
|
|
|
456
451
|
@session_info[:name] = msg["name"]
|
|
457
452
|
end
|
|
458
453
|
|
|
459
|
-
# Updates the active skills list when
|
|
454
|
+
# Updates the active skills list when Melete activates or
|
|
460
455
|
# deactivates skills. Only applies to the current session.
|
|
461
456
|
def handle_active_skills_updated(msg)
|
|
462
457
|
return unless msg["session_id"] == @session_info[:id]
|
|
@@ -464,7 +459,7 @@ module TUI
|
|
|
464
459
|
@session_info[:active_skills] = msg["active_skills"] || []
|
|
465
460
|
end
|
|
466
461
|
|
|
467
|
-
# Updates the active workflow when
|
|
462
|
+
# Updates the active workflow when Melete activates or
|
|
468
463
|
# deactivates a workflow. Only applies to the current session.
|
|
469
464
|
def handle_active_workflow_updated(msg)
|
|
470
465
|
return unless msg["session_id"] == @session_info[:id]
|
|
@@ -472,7 +467,7 @@ module TUI
|
|
|
472
467
|
@session_info[:active_workflow] = msg["active_workflow"]
|
|
473
468
|
end
|
|
474
469
|
|
|
475
|
-
# Updates the goals list when
|
|
470
|
+
# Updates the goals list when Melete creates or
|
|
476
471
|
# completes goals. Only applies to the current session.
|
|
477
472
|
def handle_goals_updated(msg)
|
|
478
473
|
return unless msg["session_id"] == @session_info[:id]
|
|
@@ -488,6 +483,20 @@ module TUI
|
|
|
488
483
|
@session_info[:children] = msg["children"] || []
|
|
489
484
|
end
|
|
490
485
|
|
|
486
|
+
# Removes a sub-agent from the HUD panel when viewport eviction has
|
|
487
|
+
# taken its last trace past the Mneme boundary. Only applies to the
|
|
488
|
+
# current session — the brain emits this only on the parent's stream.
|
|
489
|
+
#
|
|
490
|
+
# @param msg [Hash] ActionCable payload with "session_id" and "child_id" keys
|
|
491
|
+
def handle_subagent_evicted(msg)
|
|
492
|
+
return unless msg["session_id"] == @session_info[:id]
|
|
493
|
+
|
|
494
|
+
child_id = msg["child_id"]
|
|
495
|
+
return unless child_id
|
|
496
|
+
|
|
497
|
+
@session_info[:children] = @session_info[:children]&.reject { |child| child["id"] == child_id }
|
|
498
|
+
end
|
|
499
|
+
|
|
491
500
|
# Handles explicit session state transitions from the server.
|
|
492
501
|
# Drives the braille spinner animation. Only processes broadcasts
|
|
493
502
|
# matching the current session.
|
|
@@ -510,14 +519,14 @@ module TUI
|
|
|
510
519
|
child_id = msg["child_id"]
|
|
511
520
|
return unless child_id
|
|
512
521
|
|
|
513
|
-
|
|
514
|
-
|
|
522
|
+
entry = @session_info[:children]&.find { |child| child["id"] == child_id }
|
|
523
|
+
entry["session_state"] = msg["state"] if entry
|
|
515
524
|
end
|
|
516
525
|
|
|
517
526
|
# Updates the session state and synchronizes the spinner.
|
|
518
527
|
#
|
|
519
|
-
# @param state [String] one of "idle", "
|
|
520
|
-
# "
|
|
528
|
+
# @param state [String] one of "idle", "awaiting",
|
|
529
|
+
# "executing", "interrupting"
|
|
521
530
|
def update_session_state(state)
|
|
522
531
|
@session_state = state
|
|
523
532
|
@spinner.state = state
|
|
@@ -598,7 +607,7 @@ module TUI
|
|
|
598
607
|
|
|
599
608
|
# Phase 5: Paragraph widget + wrapped line count
|
|
600
609
|
base_widget = @perf_logger.measure(:paragraph) {
|
|
601
|
-
tui.paragraph(text: lines, wrap: true, style: tui.style(fg:
|
|
610
|
+
tui.paragraph(text: lines, wrap: true, style: tui.style(fg: Settings.theme_color_text))
|
|
602
611
|
}
|
|
603
612
|
wrapped_height = @perf_logger.measure(:line_count) {
|
|
604
613
|
cached_viewport_line_count(base_widget, inner_width, version)
|
|
@@ -633,9 +642,9 @@ module TUI
|
|
|
633
642
|
content_length: @max_scroll,
|
|
634
643
|
position: @scroll_offset,
|
|
635
644
|
orientation: :vertical_right,
|
|
636
|
-
thumb_style: {fg:
|
|
645
|
+
thumb_style: {fg: Settings.theme_scrollbar_thumb},
|
|
637
646
|
track_symbol: "\u2502",
|
|
638
|
-
track_style: {fg:
|
|
647
|
+
track_style: {fg: Settings.theme_scrollbar_track}
|
|
639
648
|
)
|
|
640
649
|
frame.render_widget(scrollbar, area)
|
|
641
650
|
end
|
|
@@ -678,15 +687,15 @@ module TUI
|
|
|
678
687
|
[spinner_line(tui)]
|
|
679
688
|
elsif @session_loading
|
|
680
689
|
[tui.line(spans: [
|
|
681
|
-
tui.span(content: "Loading session\u2026", style: tui.style(fg:
|
|
690
|
+
tui.span(content: "Loading session\u2026", style: tui.style(fg: Settings.theme_color_warning))
|
|
682
691
|
])]
|
|
683
692
|
else
|
|
684
693
|
[tui.line(spans: [
|
|
685
|
-
tui.span(content: "Type a message to start chatting.", style: tui.style(fg:
|
|
694
|
+
tui.span(content: "Type a message to start chatting.", style: tui.style(fg: Settings.theme_color_muted))
|
|
686
695
|
])]
|
|
687
696
|
end
|
|
688
697
|
|
|
689
|
-
widget = tui.paragraph(text: lines, wrap: true, style: tui.style(fg:
|
|
698
|
+
widget = tui.paragraph(text: lines, wrap: true, style: tui.style(fg: Settings.theme_color_text))
|
|
690
699
|
.with(scroll: [0, 0], block: tui.block(**chat_block_config))
|
|
691
700
|
frame.render_widget(widget, area)
|
|
692
701
|
@max_scroll = 0
|
|
@@ -729,12 +738,12 @@ module TUI
|
|
|
729
738
|
entry_count = entries.size
|
|
730
739
|
|
|
731
740
|
# Start a few entries before the scroll target for upward buffer
|
|
732
|
-
buf_first = [first_visible_est -
|
|
741
|
+
buf_first = [first_visible_est - Settings.chat_viewport_back_buffer, 0].max
|
|
733
742
|
|
|
734
743
|
# Build forward until we've accumulated enough lines to fill the
|
|
735
744
|
# viewport with margin. Pre-wrap count is a lower bound on visual
|
|
736
745
|
# height (wrapping only adds lines), so 2x guarantees coverage.
|
|
737
|
-
target = @visible_height *
|
|
746
|
+
target = @visible_height * Settings.chat_viewport_overflow_multiplier
|
|
738
747
|
lines = []
|
|
739
748
|
pre_wrap_count = 0
|
|
740
749
|
buf_last = buf_first
|
|
@@ -748,7 +757,7 @@ module TUI
|
|
|
748
757
|
# the bottom. Near the bottom, always include trailing entries
|
|
749
758
|
# so the viewport covers the actual end of content — otherwise
|
|
750
759
|
# the last entries become unreachable.
|
|
751
|
-
break if pre_wrap_count >= target && entry_count - idx >
|
|
760
|
+
break if pre_wrap_count >= target && entry_count - idx > Settings.chat_viewport_bottom_threshold
|
|
752
761
|
end
|
|
753
762
|
|
|
754
763
|
@perf_logger.info(
|
|
@@ -815,6 +824,10 @@ module TUI
|
|
|
815
824
|
lines = estimate_text_height(text, effective_width)
|
|
816
825
|
lines += 1 # header/label line
|
|
817
826
|
lines += 1 unless entry[:message_type] == "tool_call" # separator
|
|
827
|
+
if data["tools"].is_a?(Array) && data["tools"].any?
|
|
828
|
+
lines += 2 # blank line + "## Tools (N)" header
|
|
829
|
+
lines += estimate_text_height(tools_toon(data), effective_width)
|
|
830
|
+
end
|
|
818
831
|
lines
|
|
819
832
|
when :message
|
|
820
833
|
lines = estimate_text_height(entry[:content].to_s, effective_width)
|
|
@@ -853,7 +866,7 @@ module TUI
|
|
|
853
866
|
title: "Chat",
|
|
854
867
|
borders: [:all],
|
|
855
868
|
border_type: :rounded,
|
|
856
|
-
border_style: @chat_focused ? {fg:
|
|
869
|
+
border_style: @chat_focused ? {fg: Settings.theme_border_focused} : {fg: Settings.theme_color_info}
|
|
857
870
|
}
|
|
858
871
|
if @chat_focused
|
|
859
872
|
config[:titles] = [
|
|
@@ -873,7 +886,7 @@ module TUI
|
|
|
873
886
|
responses = counter[:responses]
|
|
874
887
|
complete = calls == responses
|
|
875
888
|
label = "#{TOOL_ICON} Tools: #{calls}/#{responses}#{" #{CHECKMARK}" if complete}"
|
|
876
|
-
color = complete ?
|
|
889
|
+
color = complete ? Settings.theme_color_success : Settings.theme_color_warning
|
|
877
890
|
[
|
|
878
891
|
tui.line(spans: [tui.span(content: label, style: tui.style(fg: color))]),
|
|
879
892
|
tui.line(spans: [tui.span(content: "")])
|
|
@@ -900,8 +913,14 @@ module TUI
|
|
|
900
913
|
render_system_entry(tui, data)
|
|
901
914
|
when "system_prompt"
|
|
902
915
|
render_system_prompt_entry(tui, data)
|
|
916
|
+
when "pending_subagent"
|
|
917
|
+
render_pending_subagent_entry(tui, data)
|
|
918
|
+
when "pending_melete"
|
|
919
|
+
render_pending_melete_entry(tui, data)
|
|
920
|
+
when "pending_mneme"
|
|
921
|
+
render_pending_mneme_entry(tui, data)
|
|
903
922
|
else
|
|
904
|
-
[tui.line(spans: [tui.span(content: data["content"].to_s, style: tui.style(fg:
|
|
923
|
+
[tui.line(spans: [tui.span(content: data["content"].to_s, style: tui.style(fg: Settings.theme_color_text))])]
|
|
905
924
|
end
|
|
906
925
|
|
|
907
926
|
# Tool calls and their responses are visually one unit — no separator
|
|
@@ -935,9 +954,9 @@ module TUI
|
|
|
935
954
|
label = role_label(role)
|
|
936
955
|
|
|
937
956
|
if pending
|
|
938
|
-
style = tui.style(fg:
|
|
957
|
+
style = tui.style(fg: Settings.theme_color_muted)
|
|
939
958
|
else
|
|
940
|
-
role_cfg =
|
|
959
|
+
role_cfg = self.class.role_styles.fetch(role, {fg: Settings.theme_color_text})
|
|
941
960
|
style = tui.style(**role_cfg)
|
|
942
961
|
end
|
|
943
962
|
|
|
@@ -949,7 +968,7 @@ module TUI
|
|
|
949
968
|
|
|
950
969
|
first_spans = if tokens && !pending
|
|
951
970
|
tok_style = {fg: token_count_color(tokens)}
|
|
952
|
-
role_bg =
|
|
971
|
+
role_bg = self.class.role_styles.dig(role, :bg)
|
|
953
972
|
tok_style[:bg] = role_bg if role_bg
|
|
954
973
|
[
|
|
955
974
|
tui.span(content: ts_prefix, style: style),
|
|
@@ -962,7 +981,7 @@ module TUI
|
|
|
962
981
|
end
|
|
963
982
|
|
|
964
983
|
lines = [tui.line(spans: first_spans)]
|
|
965
|
-
content_lines.drop(1).each { |line| lines << tui.line(spans: [tui.span(content: line, style: style)]) }
|
|
984
|
+
content_lines.drop(1).each { |line| lines << tui.line(spans: [tui.span(content: preserve_indentation(line), style: style)]) }
|
|
966
985
|
lines
|
|
967
986
|
end
|
|
968
987
|
|
|
@@ -973,11 +992,63 @@ module TUI
|
|
|
973
992
|
def render_system_entry(tui, data)
|
|
974
993
|
ts = data["timestamp"]
|
|
975
994
|
header = ts ? "[#{format_ns_timestamp(ts)}] [system]" : "[system]"
|
|
976
|
-
style = tui.style(fg:
|
|
995
|
+
style = tui.style(fg: Settings.theme_color_text)
|
|
977
996
|
|
|
978
997
|
content_lines = data["content"].to_s.split("\n", -1)
|
|
979
998
|
lines = [tui.line(spans: [tui.span(content: "#{header} #{content_lines.first}", style: style)])]
|
|
980
|
-
content_lines.drop(1).each { |line| lines << tui.line(spans: [tui.span(content: " #{line}", style: style)]) }
|
|
999
|
+
content_lines.drop(1).each { |line| lines << tui.line(spans: [tui.span(content: preserve_indentation(" #{line}"), style: style)]) }
|
|
1000
|
+
lines
|
|
1001
|
+
end
|
|
1002
|
+
|
|
1003
|
+
# Renders a pending sub-agent delivery — a sub-agent's reply that
|
|
1004
|
+
# landed on the parent's mailbox and is queued for promotion. Shows
|
|
1005
|
+
# a +[from <nickname>]+ badge in the muted color so it reads as
|
|
1006
|
+
# in-flight, distinct from a real conversation message.
|
|
1007
|
+
# @param tui [RatatuiRuby] TUI rendering API
|
|
1008
|
+
# @param data [Hash] structured data with "source", "content"
|
|
1009
|
+
# @return [Array<RatatuiRuby::Widgets::Line>]
|
|
1010
|
+
def render_pending_subagent_entry(tui, data)
|
|
1011
|
+
render_pending_badge_entry(tui, data, badge: "from #{data["source"]}")
|
|
1012
|
+
end
|
|
1013
|
+
|
|
1014
|
+
# Renders a pending Mneme recall — a memory Mneme will inject as a
|
|
1015
|
+
# phantom tool pair on the next active drain. Visible from verbose
|
|
1016
|
+
# so the user can see what context is about to enter the LLM turn.
|
|
1017
|
+
# @param tui [RatatuiRuby] TUI rendering API
|
|
1018
|
+
# @param data [Hash] structured data with "content"
|
|
1019
|
+
# @return [Array<RatatuiRuby::Widgets::Line>]
|
|
1020
|
+
def render_pending_mneme_entry(tui, data)
|
|
1021
|
+
render_pending_badge_entry(tui, data, badge: "Mneme recall")
|
|
1022
|
+
end
|
|
1023
|
+
|
|
1024
|
+
# Renders a pending Melete activation (skill / workflow / goal).
|
|
1025
|
+
# Badge label includes the activation kind and source name, matching
|
|
1026
|
+
# how Melete narrates its own choices in the transcript.
|
|
1027
|
+
# @param tui [RatatuiRuby] TUI rendering API
|
|
1028
|
+
# @param data [Hash] structured data with "kind", "source", "content"
|
|
1029
|
+
# @return [Array<RatatuiRuby::Widgets::Line>]
|
|
1030
|
+
def render_pending_melete_entry(tui, data)
|
|
1031
|
+
render_pending_badge_entry(tui, data, badge: "Melete #{data["kind"]}: #{data["source"]}")
|
|
1032
|
+
end
|
|
1033
|
+
|
|
1034
|
+
# Shared rendering for badge-prefixed pending entries. Header reads
|
|
1035
|
+
# +[<badge>]+ on the first line; body content (when present)
|
|
1036
|
+
# follows on indented continuation lines. Everything renders in the
|
|
1037
|
+
# muted theme color so the eye groups the in-flight pipeline as
|
|
1038
|
+
# distinct from real messages.
|
|
1039
|
+
# @return [Array<RatatuiRuby::Widgets::Line>]
|
|
1040
|
+
def render_pending_badge_entry(tui, data, badge:)
|
|
1041
|
+
style = tui.style(fg: Settings.theme_color_muted)
|
|
1042
|
+
header = "[#{badge}]"
|
|
1043
|
+
first_content = data["content"].to_s
|
|
1044
|
+
|
|
1045
|
+
if first_content.empty?
|
|
1046
|
+
return [tui.line(spans: [tui.span(content: header, style: style)])]
|
|
1047
|
+
end
|
|
1048
|
+
|
|
1049
|
+
content_lines = first_content.split("\n", -1)
|
|
1050
|
+
lines = [tui.line(spans: [tui.span(content: "#{header} #{content_lines.first}", style: style)])]
|
|
1051
|
+
content_lines.drop(1).each { |line| lines << tui.line(spans: [tui.span(content: preserve_indentation(" #{line}"), style: style)]) }
|
|
981
1052
|
lines
|
|
982
1053
|
end
|
|
983
1054
|
|
|
@@ -989,9 +1060,9 @@ module TUI
|
|
|
989
1060
|
# @return [Array<RatatuiRuby::Widgets::Line>]
|
|
990
1061
|
def render_system_prompt_entry(tui, data)
|
|
991
1062
|
tokens = data["tokens"]
|
|
992
|
-
bold_style = tui.style(fg:
|
|
993
|
-
style = tui.style(fg:
|
|
994
|
-
tool_style = tui.style(fg:
|
|
1063
|
+
bold_style = tui.style(fg: Settings.theme_color_accent, modifiers: [:bold])
|
|
1064
|
+
style = tui.style(fg: Settings.theme_color_accent)
|
|
1065
|
+
tool_style = tui.style(fg: Settings.theme_color_info)
|
|
995
1066
|
|
|
996
1067
|
header_spans = [tui.span(content: "[SYSTEM] ", style: bold_style)]
|
|
997
1068
|
if tokens
|
|
@@ -1001,14 +1072,14 @@ module TUI
|
|
|
1001
1072
|
|
|
1002
1073
|
lines = [tui.line(spans: header_spans)]
|
|
1003
1074
|
data["content"].to_s.split("\n").each do |line|
|
|
1004
|
-
lines << tui.line(spans: [tui.span(content: " #{line}", style: style)])
|
|
1075
|
+
lines << tui.line(spans: [tui.span(content: preserve_indentation(" #{line}"), style: style)])
|
|
1005
1076
|
end
|
|
1006
1077
|
|
|
1007
1078
|
if data["tools"].is_a?(Array) && data["tools"].any?
|
|
1008
1079
|
lines << tui.line(spans: [tui.span(content: "", style: style)])
|
|
1009
|
-
lines << tui.line(spans: [tui.span(content: "
|
|
1080
|
+
lines << tui.line(spans: [tui.span(content: preserve_indentation(" ## Tools (#{data["tools"].size})"), style: bold_style)])
|
|
1010
1081
|
tools_toon(data).split("\n").each do |line|
|
|
1011
|
-
lines << tui.line(spans: [tui.span(content: line, style: tool_style)])
|
|
1082
|
+
lines << tui.line(spans: [tui.span(content: preserve_indentation(line), style: tool_style)])
|
|
1012
1083
|
end
|
|
1013
1084
|
end
|
|
1014
1085
|
|
|
@@ -1017,18 +1088,17 @@ module TUI
|
|
|
1017
1088
|
|
|
1018
1089
|
# Converts tool schemas to TOON format for display. Caches the result
|
|
1019
1090
|
# on the data hash so the conversion runs once per broadcast, not per
|
|
1020
|
-
# frame.
|
|
1021
|
-
#
|
|
1091
|
+
# frame. NBSP substitution is applied at the span level via
|
|
1092
|
+
# preserve_indentation, not here.
|
|
1022
1093
|
# @param data [Hash] entry data containing "tools" array
|
|
1023
1094
|
# @return [String] TOON-formatted tool schemas
|
|
1024
1095
|
def tools_toon(data)
|
|
1025
1096
|
data["tools_toon"] ||= Toon.encode(data["tools"])
|
|
1026
|
-
.gsub(/^( +)/) { "\u00a0" * _1.length }
|
|
1027
1097
|
end
|
|
1028
1098
|
|
|
1029
1099
|
def build_chat_message_lines(tui, msg)
|
|
1030
1100
|
role = msg[:role]
|
|
1031
|
-
role_cfg =
|
|
1101
|
+
role_cfg = self.class.role_styles.fetch(role, {fg: Settings.theme_color_text})
|
|
1032
1102
|
role_style = tui.style(**role_cfg)
|
|
1033
1103
|
|
|
1034
1104
|
label = role_label(role)
|
|
@@ -1036,14 +1106,14 @@ module TUI
|
|
|
1036
1106
|
|
|
1037
1107
|
lines = [tui.line(spans: [
|
|
1038
1108
|
tui.span(content: "#{label}: ", style: role_style),
|
|
1039
|
-
tui.span(content: content_lines.first.to_s)
|
|
1109
|
+
tui.span(content: preserve_indentation(content_lines.first.to_s))
|
|
1040
1110
|
])]
|
|
1041
|
-
content_lines.drop(1).each { |text| lines << tui.line(spans: [tui.span(content: text)]) }
|
|
1111
|
+
content_lines.drop(1).each { |text| lines << tui.line(spans: [tui.span(content: preserve_indentation(text))]) }
|
|
1042
1112
|
lines << tui.line(spans: [tui.span(content: "")])
|
|
1043
1113
|
end
|
|
1044
1114
|
|
|
1045
1115
|
# Dynamically calculates input area height based on wrapped content.
|
|
1046
|
-
# Clamped between
|
|
1116
|
+
# Clamped between Settings.chat_min_input_height and 50% of available height.
|
|
1047
1117
|
def calculate_input_height(_tui, area_width, area_height)
|
|
1048
1118
|
inner_width = [area_width - 2, 1].max
|
|
1049
1119
|
|
|
@@ -1052,8 +1122,8 @@ module TUI
|
|
|
1052
1122
|
}
|
|
1053
1123
|
desired = content_height + 2 # top + bottom border
|
|
1054
1124
|
|
|
1055
|
-
max_height = [area_height / 2,
|
|
1056
|
-
desired.clamp(
|
|
1125
|
+
max_height = [area_height / 2, Settings.chat_min_input_height].max
|
|
1126
|
+
desired.clamp(Settings.chat_min_input_height, max_height)
|
|
1057
1127
|
end
|
|
1058
1128
|
|
|
1059
1129
|
def render_input(frame, area, tui)
|
|
@@ -1093,15 +1163,15 @@ module TUI
|
|
|
1093
1163
|
|
|
1094
1164
|
def input_styles(tui, disabled)
|
|
1095
1165
|
border_color = if disabled || @chat_focused
|
|
1096
|
-
|
|
1166
|
+
Settings.theme_border_input_disconnected
|
|
1097
1167
|
elsif @session_loading
|
|
1098
|
-
|
|
1168
|
+
Settings.theme_border_input_connecting
|
|
1099
1169
|
else
|
|
1100
|
-
|
|
1170
|
+
Settings.theme_border_input_connected
|
|
1101
1171
|
end
|
|
1102
1172
|
|
|
1103
1173
|
{
|
|
1104
|
-
text: disabled ? tui.style(fg:
|
|
1174
|
+
text: disabled ? tui.style(fg: Settings.theme_color_muted) : tui.style(fg: Settings.theme_color_text),
|
|
1105
1175
|
border: {fg: border_color}
|
|
1106
1176
|
}
|
|
1107
1177
|
end
|
|
@@ -1263,10 +1333,10 @@ module TUI
|
|
|
1263
1333
|
# @return [Boolean] true if the event was handled
|
|
1264
1334
|
def handle_chat_focused_event(event)
|
|
1265
1335
|
if event.up?
|
|
1266
|
-
scroll_up(
|
|
1336
|
+
scroll_up(Settings.chat_scroll_step)
|
|
1267
1337
|
true
|
|
1268
1338
|
elsif event.down?
|
|
1269
|
-
scroll_down(
|
|
1339
|
+
scroll_down(Settings.chat_scroll_step)
|
|
1270
1340
|
true
|
|
1271
1341
|
elsif event.home?
|
|
1272
1342
|
scroll_up(@max_scroll)
|
|
@@ -1351,9 +1421,9 @@ module TUI
|
|
|
1351
1421
|
# @return [true] always redraws after scrolling
|
|
1352
1422
|
def handle_scroll_key(event)
|
|
1353
1423
|
if event.up?
|
|
1354
|
-
scroll_up(
|
|
1424
|
+
scroll_up(Settings.chat_scroll_step)
|
|
1355
1425
|
elsif event.down?
|
|
1356
|
-
scroll_down(
|
|
1426
|
+
scroll_down(Settings.chat_scroll_step)
|
|
1357
1427
|
elsif event.page_up?
|
|
1358
1428
|
scroll_up(@visible_height)
|
|
1359
1429
|
elsif event.page_down?
|
|
@@ -1366,10 +1436,10 @@ module TUI
|
|
|
1366
1436
|
# @return [Boolean] true if the event was a scroll wheel event
|
|
1367
1437
|
def handle_mouse_event(event)
|
|
1368
1438
|
if event.scroll_up?
|
|
1369
|
-
scroll_up(
|
|
1439
|
+
scroll_up(Settings.chat_mouse_scroll_step)
|
|
1370
1440
|
true
|
|
1371
1441
|
elsif event.scroll_down?
|
|
1372
|
-
scroll_down(
|
|
1442
|
+
scroll_down(Settings.chat_mouse_scroll_step)
|
|
1373
1443
|
true
|
|
1374
1444
|
else
|
|
1375
1445
|
false
|
data/lib/tui/settings.rb
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "toml-rb"
|
|
4
|
+
|
|
5
|
+
module TUI
|
|
6
|
+
# TUI-specific configuration backed by +~/.anima/tui.toml+.
|
|
7
|
+
#
|
|
8
|
+
# Zero Rails dependency — the TUI is a standalone client process.
|
|
9
|
+
#
|
|
10
|
+
# Accessors are generated automatically from the template TOML file.
|
|
11
|
+
# Convention: method name = +section_key+ (e.g. +[hud] min_width+ →
|
|
12
|
+
# +hud_min_width+). To add a setting, add the key to +tui.toml+ — the
|
|
13
|
+
# accessor appears automatically.
|
|
14
|
+
#
|
|
15
|
+
# Settings are loaded once at startup. Restart the TUI to pick up
|
|
16
|
+
# changes — it's a thin client, the brain won't notice.
|
|
17
|
+
#
|
|
18
|
+
# @example Reading a setting
|
|
19
|
+
# TUI::Settings.connection_default_host #=> "localhost:42134"
|
|
20
|
+
# TUI::Settings.hud_min_width #=> 24
|
|
21
|
+
#
|
|
22
|
+
# @see Anima::Installer#create_tui_config creates the config file
|
|
23
|
+
module Settings
|
|
24
|
+
DEFAULT_PATH = File.expand_path("~/.anima/tui.toml")
|
|
25
|
+
TEMPLATE_PATH = File.expand_path("../../../templates/tui.toml", __FILE__)
|
|
26
|
+
TEMPLATE = TomlRB.load_file(TEMPLATE_PATH)
|
|
27
|
+
|
|
28
|
+
class MissingConfigError < StandardError; end
|
|
29
|
+
class MissingSettingError < StandardError; end
|
|
30
|
+
|
|
31
|
+
@config_path = nil
|
|
32
|
+
|
|
33
|
+
class << self
|
|
34
|
+
TEMPLATE.each do |section, keys|
|
|
35
|
+
keys.each_key { |key| attr_reader :"#{section}_#{key}" }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Override config file path (for testing).
|
|
39
|
+
# Triggers a load so the new config takes effect immediately.
|
|
40
|
+
#
|
|
41
|
+
# @param path [String, nil] custom path, or +nil+ to restore default
|
|
42
|
+
def config_path=(path)
|
|
43
|
+
@config_path = path
|
|
44
|
+
load! if path
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# @return [String] active config file path
|
|
48
|
+
def config_path
|
|
49
|
+
@config_path || DEFAULT_PATH
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Restores template defaults and clears the path override.
|
|
53
|
+
# Useful in test teardown.
|
|
54
|
+
def reset!
|
|
55
|
+
@config_path = nil
|
|
56
|
+
load_defaults!
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Populates ivars from the cached {TEMPLATE} hash without touching disk.
|
|
60
|
+
# Intended for test suites that want template defaults without paying
|
|
61
|
+
# the TOML parse cost for every example.
|
|
62
|
+
def load_defaults!
|
|
63
|
+
TEMPLATE.each do |section, keys|
|
|
64
|
+
keys.each { |key, value| instance_variable_set(:"@#{section}_#{key}", value) }
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Parses the config file and populates all setting ivars.
|
|
69
|
+
#
|
|
70
|
+
# @raise [MissingConfigError] when tui.toml does not exist
|
|
71
|
+
# @raise [MissingSettingError] when a template key is missing from config
|
|
72
|
+
def load!
|
|
73
|
+
path = config_path
|
|
74
|
+
unless File.exist?(path)
|
|
75
|
+
raise MissingConfigError,
|
|
76
|
+
"TUI config file not found: #{path}. Run `anima install` to create it."
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
parsed = TomlRB.load_file(path)
|
|
80
|
+
TEMPLATE.each do |section, keys|
|
|
81
|
+
keys.each_key do |key|
|
|
82
|
+
value = parsed.dig(section, key)
|
|
83
|
+
if value.nil?
|
|
84
|
+
raise MissingSettingError,
|
|
85
|
+
"[#{section}] #{key} is not set in #{path}. Run `anima update` to add missing settings."
|
|
86
|
+
end
|
|
87
|
+
instance_variable_set(:"@#{section}_#{key}", value)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
data/lib/workflows/definition.rb
CHANGED
|
@@ -7,10 +7,10 @@ module Workflows
|
|
|
7
7
|
|
|
8
8
|
# A workflow parsed from a Markdown definition file.
|
|
9
9
|
# YAML frontmatter holds metadata; the Markdown body contains free-form
|
|
10
|
-
# instructions that
|
|
10
|
+
# instructions that Melete reads and converts into goals.
|
|
11
11
|
#
|
|
12
12
|
# Workflows are operational recipes — they describe WHAT to do step by
|
|
13
|
-
# step.
|
|
13
|
+
# step. Melete uses judgment to decompose workflow prose
|
|
14
14
|
# into tracked goals based on the user's specific context.
|
|
15
15
|
#
|
|
16
16
|
# @example Workflow file format
|
|
@@ -25,7 +25,7 @@ module Workflows
|
|
|
25
25
|
# @return [String] unique workflow identifier used in read_workflow(name: "...")
|
|
26
26
|
attr_reader :name
|
|
27
27
|
|
|
28
|
-
# @return [String] description shown to
|
|
28
|
+
# @return [String] description shown to Melete for relevance matching
|
|
29
29
|
attr_reader :description
|
|
30
30
|
|
|
31
31
|
# @return [String] workflow content (Markdown body) — free-form instructions
|
data/lib/workflows/registry.rb
CHANGED
|
@@ -64,7 +64,7 @@ module Workflows
|
|
|
64
64
|
@workflows[name]
|
|
65
65
|
end
|
|
66
66
|
|
|
67
|
-
# Workflow names and descriptions for inclusion in
|
|
67
|
+
# Workflow names and descriptions for inclusion in Melete's context.
|
|
68
68
|
#
|
|
69
69
|
# @return [Hash{String => String}] name => description
|
|
70
70
|
def catalog
|
data/skills/github.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: github
|
|
3
|
+
description: "GitHub operations — issues, pull requests, releases, CI runs, comments. Activate when an issue or PR number comes up, or when the conversation turns to repo state."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# GitHub
|
|
7
|
+
|
|
8
|
+
`gh` is installed and authenticated.
|
|
9
|
+
|
|
10
|
+
## Sub-issues — REST only
|
|
11
|
+
|
|
12
|
+
Not in top-level `gh` commands. Use the issue's *global ID* (not its number):
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner)
|
|
16
|
+
ISSUE_ID=$(gh api repos/$REPO/issues/42 --jq '.id')
|
|
17
|
+
|
|
18
|
+
# add
|
|
19
|
+
gh api repos/$REPO/issues/36/sub_issues -X POST -F sub_issue_id=$ISSUE_ID
|
|
20
|
+
|
|
21
|
+
# list
|
|
22
|
+
gh api repos/$REPO/issues/36/sub_issues
|
|
23
|
+
|
|
24
|
+
# reorder (move one to sit after another)
|
|
25
|
+
gh api repos/$REPO/issues/36/sub_issues/priority -X PATCH \
|
|
26
|
+
-F sub_issue_id=$ISSUE_ID -F after_id=$AFTER_ID
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Reading JSON
|
|
30
|
+
|
|
31
|
+
`gh` JSON output is token-expensive. Pipe through `toon` — a lossless LLM-optimized format that round-trips back to JSON:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
gh pr view 459 --json title,body,reviewDecision | toon
|
|
35
|
+
gh api repos/$REPO/pulls/459/comments | toon
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Skip `toon` when the output is going into further tooling that needs raw JSON (jq, scripts).
|