claude_swarm 1.0.1 → 1.0.2
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/.claude/commands/release.md +1 -1
- data/.claude/hooks/lint-code-files.rb +65 -0
- data/.rubocop.yml +22 -2
- data/CHANGELOG.md +14 -1
- data/CLAUDE.md +1 -1
- data/CONTRIBUTING.md +69 -0
- data/README.md +27 -2
- data/Rakefile +71 -3
- data/analyze_coverage.rb +94 -0
- data/docs/v2/CHANGELOG.swarm_cli.md +43 -0
- data/docs/v2/CHANGELOG.swarm_memory.md +379 -0
- data/docs/v2/CHANGELOG.swarm_sdk.md +362 -0
- data/docs/v2/README.md +308 -0
- data/docs/v2/guides/claude-code-agents.md +262 -0
- data/docs/v2/guides/complete-tutorial.md +3088 -0
- data/docs/v2/guides/getting-started.md +1456 -0
- data/docs/v2/guides/memory-adapters.md +998 -0
- data/docs/v2/guides/plugins.md +816 -0
- data/docs/v2/guides/quick-start-cli.md +1745 -0
- data/docs/v2/guides/rails-integration.md +1902 -0
- data/docs/v2/guides/swarm-memory.md +599 -0
- data/docs/v2/reference/cli.md +729 -0
- data/docs/v2/reference/ruby-dsl.md +2154 -0
- data/docs/v2/reference/yaml.md +1835 -0
- data/docs-team-swarm.yml +2222 -0
- data/examples/learning-assistant/assistant.md +7 -0
- data/examples/learning-assistant/example-memories/concept-example.md +90 -0
- data/examples/learning-assistant/example-memories/experience-example.md +66 -0
- data/examples/learning-assistant/example-memories/fact-example.md +76 -0
- data/examples/learning-assistant/example-memories/memory-index.md +78 -0
- data/examples/learning-assistant/example-memories/skill-example.md +168 -0
- data/examples/learning-assistant/learning_assistant.rb +34 -0
- data/examples/learning-assistant/learning_assistant.yml +20 -0
- data/examples/v2/dsl/01_basic.rb +44 -0
- data/examples/v2/dsl/02_core_parameters.rb +59 -0
- data/examples/v2/dsl/03_capabilities.rb +71 -0
- data/examples/v2/dsl/04_llm_parameters.rb +56 -0
- data/examples/v2/dsl/05_advanced_flags.rb +73 -0
- data/examples/v2/dsl/06_permissions.rb +80 -0
- data/examples/v2/dsl/07_mcp_server.rb +62 -0
- data/examples/v2/dsl/08_swarm_hooks.rb +53 -0
- data/examples/v2/dsl/09_agent_hooks.rb +67 -0
- data/examples/v2/dsl/10_all_agents_hooks.rb +67 -0
- data/examples/v2/dsl/11_delegation.rb +60 -0
- data/examples/v2/dsl/12_complete_integration.rb +137 -0
- data/examples/v2/file_tools_swarm.yml +102 -0
- data/examples/v2/hooks/01_basic_hooks.rb +133 -0
- data/examples/v2/hooks/02_usage_tracking.rb +201 -0
- data/examples/v2/hooks/03_production_monitoring.rb +429 -0
- data/examples/v2/hooks/agent_stop_exit_0.yml +21 -0
- data/examples/v2/hooks/agent_stop_exit_1.yml +21 -0
- data/examples/v2/hooks/agent_stop_exit_2.yml +26 -0
- data/examples/v2/hooks/multiple_hooks_all_pass.yml +37 -0
- data/examples/v2/hooks/multiple_hooks_first_fails.yml +37 -0
- data/examples/v2/hooks/multiple_hooks_second_fails.yml +37 -0
- data/examples/v2/hooks/multiple_hooks_warnings.yml +37 -0
- data/examples/v2/hooks/post_tool_use_exit_0.yml +24 -0
- data/examples/v2/hooks/post_tool_use_exit_1.yml +24 -0
- data/examples/v2/hooks/post_tool_use_exit_2.yml +24 -0
- data/examples/v2/hooks/post_tool_use_multi_matcher_exit_0.yml +26 -0
- data/examples/v2/hooks/post_tool_use_multi_matcher_exit_1.yml +26 -0
- data/examples/v2/hooks/post_tool_use_multi_matcher_exit_2.yml +26 -0
- data/examples/v2/hooks/pre_tool_use_exit_0.yml +24 -0
- data/examples/v2/hooks/pre_tool_use_exit_1.yml +24 -0
- data/examples/v2/hooks/pre_tool_use_exit_2.yml +24 -0
- data/examples/v2/hooks/pre_tool_use_multi_matcher_exit_0.yml +26 -0
- data/examples/v2/hooks/pre_tool_use_multi_matcher_exit_1.yml +26 -0
- data/examples/v2/hooks/pre_tool_use_multi_matcher_exit_2.yml +27 -0
- data/examples/v2/hooks/swarm_summary.sh +44 -0
- data/examples/v2/hooks/user_prompt_exit_0.yml +21 -0
- data/examples/v2/hooks/user_prompt_exit_1.yml +21 -0
- data/examples/v2/hooks/user_prompt_exit_2.yml +21 -0
- data/examples/v2/hooks/validate_bash.rb +59 -0
- data/examples/v2/multi_directory_permissions.yml +221 -0
- data/examples/v2/node_context_demo.rb +127 -0
- data/examples/v2/node_workflow.rb +173 -0
- data/examples/v2/path_resolution_demo.rb +216 -0
- data/examples/v2/simple-swarm-v2.rb +90 -0
- data/examples/v2/simple-swarm-v2.yml +62 -0
- data/examples/v2/swarm.yml +71 -0
- data/examples/v2/swarm_with_hooks.yml +61 -0
- data/examples/v2/swarm_with_hooks_simple.yml +25 -0
- data/examples/v2/think_tool_demo.rb +62 -0
- data/exe/swarm +6 -0
- data/lib/claude_swarm/claude_mcp_server.rb +0 -6
- data/lib/claude_swarm/cli.rb +10 -3
- data/lib/claude_swarm/commands/ps.rb +19 -20
- data/lib/claude_swarm/commands/show.rb +1 -1
- data/lib/claude_swarm/configuration.rb +10 -12
- data/lib/claude_swarm/mcp_generator.rb +10 -1
- data/lib/claude_swarm/orchestrator.rb +73 -49
- data/lib/claude_swarm/system_utils.rb +37 -11
- data/lib/claude_swarm/version.rb +1 -1
- data/lib/claude_swarm/worktree_manager.rb +1 -0
- data/lib/claude_swarm/yaml_loader.rb +22 -0
- data/lib/claude_swarm.rb +6 -2
- data/lib/swarm_cli/cli.rb +201 -0
- data/lib/swarm_cli/command_registry.rb +61 -0
- data/lib/swarm_cli/commands/mcp_serve.rb +130 -0
- data/lib/swarm_cli/commands/mcp_tools.rb +148 -0
- data/lib/swarm_cli/commands/migrate.rb +55 -0
- data/lib/swarm_cli/commands/run.rb +173 -0
- data/lib/swarm_cli/config_loader.rb +97 -0
- data/lib/swarm_cli/formatters/human_formatter.rb +711 -0
- data/lib/swarm_cli/formatters/json_formatter.rb +51 -0
- data/lib/swarm_cli/interactive_repl.rb +918 -0
- data/lib/swarm_cli/mcp_serve_options.rb +44 -0
- data/lib/swarm_cli/mcp_tools_options.rb +59 -0
- data/lib/swarm_cli/migrate_options.rb +54 -0
- data/lib/swarm_cli/migrator.rb +132 -0
- data/lib/swarm_cli/options.rb +151 -0
- data/lib/swarm_cli/ui/components/agent_badge.rb +33 -0
- data/lib/swarm_cli/ui/components/content_block.rb +120 -0
- data/lib/swarm_cli/ui/components/divider.rb +57 -0
- data/lib/swarm_cli/ui/components/panel.rb +62 -0
- data/lib/swarm_cli/ui/components/usage_stats.rb +70 -0
- data/lib/swarm_cli/ui/formatters/cost.rb +49 -0
- data/lib/swarm_cli/ui/formatters/number.rb +58 -0
- data/lib/swarm_cli/ui/formatters/text.rb +77 -0
- data/lib/swarm_cli/ui/formatters/time.rb +73 -0
- data/lib/swarm_cli/ui/icons.rb +59 -0
- data/lib/swarm_cli/ui/renderers/event_renderer.rb +188 -0
- data/lib/swarm_cli/ui/state/agent_color_cache.rb +45 -0
- data/lib/swarm_cli/ui/state/depth_tracker.rb +40 -0
- data/lib/swarm_cli/ui/state/spinner_manager.rb +170 -0
- data/lib/swarm_cli/ui/state/usage_tracker.rb +62 -0
- data/lib/swarm_cli/version.rb +5 -0
- data/lib/swarm_cli.rb +44 -0
- data/lib/swarm_memory/adapters/base.rb +141 -0
- data/lib/swarm_memory/adapters/filesystem_adapter.rb +845 -0
- data/lib/swarm_memory/chat_extension.rb +34 -0
- data/lib/swarm_memory/cli/commands.rb +306 -0
- data/lib/swarm_memory/core/entry.rb +37 -0
- data/lib/swarm_memory/core/frontmatter_parser.rb +108 -0
- data/lib/swarm_memory/core/metadata_extractor.rb +68 -0
- data/lib/swarm_memory/core/path_normalizer.rb +75 -0
- data/lib/swarm_memory/core/semantic_index.rb +244 -0
- data/lib/swarm_memory/core/storage.rb +288 -0
- data/lib/swarm_memory/core/storage_read_tracker.rb +63 -0
- data/lib/swarm_memory/dsl/builder_extension.rb +40 -0
- data/lib/swarm_memory/dsl/memory_config.rb +113 -0
- data/lib/swarm_memory/embeddings/embedder.rb +36 -0
- data/lib/swarm_memory/embeddings/informers_embedder.rb +152 -0
- data/lib/swarm_memory/errors.rb +21 -0
- data/lib/swarm_memory/integration/cli_registration.rb +30 -0
- data/lib/swarm_memory/integration/configuration.rb +43 -0
- data/lib/swarm_memory/integration/registration.rb +31 -0
- data/lib/swarm_memory/integration/sdk_plugin.rb +531 -0
- data/lib/swarm_memory/optimization/analyzer.rb +244 -0
- data/lib/swarm_memory/optimization/defragmenter.rb +863 -0
- data/lib/swarm_memory/prompts/memory.md.erb +109 -0
- data/lib/swarm_memory/prompts/memory_assistant.md.erb +181 -0
- data/lib/swarm_memory/prompts/memory_researcher.md.erb +281 -0
- data/lib/swarm_memory/prompts/memory_retrieval.md.erb +78 -0
- data/lib/swarm_memory/search/semantic_search.rb +112 -0
- data/lib/swarm_memory/search/text_search.rb +42 -0
- data/lib/swarm_memory/search/text_similarity.rb +80 -0
- data/lib/swarm_memory/skills/meta/deep-learning.md +101 -0
- data/lib/swarm_memory/skills/meta/deep-learning.yml +14 -0
- data/lib/swarm_memory/tools/load_skill.rb +313 -0
- data/lib/swarm_memory/tools/memory_defrag.rb +382 -0
- data/lib/swarm_memory/tools/memory_delete.rb +99 -0
- data/lib/swarm_memory/tools/memory_edit.rb +185 -0
- data/lib/swarm_memory/tools/memory_glob.rb +160 -0
- data/lib/swarm_memory/tools/memory_grep.rb +247 -0
- data/lib/swarm_memory/tools/memory_multi_edit.rb +281 -0
- data/lib/swarm_memory/tools/memory_read.rb +123 -0
- data/lib/swarm_memory/tools/memory_write.rb +231 -0
- data/lib/swarm_memory/utils.rb +50 -0
- data/lib/swarm_memory/version.rb +5 -0
- data/lib/swarm_memory.rb +166 -0
- data/lib/swarm_sdk/agent/RETRY_LOGIC.md +127 -0
- data/lib/swarm_sdk/agent/builder.rb +461 -0
- data/lib/swarm_sdk/agent/chat/context_tracker.rb +314 -0
- data/lib/swarm_sdk/agent/chat/hook_integration.rb +372 -0
- data/lib/swarm_sdk/agent/chat/logging_helpers.rb +116 -0
- data/lib/swarm_sdk/agent/chat/system_reminder_injector.rb +152 -0
- data/lib/swarm_sdk/agent/chat.rb +1159 -0
- data/lib/swarm_sdk/agent/context.rb +112 -0
- data/lib/swarm_sdk/agent/context_manager.rb +309 -0
- data/lib/swarm_sdk/agent/definition.rb +556 -0
- data/lib/swarm_sdk/claude_code_agent_adapter.rb +205 -0
- data/lib/swarm_sdk/configuration.rb +296 -0
- data/lib/swarm_sdk/context_compactor/metrics.rb +147 -0
- data/lib/swarm_sdk/context_compactor/token_counter.rb +106 -0
- data/lib/swarm_sdk/context_compactor.rb +340 -0
- data/lib/swarm_sdk/hooks/adapter.rb +359 -0
- data/lib/swarm_sdk/hooks/context.rb +197 -0
- data/lib/swarm_sdk/hooks/definition.rb +80 -0
- data/lib/swarm_sdk/hooks/error.rb +29 -0
- data/lib/swarm_sdk/hooks/executor.rb +146 -0
- data/lib/swarm_sdk/hooks/registry.rb +147 -0
- data/lib/swarm_sdk/hooks/result.rb +150 -0
- data/lib/swarm_sdk/hooks/shell_executor.rb +254 -0
- data/lib/swarm_sdk/hooks/tool_call.rb +35 -0
- data/lib/swarm_sdk/hooks/tool_result.rb +62 -0
- data/lib/swarm_sdk/log_collector.rb +51 -0
- data/lib/swarm_sdk/log_stream.rb +69 -0
- data/lib/swarm_sdk/markdown_parser.rb +75 -0
- data/lib/swarm_sdk/model_aliases.json +5 -0
- data/lib/swarm_sdk/models.json +1 -0
- data/lib/swarm_sdk/models.rb +120 -0
- data/lib/swarm_sdk/node/agent_config.rb +49 -0
- data/lib/swarm_sdk/node/builder.rb +439 -0
- data/lib/swarm_sdk/node/transformer_executor.rb +248 -0
- data/lib/swarm_sdk/node_context.rb +170 -0
- data/lib/swarm_sdk/node_orchestrator.rb +384 -0
- data/lib/swarm_sdk/permissions/config.rb +239 -0
- data/lib/swarm_sdk/permissions/error_formatter.rb +121 -0
- data/lib/swarm_sdk/permissions/path_matcher.rb +35 -0
- data/lib/swarm_sdk/permissions/validator.rb +173 -0
- data/lib/swarm_sdk/permissions_builder.rb +122 -0
- data/lib/swarm_sdk/plugin.rb +147 -0
- data/lib/swarm_sdk/plugin_registry.rb +101 -0
- data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +243 -0
- data/lib/swarm_sdk/providers/openai_with_responses.rb +582 -0
- data/lib/swarm_sdk/result.rb +97 -0
- data/lib/swarm_sdk/swarm/agent_initializer.rb +334 -0
- data/lib/swarm_sdk/swarm/all_agents_builder.rb +140 -0
- data/lib/swarm_sdk/swarm/builder.rb +586 -0
- data/lib/swarm_sdk/swarm/mcp_configurator.rb +151 -0
- data/lib/swarm_sdk/swarm/tool_configurator.rb +419 -0
- data/lib/swarm_sdk/swarm.rb +982 -0
- data/lib/swarm_sdk/tools/bash.rb +274 -0
- data/lib/swarm_sdk/tools/clock.rb +44 -0
- data/lib/swarm_sdk/tools/delegate.rb +164 -0
- data/lib/swarm_sdk/tools/document_converters/base_converter.rb +83 -0
- data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +99 -0
- data/lib/swarm_sdk/tools/document_converters/html_converter.rb +101 -0
- data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +78 -0
- data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +194 -0
- data/lib/swarm_sdk/tools/edit.rb +150 -0
- data/lib/swarm_sdk/tools/glob.rb +158 -0
- data/lib/swarm_sdk/tools/grep.rb +228 -0
- data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +43 -0
- data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +163 -0
- data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +65 -0
- data/lib/swarm_sdk/tools/multi_edit.rb +232 -0
- data/lib/swarm_sdk/tools/path_resolver.rb +43 -0
- data/lib/swarm_sdk/tools/read.rb +251 -0
- data/lib/swarm_sdk/tools/registry.rb +93 -0
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +96 -0
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +76 -0
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +91 -0
- data/lib/swarm_sdk/tools/stores/read_tracker.rb +61 -0
- data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +224 -0
- data/lib/swarm_sdk/tools/stores/storage.rb +148 -0
- data/lib/swarm_sdk/tools/stores/todo_manager.rb +65 -0
- data/lib/swarm_sdk/tools/think.rb +95 -0
- data/lib/swarm_sdk/tools/todo_write.rb +216 -0
- data/lib/swarm_sdk/tools/web_fetch.rb +261 -0
- data/lib/swarm_sdk/tools/write.rb +117 -0
- data/lib/swarm_sdk/utils.rb +50 -0
- data/lib/swarm_sdk/version.rb +5 -0
- data/lib/swarm_sdk.rb +157 -0
- data/llm.v2.txt +13407 -0
- data/rubocop/cop/security/no_reflection_methods.rb +47 -0
- data/rubocop/cop/security/no_ruby_llm_logger.rb +32 -0
- data/swarm_cli.gemspec +57 -0
- data/swarm_memory.gemspec +28 -0
- data/swarm_sdk.gemspec +41 -0
- data/team.yml +1 -1
- data/team_full.yml +1875 -0
- data/{team_v2.yml → team_sdk.yml} +121 -52
- metadata +247 -4
- data/EXAMPLES.md +0 -164
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmCLI
|
|
4
|
+
module UI
|
|
5
|
+
module Components
|
|
6
|
+
# Renders highlighted panels for warnings, info, errors
|
|
7
|
+
# Uses top/bottom borders only (no sides per design constraint)
|
|
8
|
+
class Panel
|
|
9
|
+
TYPE_CONFIGS = {
|
|
10
|
+
warning: { color: :yellow, icon: UI::Icons::WARNING },
|
|
11
|
+
error: { color: :red, icon: UI::Icons::ERROR },
|
|
12
|
+
info: { color: :cyan, icon: UI::Icons::INFO },
|
|
13
|
+
success: { color: :green, icon: UI::Icons::SUCCESS },
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
def initialize(pastel:, terminal_width: 80)
|
|
17
|
+
@pastel = pastel
|
|
18
|
+
@terminal_width = terminal_width
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Render panel with top/bottom borders
|
|
22
|
+
#
|
|
23
|
+
# ⚠️ CONTEXT WARNING
|
|
24
|
+
# Context usage: 81.4% (threshold: 80%)
|
|
25
|
+
# Tokens remaining: 74,523
|
|
26
|
+
#
|
|
27
|
+
def render(type:, title:, lines:, indent: 0)
|
|
28
|
+
config = TYPE_CONFIGS[type] || TYPE_CONFIGS[:info]
|
|
29
|
+
prefix = " " * indent
|
|
30
|
+
|
|
31
|
+
output = []
|
|
32
|
+
|
|
33
|
+
# Title line with icon
|
|
34
|
+
icon = config[:icon]
|
|
35
|
+
colored_title = @pastel.public_send(config[:color], "#{icon} #{title}")
|
|
36
|
+
output << "#{prefix}#{colored_title}"
|
|
37
|
+
|
|
38
|
+
# Content lines
|
|
39
|
+
lines.each do |line|
|
|
40
|
+
output << "#{prefix} #{line}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
output << "" # Blank line after panel
|
|
44
|
+
|
|
45
|
+
output.join("\n")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Render compact panel (single line)
|
|
49
|
+
# ⚠️ Context approaching limit (81.4%)
|
|
50
|
+
def render_compact(type:, message:, indent: 0)
|
|
51
|
+
config = TYPE_CONFIGS[type] || TYPE_CONFIGS[:info]
|
|
52
|
+
prefix = " " * indent
|
|
53
|
+
|
|
54
|
+
icon = config[:icon]
|
|
55
|
+
colored_msg = @pastel.public_send(config[:color], "#{icon} #{message}")
|
|
56
|
+
|
|
57
|
+
"#{prefix}#{colored_msg}"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmCLI
|
|
4
|
+
module UI
|
|
5
|
+
module Components
|
|
6
|
+
# Renders usage statistics (tokens, cost, context percentage)
|
|
7
|
+
class UsageStats
|
|
8
|
+
def initialize(pastel:)
|
|
9
|
+
@pastel = pastel
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Render usage line with all available metrics
|
|
13
|
+
# 5,922 tokens │ $0.0016 │ 1.5% used, 394,078 remaining
|
|
14
|
+
def render(tokens:, cost:, context_pct: nil, remaining: nil, cumulative: nil)
|
|
15
|
+
parts = []
|
|
16
|
+
|
|
17
|
+
# Token count (always shown)
|
|
18
|
+
parts << "#{Formatters::Number.format(tokens)} tokens"
|
|
19
|
+
|
|
20
|
+
# Cost (always shown if > 0)
|
|
21
|
+
parts << Formatters::Cost.format(cost, pastel: @pastel) if cost > 0
|
|
22
|
+
|
|
23
|
+
# Context tracking (if available)
|
|
24
|
+
if context_pct
|
|
25
|
+
colored_pct = color_context_percentage(context_pct)
|
|
26
|
+
|
|
27
|
+
parts << if remaining
|
|
28
|
+
"#{colored_pct} used, #{Formatters::Number.compact(remaining)} remaining"
|
|
29
|
+
else
|
|
30
|
+
"#{colored_pct} used"
|
|
31
|
+
end
|
|
32
|
+
elsif cumulative
|
|
33
|
+
# Model doesn't have context limit, show cumulative
|
|
34
|
+
parts << "#{Formatters::Number.compact(cumulative)} cumulative"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
@pastel.dim(parts.join(" #{@pastel.dim("│")} "))
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Render compact stats for prompt display
|
|
41
|
+
# 15.2K tokens • $0.045 • 3.8% context
|
|
42
|
+
def render_compact(tokens:, cost:, context_pct: nil)
|
|
43
|
+
parts = []
|
|
44
|
+
|
|
45
|
+
parts << "#{Formatters::Number.compact(tokens)} tokens" if tokens > 0
|
|
46
|
+
parts << Formatters::Cost.format_plain(cost) if cost > 0
|
|
47
|
+
parts << "#{context_pct} context" if context_pct
|
|
48
|
+
|
|
49
|
+
parts.join(" • ")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def color_context_percentage(percentage_string)
|
|
55
|
+
percentage = percentage_string.to_s.gsub("%", "").to_f
|
|
56
|
+
|
|
57
|
+
color = if percentage < 50
|
|
58
|
+
:green
|
|
59
|
+
elsif percentage < 80
|
|
60
|
+
:yellow
|
|
61
|
+
else
|
|
62
|
+
:red
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
@pastel.public_send(color, percentage_string)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmCLI
|
|
4
|
+
module UI
|
|
5
|
+
module Formatters
|
|
6
|
+
# Cost formatting with color coding
|
|
7
|
+
class Cost
|
|
8
|
+
class << self
|
|
9
|
+
# Format cost with appropriate precision and color
|
|
10
|
+
# Small costs: green, $0.001234
|
|
11
|
+
# Medium costs: yellow, $0.1234
|
|
12
|
+
# Large costs: red, $12.34
|
|
13
|
+
def format(cost, pastel:)
|
|
14
|
+
return pastel.dim("$0.0000") if cost.nil? || cost.zero?
|
|
15
|
+
|
|
16
|
+
formatted = if cost < 0.01
|
|
17
|
+
Kernel.format("%.6f", cost)
|
|
18
|
+
elsif cost < 1.0
|
|
19
|
+
Kernel.format("%.4f", cost)
|
|
20
|
+
else
|
|
21
|
+
Kernel.format("%.2f", cost)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
if cost < 0.01
|
|
25
|
+
pastel.green("$#{formatted}")
|
|
26
|
+
elsif cost < 1.0
|
|
27
|
+
pastel.yellow("$#{formatted}")
|
|
28
|
+
else
|
|
29
|
+
pastel.red("$#{formatted}")
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Format cost without color (for plain text)
|
|
34
|
+
def format_plain(cost)
|
|
35
|
+
return "$0.0000" if cost.nil? || cost.zero?
|
|
36
|
+
|
|
37
|
+
if cost < 0.01
|
|
38
|
+
Kernel.format("$%.6f", cost)
|
|
39
|
+
elsif cost < 1.0
|
|
40
|
+
Kernel.format("$%.4f", cost)
|
|
41
|
+
else
|
|
42
|
+
Kernel.format("$%.2f", cost)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmCLI
|
|
4
|
+
module UI
|
|
5
|
+
module Formatters
|
|
6
|
+
# Number formatting utilities for terminal display
|
|
7
|
+
class Number
|
|
8
|
+
class << self
|
|
9
|
+
# Format number with thousand separators
|
|
10
|
+
# 5922 → "5,922"
|
|
11
|
+
# 1500000 → "1,500,000"
|
|
12
|
+
def format(num)
|
|
13
|
+
return "0" if num.nil? || num.zero?
|
|
14
|
+
|
|
15
|
+
num.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Format number with compact units (K, M, B)
|
|
19
|
+
# 5922 → "5.9K"
|
|
20
|
+
# 1500000 → "1.5M"
|
|
21
|
+
# 1500000000 → "1.5B"
|
|
22
|
+
def compact(num)
|
|
23
|
+
return "0" if num.nil? || num.zero?
|
|
24
|
+
|
|
25
|
+
case num
|
|
26
|
+
when 0...1_000
|
|
27
|
+
num.to_s
|
|
28
|
+
when 1_000...1_000_000
|
|
29
|
+
"#{(num / 1_000.0).round(1)}K"
|
|
30
|
+
when 1_000_000...1_000_000_000
|
|
31
|
+
"#{(num / 1_000_000.0).round(1)}M"
|
|
32
|
+
else
|
|
33
|
+
"#{(num / 1_000_000_000.0).round(1)}B"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Format bytes with units (KB, MB, GB)
|
|
38
|
+
# 1024 → "1.0 KB"
|
|
39
|
+
# 1500000 → "1.4 MB"
|
|
40
|
+
def bytes(num)
|
|
41
|
+
return "0 B" if num.nil? || num.zero?
|
|
42
|
+
|
|
43
|
+
case num
|
|
44
|
+
when 0...1024
|
|
45
|
+
"#{num} B"
|
|
46
|
+
when 1024...1024**2
|
|
47
|
+
"#{(num / 1024.0).round(1)} KB"
|
|
48
|
+
when 1024**2...1024**3
|
|
49
|
+
"#{(num / 1024.0**2).round(1)} MB"
|
|
50
|
+
else
|
|
51
|
+
"#{(num / 1024.0**3).round(1)} GB"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmCLI
|
|
4
|
+
module UI
|
|
5
|
+
module Formatters
|
|
6
|
+
# Text manipulation utilities for clean display
|
|
7
|
+
class Text
|
|
8
|
+
class << self
|
|
9
|
+
# Strip <system-reminder> tags from content
|
|
10
|
+
def strip_system_reminders(text)
|
|
11
|
+
return "" if text.nil?
|
|
12
|
+
|
|
13
|
+
text.gsub(%r{<system-reminder>.*?</system-reminder>}m, "").strip
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Truncate text to specified character/line limits
|
|
17
|
+
# Returns [display_text, truncation_message]
|
|
18
|
+
def truncate(text, chars: nil, lines: nil)
|
|
19
|
+
return [text, nil] if text.nil? || text.empty?
|
|
20
|
+
|
|
21
|
+
text_lines = text.split("\n")
|
|
22
|
+
truncated = false
|
|
23
|
+
truncation_parts = []
|
|
24
|
+
|
|
25
|
+
# Apply line limit
|
|
26
|
+
if lines && text_lines.length > lines
|
|
27
|
+
text_lines = text_lines.first(lines)
|
|
28
|
+
hidden_lines = text.split("\n").length - lines
|
|
29
|
+
truncation_parts << "#{hidden_lines} more lines"
|
|
30
|
+
truncated = true
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
result_text = text_lines.join("\n")
|
|
34
|
+
|
|
35
|
+
# Apply character limit
|
|
36
|
+
if chars && result_text.length > chars
|
|
37
|
+
result_text = result_text[0...chars]
|
|
38
|
+
hidden_chars = text.length - chars
|
|
39
|
+
truncation_parts << "#{hidden_chars} more chars"
|
|
40
|
+
truncated = true
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
truncation_msg = truncated ? "... (#{truncation_parts.join(", ")})" : nil
|
|
44
|
+
|
|
45
|
+
[result_text, truncation_msg]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Wrap text to specified width
|
|
49
|
+
def wrap(text, width:)
|
|
50
|
+
return "" if text.nil? || text.empty?
|
|
51
|
+
|
|
52
|
+
text.split("\n").flat_map do |line|
|
|
53
|
+
wrap_line(line, width)
|
|
54
|
+
end.join("\n")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Indent all lines in text
|
|
58
|
+
def indent(text, level: 0, char: " ")
|
|
59
|
+
return "" if text.nil? || text.empty?
|
|
60
|
+
|
|
61
|
+
prefix = char * level
|
|
62
|
+
text.split("\n").map { |line| "#{prefix}#{line}" }.join("\n")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
# Word wrap a single line
|
|
68
|
+
def wrap_line(line, width)
|
|
69
|
+
return [line] if line.length <= width
|
|
70
|
+
|
|
71
|
+
line.scan(/.{1,#{width}}(?:\s+|$)/).map(&:strip)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmCLI
|
|
4
|
+
module UI
|
|
5
|
+
module Formatters
|
|
6
|
+
# Time and duration formatting utilities
|
|
7
|
+
class Time
|
|
8
|
+
class << self
|
|
9
|
+
# Format timestamp as [HH:MM:SS]
|
|
10
|
+
# Time.now → "[12:34:56]"
|
|
11
|
+
def timestamp(time)
|
|
12
|
+
return "" if time.nil?
|
|
13
|
+
|
|
14
|
+
case time
|
|
15
|
+
when ::Time
|
|
16
|
+
time.strftime("[%H:%M:%S]")
|
|
17
|
+
when String
|
|
18
|
+
parsed = ::Time.parse(time)
|
|
19
|
+
parsed.strftime("[%H:%M:%S]")
|
|
20
|
+
else
|
|
21
|
+
""
|
|
22
|
+
end
|
|
23
|
+
rescue StandardError
|
|
24
|
+
""
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Format duration in human-readable form
|
|
28
|
+
# 0.5 → "500ms"
|
|
29
|
+
# 2.3 → "2.3s"
|
|
30
|
+
# 65 → "1m 5s"
|
|
31
|
+
# 3665 → "1h 1m 5s"
|
|
32
|
+
def duration(seconds)
|
|
33
|
+
return "0ms" if seconds.nil? || seconds.zero?
|
|
34
|
+
|
|
35
|
+
if seconds < 1
|
|
36
|
+
"#{(seconds * 1000).round}ms"
|
|
37
|
+
elsif seconds < 60
|
|
38
|
+
"#{seconds.round(2)}s"
|
|
39
|
+
elsif seconds < 3600
|
|
40
|
+
minutes = (seconds / 60).floor
|
|
41
|
+
secs = (seconds % 60).round
|
|
42
|
+
"#{minutes}m #{secs}s"
|
|
43
|
+
else
|
|
44
|
+
hours = (seconds / 3600).floor
|
|
45
|
+
minutes = ((seconds % 3600) / 60).floor
|
|
46
|
+
secs = (seconds % 60).round
|
|
47
|
+
"#{hours}h #{minutes}m #{secs}s"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Format relative time (future enhancement)
|
|
52
|
+
# Time.now - 120 → "2 minutes ago"
|
|
53
|
+
def relative(time)
|
|
54
|
+
return "" if time.nil?
|
|
55
|
+
|
|
56
|
+
seconds_ago = ::Time.now - time
|
|
57
|
+
|
|
58
|
+
case seconds_ago
|
|
59
|
+
when 0...60
|
|
60
|
+
"#{seconds_ago.round}s ago"
|
|
61
|
+
when 60...3600
|
|
62
|
+
"#{(seconds_ago / 60).round}m ago"
|
|
63
|
+
when 3600...86400
|
|
64
|
+
"#{(seconds_ago / 3600).round}h ago"
|
|
65
|
+
else
|
|
66
|
+
"#{(seconds_ago / 86400).round}d ago"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmCLI
|
|
4
|
+
module UI
|
|
5
|
+
# Icon definitions for terminal UI
|
|
6
|
+
# Centralized so all components use the same icons
|
|
7
|
+
module Icons
|
|
8
|
+
# Event type icons
|
|
9
|
+
THINKING = "💭"
|
|
10
|
+
RESPONSE = "💬"
|
|
11
|
+
SUCCESS = "✓"
|
|
12
|
+
ERROR = "✗"
|
|
13
|
+
INFO = "ℹ"
|
|
14
|
+
WARNING = "⚠️"
|
|
15
|
+
|
|
16
|
+
# Entity icons
|
|
17
|
+
AGENT = "🤖"
|
|
18
|
+
TOOL = "🔧"
|
|
19
|
+
DELEGATE = "📨"
|
|
20
|
+
RESULT = "📥"
|
|
21
|
+
HOOK = "🪝"
|
|
22
|
+
|
|
23
|
+
# Metric icons
|
|
24
|
+
LLM = "🧠"
|
|
25
|
+
TOKENS = "📊"
|
|
26
|
+
COST = "💰"
|
|
27
|
+
TIME = "⏱"
|
|
28
|
+
|
|
29
|
+
# Visual elements
|
|
30
|
+
SPARKLES = "✨"
|
|
31
|
+
ARROW_RIGHT = "→"
|
|
32
|
+
BULLET = "•"
|
|
33
|
+
COMPRESS = "🗜️"
|
|
34
|
+
|
|
35
|
+
# All icons as hash for backward compatibility
|
|
36
|
+
ALL = {
|
|
37
|
+
thinking: THINKING,
|
|
38
|
+
response: RESPONSE,
|
|
39
|
+
success: SUCCESS,
|
|
40
|
+
error: ERROR,
|
|
41
|
+
info: INFO,
|
|
42
|
+
warning: WARNING,
|
|
43
|
+
agent: AGENT,
|
|
44
|
+
tool: TOOL,
|
|
45
|
+
delegate: DELEGATE,
|
|
46
|
+
result: RESULT,
|
|
47
|
+
hook: HOOK,
|
|
48
|
+
llm: LLM,
|
|
49
|
+
tokens: TOKENS,
|
|
50
|
+
cost: COST,
|
|
51
|
+
time: TIME,
|
|
52
|
+
sparkles: SPARKLES,
|
|
53
|
+
arrow_right: ARROW_RIGHT,
|
|
54
|
+
bullet: BULLET,
|
|
55
|
+
compress: COMPRESS,
|
|
56
|
+
}.freeze
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmCLI
|
|
4
|
+
module UI
|
|
5
|
+
module Renderers
|
|
6
|
+
# High-level event rendering by composing lower-level components
|
|
7
|
+
# Returns formatted strings for each event type
|
|
8
|
+
class EventRenderer
|
|
9
|
+
def initialize(pastel:, agent_badge:, depth_tracker:)
|
|
10
|
+
@pastel = pastel
|
|
11
|
+
@agent_badge = agent_badge
|
|
12
|
+
@depth_tracker = depth_tracker
|
|
13
|
+
@usage_stats = Components::UsageStats.new(pastel: pastel)
|
|
14
|
+
@content_block = Components::ContentBlock.new(pastel: pastel)
|
|
15
|
+
@panel = Components::Panel.new(pastel: pastel)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Render agent thinking event
|
|
19
|
+
# [12:34:56] 💭 architect (gpt-5-mini)
|
|
20
|
+
def agent_thinking(agent:, model:, timestamp:)
|
|
21
|
+
indent = @depth_tracker.indent(agent)
|
|
22
|
+
time = Formatters::Time.timestamp(timestamp)
|
|
23
|
+
agent_name = @agent_badge.render(agent, icon: UI::Icons::THINKING)
|
|
24
|
+
model_info = @pastel.dim("(#{model})")
|
|
25
|
+
|
|
26
|
+
"#{indent}#{@pastel.dim(time)} #{agent_name} #{model_info}"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Render agent response event
|
|
30
|
+
# [12:34:56] 💬 architect responded:
|
|
31
|
+
def agent_response(agent:, timestamp:)
|
|
32
|
+
indent = @depth_tracker.indent(agent)
|
|
33
|
+
time = Formatters::Time.timestamp(timestamp)
|
|
34
|
+
agent_name = @agent_badge.render(agent, icon: UI::Icons::RESPONSE)
|
|
35
|
+
|
|
36
|
+
"#{indent}#{@pastel.dim(time)} #{agent_name} responded:"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Render agent completion
|
|
40
|
+
# ✓ architect completed
|
|
41
|
+
def agent_completed(agent:)
|
|
42
|
+
indent = @depth_tracker.indent(agent)
|
|
43
|
+
agent_name = @agent_badge.render(agent)
|
|
44
|
+
|
|
45
|
+
"#{indent}#{@pastel.green("#{UI::Icons::SUCCESS} #{agent_name} completed")}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Render tool call event
|
|
49
|
+
# [12:34:56] architect 🔧 uses tool Read
|
|
50
|
+
def tool_call(agent:, tool:, timestamp:)
|
|
51
|
+
indent = @depth_tracker.indent(agent)
|
|
52
|
+
time = Formatters::Time.timestamp(timestamp)
|
|
53
|
+
agent_name = @agent_badge.render(agent)
|
|
54
|
+
tool_name = @pastel.bold.blue(tool)
|
|
55
|
+
|
|
56
|
+
"#{indent}#{@pastel.dim(time)} #{agent_name} #{@pastel.blue("#{UI::Icons::TOOL} uses tool")} #{tool_name}"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Render tool result received
|
|
60
|
+
# [12:34:56] 📥 Tool result received by architect
|
|
61
|
+
def tool_result(agent:, timestamp:, tool: nil)
|
|
62
|
+
indent = @depth_tracker.indent(agent)
|
|
63
|
+
time = Formatters::Time.timestamp(timestamp)
|
|
64
|
+
|
|
65
|
+
"#{indent}#{@pastel.dim(time)} #{@pastel.green("#{UI::Icons::RESULT} Tool result")} received by #{agent}"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Render delegation event
|
|
69
|
+
# [12:34:56] architect 📨 delegates to worker
|
|
70
|
+
def delegation(from:, to:, timestamp:)
|
|
71
|
+
indent = @depth_tracker.indent(from)
|
|
72
|
+
time = Formatters::Time.timestamp(timestamp)
|
|
73
|
+
from_name = @agent_badge.render(from)
|
|
74
|
+
to_name = @agent_badge.render(to)
|
|
75
|
+
|
|
76
|
+
"#{indent}#{@pastel.dim(time)} #{from_name} #{@pastel.yellow("#{UI::Icons::DELEGATE} delegates to")} #{to_name}"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Render delegation result
|
|
80
|
+
# [12:34:56] 📥 Delegation result from worker → architect
|
|
81
|
+
def delegation_result(from:, to:, timestamp:)
|
|
82
|
+
indent = @depth_tracker.indent(to)
|
|
83
|
+
time = Formatters::Time.timestamp(timestamp)
|
|
84
|
+
from_name = @agent_badge.render(from)
|
|
85
|
+
to_name = @agent_badge.render(to)
|
|
86
|
+
|
|
87
|
+
"#{indent}#{@pastel.dim(time)} #{@pastel.green("#{UI::Icons::RESULT} Delegation result")} from #{from_name} #{@pastel.dim("→")} #{to_name}"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Render hook execution
|
|
91
|
+
# [12:34:56] 🪝 Hook executed PreToolUse architect
|
|
92
|
+
def hook_executed(hook_event:, agent:, timestamp:, success:, blocked:)
|
|
93
|
+
indent = @depth_tracker.indent(agent)
|
|
94
|
+
time = Formatters::Time.timestamp(timestamp)
|
|
95
|
+
hook_display = @pastel.cyan(hook_event)
|
|
96
|
+
agent_name = @agent_badge.render(agent)
|
|
97
|
+
|
|
98
|
+
status = if blocked
|
|
99
|
+
@pastel.red("BLOCKED")
|
|
100
|
+
elsif success
|
|
101
|
+
@pastel.green("executed")
|
|
102
|
+
else
|
|
103
|
+
@pastel.yellow("warning")
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
color = if blocked
|
|
107
|
+
:red
|
|
108
|
+
else
|
|
109
|
+
(success ? :green : :yellow)
|
|
110
|
+
end
|
|
111
|
+
icon_colored = @pastel.public_send(color, UI::Icons::HOOK)
|
|
112
|
+
|
|
113
|
+
"#{indent}#{@pastel.dim(time)} #{icon_colored} Hook #{status} #{hook_display} #{agent_name}"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Render usage stats line
|
|
117
|
+
# 5,922 tokens │ $0.0016 │ 1.5% used, 394,078 remaining
|
|
118
|
+
def usage_stats(tokens:, cost:, context_pct: nil, remaining: nil, cumulative: nil, indent: 0)
|
|
119
|
+
prefix = " " * indent
|
|
120
|
+
stats = @usage_stats.render(
|
|
121
|
+
tokens: tokens,
|
|
122
|
+
cost: cost,
|
|
123
|
+
context_pct: context_pct,
|
|
124
|
+
remaining: remaining,
|
|
125
|
+
cumulative: cumulative,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
"#{prefix} #{stats}"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Render tool list
|
|
132
|
+
# Tools available: Read, Write, Bash
|
|
133
|
+
def tools_available(tools, indent: 0)
|
|
134
|
+
return "" if tools.nil? || tools.empty?
|
|
135
|
+
|
|
136
|
+
prefix = " " * indent
|
|
137
|
+
tools_list = tools.join(", ")
|
|
138
|
+
|
|
139
|
+
"#{prefix} #{@pastel.dim("Tools available: #{tools_list}")}"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Render delegation list
|
|
143
|
+
# Can delegate to: frontend_dev, backend_dev
|
|
144
|
+
def delegates_to(agents, indent: 0, color_cache:)
|
|
145
|
+
return "" if agents.nil? || agents.empty?
|
|
146
|
+
|
|
147
|
+
prefix = " " * indent
|
|
148
|
+
agent_badge = Components::AgentBadge.new(pastel: @pastel, color_cache: color_cache)
|
|
149
|
+
delegates_list = agent_badge.render_list(agents)
|
|
150
|
+
|
|
151
|
+
"#{prefix} #{@pastel.dim("Can delegate to:")} #{delegates_list}"
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Render thinking text (italic, indented)
|
|
155
|
+
def thinking_text(content, indent: 0)
|
|
156
|
+
return "" if content.nil? || content.empty?
|
|
157
|
+
|
|
158
|
+
# Strip system reminders
|
|
159
|
+
text = Formatters::Text.strip_system_reminders(content)
|
|
160
|
+
return "" if text.empty?
|
|
161
|
+
|
|
162
|
+
prefix = " " * indent
|
|
163
|
+
|
|
164
|
+
text.split("\n").map do |line|
|
|
165
|
+
"#{prefix} #{@pastel.italic(line)}"
|
|
166
|
+
end.join("\n")
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Render tool arguments
|
|
170
|
+
def tool_arguments(args, indent: 0, truncate: false)
|
|
171
|
+
@content_block.render_hash(args, indent: indent, label: "Arguments", truncate: truncate)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Render tool result content
|
|
175
|
+
def tool_result_content(content, indent: 0, truncate: false)
|
|
176
|
+
@content_block.render_text(
|
|
177
|
+
content,
|
|
178
|
+
indent: indent,
|
|
179
|
+
color: :bright_green,
|
|
180
|
+
truncate: truncate,
|
|
181
|
+
max_lines: 2,
|
|
182
|
+
max_chars: 300,
|
|
183
|
+
)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmCLI
|
|
4
|
+
module UI
|
|
5
|
+
module State
|
|
6
|
+
# Caches agent name → color assignments for consistent coloring
|
|
7
|
+
class AgentColorCache
|
|
8
|
+
# Professional color palette inspired by modern CLIs
|
|
9
|
+
PALETTE = [
|
|
10
|
+
:cyan,
|
|
11
|
+
:magenta,
|
|
12
|
+
:yellow,
|
|
13
|
+
:blue,
|
|
14
|
+
:green,
|
|
15
|
+
:bright_cyan,
|
|
16
|
+
:bright_magenta,
|
|
17
|
+
].freeze
|
|
18
|
+
|
|
19
|
+
def initialize
|
|
20
|
+
@cache = {}
|
|
21
|
+
@next_index = 0
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Get color for agent (cached)
|
|
25
|
+
def get(agent_name)
|
|
26
|
+
@cache[agent_name] ||= assign_next_color
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Reset cache (for testing)
|
|
30
|
+
def reset
|
|
31
|
+
@cache.clear
|
|
32
|
+
@next_index = 0
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def assign_next_color
|
|
38
|
+
color = PALETTE[@next_index % PALETTE.size]
|
|
39
|
+
@next_index += 1
|
|
40
|
+
color
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|