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,177 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "components/message_component"
4
+ require_relative "components/tool_component"
5
+ require_relative "components/common_component"
6
+ require_relative "markdown_renderer"
7
+
8
+ module Octo
9
+ module UI2
10
+ # ViewRenderer coordinates all UI components and provides a unified rendering interface
11
+ class ViewRenderer
12
+ def initialize
13
+ @message_component = Components::MessageComponent.new
14
+ @tool_component = Components::ToolComponent.new
15
+ @common_component = Components::CommonComponent.new
16
+ end
17
+
18
+ # Render a user message
19
+ # @param content [String] Message content
20
+ # @param timestamp [Time, nil] Optional timestamp
21
+ # @param files [Array<Hash>] Optional file hashes { name:, mime_type:, ... }
22
+ # @return [String] Rendered message
23
+ def render_user_message(content, timestamp: nil, files: [])
24
+ @message_component.render(
25
+ role: "user",
26
+ content: content,
27
+ timestamp: timestamp,
28
+ files: files
29
+ )
30
+ end
31
+
32
+ # Render an assistant message
33
+ # @param content [String] Message content
34
+ # @param timestamp [Time, nil] Optional timestamp
35
+ # @return [String] Rendered message
36
+ def render_assistant_message(content, timestamp: nil)
37
+ # Render markdown if content contains markdown syntax
38
+ rendered_content = if MarkdownRenderer.markdown?(content)
39
+ MarkdownRenderer.render(content)
40
+ else
41
+ content
42
+ end
43
+
44
+ @message_component.render(
45
+ role: "assistant",
46
+ content: rendered_content,
47
+ timestamp: timestamp
48
+ )
49
+ end
50
+
51
+ # Render a system message
52
+ # @param content [String] Message content
53
+ # @param timestamp [Time, nil] Optional timestamp
54
+ # @param prefix_newline [Boolean] Whether to add newline before message
55
+ # @return [String] Rendered message
56
+ def render_system_message(content, timestamp: nil, prefix_newline: true)
57
+ @message_component.render(
58
+ role: "system",
59
+ content: content,
60
+ timestamp: timestamp,
61
+ prefix_newline: prefix_newline
62
+ )
63
+ end
64
+
65
+ # Render a tool call
66
+ # @param tool_name [String] Tool name
67
+ # @param formatted_call [String] Formatted call description
68
+ # @return [String] Rendered tool call
69
+ def render_tool_call(tool_name:, formatted_call:)
70
+ @tool_component.render(
71
+ type: :call,
72
+ tool_name: tool_name,
73
+ formatted_call: formatted_call
74
+ )
75
+ end
76
+
77
+ # Render a tool result
78
+ # @param result [String] Tool result
79
+ # @return [String] Rendered tool result
80
+ def render_tool_result(result:)
81
+ @tool_component.render(
82
+ type: :result,
83
+ result: result
84
+ )
85
+ end
86
+
87
+ # Render a tool error
88
+ # @param error [String] Error message
89
+ # @return [String] Rendered tool error
90
+ def render_tool_error(error:)
91
+ @tool_component.render(
92
+ type: :error,
93
+ error: error
94
+ )
95
+ end
96
+
97
+ # Render a tool denied message
98
+ # @param tool_name [String] Tool name
99
+ # @return [String] Rendered tool denied
100
+ def render_tool_denied(tool_name:)
101
+ @tool_component.render(
102
+ type: :denied,
103
+ tool_name: tool_name
104
+ )
105
+ end
106
+
107
+ # Render a tool planned message
108
+ # @param tool_name [String] Tool name
109
+ # @return [String] Rendered tool planned
110
+ def render_tool_planned(tool_name:)
111
+ @tool_component.render(
112
+ type: :planned,
113
+ tool_name: tool_name
114
+ )
115
+ end
116
+
117
+ # Render thinking indicator
118
+ # @return [String] Thinking indicator
119
+ def render_thinking
120
+ @common_component.render_thinking
121
+ end
122
+
123
+ # Render progress message (stopped state, gray)
124
+ # @param message [String] Progress message
125
+ # @return [String] Progress indicator
126
+ def render_progress(message)
127
+ @common_component.render_progress(message)
128
+ end
129
+
130
+ # Render working message (active state, yellow)
131
+ # @param message [String] Progress message
132
+ # @return [String] Working indicator
133
+ def render_working(message)
134
+ @common_component.render_working(message)
135
+ end
136
+
137
+ # Render success message
138
+ # @param message [String] Success message
139
+ # @return [String] Success message
140
+ def render_success(message)
141
+ @common_component.render_success(message)
142
+ end
143
+
144
+ # Render error message
145
+ # @param message [String] Error message
146
+ # @return [String] Error message
147
+ def render_error(message)
148
+ @common_component.render_error(message)
149
+ end
150
+
151
+ # Render warning message
152
+ # @param message [String] Warning message
153
+ # @return [String] Warning message
154
+ def render_warning(message)
155
+ @common_component.render_warning(message)
156
+ end
157
+
158
+ # Render task completion summary
159
+ # @param iterations [Integer] Number of iterations
160
+ # @param duration [Float] Duration in seconds
161
+ # @param cache_tokens [Integer] Cache read tokens
162
+ # @param cache_requests [Integer] Total cache requests count
163
+ # @param cache_hits [Integer] Cache hit requests count
164
+ # @return [String] Formatted completion summary
165
+ def render_task_complete(iterations:, duration: nil, cache_tokens: nil, cache_requests: nil, cache_hits: nil)
166
+ @common_component.render_task_complete(
167
+ iterations: iterations,
168
+ duration: duration,
169
+ cache_tokens: cache_tokens,
170
+ cache_requests: cache_requests,
171
+ cache_hits: cache_hits
172
+ )
173
+ end
174
+
175
+ end
176
+ end
177
+ end
data/lib/octo/ui2.rb ADDED
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ # UI2 - MVC-based terminal UI system for Octo
4
+ # Provides split-screen interface with scrollable output and fixed input
5
+
6
+ require_relative "ui2/thinking_verbs"
7
+ require_relative "ui2/progress_indicator"
8
+ require_relative "ui2/terminal_detector"
9
+ require_relative "ui2/theme_manager"
10
+ require_relative "ui2/screen_buffer"
11
+ require_relative "ui2/layout_manager"
12
+ require_relative "ui2/view_renderer"
13
+ require_relative "ui2/ui_controller"
14
+
15
+ require_relative "ui2/components/base_component"
16
+ require_relative "ui2/components/input_area"
17
+ require_relative "ui2/components/message_component"
18
+ require_relative "ui2/components/tool_component"
19
+ require_relative "ui2/components/common_component"
20
+ require_relative "ui2/components/welcome_banner"
21
+ require_relative "ui2/components/modal_component"
22
+
23
+ module Octo
24
+ module UI2
25
+ # Version of the UI2 system
26
+ VERSION = "1.0.0"
27
+
28
+ # Quick start: Create a UI controller and run
29
+ # @param config [Hash] Optional configuration (working_dir, mode, model)
30
+ # @example
31
+ # controller = Octo::UI2::UIController.new
32
+ # controller.on_input { |input| puts "Got: #{input}" }
33
+ # controller.start
34
+ def self.start(config = {}, &block)
35
+ controller = UIController.new(config)
36
+ controller.on_input(&block) if block_given?
37
+ controller.start
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Octo
4
+ # UIInterface defines the standard interface between Agent/CLI and UI implementations.
5
+ # All UI controllers (UIController, JsonUIController) must implement these methods.
6
+ module UIInterface
7
+ # === Output display ===
8
+ # @param content [String] text portion of the assistant reply (file:// links stripped)
9
+ # @param files [Array<Hash>] extracted file refs: [{ name:, path:, inline: }]
10
+ def show_assistant_message(content, files:); end
11
+ def show_tool_call(name, args); end
12
+ def show_tool_result(result, ui_payload: nil); end
13
+ def show_tool_stdout(lines); end
14
+ def show_tool_error(error); end
15
+ def show_tool_args(formatted_args); end
16
+ def show_file_write_preview(path, is_new_file:); end
17
+ def show_file_edit_preview(path); end
18
+ def show_file_error(error_message); end
19
+ def show_shell_preview(command); end
20
+ def show_diff(old_content, new_content, max_lines: 50); end
21
+ def show_token_usage(token_data); end
22
+ def show_complete(iterations:, duration: nil, cache_stats: nil, awaiting_user_feedback: false); end
23
+ def append_output(content); end
24
+
25
+ # === Status messages ===
26
+ def show_info(message, prefix_newline: true); end
27
+ def show_warning(message); end
28
+ def show_error(message); end
29
+ def show_success(message); end
30
+ def log(message, level: :info); end
31
+
32
+ # === Progress ===
33
+ # Unified progress indicator with type-based display customization.
34
+ # progress_type: "thinking" | "retrying" | "idle_compress" | custom
35
+ # phase: "active" | "done"
36
+ # metadata: extensible hash (e.g., {attempt: 3, total: 10} for retries)
37
+ def show_progress(message = nil, prefix_newline: true, progress_type: "thinking", phase: "active", metadata: {}); end
38
+
39
+ # Update the live "thinking" progress with streamed token counts.
40
+ # This is *purely decorative*: it must NEVER start a new progress
41
+ # indicator. If no thinking progress is currently active (e.g. during
42
+ # idle compression, where only a quiet "Compressing..." progress is
43
+ # live), the call is a no-op. UI2 overrides this; other UIs delegate
44
+ # to show_progress.
45
+ def stream_thinking_progress(input_tokens:, output_tokens:)
46
+ show_progress(
47
+ progress_type: "thinking",
48
+ phase: "active",
49
+ metadata: { input_tokens: input_tokens, output_tokens: output_tokens }
50
+ )
51
+ end
52
+
53
+ # === Progress (v2: owned handles) ===
54
+ #
55
+ # Start a new progress indicator and return an owned handle. The caller
56
+ # is responsible for finishing it — use +with_progress+ (below) whenever
57
+ # possible to get ensure-based auto-close.
58
+ #
59
+ # @param message [String, nil] Initial progress message (nil picks a random thinking verb).
60
+ # @param style [Symbol] :primary (foreground, yellow, bumps sessionbar)
61
+ # or :quiet (background, gray, no sessionbar change).
62
+ # @param quiet_on_fast_finish [Boolean] When true, a finish under
63
+ # FAST_FINISH_THRESHOLD_SECONDS removes the progress line entirely
64
+ # (preferred for per-tool wrappers so fast tools don't leave a
65
+ # permanent "Executing foo… (0s)" log line). The default
66
+ # implementation ignores this flag — it only affects the native
67
+ # UI2::UIController + ProgressHandle path.
68
+ # @return [#update, #finish, #cancel] a ProgressHandle-like object.
69
+ #
70
+ # Default implementation degrades gracefully to the old show_progress API
71
+ # so UI implementations that haven't migrated still behave correctly.
72
+ def start_progress(message: nil, style: :primary, quiet_on_fast_finish: false)
73
+ _ = quiet_on_fast_finish # default impl doesn't honor fast-collapse
74
+ progress_type = style == :primary ? "thinking" : "idle_compress"
75
+ show_progress(message, progress_type: progress_type, phase: "active")
76
+ LegacyProgressHandleAdapter.new(self, progress_type: progress_type)
77
+ end
78
+
79
+ # Run the given block with a progress indicator active. The handle is
80
+ # always finished in an +ensure+ block — exceptions (including
81
+ # AgentInterrupted) cannot leave the ticker or entry orphaned.
82
+ #
83
+ # @yieldparam handle the progress handle
84
+ def with_progress(message: nil, style: :primary, quiet_on_fast_finish: false)
85
+ handle = start_progress(
86
+ message: message,
87
+ style: style,
88
+ quiet_on_fast_finish: quiet_on_fast_finish
89
+ )
90
+ begin
91
+ yield handle
92
+ ensure
93
+ handle.finish
94
+ end
95
+ end
96
+
97
+ # Minimal adapter that lets UIs without a native ProgressHandle still
98
+ # participate in the new +with_progress+ API by delegating to the old
99
+ # +show_progress(phase: ...)+ contract. UI2::UIController overrides
100
+ # +start_progress+ directly with a native ProgressHandle, so this
101
+ # adapter is only used by plain/json/web/channel UIs.
102
+ class LegacyProgressHandleAdapter
103
+ def initialize(ui, progress_type:)
104
+ @ui = ui
105
+ @progress_type = progress_type
106
+ @closed = false
107
+ end
108
+
109
+ def update(message: nil, metadata: nil)
110
+ return if @closed
111
+ @ui.show_progress(message, progress_type: @progress_type, phase: "active", metadata: metadata || {})
112
+ end
113
+
114
+ def finish(final_message: nil)
115
+ return if @closed
116
+ @closed = true
117
+ @ui.show_progress(final_message, progress_type: @progress_type, phase: "done")
118
+ end
119
+ alias_method :cancel, :finish
120
+ end
121
+
122
+ # === State updates ===
123
+ def update_sessionbar(tasks: nil, status: nil, latency: nil); end
124
+ def update_todos(todos); end
125
+
126
+ def update_background_tasks(running: 0, tasks: []); end
127
+ def show_background_task_notice(command: nil, handle_id: nil, status: "success"); end
128
+
129
+ def set_working_status; end
130
+ def set_idle_status; end
131
+
132
+ # Broadcast the count of user messages currently sitting in @inbox waiting
133
+ # for the next iteration-boundary drain. Web renders a small "{{n}} messages
134
+ # waiting" hint above the input. Emitted by Agent on two occasions:
135
+ # - enqueue_user_message returned :running (msg will wait behind an in-flight run)
136
+ # - drain_inbox_into_history! consumed items (count typically drops to 0)
137
+ # CLI / JSON / channel UIs no-op by default.
138
+ #
139
+ # @param pending [Integer] number of user_msg items still queued
140
+ def update_user_message_queue_status(pending: 0); end
141
+
142
+ # === Blocking interaction ===
143
+ def request_confirmation(message, default: true); end
144
+
145
+ # === Input control (CLI layer) ===
146
+ def clear_input; end
147
+ def set_input_tips(message, type: :info); end
148
+ def show_next_message_suggestion(text); end
149
+ def hide_next_message_suggestion; end
150
+
151
+ # === Lifecycle ===
152
+ def stop; end
153
+ end
154
+ end
@@ -0,0 +1,191 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Octo
6
+ module Utils
7
+ class ArgumentsParser
8
+ # Parse and validate tool call arguments with JSON repair capability
9
+ def self.parse_and_validate(call, tool_registry)
10
+ # 1. Try standard parsing
11
+ begin
12
+ args = JSON.parse(call[:arguments], symbolize_names: true)
13
+
14
+ # Check if any key contains XML tags (< or >) indicating contamination
15
+ # Even though JSON.parse succeeded, the keys might be malformed
16
+ has_xml_contamination = args.keys.any? { |k| k.to_s.include?('<') || k.to_s.include?('>') }
17
+
18
+ if has_xml_contamination
19
+ # Force repair even though JSON.parse succeeded
20
+ raise JSON::ParserError.new("Keys contain XML contamination")
21
+ end
22
+
23
+ return validate_required_params(call, args, tool_registry)
24
+ rescue JSON::ParserError => e
25
+ # Continue to repair
26
+ end
27
+
28
+ # 2. Try simple repair
29
+ repaired = repair_json(call[:arguments])
30
+
31
+ begin
32
+ args = JSON.parse(repaired, symbolize_names: true)
33
+ return validate_required_params(call, args, tool_registry)
34
+ rescue JSON::ParserError, MissingRequiredParamsError => e
35
+ # 3. Repair failed or missing params, return helpful error
36
+ raise_helpful_error(call, tool_registry, e)
37
+ end
38
+ end
39
+
40
+
41
+ # Simple JSON repair: complete brackets and quotes, and remove XML contamination
42
+ def self.repair_json(json_str)
43
+
44
+ result = json_str.strip
45
+ # Step 0: Convert literal \n (backslash+n) to real newlines
46
+ result = result.gsub(/\\n/, "\n")
47
+ # Step 0.5: Unescape quotes in JSON keys and values (\" -> ")
48
+ # This handles cases like {"end_line\":550 or name=\"path\"
49
+ result = result.gsub(/\\"/, '"')
50
+ # Step 1: Remove XML-style parameter tags that Claude might mix in
51
+ # Pattern 1: </parameter> closing tags - remove completely
52
+ result = result.gsub(/<\/parameter>/, '')
53
+
54
+ # Pattern 2: <parameter name="key"> or <parameter name="key": opening tags -> convert to JSON key
55
+ # Example: \n<parameter name="end_line"> 330 -> , "end_line": 330
56
+ # Also handles: \n<parameter name="end_line": 330 -> , "end_line": 330
57
+ # result = result.gsub(/<parameter\s+name="([^"\\]+)":\s*/) { |match| ", \"#{$1}\": " }
58
+ # result = result.gsub(/<parameter\s+name="([^"\\]+)">/) { |match| ", \"#{$1}\":" }
59
+ result = result.gsub(/<parameter\s+name=\\?"([^"\\]+)\\?"[>:]?\s*/) { |match| ", \"#{$1}\": " }
60
+
61
+ # Pattern 3: Remove any remaining XML-like tags
62
+ result = result.gsub(/<[^>]+>/, '')
63
+
64
+ # Step 2: Clean up newlines with commas
65
+ # Example: 315\n, "end_line" -> 315, "end_line"
66
+ result = result.gsub(/\n\s*,/, ',')
67
+ result = result.gsub(/\n,/, ',')
68
+ result = result.gsub(/,\s*\n/, ',')
69
+
70
+ # Step 3: Clean up formatting issues
71
+ # Remove multiple consecutive commas
72
+ result = result.gsub(/,+/, ',')
73
+ # Remove trailing commas before closing braces/brackets
74
+ result = result.gsub(/,\s*}/, '}')
75
+ result = result.gsub(/,\s*\]/, ']')
76
+ # Remove leading commas after opening braces/brackets
77
+ result = result.gsub(/\{\s*,/, '{')
78
+ result = result.gsub(/\[\s*,/, '[')
79
+
80
+ # Step 4: Complete unclosed strings
81
+ result += '"' if result.count('"').odd?
82
+
83
+ # Step 5: Complete unclosed braces
84
+ depth = 0
85
+ result.each_char { |c| depth += 1 if c == '{'; depth -= 1 if c == '}' }
86
+ result += '}' * depth if depth > 0
87
+
88
+ result
89
+ end
90
+
91
+ # Validate required parameters and filter unknown parameters
92
+ def self.validate_required_params(call, args, tool_registry)
93
+ tool = tool_registry.get(call[:name])
94
+ required = tool.parameters&.dig(:required) || []
95
+ properties = tool.parameters&.dig(:properties) || {}
96
+
97
+ missing = required.reject { |param|
98
+ args.key?(param.to_sym) || args.key?(param.to_s)
99
+ }
100
+
101
+ if missing.any?
102
+ raise MissingRequiredParamsError.new(call[:name], missing, args.keys)
103
+ end
104
+
105
+ # Filter out unknown parameters to prevent errors when LLM sends extra arguments
106
+ known_params = properties.keys.map(&:to_sym) + properties.keys.map(&:to_s)
107
+ filtered_args = args.select { |key, _| known_params.include?(key) }
108
+
109
+ filtered_args
110
+ end
111
+
112
+ # Generate error message with tool definition
113
+ def self.raise_helpful_error(call, tool_registry, original_error)
114
+ tool = tool_registry.get(call[:name])
115
+ error_msg = build_error_message(call, tool, original_error)
116
+ raise BadArgumentsError, error_msg
117
+ end
118
+
119
+ def self.build_error_message(call, tool, original_error)
120
+ # Extract tool information
121
+ required_params = tool.parameters&.dig(:required) || []
122
+
123
+ # Try to parse provided parameters from incomplete JSON
124
+ provided_params = extract_provided_params(call[:arguments])
125
+
126
+ # Build clear error message
127
+ msg = []
128
+ msg << "Failed to parse arguments for tool '#{call[:name]}'."
129
+ msg << ""
130
+ msg << "Error: #{original_error.message}"
131
+ msg << ""
132
+
133
+ if provided_params.any?
134
+ msg << "Provided parameters: #{provided_params.join(', ')}"
135
+ else
136
+ msg << "No valid parameters could be extracted."
137
+ end
138
+
139
+ msg << "Required parameters: #{required_params.join(', ')}"
140
+ msg << ""
141
+ msg << "Tool definition:"
142
+ msg << format_tool_definition(tool)
143
+ msg << ""
144
+ msg << "Suggestions:"
145
+ msg << "- If the parameter value is too large (e.g., large file content), consider breaking it into smaller operations"
146
+ msg << "- Ensure all required parameters are provided"
147
+ msg << "- Simplify complex parameter values"
148
+
149
+ msg.join("\n")
150
+ end
151
+
152
+ # Extract parameter names from incomplete JSON
153
+ def self.extract_provided_params(json_str)
154
+ # Simple extraction: find all "key": patterns
155
+ json_str.scan(/"(\w+)"\s*:/).flatten.uniq
156
+ end
157
+
158
+ # Format tool definition (concise version)
159
+ def self.format_tool_definition(tool)
160
+ lines = []
161
+ lines << " Name: #{tool.name}"
162
+ lines << " Description: #{tool.description}"
163
+
164
+ if tool.parameters[:properties]
165
+ lines << " Parameters:"
166
+ tool.parameters[:properties].each do |param, spec|
167
+ required_mark = tool.parameters[:required]&.include?(param.to_s) ? " (required)" : ""
168
+ lines << " - #{param}#{required_mark}: #{spec[:description]}"
169
+ end
170
+ end
171
+
172
+ lines.join("\n")
173
+ end
174
+ end
175
+
176
+ # Raised when tool call arguments are malformed or missing required params.
177
+ class BadArgumentsError < StandardError; end
178
+
179
+ # Custom exception for missing required parameters
180
+ class MissingRequiredParamsError < BadArgumentsError
181
+ attr_reader :tool_name, :missing_params, :provided_params
182
+
183
+ def initialize(tool_name, missing_params, provided_params)
184
+ @tool_name = tool_name
185
+ @missing_params = missing_params
186
+ @provided_params = provided_params
187
+ super("Missing required parameters: #{missing_params.join(', ')}")
188
+ end
189
+ end
190
+ end
191
+ end