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.
Files changed (319) hide show
  1. checksums.yaml +7 -0
  2. data/.clacky/skills/commit/SKILL.md +423 -0
  3. data/.clacky/skills/gem-release/SKILL.md +199 -0
  4. data/.clacky/skills/gem-release/scripts/release.sh +304 -0
  5. data/.clacky/skills/oss-upload/SKILL.md +47 -0
  6. data/.octorules +106 -0
  7. data/.rspec +3 -0
  8. data/.rubocop.yml +8 -0
  9. data/CHANGELOG.md +76 -0
  10. data/CODE_OF_CONDUCT.md +132 -0
  11. data/CONTRIBUTING.md +92 -0
  12. data/Dockerfile +28 -0
  13. data/LICENSE.txt +22 -0
  14. data/POSITIONING.md +46 -0
  15. data/README.md +134 -0
  16. data/README_CN.md +134 -0
  17. data/Rakefile +34 -0
  18. data/benchmark/fixtures/sample_project/Gemfile +3 -0
  19. data/benchmark/fixtures/sample_project/lib/api_handler.rb +32 -0
  20. data/benchmark/fixtures/sample_project/lib/order_calculator.rb +23 -0
  21. data/benchmark/fixtures/sample_project/lib/user_renderer.rb +20 -0
  22. data/benchmark/fixtures/sample_project/spec/order_calculator_spec.rb +20 -0
  23. data/benchmark/results/EVALUATION_REPORT.md +165 -0
  24. data/benchmark/results/baseline_20260511_174424.json +128 -0
  25. data/benchmark/results/report_20260511_175256.json +271 -0
  26. data/benchmark/results/report_20260511_175444.json +271 -0
  27. data/benchmark/results/treatment_20260511_175103.json +130 -0
  28. data/benchmark/runner.rb +441 -0
  29. data/bin/octo +7 -0
  30. data/docs/agent-first-ui-design.md +77 -0
  31. data/docs/billing-system.md +318 -0
  32. data/docs/channel-architecture.md +235 -0
  33. data/docs/engineering-article.md +343 -0
  34. data/docs/session-skill-invocation.md +69 -0
  35. data/docs/time_machine_design.md +247 -0
  36. data/docs/ui2-architecture.md +124 -0
  37. data/homebrew/README.md +96 -0
  38. data/homebrew/openocto.rb +24 -0
  39. data/lib/octo/agent/hook_manager.rb +61 -0
  40. data/lib/octo/agent/llm_caller.rb +800 -0
  41. data/lib/octo/agent/memory_updater.rb +246 -0
  42. data/lib/octo/agent/message_compressor.rb +225 -0
  43. data/lib/octo/agent/message_compressor_helper.rb +869 -0
  44. data/lib/octo/agent/next_message_suggester.rb +215 -0
  45. data/lib/octo/agent/session_serializer.rb +685 -0
  46. data/lib/octo/agent/skill_auto_creator.rb +114 -0
  47. data/lib/octo/agent/skill_evolution.rb +61 -0
  48. data/lib/octo/agent/skill_manager.rb +466 -0
  49. data/lib/octo/agent/skill_reflector.rb +89 -0
  50. data/lib/octo/agent/system_prompt_builder.rb +101 -0
  51. data/lib/octo/agent/time_machine.rb +214 -0
  52. data/lib/octo/agent/tool_executor.rb +454 -0
  53. data/lib/octo/agent/tool_registry.rb +150 -0
  54. data/lib/octo/agent.rb +2180 -0
  55. data/lib/octo/agent_config.rb +989 -0
  56. data/lib/octo/agent_profile.rb +112 -0
  57. data/lib/octo/anthropic_stream_aggregator.rb +137 -0
  58. data/lib/octo/background_task_registry.rb +324 -0
  59. data/lib/octo/banner.rb +34 -0
  60. data/lib/octo/bedrock_stream_aggregator.rb +137 -0
  61. data/lib/octo/block_font.rb +331 -0
  62. data/lib/octo/cli.rb +968 -0
  63. data/lib/octo/client.rb +623 -0
  64. data/lib/octo/default_agents/SOUL.md +3 -0
  65. data/lib/octo/default_agents/USER.md +1 -0
  66. data/lib/octo/default_agents/base_prompt.md +66 -0
  67. data/lib/octo/default_agents/coding/profile.yml +2 -0
  68. data/lib/octo/default_agents/coding/system_prompt.md +67 -0
  69. data/lib/octo/default_agents/general/profile.yml +2 -0
  70. data/lib/octo/default_agents/general/system_prompt.md +16 -0
  71. data/lib/octo/default_parsers/doc_parser.rb +69 -0
  72. data/lib/octo/default_parsers/docx_parser.rb +188 -0
  73. data/lib/octo/default_parsers/pdf_parser.rb +120 -0
  74. data/lib/octo/default_parsers/pdf_parser_ocr.py +103 -0
  75. data/lib/octo/default_parsers/pdf_parser_plumber.py +62 -0
  76. data/lib/octo/default_parsers/pptx_parser.rb +140 -0
  77. data/lib/octo/default_parsers/xlsx_parser.rb +121 -0
  78. data/lib/octo/default_skills/browser-setup/SKILL.md +426 -0
  79. data/lib/octo/default_skills/channel-manager/SKILL.md +623 -0
  80. data/lib/octo/default_skills/channel-manager/dingtalk_setup.rb +191 -0
  81. data/lib/octo/default_skills/channel-manager/discord_setup.rb +199 -0
  82. data/lib/octo/default_skills/channel-manager/feishu_setup.rb +574 -0
  83. data/lib/octo/default_skills/channel-manager/import_lark_skills.rb +97 -0
  84. data/lib/octo/default_skills/channel-manager/install_feishu_skills.rb +105 -0
  85. data/lib/octo/default_skills/channel-manager/weixin_setup.rb +274 -0
  86. data/lib/octo/default_skills/code-explorer/SKILL.md +36 -0
  87. data/lib/octo/default_skills/cron-task-creator/SKILL.md +257 -0
  88. data/lib/octo/default_skills/cron-task-creator/evals/evals.json +38 -0
  89. data/lib/octo/default_skills/onboard/SKILL.md +578 -0
  90. data/lib/octo/default_skills/onboard/scripts/import_external_skills.rb +413 -0
  91. data/lib/octo/default_skills/onboard/scripts/install_builtin_skills.rb +97 -0
  92. data/lib/octo/default_skills/persist-memory/SKILL.md +59 -0
  93. data/lib/octo/default_skills/personal-website/SKILL.md +113 -0
  94. data/lib/octo/default_skills/personal-website/publish.rb +235 -0
  95. data/lib/octo/default_skills/product-help/SKILL.md +123 -0
  96. data/lib/octo/default_skills/product-help/docs/agent-config.md +74 -0
  97. data/lib/octo/default_skills/product-help/docs/best-practices.md +49 -0
  98. data/lib/octo/default_skills/product-help/docs/browser-tool.md +53 -0
  99. data/lib/octo/default_skills/product-help/docs/built-in-skills.md +43 -0
  100. data/lib/octo/default_skills/product-help/docs/cli-reference.md +82 -0
  101. data/lib/octo/default_skills/product-help/docs/create-your-first-skill.md +47 -0
  102. data/lib/octo/default_skills/product-help/docs/faq.md +98 -0
  103. data/lib/octo/default_skills/product-help/docs/how-to-use-a-skill.md +58 -0
  104. data/lib/octo/default_skills/product-help/docs/installation.md +59 -0
  105. data/lib/octo/default_skills/product-help/docs/memory-system.md +61 -0
  106. data/lib/octo/default_skills/product-help/docs/octorules.md +62 -0
  107. data/lib/octo/default_skills/product-help/docs/session-management.md +63 -0
  108. data/lib/octo/default_skills/product-help/docs/skill-basics.md +55 -0
  109. data/lib/octo/default_skills/product-help/docs/skill-frontmatter.md +61 -0
  110. data/lib/octo/default_skills/product-help/docs/web-server.md +49 -0
  111. data/lib/octo/default_skills/product-help/docs/what-is-octo.md +37 -0
  112. data/lib/octo/default_skills/product-help/docs/windows-installation.md +36 -0
  113. data/lib/octo/default_skills/product-help/docs/writing-tips.md +53 -0
  114. data/lib/octo/default_skills/recall-memory/SKILL.md +65 -0
  115. data/lib/octo/default_skills/skill-add/SKILL.md +59 -0
  116. data/lib/octo/default_skills/skill-add/scripts/install_from_zip.rb +295 -0
  117. data/lib/octo/default_skills/skill-creator/SKILL.md +602 -0
  118. data/lib/octo/default_skills/skill-creator/agents/analyzer.md +274 -0
  119. data/lib/octo/default_skills/skill-creator/agents/comparator.md +202 -0
  120. data/lib/octo/default_skills/skill-creator/agents/grader.md +223 -0
  121. data/lib/octo/default_skills/skill-creator/eval-viewer/generate_review.py +471 -0
  122. data/lib/octo/default_skills/skill-creator/eval-viewer/viewer.html +1325 -0
  123. data/lib/octo/default_skills/skill-creator/references/schemas.md +430 -0
  124. data/lib/octo/default_skills/skill-creator/scripts/__init__.py +0 -0
  125. data/lib/octo/default_skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
  126. data/lib/octo/default_skills/skill-creator/scripts/generate_report.py +326 -0
  127. data/lib/octo/default_skills/skill-creator/scripts/improve_description.py +310 -0
  128. data/lib/octo/default_skills/skill-creator/scripts/quick_validate.py +103 -0
  129. data/lib/octo/default_skills/skill-creator/scripts/run_eval.py +317 -0
  130. data/lib/octo/default_skills/skill-creator/scripts/run_loop.py +331 -0
  131. data/lib/octo/default_skills/skill-creator/scripts/utils.py +47 -0
  132. data/lib/octo/default_skills/skill-creator/scripts/validate_skill_frontmatter.rb +143 -0
  133. data/lib/octo/idle_compression_timer.rb +115 -0
  134. data/lib/octo/json_ui_controller.rb +204 -0
  135. data/lib/octo/message_format/anthropic.rb +409 -0
  136. data/lib/octo/message_format/bedrock.rb +361 -0
  137. data/lib/octo/message_format/open_ai.rb +222 -0
  138. data/lib/octo/message_history.rb +373 -0
  139. data/lib/octo/openai_stream_aggregator.rb +130 -0
  140. data/lib/octo/plain_ui_controller.rb +166 -0
  141. data/lib/octo/providers.rb +534 -0
  142. data/lib/octo/server/browser_manager.rb +397 -0
  143. data/lib/octo/server/channel/adapters/base.rb +82 -0
  144. data/lib/octo/server/channel/adapters/dingtalk/adapter.rb +314 -0
  145. data/lib/octo/server/channel/adapters/dingtalk/api_client.rb +391 -0
  146. data/lib/octo/server/channel/adapters/dingtalk/stream_client.rb +203 -0
  147. data/lib/octo/server/channel/adapters/discord/adapter.rb +229 -0
  148. data/lib/octo/server/channel/adapters/discord/api_client.rb +107 -0
  149. data/lib/octo/server/channel/adapters/discord/gateway_client.rb +270 -0
  150. data/lib/octo/server/channel/adapters/feishu/adapter.rb +320 -0
  151. data/lib/octo/server/channel/adapters/feishu/bot.rb +478 -0
  152. data/lib/octo/server/channel/adapters/feishu/file_processor.rb +36 -0
  153. data/lib/octo/server/channel/adapters/feishu/message_parser.rb +129 -0
  154. data/lib/octo/server/channel/adapters/feishu/ws_client.rb +423 -0
  155. data/lib/octo/server/channel/adapters/telegram/adapter.rb +375 -0
  156. data/lib/octo/server/channel/adapters/telegram/api_client.rb +205 -0
  157. data/lib/octo/server/channel/adapters/wecom/adapter.rb +148 -0
  158. data/lib/octo/server/channel/adapters/wecom/media_downloader.rb +115 -0
  159. data/lib/octo/server/channel/adapters/wecom/ws_client.rb +395 -0
  160. data/lib/octo/server/channel/adapters/weixin/adapter.rb +692 -0
  161. data/lib/octo/server/channel/adapters/weixin/api_client.rb +402 -0
  162. data/lib/octo/server/channel/channel_config.rb +178 -0
  163. data/lib/octo/server/channel/channel_manager.rb +468 -0
  164. data/lib/octo/server/channel/channel_ui_controller.rb +224 -0
  165. data/lib/octo/server/channel.rb +33 -0
  166. data/lib/octo/server/discover.rb +77 -0
  167. data/lib/octo/server/epipe_safe_io.rb +105 -0
  168. data/lib/octo/server/http_server.rb +3554 -0
  169. data/lib/octo/server/scheduler.rb +317 -0
  170. data/lib/octo/server/server_master.rb +325 -0
  171. data/lib/octo/server/session_registry.rb +431 -0
  172. data/lib/octo/server/web_ui_controller.rb +487 -0
  173. data/lib/octo/session_manager.rb +385 -0
  174. data/lib/octo/skill.rb +466 -0
  175. data/lib/octo/skill_loader.rb +328 -0
  176. data/lib/octo/tools/base.rb +118 -0
  177. data/lib/octo/tools/browser.rb +625 -0
  178. data/lib/octo/tools/edit.rb +165 -0
  179. data/lib/octo/tools/file_reader.rb +549 -0
  180. data/lib/octo/tools/glob.rb +162 -0
  181. data/lib/octo/tools/grep.rb +356 -0
  182. data/lib/octo/tools/invoke_skill.rb +96 -0
  183. data/lib/octo/tools/list_tasks.rb +54 -0
  184. data/lib/octo/tools/redo_task.rb +41 -0
  185. data/lib/octo/tools/request_user_feedback.rb +84 -0
  186. data/lib/octo/tools/security.rb +333 -0
  187. data/lib/octo/tools/terminal/output_cleaner.rb +63 -0
  188. data/lib/octo/tools/terminal/persistent_session.rb +268 -0
  189. data/lib/octo/tools/terminal/safe_rm.sh +106 -0
  190. data/lib/octo/tools/terminal/session_manager.rb +213 -0
  191. data/lib/octo/tools/terminal.rb +1828 -0
  192. data/lib/octo/tools/todo_manager.rb +374 -0
  193. data/lib/octo/tools/trash_manager.rb +388 -0
  194. data/lib/octo/tools/undo_task.rb +35 -0
  195. data/lib/octo/tools/web_fetch.rb +242 -0
  196. data/lib/octo/tools/web_search.rb +260 -0
  197. data/lib/octo/tools/write.rb +77 -0
  198. data/lib/octo/ui2/block_font.rb +10 -0
  199. data/lib/octo/ui2/components/base_component.rb +163 -0
  200. data/lib/octo/ui2/components/command_suggestions.rb +290 -0
  201. data/lib/octo/ui2/components/common_component.rb +96 -0
  202. data/lib/octo/ui2/components/inline_input.rb +226 -0
  203. data/lib/octo/ui2/components/input_area.rb +1338 -0
  204. data/lib/octo/ui2/components/message_component.rb +99 -0
  205. data/lib/octo/ui2/components/modal_component.rb +419 -0
  206. data/lib/octo/ui2/components/todo_area.rb +149 -0
  207. data/lib/octo/ui2/components/tool_component.rb +107 -0
  208. data/lib/octo/ui2/components/welcome_banner.rb +139 -0
  209. data/lib/octo/ui2/layout_manager.rb +807 -0
  210. data/lib/octo/ui2/line_editor.rb +363 -0
  211. data/lib/octo/ui2/markdown_renderer.rb +100 -0
  212. data/lib/octo/ui2/output_buffer.rb +370 -0
  213. data/lib/octo/ui2/progress_handle.rb +362 -0
  214. data/lib/octo/ui2/progress_indicator.rb +55 -0
  215. data/lib/octo/ui2/screen_buffer.rb +273 -0
  216. data/lib/octo/ui2/terminal_detector.rb +119 -0
  217. data/lib/octo/ui2/theme_manager.rb +85 -0
  218. data/lib/octo/ui2/themes/base_theme.rb +105 -0
  219. data/lib/octo/ui2/themes/hacker_theme.rb +62 -0
  220. data/lib/octo/ui2/themes/minimal_theme.rb +56 -0
  221. data/lib/octo/ui2/thinking_verbs.rb +26 -0
  222. data/lib/octo/ui2/ui_controller.rb +1625 -0
  223. data/lib/octo/ui2/view_renderer.rb +177 -0
  224. data/lib/octo/ui2.rb +40 -0
  225. data/lib/octo/ui_interface.rb +154 -0
  226. data/lib/octo/utils/arguments_parser.rb +191 -0
  227. data/lib/octo/utils/browser_detector.rb +195 -0
  228. data/lib/octo/utils/encoding.rb +92 -0
  229. data/lib/octo/utils/environment_detector.rb +140 -0
  230. data/lib/octo/utils/file_ignore_helper.rb +170 -0
  231. data/lib/octo/utils/file_processor.rb +601 -0
  232. data/lib/octo/utils/gitignore_parser.rb +154 -0
  233. data/lib/octo/utils/limit_stack.rb +152 -0
  234. data/lib/octo/utils/logger.rb +124 -0
  235. data/lib/octo/utils/login_shell.rb +72 -0
  236. data/lib/octo/utils/model_pricing.rb +646 -0
  237. data/lib/octo/utils/parser_manager.rb +165 -0
  238. data/lib/octo/utils/path_helper.rb +15 -0
  239. data/lib/octo/utils/scripts_manager.rb +59 -0
  240. data/lib/octo/utils/string_matcher.rb +158 -0
  241. data/lib/octo/utils/trash_directory.rb +112 -0
  242. data/lib/octo/utils/workspace_rules.rb +46 -0
  243. data/lib/octo/version.rb +5 -0
  244. data/lib/octo/web/app.css +7141 -0
  245. data/lib/octo/web/app.js +543 -0
  246. data/lib/octo/web/apple-touch-icon.png +0 -0
  247. data/lib/octo/web/auth.js +150 -0
  248. data/lib/octo/web/channels.js +276 -0
  249. data/lib/octo/web/datepicker.js +205 -0
  250. data/lib/octo/web/favicon.png +0 -0
  251. data/lib/octo/web/i18n.js +1073 -0
  252. data/lib/octo/web/icon-512.png +0 -0
  253. data/lib/octo/web/icon-dark.svg +25 -0
  254. data/lib/octo/web/icon.svg +29 -0
  255. data/lib/octo/web/index.html +871 -0
  256. data/lib/octo/web/marked.min.js +69 -0
  257. data/lib/octo/web/onboard.js +491 -0
  258. data/lib/octo/web/profile.js +442 -0
  259. data/lib/octo/web/sessions.js +4421 -0
  260. data/lib/octo/web/settings.js +913 -0
  261. data/lib/octo/web/sidebar.js +32 -0
  262. data/lib/octo/web/skills.js +885 -0
  263. data/lib/octo/web/tasks.js +297 -0
  264. data/lib/octo/web/theme.js +105 -0
  265. data/lib/octo/web/trash.js +343 -0
  266. data/lib/octo/web/vendor/hljs/highlight.min.js +1244 -0
  267. data/lib/octo/web/vendor/hljs/hljs-theme.css +95 -0
  268. data/lib/octo/web/vendor/katex/auto-render.min.js +1 -0
  269. data/lib/octo/web/vendor/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  270. data/lib/octo/web/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  271. data/lib/octo/web/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  272. data/lib/octo/web/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  273. data/lib/octo/web/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  274. data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
  275. data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  276. data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
  277. data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
  278. data/lib/octo/web/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  279. data/lib/octo/web/vendor/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
  280. data/lib/octo/web/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  281. data/lib/octo/web/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  282. data/lib/octo/web/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  283. data/lib/octo/web/vendor/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
  284. data/lib/octo/web/vendor/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  285. data/lib/octo/web/vendor/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  286. data/lib/octo/web/vendor/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  287. data/lib/octo/web/vendor/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  288. data/lib/octo/web/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  289. data/lib/octo/web/vendor/katex/katex.min.css +1 -0
  290. data/lib/octo/web/vendor/katex/katex.min.js +1 -0
  291. data/lib/octo/web/version.js +449 -0
  292. data/lib/octo/web/weixin-qr.html +209 -0
  293. data/lib/octo/web/ws-dispatcher.js +357 -0
  294. data/lib/octo/web/ws.js +128 -0
  295. data/lib/octo.rb +145 -0
  296. data/scripts/build/build.sh +329 -0
  297. data/scripts/build/lib/apt.sh +56 -0
  298. data/scripts/build/lib/brew.sh +89 -0
  299. data/scripts/build/lib/colors.sh +17 -0
  300. data/scripts/build/lib/gem.sh +95 -0
  301. data/scripts/build/lib/mise.sh +125 -0
  302. data/scripts/build/lib/network.sh +157 -0
  303. data/scripts/build/lib/os.sh +57 -0
  304. data/scripts/build/lib/shell.sh +37 -0
  305. data/scripts/build/src/install.sh.cc +174 -0
  306. data/scripts/build/src/install_browser.sh.cc +101 -0
  307. data/scripts/build/src/install_full.sh.cc +290 -0
  308. data/scripts/build/src/install_rails_deps.sh.cc +145 -0
  309. data/scripts/build/src/install_system_deps.sh.cc +123 -0
  310. data/scripts/build/src/uninstall.sh.cc +101 -0
  311. data/scripts/install.ps1 +532 -0
  312. data/scripts/install.sh +567 -0
  313. data/scripts/install_browser.sh +479 -0
  314. data/scripts/install_full.sh +838 -0
  315. data/scripts/install_rails_deps.sh +746 -0
  316. data/scripts/install_system_deps.sh +518 -0
  317. data/scripts/uninstall.sh +287 -0
  318. data/sig/octo.rbs +4 -0
  319. 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