rubino-agent 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +115 -0
- data/.rubocop_todo.yml +955 -0
- data/.ruby-version +1 -0
- data/AGENTS.md +97 -0
- data/CHANGELOG.md +344 -0
- data/CONTRIBUTING.md +69 -0
- data/LICENSE +21 -0
- data/README.md +200 -0
- data/Rakefile +8 -0
- data/docs/agents.md +190 -0
- data/docs/api/v1.md +414 -0
- data/docs/architecture.md +177 -0
- data/docs/commands.md +375 -0
- data/docs/configuration.md +590 -0
- data/docs/getting-started.md +143 -0
- data/docs/jobs.md +332 -0
- data/docs/mcp.md +128 -0
- data/docs/memory.md +98 -0
- data/docs/models-and-keys.md +173 -0
- data/docs/oauth-providers.md +145 -0
- data/docs/plugins.md +195 -0
- data/docs/security.md +145 -0
- data/docs/skills.md +322 -0
- data/docs/tools.md +395 -0
- data/docs/troubleshooting.md +73 -0
- data/exe/rubino +9 -0
- data/install.sh +275 -0
- data/lib/rubino/active_skill.rb +50 -0
- data/lib/rubino/agent/agent_registry.rb +120 -0
- data/lib/rubino/agent/backoff_policy.rb +116 -0
- data/lib/rubino/agent/definition.rb +128 -0
- data/lib/rubino/agent/degenerate_recovery.rb +271 -0
- data/lib/rubino/agent/fallback_chain.rb +194 -0
- data/lib/rubino/agent/iteration_budget.rb +50 -0
- data/lib/rubino/agent/loop.rb +617 -0
- data/lib/rubino/agent/model_call_runner.rb +383 -0
- data/lib/rubino/agent/prompts/build.txt +69 -0
- data/lib/rubino/agent/prompts/compaction.txt +20 -0
- data/lib/rubino/agent/prompts/explore.txt +19 -0
- data/lib/rubino/agent/prompts/general.txt +20 -0
- data/lib/rubino/agent/prompts/plan.txt +31 -0
- data/lib/rubino/agent/response_validator.rb +70 -0
- data/lib/rubino/agent/router.rb +65 -0
- data/lib/rubino/agent/runner.rb +195 -0
- data/lib/rubino/agent/tool_executor.rb +402 -0
- data/lib/rubino/agent/truncation_continuation.rb +137 -0
- data/lib/rubino/api/middleware/auth.rb +43 -0
- data/lib/rubino/api/middleware/error_handler.rb +65 -0
- data/lib/rubino/api/middleware/json_parser.rb +100 -0
- data/lib/rubino/api/middleware/observability.rb +59 -0
- data/lib/rubino/api/middleware/rate_limit.rb +136 -0
- data/lib/rubino/api/operations/approvals/decide_operation.rb +49 -0
- data/lib/rubino/api/operations/clarifications/decide_operation.rb +44 -0
- data/lib/rubino/api/operations/cron_jobs/create_operation.rb +46 -0
- data/lib/rubino/api/operations/cron_jobs/delete_operation.rb +36 -0
- data/lib/rubino/api/operations/cron_jobs/list_operation.rb +55 -0
- data/lib/rubino/api/operations/cron_jobs/pause_operation.rb +34 -0
- data/lib/rubino/api/operations/cron_jobs/resume_operation.rb +34 -0
- data/lib/rubino/api/operations/cron_jobs/schedule_validation.rb +30 -0
- data/lib/rubino/api/operations/cron_jobs/show_operation.rb +32 -0
- data/lib/rubino/api/operations/cron_jobs/trigger_operation.rb +38 -0
- data/lib/rubino/api/operations/cron_jobs/update_operation.rb +42 -0
- data/lib/rubino/api/operations/files/read_operation.rb +40 -0
- data/lib/rubino/api/operations/files/upload_operation.rb +175 -0
- data/lib/rubino/api/operations/health_operation.rb +46 -0
- data/lib/rubino/api/operations/memory/delete_operation.rb +32 -0
- data/lib/rubino/api/operations/memory/index_operation.rb +80 -0
- data/lib/rubino/api/operations/memory/stats_operation.rb +28 -0
- data/lib/rubino/api/operations/metrics_operation.rb +18 -0
- data/lib/rubino/api/operations/mode/show_operation.rb +29 -0
- data/lib/rubino/api/operations/mode/update_operation.rb +42 -0
- data/lib/rubino/api/operations/models/list_operation.rb +45 -0
- data/lib/rubino/api/operations/oauth/connections/disconnect_operation.rb +77 -0
- data/lib/rubino/api/operations/oauth/connections/list_operation.rb +36 -0
- data/lib/rubino/api/operations/oauth/providers/callback_operation.rb +82 -0
- data/lib/rubino/api/operations/oauth/providers/connect_operation.rb +44 -0
- data/lib/rubino/api/operations/oauth/providers/list_operation.rb +35 -0
- data/lib/rubino/api/operations/oauth/serializer.rb +21 -0
- data/lib/rubino/api/operations/runs/create_operation.rb +77 -0
- data/lib/rubino/api/operations/runs/events_operation.rb +195 -0
- data/lib/rubino/api/operations/runs/stop_operation.rb +34 -0
- data/lib/rubino/api/operations/sessions/create_operation.rb +46 -0
- data/lib/rubino/api/operations/sessions/delete_operation.rb +33 -0
- data/lib/rubino/api/operations/sessions/index_operation.rb +82 -0
- data/lib/rubino/api/operations/sessions/retry_operation.rb +45 -0
- data/lib/rubino/api/operations/sessions/show_operation.rb +59 -0
- data/lib/rubino/api/operations/sessions/undo_operation.rb +38 -0
- data/lib/rubino/api/operations/skills/list_operation.rb +34 -0
- data/lib/rubino/api/operations/skills/toggle_operation.rb +40 -0
- data/lib/rubino/api/operations/tasks/index_operation.rb +30 -0
- data/lib/rubino/api/operations/tasks/serializer.rb +60 -0
- data/lib/rubino/api/operations/tasks/show_operation.rb +33 -0
- data/lib/rubino/api/operations/tasks/stop_operation.rb +47 -0
- data/lib/rubino/api/request.rb +54 -0
- data/lib/rubino/api/responses.rb +64 -0
- data/lib/rubino/api/router.rb +72 -0
- data/lib/rubino/api/schemas.rb +103 -0
- data/lib/rubino/api/server.rb +102 -0
- data/lib/rubino/api/tls.rb +108 -0
- data/lib/rubino/attachments/classification.rb +16 -0
- data/lib/rubino/attachments/classify.rb +171 -0
- data/lib/rubino/attachments/defang.rb +47 -0
- data/lib/rubino/attachments/policy.rb +36 -0
- data/lib/rubino/attachments/preamble.rb +120 -0
- data/lib/rubino/boot/encryption_key.rb +32 -0
- data/lib/rubino/cli/chat/bang_shell.rb +257 -0
- data/lib/rubino/cli/chat/completion_builder.rb +290 -0
- data/lib/rubino/cli/chat/idle_card_host.rb +69 -0
- data/lib/rubino/cli/chat/image_inbox.rb +168 -0
- data/lib/rubino/cli/chat/session_resolver.rb +176 -0
- data/lib/rubino/cli/chat_command.rb +1674 -0
- data/lib/rubino/cli/commands.rb +250 -0
- data/lib/rubino/cli/config_command.rb +96 -0
- data/lib/rubino/cli/doctor_command.rb +251 -0
- data/lib/rubino/cli/jobs_command.rb +60 -0
- data/lib/rubino/cli/memory_command.rb +135 -0
- data/lib/rubino/cli/onboarding_wizard.rb +207 -0
- data/lib/rubino/cli/server_command.rb +139 -0
- data/lib/rubino/cli/session_command.rb +125 -0
- data/lib/rubino/cli/setup_command.rb +107 -0
- data/lib/rubino/cli/skills_command.rb +85 -0
- data/lib/rubino/cli/tools_command.rb +81 -0
- data/lib/rubino/cli/trust_gate.rb +71 -0
- data/lib/rubino/commands/built_ins.rb +46 -0
- data/lib/rubino/commands/command.rb +116 -0
- data/lib/rubino/commands/executor.rb +550 -0
- data/lib/rubino/commands/handlers/agents.rb +510 -0
- data/lib/rubino/commands/handlers/config.rb +88 -0
- data/lib/rubino/commands/handlers/help.rb +148 -0
- data/lib/rubino/commands/handlers/jobs.rb +71 -0
- data/lib/rubino/commands/handlers/mcp.rb +229 -0
- data/lib/rubino/commands/handlers/memory.rb +200 -0
- data/lib/rubino/commands/handlers/sessions.rb +207 -0
- data/lib/rubino/commands/handlers/skills.rb +195 -0
- data/lib/rubino/commands/handlers/status.rb +211 -0
- data/lib/rubino/commands/loader.rb +90 -0
- data/lib/rubino/config/configuration.rb +455 -0
- data/lib/rubino/config/defaults.rb +569 -0
- data/lib/rubino/config/loader.rb +115 -0
- data/lib/rubino/config/reasoning_prefs.rb +67 -0
- data/lib/rubino/config/writer.rb +72 -0
- data/lib/rubino/context/compressor.rb +149 -0
- data/lib/rubino/context/environment_inspector.rb +176 -0
- data/lib/rubino/context/file_discovery.rb +45 -0
- data/lib/rubino/context/message_boundary.rb +39 -0
- data/lib/rubino/context/prompt_assembler.rb +382 -0
- data/lib/rubino/context/summary_builder.rb +159 -0
- data/lib/rubino/context/token_budget.rb +68 -0
- data/lib/rubino/context/tool_pair_sanitizer.rb +70 -0
- data/lib/rubino/database/connection.rb +77 -0
- data/lib/rubino/database/migrations/001_create_initial_schema.rb +156 -0
- data/lib/rubino/database/migrations/002_create_runs.rb +45 -0
- data/lib/rubino/database/migrations/003_create_skill_states.rb +15 -0
- data/lib/rubino/database/migrations/004_create_cron_jobs.rb +36 -0
- data/lib/rubino/database/migrations/005_create_oauth_connections.rb +27 -0
- data/lib/rubino/database/migrations/006_create_webhook_deliveries.rb +34 -0
- data/lib/rubino/database/migrations/007_create_messages_fts.rb +59 -0
- data/lib/rubino/database/migrations/008_create_memory_facts.rb +75 -0
- data/lib/rubino/database/migrations/009_create_memory_graph.rb +55 -0
- data/lib/rubino/database/migrations/010_add_owner_pid_to_sessions.rb +20 -0
- data/lib/rubino/database/migrator.rb +48 -0
- data/lib/rubino/documents/converters/csv.rb +79 -0
- data/lib/rubino/documents/converters/docx.rb +129 -0
- data/lib/rubino/documents/converters/html.rb +28 -0
- data/lib/rubino/documents/converters/json.rb +35 -0
- data/lib/rubino/documents/converters/pdf.rb +59 -0
- data/lib/rubino/documents/converters/plain.rb +68 -0
- data/lib/rubino/documents/converters/pptx.rb +64 -0
- data/lib/rubino/documents/converters/xlsx.rb +62 -0
- data/lib/rubino/documents/converters/xml.rb +45 -0
- data/lib/rubino/documents/html.rb +71 -0
- data/lib/rubino/documents/registry.rb +68 -0
- data/lib/rubino/documents/table.rb +63 -0
- data/lib/rubino/documents.rb +50 -0
- data/lib/rubino/errors.rb +119 -0
- data/lib/rubino/files/workspace.rb +93 -0
- data/lib/rubino/interaction/cancel_token.rb +43 -0
- data/lib/rubino/interaction/clipboard_image.rb +84 -0
- data/lib/rubino/interaction/event_bus.rb +48 -0
- data/lib/rubino/interaction/events.rb +101 -0
- data/lib/rubino/interaction/image_input.rb +127 -0
- data/lib/rubino/interaction/input_queue.rb +117 -0
- data/lib/rubino/interaction/lifecycle.rb +299 -0
- data/lib/rubino/interaction/probe.rb +65 -0
- data/lib/rubino/interaction/state.rb +56 -0
- data/lib/rubino/jobs/cron_job_repository.rb +75 -0
- data/lib/rubino/jobs/handlers/cleanup_sessions_job.rb +32 -0
- data/lib/rubino/jobs/handlers/compact_session_job.rb +21 -0
- data/lib/rubino/jobs/handlers/distill_skill_job.rb +186 -0
- data/lib/rubino/jobs/handlers/extract_memory_job.rb +37 -0
- data/lib/rubino/jobs/handlers/summarize_session_job.rb +21 -0
- data/lib/rubino/jobs/queue.rb +184 -0
- data/lib/rubino/jobs/registry.rb +45 -0
- data/lib/rubino/jobs/runner.rb +79 -0
- data/lib/rubino/jobs/scheduler.rb +138 -0
- data/lib/rubino/jobs/webhook_delivery.rb +225 -0
- data/lib/rubino/jobs/worker.rb +59 -0
- data/lib/rubino/llm/adapter_factory.rb +47 -0
- data/lib/rubino/llm/adapter_response.rb +65 -0
- data/lib/rubino/llm/auxiliary_client.rb +61 -0
- data/lib/rubino/llm/bedrock_bearer_client.rb +235 -0
- data/lib/rubino/llm/content_builder.rb +55 -0
- data/lib/rubino/llm/credential_check.rb +93 -0
- data/lib/rubino/llm/error_classifier.rb +364 -0
- data/lib/rubino/llm/fake_provider.rb +292 -0
- data/lib/rubino/llm/inline_think_filter.rb +58 -0
- data/lib/rubino/llm/model_catalog.rb +29 -0
- data/lib/rubino/llm/provider_resolver.rb +48 -0
- data/lib/rubino/llm/reasoning_manager.rb +100 -0
- data/lib/rubino/llm/request.rb +56 -0
- data/lib/rubino/llm/ruby_llm_adapter.rb +794 -0
- data/lib/rubino/llm/scenario_loader.rb +68 -0
- data/lib/rubino/llm/scenario_selector.rb +80 -0
- data/lib/rubino/llm/scenarios/agent-creates-cron-failure.yml +29 -0
- data/lib/rubino/llm/scenarios/agent-creates-cron.yml +36 -0
- data/lib/rubino/llm/scenarios/analysis.yml +501 -0
- data/lib/rubino/llm/scenarios/complex-analysis.yml +598 -0
- data/lib/rubino/llm/scenarios/failure.yml +65 -0
- data/lib/rubino/llm/scenarios/happy-path.yml +24 -0
- data/lib/rubino/llm/scenarios/provider-quota-completed.yml +14 -0
- data/lib/rubino/llm/scenarios/wide-table.yml +121 -0
- data/lib/rubino/llm/scenarios/with-approvals.yml +50 -0
- data/lib/rubino/llm/scenarios/with-artifacts.yml +98 -0
- data/lib/rubino/llm/scenarios/with-clarify.yml +32 -0
- data/lib/rubino/llm/scenarios/with-reasoning.yml +175 -0
- data/lib/rubino/llm/scenarios/with-uploads.yml +104 -0
- data/lib/rubino/llm/thinking_support.rb +84 -0
- data/lib/rubino/llm/tool_bridge.rb +89 -0
- data/lib/rubino/logger.rb +99 -0
- data/lib/rubino/mcp/manager.rb +180 -0
- data/lib/rubino/mcp/mcp_tool_wrapper.rb +69 -0
- data/lib/rubino/mcp.rb +57 -0
- data/lib/rubino/memory/backend.rb +104 -0
- data/lib/rubino/memory/backends/default.rb +101 -0
- data/lib/rubino/memory/backends/sqlite.rb +653 -0
- data/lib/rubino/memory/backends.rb +53 -0
- data/lib/rubino/memory/deduplicator.rb +74 -0
- data/lib/rubino/memory/extractor.rb +85 -0
- data/lib/rubino/memory/flusher.rb +31 -0
- data/lib/rubino/memory/retriever.rb +50 -0
- data/lib/rubino/memory/sqlite_extraction_prompt.rb +70 -0
- data/lib/rubino/memory/sqlite_graph.rb +154 -0
- data/lib/rubino/memory/store.rb +228 -0
- data/lib/rubino/memory/threat_scanner.rb +68 -0
- data/lib/rubino/metrics.rb +175 -0
- data/lib/rubino/modes.rb +93 -0
- data/lib/rubino/oauth/connection_repository.rb +95 -0
- data/lib/rubino/oauth/provider/github.rb +75 -0
- data/lib/rubino/oauth/provider/google.rb +59 -0
- data/lib/rubino/oauth/provider.rb +149 -0
- data/lib/rubino/oauth/registry.rb +86 -0
- data/lib/rubino/oauth/token_encryptor.rb +87 -0
- data/lib/rubino/plugins/registry.rb +75 -0
- data/lib/rubino/plugins.rb +86 -0
- data/lib/rubino/run/approval_gate.rb +243 -0
- data/lib/rubino/run/attachment_downloader.rb +166 -0
- data/lib/rubino/run/event_store.rb +74 -0
- data/lib/rubino/run/executor.rb +383 -0
- data/lib/rubino/run/gate_registry.rb +39 -0
- data/lib/rubino/run/recorder.rb +69 -0
- data/lib/rubino/run/repository.rb +118 -0
- data/lib/rubino/run/session_approval_cache.rb +118 -0
- data/lib/rubino/security/allowlist_persister.rb +55 -0
- data/lib/rubino/security/approval_policy.rb +227 -0
- data/lib/rubino/security/command_allowlist.rb +24 -0
- data/lib/rubino/security/dangerous_patterns.rb +118 -0
- data/lib/rubino/security/deny_persister.rb +73 -0
- data/lib/rubino/security/doom_loop_detector.rb +43 -0
- data/lib/rubino/security/hardline_guard.rb +105 -0
- data/lib/rubino/security/pattern_matcher.rb +62 -0
- data/lib/rubino/security/prefix_deriver.rb +124 -0
- data/lib/rubino/security/readonly_commands.rb +211 -0
- data/lib/rubino/session/exporter.rb +101 -0
- data/lib/rubino/session/message.rb +77 -0
- data/lib/rubino/session/repository.rb +295 -0
- data/lib/rubino/session/store.rb +198 -0
- data/lib/rubino/session/summary_store.rb +65 -0
- data/lib/rubino/skills/prompt_index.rb +85 -0
- data/lib/rubino/skills/registry.rb +208 -0
- data/lib/rubino/skills/skill.rb +176 -0
- data/lib/rubino/skills/skill_tool.rb +215 -0
- data/lib/rubino/skills/state_repository.rb +37 -0
- data/lib/rubino/skills/toggle.rb +26 -0
- data/lib/rubino/tools/answer_child_tool.rb +83 -0
- data/lib/rubino/tools/ask_parent_tool.rb +232 -0
- data/lib/rubino/tools/attach_file_tool.rb +120 -0
- data/lib/rubino/tools/background_tasks.rb +520 -0
- data/lib/rubino/tools/base.rb +222 -0
- data/lib/rubino/tools/custom_tool_loader.rb +119 -0
- data/lib/rubino/tools/edit_tool.rb +122 -0
- data/lib/rubino/tools/git_tool.rb +71 -0
- data/lib/rubino/tools/github_tool.rb +233 -0
- data/lib/rubino/tools/glob_tool.rb +69 -0
- data/lib/rubino/tools/grep_tool.rb +206 -0
- data/lib/rubino/tools/memory_tool.rb +184 -0
- data/lib/rubino/tools/multi_edit_tool.rb +110 -0
- data/lib/rubino/tools/patch_tool.rb +260 -0
- data/lib/rubino/tools/probe_tool.rb +175 -0
- data/lib/rubino/tools/question_tool.rb +128 -0
- data/lib/rubino/tools/read_attachment_tool.rb +180 -0
- data/lib/rubino/tools/read_tool.rb +212 -0
- data/lib/rubino/tools/read_tracker.rb +98 -0
- data/lib/rubino/tools/registry.rb +166 -0
- data/lib/rubino/tools/result.rb +113 -0
- data/lib/rubino/tools/ruby_tool.rb +0 -0
- data/lib/rubino/tools/session_search_tool.rb +103 -0
- data/lib/rubino/tools/shell_input_tool.rb +96 -0
- data/lib/rubino/tools/shell_kill_tool.rb +76 -0
- data/lib/rubino/tools/shell_output_tool.rb +72 -0
- data/lib/rubino/tools/shell_registry.rb +158 -0
- data/lib/rubino/tools/shell_tail_tool.rb +118 -0
- data/lib/rubino/tools/shell_tool.rb +330 -0
- data/lib/rubino/tools/steer_tool.rb +118 -0
- data/lib/rubino/tools/subagent_probe.rb +89 -0
- data/lib/rubino/tools/summarize_file_tool.rb +182 -0
- data/lib/rubino/tools/task_result_tool.rb +90 -0
- data/lib/rubino/tools/task_stop_tool.rb +80 -0
- data/lib/rubino/tools/task_tool.rb +622 -0
- data/lib/rubino/tools/test_tool.rb +454 -0
- data/lib/rubino/tools/todo_tool.rb +93 -0
- data/lib/rubino/tools/tool_call_repository.rb +33 -0
- data/lib/rubino/tools/vision_tool.rb +85 -0
- data/lib/rubino/tools/webfetch_tool.rb +153 -0
- data/lib/rubino/tools/websearch_tool.rb +179 -0
- data/lib/rubino/tools/write_tool.rb +61 -0
- data/lib/rubino/trust.rb +88 -0
- data/lib/rubino/ui/api.rb +296 -0
- data/lib/rubino/ui/base.rb +252 -0
- data/lib/rubino/ui/bottom_composer.rb +1599 -0
- data/lib/rubino/ui/cli.rb +1987 -0
- data/lib/rubino/ui/completion_menu.rb +321 -0
- data/lib/rubino/ui/completion_source.rb +284 -0
- data/lib/rubino/ui/escape_reader.rb +169 -0
- data/lib/rubino/ui/indented_io.rb +88 -0
- data/lib/rubino/ui/input_history.rb +108 -0
- data/lib/rubino/ui/live_region.rb +183 -0
- data/lib/rubino/ui/markdown_renderer.rb +506 -0
- data/lib/rubino/ui/notifier.rb +163 -0
- data/lib/rubino/ui/null.rb +195 -0
- data/lib/rubino/ui/paste_store.rb +176 -0
- data/lib/rubino/ui/printer_base.rb +79 -0
- data/lib/rubino/ui/probe_wait_indicator.rb +75 -0
- data/lib/rubino/ui/queued_indicators.rb +66 -0
- data/lib/rubino/ui/status_bar.rb +100 -0
- data/lib/rubino/ui/stdout_proxy.rb +161 -0
- data/lib/rubino/ui/streaming_markdown.rb +186 -0
- data/lib/rubino/ui/subagent_cards.rb +134 -0
- data/lib/rubino/ui/subagent_view.rb +255 -0
- data/lib/rubino/ui.rb +21 -0
- data/lib/rubino/update_check.rb +187 -0
- data/lib/rubino/util/duration.rb +23 -0
- data/lib/rubino/util/hyperlink.rb +105 -0
- data/lib/rubino/util/output.rb +145 -0
- data/lib/rubino/util/secrets_mask.rb +83 -0
- data/lib/rubino/version.rb +5 -0
- data/lib/rubino/workspace.rb +85 -0
- data/lib/rubino-agent.rb +5 -0
- data/lib/rubino.rb +318 -0
- data/mise.toml +2 -0
- data/rubino-agent.gemspec +103 -0
- data/skills/ruby-expert/SKILL.md +67 -0
- data/skills/ruby-expert/references/concurrency.md +357 -0
- data/skills/ruby-expert/references/datetime-and-encoding.md +363 -0
- data/skills/ruby-expert/references/errors-and-types.md +460 -0
- data/skills/ruby-expert/references/gem-authoring.md +459 -0
- data/skills/ruby-expert/references/language-idioms.md +465 -0
- data/skills/ruby-expert/references/metaprogramming.md +339 -0
- data/skills/ruby-expert/references/oo-design.md +553 -0
- data/skills/ruby-expert/references/performance.md +383 -0
- data/skills/ruby-expert/references/rails.md +424 -0
- data/skills/ruby-expert/references/security.md +404 -0
- data/skills/ruby-expert/references/testing.md +473 -0
- data/skills/ruby-expert/references/tooling.md +466 -0
- metadata +856 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubino
|
|
4
|
+
module UI
|
|
5
|
+
# An IO-shaped shim that routes everything written to it through a
|
|
6
|
+
# {BottomComposer#print_above}, so the ~30 existing +$stdout.print/puts+ call
|
|
7
|
+
# sites across UI::CLI / PrinterBase need ZERO changes. While a turn is
|
|
8
|
+
# active, the chat command swaps +$stdout+ for one of these (prompt_toolkit's
|
|
9
|
+
# +StdoutProxy+ model); on turn end it swaps the real IO back.
|
|
10
|
+
#
|
|
11
|
+
# Line buffering — the critical streaming nuance:
|
|
12
|
+
# UI::CLI#stream emits PARTIAL tokens with NO trailing newline during model
|
|
13
|
+
# streaming. A naive "print each write above the prompt" would scroll every
|
|
14
|
+
# token onto its own row. Instead we hold the in-progress line in
|
|
15
|
+
# +@partial+ and re-render it (the accumulating line) ABOVE the composer via
|
|
16
|
+
# {BottomComposer#set_partial} as it grows — a transient row redrawn in
|
|
17
|
+
# place — committing it to scrollback (via {BottomComposer#print_above})
|
|
18
|
+
# only when a newline arrives. The way prompt_toolkit buffers and batches:
|
|
19
|
+
# each newline-terminated segment becomes one committed row; the trailing
|
|
20
|
+
# partial keeps showing live.
|
|
21
|
+
#
|
|
22
|
+
# The render mutex lives in the composer, so concurrent writes from the
|
|
23
|
+
# streaming thread and keystroke redraws stay serialized.
|
|
24
|
+
class StdoutProxy
|
|
25
|
+
# @param composer [BottomComposer] coordinator that owns print_above.
|
|
26
|
+
def initialize(composer)
|
|
27
|
+
@composer = composer
|
|
28
|
+
@partial = +""
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# The two methods UI code actually uses are #print and #puts; #write backs
|
|
32
|
+
# both formattings and is also what e.g. StringIO/IO duck-typers call.
|
|
33
|
+
def write(*args)
|
|
34
|
+
args.sum do |a|
|
|
35
|
+
append(a.to_s)
|
|
36
|
+
a.to_s.bytesize
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def print(*args)
|
|
41
|
+
args.each { |a| append(a.to_s) }
|
|
42
|
+
nil
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def puts(*args)
|
|
46
|
+
if args.empty?
|
|
47
|
+
append("\n")
|
|
48
|
+
else
|
|
49
|
+
args.each do |a|
|
|
50
|
+
if a.is_a?(Array)
|
|
51
|
+
a.each { |e| puts(e) }
|
|
52
|
+
else
|
|
53
|
+
# Append the line and its terminating newline in ONE append so the
|
|
54
|
+
# text commits straight to scrollback. Appending them separately
|
|
55
|
+
# showed the line as a TRANSIENT partial row below the subagent
|
|
56
|
+
# cards for a frame before the commit moved it above them — the
|
|
57
|
+
# user saw the same line twice around the live card block (#153).
|
|
58
|
+
s = a.to_s
|
|
59
|
+
append(s.end_with?("\n") ? s : "#{s}\n")
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
nil
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def printf(format, *)
|
|
67
|
+
append(format(format, *))
|
|
68
|
+
nil
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def <<(obj)
|
|
72
|
+
append(obj.to_s)
|
|
73
|
+
self
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Streaming writers call flush after each token. We treat flush as "show
|
|
77
|
+
# what you have now": re-render the accumulating partial line above the
|
|
78
|
+
# composer so streamed text appears live, without committing it to
|
|
79
|
+
# scrollback (it has no newline yet).
|
|
80
|
+
def flush
|
|
81
|
+
render_partial
|
|
82
|
+
self
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# REPLACE the live region with +str+ (replace, not accumulate). The normal
|
|
86
|
+
# #append path GROWS @partial — right for token-by-token line buffering, but
|
|
87
|
+
# wrong for the streaming-markdown tail, which is the WHOLE in-progress block
|
|
88
|
+
# re-shown each time it changes. So we reset our own buffer and hand the raw
|
|
89
|
+
# tail straight to the composer's transient row. Used by UI::CLI#stream to
|
|
90
|
+
# show the incomplete block live while completed blocks commit above it.
|
|
91
|
+
def live(str)
|
|
92
|
+
@partial = +""
|
|
93
|
+
@composer.set_partial(str.to_s)
|
|
94
|
+
self
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Commit any held partial line as a final row. Called when the proxy is
|
|
98
|
+
# torn down so an unterminated last line (e.g. a stream that ended without
|
|
99
|
+
# stream_end) isn't lost.
|
|
100
|
+
def finish
|
|
101
|
+
return if @partial.empty?
|
|
102
|
+
|
|
103
|
+
line = @partial
|
|
104
|
+
@partial = +""
|
|
105
|
+
@composer.print_above(line)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Best-effort IO compatibility for code that probes the stream.
|
|
109
|
+
def tty? = false
|
|
110
|
+
def isatty = false
|
|
111
|
+
def sync = true
|
|
112
|
+
def fileno = nil
|
|
113
|
+
|
|
114
|
+
# A faithful IO duck MUST answer #close: stdlib Logger::LogDevice treats a
|
|
115
|
+
# logdev that responds to :write but NOT :close as a FILENAME and does
|
|
116
|
+
# File.open(it) → "no implicit conversion of StdoutProxy into String" if a
|
|
117
|
+
# Logger is ever built against $stdout while we hold the swap. No-op close.
|
|
118
|
+
def close; end
|
|
119
|
+
def closed? = false
|
|
120
|
+
|
|
121
|
+
def sync=(_)
|
|
122
|
+
true
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
private
|
|
126
|
+
|
|
127
|
+
# Accumulate text, committing each complete (newline-terminated) line to
|
|
128
|
+
# scrollback via print_above and keeping any trailing remainder as the live
|
|
129
|
+
# partial. The partial is shown via #flush; many writers flush right after,
|
|
130
|
+
# but we also render it here so a partial that arrives without a following
|
|
131
|
+
# flush still appears.
|
|
132
|
+
def append(str)
|
|
133
|
+
return if str.nil? || str.empty?
|
|
134
|
+
|
|
135
|
+
@partial << str
|
|
136
|
+
commit_complete_lines
|
|
137
|
+
render_partial
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def commit_complete_lines
|
|
141
|
+
while (idx = @partial.index("\n"))
|
|
142
|
+
line = @partial[0...idx]
|
|
143
|
+
@partial = @partial[(idx + 1)..] || +""
|
|
144
|
+
# A committed line is a finished row; embedded "\r" (e.g. the CLI's
|
|
145
|
+
# in-place clear before a streamed chunk) is preserved so print_above's
|
|
146
|
+
# clear-line semantics still apply.
|
|
147
|
+
@composer.print_above(line)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Show the in-progress (un-newlined) line above the composer without
|
|
152
|
+
# committing it. set_partial renders it on a transient row directly above
|
|
153
|
+
# the input line, redrawn in place — so the live partial grows in place as
|
|
154
|
+
# tokens stream in rather than scrolling a copy per token. When the partial
|
|
155
|
+
# is empty (just committed a line), clear the transient row.
|
|
156
|
+
def render_partial
|
|
157
|
+
@composer.set_partial(@partial)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubino
|
|
4
|
+
module UI
|
|
5
|
+
# Incremental block splitter for streamed markdown.
|
|
6
|
+
#
|
|
7
|
+
# The model streams an assistant message token-by-token; we want to render
|
|
8
|
+
# COMPLETED markdown blocks above the composer as soon as they finish, while
|
|
9
|
+
# showing the still-incoming (incomplete) block raw in the live region. This
|
|
10
|
+
# buffer accumulates streamed text and decides where one block ends and the
|
|
11
|
+
# next begins, so {UI::CLI} can render+commit a finished block and leave only
|
|
12
|
+
# the in-progress tail live.
|
|
13
|
+
#
|
|
14
|
+
# Block boundary detection — a small line-oriented fence state machine (the
|
|
15
|
+
# mainstream approach used by md2term / mdterm / Glamour-style streamers: you
|
|
16
|
+
# must NOT render a fenced code block until its closing ``` arrives, or
|
|
17
|
+
# half-open fences render as garbage):
|
|
18
|
+
#
|
|
19
|
+
# * Lines are split on "\n". A line is "complete" once its terminating "\n"
|
|
20
|
+
# has been seen; the trailing remainder (no "\n" yet) is the live tail.
|
|
21
|
+
# * A line matching ^\s*``` toggles the fence state.
|
|
22
|
+
# - Entering a fence STARTS a code block (the fence line joins it).
|
|
23
|
+
# - Leaving a fence ENDS the code block (the closing fence joins it);
|
|
24
|
+
# the block is reported complete.
|
|
25
|
+
# * While INSIDE a fence, blank lines do NOT split — code keeps its blanks.
|
|
26
|
+
# * While OUTSIDE a fence, a blank line ENDS the current prose block. The
|
|
27
|
+
# blank line itself is consumed as the separator (not re-emitted).
|
|
28
|
+
#
|
|
29
|
+
# API:
|
|
30
|
+
# feed(text) -> Array<String> newly-completed block texts (state advances)
|
|
31
|
+
# tail -> String the current incomplete block, raw (live)
|
|
32
|
+
# flush -> String|nil the remaining buffered block on stream end
|
|
33
|
+
# (an unclosed fence is returned so the caller
|
|
34
|
+
# can emit it as plain text — never lost)
|
|
35
|
+
class StreamingMarkdown
|
|
36
|
+
FENCE_RE = /\A\s*```/
|
|
37
|
+
# An ordered ("1. ", "2) ") or unordered ("- ", "* ", "+ ") list item.
|
|
38
|
+
# Used so a loose list (blank lines BETWEEN items) is kept as ONE block
|
|
39
|
+
# instead of being split per-item: each split item was re-rendered on its
|
|
40
|
+
# own, and kramdown restarts ordered numbering at 1 for every block, which
|
|
41
|
+
# produced the "1. Mercury / 1. Venus / 1. Earth" off-by-one (B4).
|
|
42
|
+
LIST_ITEM_RE = /\A\s*(?:[-*+]|\d+[.)])\s/
|
|
43
|
+
|
|
44
|
+
def initialize
|
|
45
|
+
@pending = +"" # un-newlined remainder (the live tail-in-progress line)
|
|
46
|
+
@block = [] # completed lines accumulated for the current block
|
|
47
|
+
@in_fence = false
|
|
48
|
+
@in_list = false # current block is a markdown list (keep loose items together)
|
|
49
|
+
@blanks = 0 # blank lines buffered inside a list, re-emitted iff it continues
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Accumulate streamed text; return the list of block texts that became
|
|
53
|
+
# COMPLETE as a result of this feed (possibly empty). Advances state.
|
|
54
|
+
def feed(text)
|
|
55
|
+
return [] if text.nil? || text.empty?
|
|
56
|
+
|
|
57
|
+
@pending << text
|
|
58
|
+
completed = []
|
|
59
|
+
|
|
60
|
+
while (idx = @pending.index("\n"))
|
|
61
|
+
line = @pending[0...idx]
|
|
62
|
+
@pending = @pending[(idx + 1)..] || +""
|
|
63
|
+
block = consume_line(line)
|
|
64
|
+
completed << block if block
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
completed
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# The current incomplete block as raw text: any lines already buffered for
|
|
71
|
+
# the in-progress block plus the un-newlined remainder. Shown live; it gets
|
|
72
|
+
# re-rendered + committed once its block completes.
|
|
73
|
+
def tail
|
|
74
|
+
parts = @block.dup
|
|
75
|
+
parts << @pending unless @pending.empty?
|
|
76
|
+
parts.join("\n")
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# The in-progress tail to show live (raw): the LAST +rows+ lines of the
|
|
80
|
+
# in-flight block — its most recent already-newlined lines plus the
|
|
81
|
+
# un-newlined remainder. Newline-joined; the live region renders one row
|
|
82
|
+
# per line.
|
|
83
|
+
#
|
|
84
|
+
# Why a rolling window and not the whole #tail: the live region must stay
|
|
85
|
+
# bounded (a long open fence/table must never push the prompt off-screen),
|
|
86
|
+
# so we keep "only the last block can change" (Textual/Rich, Streamdown,
|
|
87
|
+
# Glamour-style streamers) but show a FEW trailing lines instead of just
|
|
88
|
+
# the one being typed — a long list block used to vanish line-by-line as
|
|
89
|
+
# each item completed, leaving a single flickering raw line until the
|
|
90
|
+
# whole block committed (#127). Earlier lines stay buffered and the block
|
|
91
|
+
# still snaps to rendered markdown the moment it completes.
|
|
92
|
+
def live_tail(rows = 1)
|
|
93
|
+
lines = @block.last(rows)
|
|
94
|
+
lines += [@pending] unless @pending.empty?
|
|
95
|
+
lines.last(rows).join("\n")
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Drain the remainder on stream end. Promotes any un-newlined remainder to
|
|
99
|
+
# a final line, then returns the buffered block text (or nil if empty). An
|
|
100
|
+
# unclosed fence is returned all the same — the caller emits it as plain so
|
|
101
|
+
# output is never dropped.
|
|
102
|
+
def flush
|
|
103
|
+
unless @pending.empty?
|
|
104
|
+
@block << @pending
|
|
105
|
+
@pending = +""
|
|
106
|
+
end
|
|
107
|
+
return nil if @block.empty?
|
|
108
|
+
|
|
109
|
+
text = @block.join("\n")
|
|
110
|
+
@block = []
|
|
111
|
+
@in_fence = false
|
|
112
|
+
@in_list = false
|
|
113
|
+
@blanks = 0
|
|
114
|
+
text
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
|
|
119
|
+
# Feed one complete (newline-stripped) line through the state machine.
|
|
120
|
+
# Returns the finished block's text when this line closes a block, else nil.
|
|
121
|
+
def consume_line(line)
|
|
122
|
+
if @in_fence
|
|
123
|
+
@block << line
|
|
124
|
+
if line.match?(FENCE_RE) # closing fence ends the code block
|
|
125
|
+
@in_fence = false
|
|
126
|
+
return take_block
|
|
127
|
+
end
|
|
128
|
+
return nil
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
if line.match?(FENCE_RE) # opening fence starts a code block
|
|
132
|
+
@in_fence = true
|
|
133
|
+
flush_blanks
|
|
134
|
+
@block << line
|
|
135
|
+
return nil
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
if line.strip.empty?
|
|
139
|
+
# A blank line inside a list is BUFFERED, not a separator: it only ends
|
|
140
|
+
# the block if the list doesn't continue (handled when the next
|
|
141
|
+
# non-blank, non-item line arrives, or at flush). Outside a list a
|
|
142
|
+
# blank line ends the current prose block (separator consumed).
|
|
143
|
+
if @in_list
|
|
144
|
+
@blanks += 1
|
|
145
|
+
return nil
|
|
146
|
+
end
|
|
147
|
+
return nil if @block.empty?
|
|
148
|
+
|
|
149
|
+
return take_block
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
is_item = line.match?(LIST_ITEM_RE)
|
|
153
|
+
|
|
154
|
+
# A non-item line after a blank-separated list closes the list block
|
|
155
|
+
# first (so the list renders as one well-numbered unit), then this line
|
|
156
|
+
# starts a fresh block — its buffered blanks are dropped as the separator.
|
|
157
|
+
if @in_list && !is_item && @blanks.positive?
|
|
158
|
+
@blanks = 0 # drop the trailing blank(s) that separated list from this line
|
|
159
|
+
finished = take_block
|
|
160
|
+
@block << line
|
|
161
|
+
return finished
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
flush_blanks
|
|
165
|
+
@in_list = true if is_item
|
|
166
|
+
@block << line
|
|
167
|
+
nil
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Re-emit blank lines buffered inside a continuing list so loose-list
|
|
171
|
+
# spacing is preserved in the committed block text.
|
|
172
|
+
def flush_blanks
|
|
173
|
+
@blanks.times { @block << "" }
|
|
174
|
+
@blanks = 0
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def take_block
|
|
178
|
+
flush_blanks
|
|
179
|
+
text = @block.join("\n")
|
|
180
|
+
@block = []
|
|
181
|
+
@in_list = false
|
|
182
|
+
text
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pastel"
|
|
4
|
+
|
|
5
|
+
module Rubino
|
|
6
|
+
module UI
|
|
7
|
+
# Formats BackgroundTasks registry entries into the COLLAPSED LIVE CARDS the
|
|
8
|
+
# parent shows while one or more background subagents run (Variant A of the
|
|
9
|
+
# orchestration-UX blueprint). This is the single source of card text: the
|
|
10
|
+
# live region (UI::CLI#set_subagent_cards → BottomComposer) renders it while a
|
|
11
|
+
# turn runs, and the /agents drill-in reuses the same formatter for the
|
|
12
|
+
# expanded view. Pure formatting — it never touches the registry mutex itself
|
|
13
|
+
# (callers pass a snapshot) and writes nothing; the renderer decides where the
|
|
14
|
+
# lines go.
|
|
15
|
+
#
|
|
16
|
+
# Collapsed card (one row per running subagent, updates in place):
|
|
17
|
+
# ▸ sa_9ae4 · explore · running · 14 tools · 38s · grep "def authenticate"
|
|
18
|
+
# plus a single shared hint line under the block.
|
|
19
|
+
#
|
|
20
|
+
# An entry parked on a human approval shows the approval prominently instead:
|
|
21
|
+
# ● sa_9ae4 · explore · needs approval · shell rm -rf build
|
|
22
|
+
#
|
|
23
|
+
# Up to MAX_CARDS cards stack; a longer list collapses the overflow into a
|
|
24
|
+
# "+N more" tail so the live region stays bounded (and the single-row clamp
|
|
25
|
+
# in the composer never has to host an unbounded block).
|
|
26
|
+
class SubagentCards
|
|
27
|
+
# Cap the live block so it never grows past the registry's own
|
|
28
|
+
# MAX_CONCURRENT (3) live children — but defend against a stale/over-long
|
|
29
|
+
# list anyway with an explicit overflow tail.
|
|
30
|
+
MAX_CARDS = Tools::BackgroundTasks::MAX_CONCURRENT
|
|
31
|
+
|
|
32
|
+
# Collapsed glyph (a running card) / approval glyph (needs the human) /
|
|
33
|
+
# BLOCKED glyph (an escalated ask_parent waiting on the human — RESERVED for
|
|
34
|
+
# "the tree is blocked on you" and nothing else, the distinct-signal rule).
|
|
35
|
+
COLLAPSED = "▸"
|
|
36
|
+
APPROVAL = "●"
|
|
37
|
+
BLOCKED = "⛔"
|
|
38
|
+
|
|
39
|
+
def initialize(pastel: Pastel.new)
|
|
40
|
+
@pastel = pastel
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Renders the live CARD BLOCK for the running (or approval-pending)
|
|
44
|
+
# children in +entries+ as an array of ready-to-print lines. Returns [] when
|
|
45
|
+
# nothing is live, so the renderer can clear the region. +entries+ is a
|
|
46
|
+
# snapshot (BackgroundTasks#running) taken under the registry mutex by the
|
|
47
|
+
# caller — this method only reads the plain struct fields.
|
|
48
|
+
def card_lines(entries)
|
|
49
|
+
live = entries.select { |e| live?(e) }
|
|
50
|
+
return [] if live.empty?
|
|
51
|
+
|
|
52
|
+
shown = live.first(MAX_CARDS)
|
|
53
|
+
overflow = live.size - shown.size
|
|
54
|
+
lines = shown.map { |e| card_line(e) }
|
|
55
|
+
lines << @pastel.dim(" + #{overflow} more · /agents") if overflow.positive?
|
|
56
|
+
lines << hint_line(shown)
|
|
57
|
+
lines
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# One collapsed card row for a single entry.
|
|
61
|
+
def card_line(entry)
|
|
62
|
+
if entry.status == :blocked_on_human
|
|
63
|
+
blocked_card_line(entry)
|
|
64
|
+
elsif entry.status == :needs_approval
|
|
65
|
+
approval_card_line(entry)
|
|
66
|
+
else
|
|
67
|
+
glyph = @pastel.cyan(COLLAPSED)
|
|
68
|
+
state = entry.status == :stopping ? "stopping" : "running"
|
|
69
|
+
count = entry.tool_count.to_i
|
|
70
|
+
body = "#{entry.id} · #{entry.subagent} · #{state} · " \
|
|
71
|
+
"#{count} tool#{"s" if count != 1} · #{elapsed(entry)}"
|
|
72
|
+
body += " · #{entry.last_activity}" unless entry.last_activity.to_s.empty?
|
|
73
|
+
" #{glyph} #{body}"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# A card for a child parked on an escalated ask_parent — the ⛔ "tree is
|
|
78
|
+
# blocked on YOU" row, the loudest state. Leads with the red ⛔ glyph and
|
|
79
|
+
# the question, and points at /reply <id> (the answer verb), distinct from
|
|
80
|
+
# the approval row's /agents <id>.
|
|
81
|
+
def blocked_card_line(entry)
|
|
82
|
+
glyph = @pastel.red(BLOCKED)
|
|
83
|
+
question = entry.ask_question.to_s
|
|
84
|
+
" #{glyph} #{entry.id} · #{entry.subagent} · " +
|
|
85
|
+
@pastel.red("waiting on you") + ": #{first_line(question, 60)} " \
|
|
86
|
+
"· /reply #{entry.id}"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# A card for a child parked on a human approval — the approval is the most
|
|
90
|
+
# important thing on the row, so it leads (amber ●) with the command.
|
|
91
|
+
def approval_card_line(entry)
|
|
92
|
+
glyph = @pastel.yellow(APPROVAL)
|
|
93
|
+
command = entry.approval_command.to_s
|
|
94
|
+
command = entry.approval_question.to_s if command.empty?
|
|
95
|
+
" #{glyph} #{entry.id} · #{entry.subagent} · " +
|
|
96
|
+
@pastel.yellow("needs approval") + ": #{first_line(command, 60)} " \
|
|
97
|
+
"· /agents #{entry.id}"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
def live?(entry)
|
|
103
|
+
%i[running needs_approval blocked_on_human stopping].include?(entry.status)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Shared hint under the block. When something needs approval the hint leads
|
|
107
|
+
# with the answer affordance; otherwise it's the watch/stop hint.
|
|
108
|
+
def hint_line(shown)
|
|
109
|
+
blocked = shown.count { |e| e.status == :blocked_on_human }
|
|
110
|
+
if blocked.positive?
|
|
111
|
+
@pastel.red(" \u26d4 #{blocked} subagent waiting on you · /reply <id> to answer")
|
|
112
|
+
elsif shown.any? { |e| e.status == :needs_approval }
|
|
113
|
+
@pastel.dim(" └ /agents <id> to approve · --stop to cancel")
|
|
114
|
+
else
|
|
115
|
+
@pastel.dim(" └ /agents <id> to watch · --stop to cancel")
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def elapsed(entry)
|
|
120
|
+
return "" unless entry.started_at
|
|
121
|
+
|
|
122
|
+
finish = entry.finished_at || Time.now
|
|
123
|
+
Rubino::Util::Duration.human_duration(finish - entry.started_at)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# First NON-BLANK line, elided to +max+. A ruby/shell approval command
|
|
127
|
+
# often starts with a newline or a blank line — taking `.lines.first`
|
|
128
|
+
# there rendered an EMPTY "needs approval:" body on the card (#141).
|
|
129
|
+
def first_line(text, max)
|
|
130
|
+
Rubino::Util::Output.first_line(text, max)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|