octo-agent 0.11.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 +7 -0
- data/.clacky/skills/commit/SKILL.md +423 -0
- data/.clacky/skills/gem-release/SKILL.md +199 -0
- data/.clacky/skills/gem-release/scripts/release.sh +304 -0
- data/.clacky/skills/oss-upload/SKILL.md +47 -0
- data/.octorules +106 -0
- data/.rspec +3 -0
- data/.rubocop.yml +8 -0
- data/CHANGELOG.md +76 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/CONTRIBUTING.md +92 -0
- data/Dockerfile +28 -0
- data/LICENSE.txt +22 -0
- data/POSITIONING.md +46 -0
- data/README.md +134 -0
- data/README_CN.md +134 -0
- data/Rakefile +34 -0
- data/benchmark/fixtures/sample_project/Gemfile +3 -0
- data/benchmark/fixtures/sample_project/lib/api_handler.rb +32 -0
- data/benchmark/fixtures/sample_project/lib/order_calculator.rb +23 -0
- data/benchmark/fixtures/sample_project/lib/user_renderer.rb +20 -0
- data/benchmark/fixtures/sample_project/spec/order_calculator_spec.rb +20 -0
- data/benchmark/results/EVALUATION_REPORT.md +165 -0
- data/benchmark/results/baseline_20260511_174424.json +128 -0
- data/benchmark/results/report_20260511_175256.json +271 -0
- data/benchmark/results/report_20260511_175444.json +271 -0
- data/benchmark/results/treatment_20260511_175103.json +130 -0
- data/benchmark/runner.rb +441 -0
- data/bin/octo +7 -0
- data/docs/agent-first-ui-design.md +77 -0
- data/docs/billing-system.md +318 -0
- data/docs/channel-architecture.md +235 -0
- data/docs/engineering-article.md +343 -0
- data/docs/session-skill-invocation.md +69 -0
- data/docs/time_machine_design.md +247 -0
- data/docs/ui2-architecture.md +124 -0
- data/homebrew/README.md +96 -0
- data/homebrew/openocto.rb +24 -0
- data/lib/octo/agent/hook_manager.rb +61 -0
- data/lib/octo/agent/llm_caller.rb +800 -0
- data/lib/octo/agent/memory_updater.rb +246 -0
- data/lib/octo/agent/message_compressor.rb +225 -0
- data/lib/octo/agent/message_compressor_helper.rb +869 -0
- data/lib/octo/agent/next_message_suggester.rb +215 -0
- data/lib/octo/agent/session_serializer.rb +685 -0
- data/lib/octo/agent/skill_auto_creator.rb +114 -0
- data/lib/octo/agent/skill_evolution.rb +61 -0
- data/lib/octo/agent/skill_manager.rb +466 -0
- data/lib/octo/agent/skill_reflector.rb +89 -0
- data/lib/octo/agent/system_prompt_builder.rb +101 -0
- data/lib/octo/agent/time_machine.rb +214 -0
- data/lib/octo/agent/tool_executor.rb +454 -0
- data/lib/octo/agent/tool_registry.rb +150 -0
- data/lib/octo/agent.rb +2180 -0
- data/lib/octo/agent_config.rb +989 -0
- data/lib/octo/agent_profile.rb +112 -0
- data/lib/octo/anthropic_stream_aggregator.rb +137 -0
- data/lib/octo/background_task_registry.rb +324 -0
- data/lib/octo/banner.rb +34 -0
- data/lib/octo/bedrock_stream_aggregator.rb +137 -0
- data/lib/octo/block_font.rb +331 -0
- data/lib/octo/cli.rb +968 -0
- data/lib/octo/client.rb +623 -0
- data/lib/octo/default_agents/SOUL.md +3 -0
- data/lib/octo/default_agents/USER.md +1 -0
- data/lib/octo/default_agents/base_prompt.md +66 -0
- data/lib/octo/default_agents/coding/profile.yml +2 -0
- data/lib/octo/default_agents/coding/system_prompt.md +67 -0
- data/lib/octo/default_agents/general/profile.yml +2 -0
- data/lib/octo/default_agents/general/system_prompt.md +16 -0
- data/lib/octo/default_parsers/doc_parser.rb +69 -0
- data/lib/octo/default_parsers/docx_parser.rb +188 -0
- data/lib/octo/default_parsers/pdf_parser.rb +120 -0
- data/lib/octo/default_parsers/pdf_parser_ocr.py +103 -0
- data/lib/octo/default_parsers/pdf_parser_plumber.py +62 -0
- data/lib/octo/default_parsers/pptx_parser.rb +140 -0
- data/lib/octo/default_parsers/xlsx_parser.rb +121 -0
- data/lib/octo/default_skills/browser-setup/SKILL.md +426 -0
- data/lib/octo/default_skills/channel-manager/SKILL.md +623 -0
- data/lib/octo/default_skills/channel-manager/dingtalk_setup.rb +191 -0
- data/lib/octo/default_skills/channel-manager/discord_setup.rb +199 -0
- data/lib/octo/default_skills/channel-manager/feishu_setup.rb +574 -0
- data/lib/octo/default_skills/channel-manager/import_lark_skills.rb +97 -0
- data/lib/octo/default_skills/channel-manager/install_feishu_skills.rb +105 -0
- data/lib/octo/default_skills/channel-manager/weixin_setup.rb +274 -0
- data/lib/octo/default_skills/code-explorer/SKILL.md +36 -0
- data/lib/octo/default_skills/cron-task-creator/SKILL.md +257 -0
- data/lib/octo/default_skills/cron-task-creator/evals/evals.json +38 -0
- data/lib/octo/default_skills/onboard/SKILL.md +578 -0
- data/lib/octo/default_skills/onboard/scripts/import_external_skills.rb +413 -0
- data/lib/octo/default_skills/onboard/scripts/install_builtin_skills.rb +97 -0
- data/lib/octo/default_skills/persist-memory/SKILL.md +59 -0
- data/lib/octo/default_skills/personal-website/SKILL.md +113 -0
- data/lib/octo/default_skills/personal-website/publish.rb +235 -0
- data/lib/octo/default_skills/product-help/SKILL.md +123 -0
- data/lib/octo/default_skills/product-help/docs/agent-config.md +74 -0
- data/lib/octo/default_skills/product-help/docs/best-practices.md +49 -0
- data/lib/octo/default_skills/product-help/docs/browser-tool.md +53 -0
- data/lib/octo/default_skills/product-help/docs/built-in-skills.md +43 -0
- data/lib/octo/default_skills/product-help/docs/cli-reference.md +82 -0
- data/lib/octo/default_skills/product-help/docs/create-your-first-skill.md +47 -0
- data/lib/octo/default_skills/product-help/docs/faq.md +98 -0
- data/lib/octo/default_skills/product-help/docs/how-to-use-a-skill.md +58 -0
- data/lib/octo/default_skills/product-help/docs/installation.md +59 -0
- data/lib/octo/default_skills/product-help/docs/memory-system.md +61 -0
- data/lib/octo/default_skills/product-help/docs/octorules.md +62 -0
- data/lib/octo/default_skills/product-help/docs/session-management.md +63 -0
- data/lib/octo/default_skills/product-help/docs/skill-basics.md +55 -0
- data/lib/octo/default_skills/product-help/docs/skill-frontmatter.md +61 -0
- data/lib/octo/default_skills/product-help/docs/web-server.md +49 -0
- data/lib/octo/default_skills/product-help/docs/what-is-octo.md +37 -0
- data/lib/octo/default_skills/product-help/docs/windows-installation.md +36 -0
- data/lib/octo/default_skills/product-help/docs/writing-tips.md +53 -0
- data/lib/octo/default_skills/recall-memory/SKILL.md +65 -0
- data/lib/octo/default_skills/skill-add/SKILL.md +59 -0
- data/lib/octo/default_skills/skill-add/scripts/install_from_zip.rb +295 -0
- data/lib/octo/default_skills/skill-creator/SKILL.md +602 -0
- data/lib/octo/default_skills/skill-creator/agents/analyzer.md +274 -0
- data/lib/octo/default_skills/skill-creator/agents/comparator.md +202 -0
- data/lib/octo/default_skills/skill-creator/agents/grader.md +223 -0
- data/lib/octo/default_skills/skill-creator/eval-viewer/generate_review.py +471 -0
- data/lib/octo/default_skills/skill-creator/eval-viewer/viewer.html +1325 -0
- data/lib/octo/default_skills/skill-creator/references/schemas.md +430 -0
- data/lib/octo/default_skills/skill-creator/scripts/__init__.py +0 -0
- data/lib/octo/default_skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
- data/lib/octo/default_skills/skill-creator/scripts/generate_report.py +326 -0
- data/lib/octo/default_skills/skill-creator/scripts/improve_description.py +310 -0
- data/lib/octo/default_skills/skill-creator/scripts/quick_validate.py +103 -0
- data/lib/octo/default_skills/skill-creator/scripts/run_eval.py +317 -0
- data/lib/octo/default_skills/skill-creator/scripts/run_loop.py +331 -0
- data/lib/octo/default_skills/skill-creator/scripts/utils.py +47 -0
- data/lib/octo/default_skills/skill-creator/scripts/validate_skill_frontmatter.rb +143 -0
- data/lib/octo/idle_compression_timer.rb +115 -0
- data/lib/octo/json_ui_controller.rb +204 -0
- data/lib/octo/message_format/anthropic.rb +409 -0
- data/lib/octo/message_format/bedrock.rb +361 -0
- data/lib/octo/message_format/open_ai.rb +222 -0
- data/lib/octo/message_history.rb +373 -0
- data/lib/octo/openai_stream_aggregator.rb +130 -0
- data/lib/octo/plain_ui_controller.rb +166 -0
- data/lib/octo/providers.rb +534 -0
- data/lib/octo/server/browser_manager.rb +397 -0
- data/lib/octo/server/channel/adapters/base.rb +82 -0
- data/lib/octo/server/channel/adapters/dingtalk/adapter.rb +314 -0
- data/lib/octo/server/channel/adapters/dingtalk/api_client.rb +391 -0
- data/lib/octo/server/channel/adapters/dingtalk/stream_client.rb +203 -0
- data/lib/octo/server/channel/adapters/discord/adapter.rb +229 -0
- data/lib/octo/server/channel/adapters/discord/api_client.rb +107 -0
- data/lib/octo/server/channel/adapters/discord/gateway_client.rb +270 -0
- data/lib/octo/server/channel/adapters/feishu/adapter.rb +320 -0
- data/lib/octo/server/channel/adapters/feishu/bot.rb +478 -0
- data/lib/octo/server/channel/adapters/feishu/file_processor.rb +36 -0
- data/lib/octo/server/channel/adapters/feishu/message_parser.rb +129 -0
- data/lib/octo/server/channel/adapters/feishu/ws_client.rb +423 -0
- data/lib/octo/server/channel/adapters/telegram/adapter.rb +375 -0
- data/lib/octo/server/channel/adapters/telegram/api_client.rb +205 -0
- data/lib/octo/server/channel/adapters/wecom/adapter.rb +148 -0
- data/lib/octo/server/channel/adapters/wecom/media_downloader.rb +115 -0
- data/lib/octo/server/channel/adapters/wecom/ws_client.rb +395 -0
- data/lib/octo/server/channel/adapters/weixin/adapter.rb +692 -0
- data/lib/octo/server/channel/adapters/weixin/api_client.rb +402 -0
- data/lib/octo/server/channel/channel_config.rb +178 -0
- data/lib/octo/server/channel/channel_manager.rb +468 -0
- data/lib/octo/server/channel/channel_ui_controller.rb +224 -0
- data/lib/octo/server/channel.rb +33 -0
- data/lib/octo/server/discover.rb +77 -0
- data/lib/octo/server/epipe_safe_io.rb +105 -0
- data/lib/octo/server/http_server.rb +3554 -0
- data/lib/octo/server/scheduler.rb +317 -0
- data/lib/octo/server/server_master.rb +325 -0
- data/lib/octo/server/session_registry.rb +431 -0
- data/lib/octo/server/web_ui_controller.rb +487 -0
- data/lib/octo/session_manager.rb +385 -0
- data/lib/octo/skill.rb +466 -0
- data/lib/octo/skill_loader.rb +328 -0
- data/lib/octo/tools/base.rb +118 -0
- data/lib/octo/tools/browser.rb +625 -0
- data/lib/octo/tools/edit.rb +165 -0
- data/lib/octo/tools/file_reader.rb +549 -0
- data/lib/octo/tools/glob.rb +162 -0
- data/lib/octo/tools/grep.rb +356 -0
- data/lib/octo/tools/invoke_skill.rb +96 -0
- data/lib/octo/tools/list_tasks.rb +54 -0
- data/lib/octo/tools/redo_task.rb +41 -0
- data/lib/octo/tools/request_user_feedback.rb +84 -0
- data/lib/octo/tools/security.rb +333 -0
- data/lib/octo/tools/terminal/output_cleaner.rb +63 -0
- data/lib/octo/tools/terminal/persistent_session.rb +268 -0
- data/lib/octo/tools/terminal/safe_rm.sh +106 -0
- data/lib/octo/tools/terminal/session_manager.rb +213 -0
- data/lib/octo/tools/terminal.rb +1828 -0
- data/lib/octo/tools/todo_manager.rb +374 -0
- data/lib/octo/tools/trash_manager.rb +388 -0
- data/lib/octo/tools/undo_task.rb +35 -0
- data/lib/octo/tools/web_fetch.rb +242 -0
- data/lib/octo/tools/web_search.rb +260 -0
- data/lib/octo/tools/write.rb +77 -0
- data/lib/octo/ui2/block_font.rb +10 -0
- data/lib/octo/ui2/components/base_component.rb +163 -0
- data/lib/octo/ui2/components/command_suggestions.rb +290 -0
- data/lib/octo/ui2/components/common_component.rb +96 -0
- data/lib/octo/ui2/components/inline_input.rb +226 -0
- data/lib/octo/ui2/components/input_area.rb +1338 -0
- data/lib/octo/ui2/components/message_component.rb +99 -0
- data/lib/octo/ui2/components/modal_component.rb +419 -0
- data/lib/octo/ui2/components/todo_area.rb +149 -0
- data/lib/octo/ui2/components/tool_component.rb +107 -0
- data/lib/octo/ui2/components/welcome_banner.rb +139 -0
- data/lib/octo/ui2/layout_manager.rb +807 -0
- data/lib/octo/ui2/line_editor.rb +363 -0
- data/lib/octo/ui2/markdown_renderer.rb +100 -0
- data/lib/octo/ui2/output_buffer.rb +370 -0
- data/lib/octo/ui2/progress_handle.rb +362 -0
- data/lib/octo/ui2/progress_indicator.rb +55 -0
- data/lib/octo/ui2/screen_buffer.rb +273 -0
- data/lib/octo/ui2/terminal_detector.rb +119 -0
- data/lib/octo/ui2/theme_manager.rb +85 -0
- data/lib/octo/ui2/themes/base_theme.rb +105 -0
- data/lib/octo/ui2/themes/hacker_theme.rb +62 -0
- data/lib/octo/ui2/themes/minimal_theme.rb +56 -0
- data/lib/octo/ui2/thinking_verbs.rb +26 -0
- data/lib/octo/ui2/ui_controller.rb +1625 -0
- data/lib/octo/ui2/view_renderer.rb +177 -0
- data/lib/octo/ui2.rb +40 -0
- data/lib/octo/ui_interface.rb +154 -0
- data/lib/octo/utils/arguments_parser.rb +191 -0
- data/lib/octo/utils/browser_detector.rb +195 -0
- data/lib/octo/utils/encoding.rb +92 -0
- data/lib/octo/utils/environment_detector.rb +140 -0
- data/lib/octo/utils/file_ignore_helper.rb +170 -0
- data/lib/octo/utils/file_processor.rb +601 -0
- data/lib/octo/utils/gitignore_parser.rb +154 -0
- data/lib/octo/utils/limit_stack.rb +152 -0
- data/lib/octo/utils/logger.rb +124 -0
- data/lib/octo/utils/login_shell.rb +72 -0
- data/lib/octo/utils/model_pricing.rb +646 -0
- data/lib/octo/utils/parser_manager.rb +165 -0
- data/lib/octo/utils/path_helper.rb +15 -0
- data/lib/octo/utils/scripts_manager.rb +59 -0
- data/lib/octo/utils/string_matcher.rb +158 -0
- data/lib/octo/utils/trash_directory.rb +112 -0
- data/lib/octo/utils/workspace_rules.rb +46 -0
- data/lib/octo/version.rb +5 -0
- data/lib/octo/web/app.css +7141 -0
- data/lib/octo/web/app.js +543 -0
- data/lib/octo/web/apple-touch-icon.png +0 -0
- data/lib/octo/web/auth.js +150 -0
- data/lib/octo/web/channels.js +276 -0
- data/lib/octo/web/datepicker.js +205 -0
- data/lib/octo/web/favicon.png +0 -0
- data/lib/octo/web/i18n.js +1073 -0
- data/lib/octo/web/icon-512.png +0 -0
- data/lib/octo/web/icon-dark.svg +25 -0
- data/lib/octo/web/icon.svg +29 -0
- data/lib/octo/web/index.html +871 -0
- data/lib/octo/web/marked.min.js +69 -0
- data/lib/octo/web/onboard.js +491 -0
- data/lib/octo/web/profile.js +442 -0
- data/lib/octo/web/sessions.js +4421 -0
- data/lib/octo/web/settings.js +913 -0
- data/lib/octo/web/sidebar.js +32 -0
- data/lib/octo/web/skills.js +885 -0
- data/lib/octo/web/tasks.js +297 -0
- data/lib/octo/web/theme.js +105 -0
- data/lib/octo/web/trash.js +343 -0
- data/lib/octo/web/vendor/hljs/highlight.min.js +1244 -0
- data/lib/octo/web/vendor/hljs/hljs-theme.css +95 -0
- data/lib/octo/web/vendor/katex/auto-render.min.js +1 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/katex.min.css +1 -0
- data/lib/octo/web/vendor/katex/katex.min.js +1 -0
- data/lib/octo/web/version.js +449 -0
- data/lib/octo/web/weixin-qr.html +209 -0
- data/lib/octo/web/ws-dispatcher.js +357 -0
- data/lib/octo/web/ws.js +128 -0
- data/lib/octo.rb +145 -0
- data/scripts/build/build.sh +329 -0
- data/scripts/build/lib/apt.sh +56 -0
- data/scripts/build/lib/brew.sh +89 -0
- data/scripts/build/lib/colors.sh +17 -0
- data/scripts/build/lib/gem.sh +95 -0
- data/scripts/build/lib/mise.sh +125 -0
- data/scripts/build/lib/network.sh +157 -0
- data/scripts/build/lib/os.sh +57 -0
- data/scripts/build/lib/shell.sh +37 -0
- data/scripts/build/src/install.sh.cc +174 -0
- data/scripts/build/src/install_browser.sh.cc +101 -0
- data/scripts/build/src/install_full.sh.cc +290 -0
- data/scripts/build/src/install_rails_deps.sh.cc +145 -0
- data/scripts/build/src/install_system_deps.sh.cc +123 -0
- data/scripts/build/src/uninstall.sh.cc +101 -0
- data/scripts/install.ps1 +532 -0
- data/scripts/install.sh +567 -0
- data/scripts/install_browser.sh +479 -0
- data/scripts/install_full.sh +838 -0
- data/scripts/install_rails_deps.sh +746 -0
- data/scripts/install_system_deps.sh +518 -0
- data/scripts/uninstall.sh +287 -0
- data/sig/octo.rbs +4 -0
- metadata +614 -0
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pastel"
|
|
4
|
+
|
|
5
|
+
module Octo
|
|
6
|
+
module UI2
|
|
7
|
+
# LineEditor module provides single-line text editing functionality
|
|
8
|
+
# Shared by InputArea and InlineInput components
|
|
9
|
+
module LineEditor
|
|
10
|
+
# Maximum content width ratio (percentage of terminal width)
|
|
11
|
+
# Use 90% of terminal width for better readability on wide screens
|
|
12
|
+
# This dynamically adjusts based on terminal size
|
|
13
|
+
MAX_CONTENT_WIDTH_RATIO = 0.9
|
|
14
|
+
|
|
15
|
+
attr_reader :cursor_position
|
|
16
|
+
|
|
17
|
+
def initialize_line_editor
|
|
18
|
+
@line = ""
|
|
19
|
+
@cursor_position = 0
|
|
20
|
+
@pastel = Pastel.new
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Get current line content
|
|
24
|
+
def current_line
|
|
25
|
+
@line
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Set line content
|
|
29
|
+
def set_line(text)
|
|
30
|
+
@line = text
|
|
31
|
+
@cursor_position = [@cursor_position, @line.chars.length].min
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Clear line
|
|
35
|
+
def clear_line_content
|
|
36
|
+
@line = ""
|
|
37
|
+
@cursor_position = 0
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Insert character at cursor position
|
|
41
|
+
def insert_char(char)
|
|
42
|
+
chars = @line.chars
|
|
43
|
+
chars.insert(@cursor_position, char)
|
|
44
|
+
@line = chars.join
|
|
45
|
+
@cursor_position += 1
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Backspace - delete character before cursor
|
|
49
|
+
def backspace
|
|
50
|
+
return if @cursor_position == 0
|
|
51
|
+
chars = @line.chars
|
|
52
|
+
chars.delete_at(@cursor_position - 1)
|
|
53
|
+
@line = chars.join
|
|
54
|
+
@cursor_position -= 1
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Delete character at cursor position
|
|
58
|
+
def delete_char
|
|
59
|
+
chars = @line.chars
|
|
60
|
+
return if @cursor_position >= chars.length
|
|
61
|
+
chars.delete_at(@cursor_position)
|
|
62
|
+
@line = chars.join
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Move cursor left
|
|
66
|
+
def cursor_left
|
|
67
|
+
@cursor_position = [@cursor_position - 1, 0].max
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Move cursor right
|
|
71
|
+
def cursor_right
|
|
72
|
+
@cursor_position = [@cursor_position + 1, @line.chars.length].min
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Move cursor to start of line
|
|
76
|
+
def cursor_home
|
|
77
|
+
@cursor_position = 0
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Move cursor to end of line
|
|
81
|
+
def cursor_end
|
|
82
|
+
@cursor_position = @line.chars.length
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Kill from cursor to end of line (Ctrl+K)
|
|
86
|
+
def kill_to_end
|
|
87
|
+
chars = @line.chars
|
|
88
|
+
@line = chars[0...@cursor_position].join
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Kill from start to cursor (Ctrl+U)
|
|
92
|
+
def kill_to_start
|
|
93
|
+
chars = @line.chars
|
|
94
|
+
@line = chars[@cursor_position..-1]&.join || ""
|
|
95
|
+
@cursor_position = 0
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Kill word before cursor (Ctrl+W)
|
|
99
|
+
def kill_word
|
|
100
|
+
chars = @line.chars
|
|
101
|
+
pos = @cursor_position - 1
|
|
102
|
+
|
|
103
|
+
# Skip whitespace
|
|
104
|
+
while pos >= 0 && chars[pos] =~ /\s/
|
|
105
|
+
pos -= 1
|
|
106
|
+
end
|
|
107
|
+
# Delete word characters
|
|
108
|
+
while pos >= 0 && chars[pos] =~ /\S/
|
|
109
|
+
pos -= 1
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
delete_start = pos + 1
|
|
113
|
+
chars.slice!(delete_start...@cursor_position)
|
|
114
|
+
@line = chars.join
|
|
115
|
+
@cursor_position = delete_start
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Insert text at cursor position
|
|
119
|
+
def insert_text(text)
|
|
120
|
+
return if text.nil? || text.empty?
|
|
121
|
+
chars = @line.chars
|
|
122
|
+
text.chars.each_with_index do |c, i|
|
|
123
|
+
chars.insert(@cursor_position + i, c)
|
|
124
|
+
end
|
|
125
|
+
@line = chars.join
|
|
126
|
+
@cursor_position += text.length
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Expand placeholders and normalize line endings
|
|
130
|
+
def expand_placeholders(text, placeholders)
|
|
131
|
+
result = text.dup
|
|
132
|
+
placeholders.each do |placeholder, actual_content|
|
|
133
|
+
# Normalize line endings to \n
|
|
134
|
+
normalized_content = actual_content.gsub(/\r\n|\r/, "\n")
|
|
135
|
+
result.gsub!(placeholder, normalized_content)
|
|
136
|
+
end
|
|
137
|
+
result
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Render line with cursor highlight
|
|
141
|
+
# @return [String] Rendered line with cursor
|
|
142
|
+
def render_line_with_cursor
|
|
143
|
+
chars = @line.chars
|
|
144
|
+
before_cursor = chars[0...@cursor_position].join
|
|
145
|
+
cursor_char = chars[@cursor_position] || " "
|
|
146
|
+
after_cursor = chars[(@cursor_position + 1)..-1]&.join || ""
|
|
147
|
+
|
|
148
|
+
"#{@pastel.white(before_cursor)}#{@pastel.on_white(@pastel.black(cursor_char))}#{@pastel.white(after_cursor)}"
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Calculate display width of a string, considering multi-byte characters
|
|
152
|
+
# East Asian Wide and Fullwidth characters (like Chinese) take 2 columns
|
|
153
|
+
# @param text [String] UTF-8 encoded text
|
|
154
|
+
# @return [Integer] Display width in terminal columns
|
|
155
|
+
def calculate_display_width(text)
|
|
156
|
+
width = 0
|
|
157
|
+
text.each_char do |char|
|
|
158
|
+
code = char.ord
|
|
159
|
+
# East Asian Wide and Fullwidth characters
|
|
160
|
+
# See: https://www.unicode.org/reports/tr11/
|
|
161
|
+
if (code >= 0x1100 && code <= 0x115F) || # Hangul Jamo
|
|
162
|
+
(code >= 0x2329 && code <= 0x232A) || # Left/Right-Pointing Angle Brackets
|
|
163
|
+
(code >= 0x2E80 && code <= 0x303E) || # CJK Radicals Supplement .. CJK Symbols and Punctuation
|
|
164
|
+
(code >= 0x3040 && code <= 0xA4CF) || # Hiragana .. Yi Radicals
|
|
165
|
+
(code >= 0xAC00 && code <= 0xD7A3) || # Hangul Syllables
|
|
166
|
+
(code >= 0xF900 && code <= 0xFAFF) || # CJK Compatibility Ideographs
|
|
167
|
+
(code >= 0xFE10 && code <= 0xFE19) || # Vertical Forms
|
|
168
|
+
(code >= 0xFE30 && code <= 0xFE6F) || # CJK Compatibility Forms .. Small Form Variants
|
|
169
|
+
(code >= 0xFF00 && code <= 0xFF60) || # Fullwidth Forms
|
|
170
|
+
(code >= 0xFFE0 && code <= 0xFFE6) || # Fullwidth Forms
|
|
171
|
+
(code >= 0x1F300 && code <= 0x1F9FF) || # Emoticons, Symbols, etc.
|
|
172
|
+
(code >= 0x20000 && code <= 0x2FFFD) || # CJK Unified Ideographs Extension B..F
|
|
173
|
+
(code >= 0x30000 && code <= 0x3FFFD) # CJK Unified Ideographs Extension G
|
|
174
|
+
width += 2
|
|
175
|
+
else
|
|
176
|
+
width += 1
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
width
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Strip ANSI escape codes from a string
|
|
183
|
+
# @param text [String] Text with ANSI codes
|
|
184
|
+
# @return [String] Text without ANSI codes
|
|
185
|
+
def strip_ansi_codes(text)
|
|
186
|
+
text.gsub(/\e\[[0-9;]*m/, '')
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Get cursor column position (considering multi-byte characters)
|
|
190
|
+
# @param prompt [String] Prompt string before the line (may contain ANSI codes)
|
|
191
|
+
# @return [Integer] Column position for cursor
|
|
192
|
+
def cursor_column(prompt = "")
|
|
193
|
+
# Strip ANSI codes from prompt to get actual display width
|
|
194
|
+
visible_prompt = strip_ansi_codes(prompt)
|
|
195
|
+
prompt_display_width = calculate_display_width(visible_prompt)
|
|
196
|
+
|
|
197
|
+
# Calculate display width of text before cursor
|
|
198
|
+
chars = @line.chars
|
|
199
|
+
text_before_cursor = chars[0...@cursor_position].join
|
|
200
|
+
text_display_width = calculate_display_width(text_before_cursor)
|
|
201
|
+
|
|
202
|
+
prompt_display_width + text_display_width
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Get cursor position considering line wrapping
|
|
206
|
+
# @param prompt [String] Prompt string before the line (may contain ANSI codes)
|
|
207
|
+
# @param width [Integer] Terminal width for wrapping
|
|
208
|
+
# @param continuation_prompt [String] Prompt for continuation lines (default: "> ")
|
|
209
|
+
# @return [Array<Integer>] Row and column position (0-indexed)
|
|
210
|
+
def cursor_position_with_wrap(prompt = "", width = TTY::Screen.width, continuation_prompt = "> ")
|
|
211
|
+
return [0, cursor_column(prompt)] if width <= 0
|
|
212
|
+
|
|
213
|
+
prompt_width = calculate_display_width(strip_ansi_codes(prompt))
|
|
214
|
+
available_width = width - prompt_width
|
|
215
|
+
|
|
216
|
+
# Get wrapped segments for current line
|
|
217
|
+
wrapped_segments = wrap_line(@line, available_width)
|
|
218
|
+
|
|
219
|
+
# Find which segment contains cursor
|
|
220
|
+
cursor_segment_idx = 0
|
|
221
|
+
cursor_pos_in_segment = @cursor_position
|
|
222
|
+
|
|
223
|
+
wrapped_segments.each_with_index do |segment, idx|
|
|
224
|
+
if @cursor_position >= segment[:start] && @cursor_position < segment[:end]
|
|
225
|
+
cursor_segment_idx = idx
|
|
226
|
+
cursor_pos_in_segment = @cursor_position - segment[:start]
|
|
227
|
+
break
|
|
228
|
+
elsif @cursor_position >= segment[:end] && idx == wrapped_segments.size - 1
|
|
229
|
+
cursor_segment_idx = idx
|
|
230
|
+
cursor_pos_in_segment = segment[:end] - segment[:start]
|
|
231
|
+
break
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Calculate display width of text before cursor in this segment
|
|
236
|
+
chars = @line.chars
|
|
237
|
+
segment_start = wrapped_segments[cursor_segment_idx][:start]
|
|
238
|
+
text_in_segment_before_cursor = chars[segment_start...(segment_start + cursor_pos_in_segment)].join
|
|
239
|
+
display_width = calculate_display_width(text_in_segment_before_cursor)
|
|
240
|
+
|
|
241
|
+
# Use appropriate prompt width based on which segment (row) we're on
|
|
242
|
+
# First line uses original prompt, subsequent lines use continuation prompt
|
|
243
|
+
actual_prompt_width = if cursor_segment_idx == 0
|
|
244
|
+
prompt_width
|
|
245
|
+
else
|
|
246
|
+
calculate_display_width(strip_ansi_codes(continuation_prompt))
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
col = actual_prompt_width + display_width
|
|
250
|
+
row = cursor_segment_idx
|
|
251
|
+
|
|
252
|
+
[row, col]
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Wrap a line into multiple segments based on available width
|
|
256
|
+
# Considers display width of characters (multi-byte characters like Chinese)
|
|
257
|
+
# @param line [String] The line to wrap
|
|
258
|
+
# @param max_width [Integer] Maximum display width per wrapped line
|
|
259
|
+
# @return [Array<Hash>] Array of segment info: { text: String, start: Integer, end: Integer }
|
|
260
|
+
def wrap_line(line, max_width)
|
|
261
|
+
return [{ text: "", start: 0, end: 0 }] if line.empty?
|
|
262
|
+
return [{ text: line, start: 0, end: line.length }] if max_width <= 0
|
|
263
|
+
|
|
264
|
+
segments = []
|
|
265
|
+
chars = line.chars
|
|
266
|
+
segment_start = 0
|
|
267
|
+
current_width = 0
|
|
268
|
+
current_end = 0
|
|
269
|
+
|
|
270
|
+
chars.each_with_index do |char, idx|
|
|
271
|
+
char_width = char_display_width(char)
|
|
272
|
+
|
|
273
|
+
# If adding this character exceeds max width, complete current segment
|
|
274
|
+
if current_width + char_width > max_width && current_end > segment_start
|
|
275
|
+
segments << {
|
|
276
|
+
text: chars[segment_start...current_end].join,
|
|
277
|
+
start: segment_start,
|
|
278
|
+
end: current_end
|
|
279
|
+
}
|
|
280
|
+
segment_start = idx
|
|
281
|
+
current_end = idx + 1
|
|
282
|
+
current_width = char_width
|
|
283
|
+
else
|
|
284
|
+
current_end = idx + 1
|
|
285
|
+
current_width += char_width
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Add the last segment
|
|
290
|
+
if current_end > segment_start
|
|
291
|
+
segments << {
|
|
292
|
+
text: chars[segment_start...current_end].join,
|
|
293
|
+
start: segment_start,
|
|
294
|
+
end: current_end
|
|
295
|
+
}
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
segments.empty? ? [{ text: "", start: 0, end: 0 }] : segments
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# Calculate display width of a single character
|
|
302
|
+
# @param char [String] Single character
|
|
303
|
+
# @return [Integer] Display width (1 or 2)
|
|
304
|
+
def char_display_width(char)
|
|
305
|
+
code = char.ord
|
|
306
|
+
# East Asian Wide and Fullwidth characters take 2 columns
|
|
307
|
+
if (code >= 0x1100 && code <= 0x115F) ||
|
|
308
|
+
(code >= 0x2329 && code <= 0x232A) ||
|
|
309
|
+
(code >= 0x2E80 && code <= 0x303E) ||
|
|
310
|
+
(code >= 0x3040 && code <= 0xA4CF) ||
|
|
311
|
+
(code >= 0xAC00 && code <= 0xD7A3) ||
|
|
312
|
+
(code >= 0xF900 && code <= 0xFAFF) ||
|
|
313
|
+
(code >= 0xFE10 && code <= 0xFE19) ||
|
|
314
|
+
(code >= 0xFE30 && code <= 0xFE6F) ||
|
|
315
|
+
(code >= 0xFF00 && code <= 0xFF60) ||
|
|
316
|
+
(code >= 0xFFE0 && code <= 0xFFE6) ||
|
|
317
|
+
(code >= 0x1F300 && code <= 0x1F9FF) ||
|
|
318
|
+
(code >= 0x20000 && code <= 0x2FFFD) ||
|
|
319
|
+
(code >= 0x30000 && code <= 0x3FFFD)
|
|
320
|
+
2
|
|
321
|
+
else
|
|
322
|
+
1
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# Calculate effective content width (respecting MAX_CONTENT_WIDTH_RATIO)
|
|
327
|
+
# @param screen_width [Integer] Terminal screen width
|
|
328
|
+
# @return [Integer] Effective content width to use
|
|
329
|
+
private def effective_content_width(screen_width)
|
|
330
|
+
(screen_width * MAX_CONTENT_WIDTH_RATIO).to_i
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# Render a segment of a line with cursor if cursor is in this segment
|
|
334
|
+
# @param line [String] Full line text
|
|
335
|
+
# @param segment_start [Integer] Start position of segment in line (char index)
|
|
336
|
+
# @param segment_end [Integer] End position of segment in line (char index)
|
|
337
|
+
# @return [String] Rendered segment with cursor if applicable (without text color, only cursor highlight)
|
|
338
|
+
def render_line_segment_with_cursor(line, segment_start, segment_end)
|
|
339
|
+
chars = line.chars
|
|
340
|
+
segment_chars = chars[segment_start...segment_end]
|
|
341
|
+
|
|
342
|
+
# Check if cursor is in this segment
|
|
343
|
+
if @cursor_position >= segment_start && @cursor_position < segment_end
|
|
344
|
+
# Cursor is in this segment
|
|
345
|
+
cursor_pos_in_segment = @cursor_position - segment_start
|
|
346
|
+
before_cursor = segment_chars[0...cursor_pos_in_segment].join
|
|
347
|
+
cursor_char = segment_chars[cursor_pos_in_segment] || " "
|
|
348
|
+
after_cursor = segment_chars[(cursor_pos_in_segment + 1)..-1]&.join || ""
|
|
349
|
+
|
|
350
|
+
# Only apply cursor highlight, let subclasses apply text color
|
|
351
|
+
"#{before_cursor}#{@pastel.on_white(@pastel.black(cursor_char))}#{after_cursor}"
|
|
352
|
+
elsif @cursor_position == segment_end && segment_end == line.length
|
|
353
|
+
# Cursor is at the very end of the line, show it in last segment
|
|
354
|
+
segment_text = segment_chars.join
|
|
355
|
+
"#{segment_text}#{@pastel.on_white(@pastel.black(' '))}"
|
|
356
|
+
else
|
|
357
|
+
# Cursor is not in this segment, return plain text without color
|
|
358
|
+
segment_chars.join
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tty-markdown"
|
|
4
|
+
require_relative "theme_manager"
|
|
5
|
+
|
|
6
|
+
module Octo
|
|
7
|
+
module UI2
|
|
8
|
+
# MarkdownRenderer handles rendering Markdown content with syntax highlighting
|
|
9
|
+
module MarkdownRenderer
|
|
10
|
+
class << self
|
|
11
|
+
# Render markdown content with theme-aware colors
|
|
12
|
+
# @param content [String] Markdown content to render
|
|
13
|
+
# @return [String] Rendered content with ANSI colors
|
|
14
|
+
def render(content)
|
|
15
|
+
return content if content.nil? || content.empty?
|
|
16
|
+
|
|
17
|
+
# Get current theme colors
|
|
18
|
+
theme = ThemeManager.current_theme
|
|
19
|
+
|
|
20
|
+
# Configure tty-markdown with custom theme and symbols
|
|
21
|
+
parsed = TTY::Markdown.parse(content,
|
|
22
|
+
theme: theme_colors,
|
|
23
|
+
symbols: custom_symbols,
|
|
24
|
+
width: TTY::Screen.width - 4 # Leave some margin
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
parsed
|
|
28
|
+
rescue StandardError => e
|
|
29
|
+
warn "[markdown] render failed: #{e.class}: #{e.message}" if ENV["OCTO_DEBUG"]
|
|
30
|
+
content
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Check if content looks like markdown
|
|
34
|
+
# @param content [String] Content to check
|
|
35
|
+
# @return [Boolean] true if content appears to be markdown
|
|
36
|
+
def markdown?(content)
|
|
37
|
+
return false if content.nil? || content.empty?
|
|
38
|
+
|
|
39
|
+
# Check for common markdown patterns
|
|
40
|
+
content.match?(/^#+ /) || # Headers
|
|
41
|
+
content.match?(/```/) || # Code blocks
|
|
42
|
+
content.match?(/^\s*[-*+] /) || # Unordered lists
|
|
43
|
+
content.match?(/^\s*\d+\. /) || # Ordered lists
|
|
44
|
+
content.match?(/\[.+\]\(.+\)/) || # Links
|
|
45
|
+
content.match?(/^\s*> /) || # Blockquotes
|
|
46
|
+
content.match?(/\*\*.+\*\*/) || # Bold
|
|
47
|
+
content.match?(/`.+`/) || # Inline code
|
|
48
|
+
content.match?(/^\s*\|.+\|/) || # Tables
|
|
49
|
+
content.match?(/^---+$/) # Horizontal rules
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# Get theme-aware colors for markdown rendering
|
|
54
|
+
# @return [Hash] Color configuration for tty-markdown
|
|
55
|
+
def theme_colors
|
|
56
|
+
theme = ThemeManager.current_theme
|
|
57
|
+
|
|
58
|
+
# Map our theme colors to tty-markdown's expected format
|
|
59
|
+
# Note: theme.colors values are already arrays, so we need to flatten when adding styles
|
|
60
|
+
{
|
|
61
|
+
# Headers use info color (cyan/blue)
|
|
62
|
+
h1: Array(theme.colors[:info]) + [:bold],
|
|
63
|
+
h2: Array(theme.colors[:info]) + [:bold],
|
|
64
|
+
h3: Array(theme.colors[:info]),
|
|
65
|
+
h4: Array(theme.colors[:info]),
|
|
66
|
+
h5: Array(theme.colors[:info]),
|
|
67
|
+
h6: Array(theme.colors[:info]),
|
|
68
|
+
# Horizontal rule - make it subtle (dim gray)
|
|
69
|
+
hr: [:bright_black],
|
|
70
|
+
# Code blocks use dim color
|
|
71
|
+
code: Array(theme.colors[:thinking]),
|
|
72
|
+
# Links use success color (green)
|
|
73
|
+
link: Array(theme.colors[:success]),
|
|
74
|
+
# Lists use default text color
|
|
75
|
+
list: [:bright_white],
|
|
76
|
+
# Strong/bold use bright white
|
|
77
|
+
strong: [:bright_white, :bold],
|
|
78
|
+
# Emphasis/italic use white
|
|
79
|
+
em: [:white],
|
|
80
|
+
# Note/blockquote use dim color
|
|
81
|
+
note: Array(theme.colors[:thinking]),
|
|
82
|
+
quote: Array(theme.colors[:thinking]),
|
|
83
|
+
}
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Get custom symbols for markdown rendering
|
|
87
|
+
# @return [Hash] Symbol configuration for tty-markdown
|
|
88
|
+
def custom_symbols
|
|
89
|
+
{
|
|
90
|
+
override: {
|
|
91
|
+
# Make horizontal rule simpler - just a line without decorative diamonds
|
|
92
|
+
diamond: "",
|
|
93
|
+
line: "-"
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|