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,290 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pastel"
4
+ require_relative "../theme_manager"
5
+
6
+ module Octo
7
+ module UI2
8
+ module Components
9
+ # CommandSuggestions displays a dropdown menu of available commands
10
+ # Supports keyboard navigation and filtering
11
+ class CommandSuggestions
12
+ attr_reader :selected_index, :visible
13
+
14
+ # System commands available by default
15
+ SYSTEM_COMMANDS = [
16
+ { command: "/clear", description: "Clear chat history and restart session" },
17
+ { command: "/config", description: "Open configuration (models, API keys, settings)" },
18
+ { command: "/undo", description: "Undo the last task and restore previous state" },
19
+ { command: "/help", description: "Show help information" },
20
+ { command: "/exit", description: "Exit the chat session" },
21
+ { command: "/quit", description: "Quit the application" }
22
+ ].freeze
23
+
24
+ def initialize
25
+ @pastel = Pastel.new
26
+ @commands = []
27
+ @filtered_commands = []
28
+ @selected_index = 0
29
+ @visible = false
30
+ @filter_text = ""
31
+ @skill_commands = []
32
+
33
+ # Initialize with system commands
34
+ update_commands
35
+ end
36
+
37
+ # Get current theme from ThemeManager
38
+ def theme
39
+ UI2::ThemeManager.current_theme
40
+ end
41
+
42
+ # Load skill commands from skill loader, filtered by agent profile whitelist
43
+ # @param skill_loader [Octo::SkillLoader] The skill loader instance
44
+ # @param agent_profile [Octo::AgentProfile, nil] Current agent profile (nil = allow all)
45
+ def load_skill_commands(skill_loader, agent_profile = nil)
46
+ return unless skill_loader
47
+
48
+ skills = skill_loader.user_invocable_skills
49
+ skills = skills.select { |s| s.allowed_for_agent?(agent_profile.name) } if agent_profile
50
+
51
+ @skill_commands = skills.map do |skill|
52
+ {
53
+ command: skill.slash_command,
54
+ description: skill.description || "No description available",
55
+ type: :skill,
56
+ argument_hint: skill.argument_hint
57
+ }
58
+ end
59
+
60
+ update_commands
61
+ end
62
+
63
+ # Show the suggestions dropdown
64
+ # @param filter_text [String] Initial filter text (everything after the /)
65
+ def show(filter_text = "")
66
+ @filter_text = filter_text
67
+ @visible = true
68
+ update_filtered_commands
69
+ @selected_index = 0
70
+ end
71
+
72
+ # Hide the suggestions dropdown
73
+ def hide
74
+ @visible = false
75
+ @filter_text = ""
76
+ @filtered_commands = []
77
+ @selected_index = 0
78
+ end
79
+
80
+ # Update filter text and refresh filtered commands
81
+ # @param text [String] Filter text (everything after the /)
82
+ def update_filter(text)
83
+ @filter_text = text
84
+ update_filtered_commands
85
+ @selected_index = 0 # Reset selection when filter changes
86
+ end
87
+
88
+ # Move selection up
89
+ def select_previous
90
+ return if @filtered_commands.empty?
91
+ @selected_index = (@selected_index - 1) % @filtered_commands.size
92
+ end
93
+
94
+ # Move selection down
95
+ def select_next
96
+ return if @filtered_commands.empty?
97
+ @selected_index = (@selected_index + 1) % @filtered_commands.size
98
+ end
99
+
100
+ # Get the currently selected command
101
+ # @return [Hash, nil] Selected command hash or nil if none selected
102
+ def selected_command
103
+ return nil if @filtered_commands.empty?
104
+ @filtered_commands[@selected_index]
105
+ end
106
+
107
+ # Get the currently selected command text
108
+ # @return [String, nil] Selected command text or nil if none selected
109
+ def selected_command_text
110
+ cmd = selected_command
111
+ cmd ? cmd[:command] : nil
112
+ end
113
+
114
+ # Get the argument hint for the currently selected command
115
+ # @return [String, nil] Argument hint string or nil if none
116
+ def selected_argument_hint
117
+ cmd = selected_command
118
+ cmd ? cmd[:argument_hint] : nil
119
+ end
120
+
121
+ # Check if there are any suggestions to show
122
+ # @return [Boolean]
123
+ def has_suggestions?
124
+ @visible && !@filtered_commands.empty?
125
+ end
126
+
127
+ # Calculate required height for rendering
128
+ # @return [Integer] Number of lines needed
129
+ def required_height
130
+ return 0 unless @visible
131
+ return 0 if @filtered_commands.empty?
132
+
133
+ # Header + commands + footer
134
+ 1 + [@filtered_commands.size, 5].min + 1 # Max 5 visible items
135
+ end
136
+
137
+ # Render the suggestions dropdown
138
+ # @param row [Integer] Starting row position
139
+ # @param col [Integer] Starting column position
140
+ # @param width [Integer] Maximum width for the dropdown
141
+ # @return [String] Rendered output
142
+ def render(row:, col:, width: 60)
143
+ return "" unless @visible
144
+ return "" if @filtered_commands.empty?
145
+
146
+ output = []
147
+ max_items = 5 # Maximum visible items
148
+
149
+ # Sliding window: keep selected item visible
150
+ start_idx = [@selected_index - max_items + 1, 0].max
151
+ start_idx = [start_idx, [@filtered_commands.size - max_items, 0].max].min
152
+ visible_commands = @filtered_commands[start_idx, max_items] || []
153
+
154
+ # Header
155
+ header = @pastel.dim("┌─ Commands ") + @pastel.dim("─" * (width - 13)) + @pastel.dim("┐")
156
+ output << position_cursor(row, col) + header
157
+
158
+ # Items
159
+ visible_commands.each_with_index do |cmd, idx|
160
+ is_selected = (start_idx + idx == @selected_index)
161
+ line = render_command_item(cmd, is_selected, width)
162
+ output << position_cursor(row + 1 + idx, col) + line
163
+ end
164
+
165
+ # Footer with navigation hint
166
+ footer_row = row + 1 + visible_commands.size
167
+ total = @filtered_commands.size
168
+ hint = total > max_items ? " (#{total - max_items} more...)" : ""
169
+ footer = @pastel.dim("└") + @pastel.dim("─" * (width - 2)) + @pastel.dim("┘")
170
+ output << position_cursor(footer_row, col) + footer
171
+
172
+ output.join
173
+ end
174
+
175
+ # Clear the rendered dropdown from screen
176
+ # @param row [Integer] Starting row position
177
+ # @param col [Integer] Starting column position
178
+ def clear_from_screen(row:, col:)
179
+ return unless @visible
180
+
181
+ height = required_height
182
+ output = []
183
+
184
+ height.times do |i|
185
+ output << position_cursor(row + i, col) + clear_line
186
+ end
187
+
188
+ print output.join
189
+ flush
190
+ end
191
+
192
+
193
+ # Update the complete commands list (system + skills)
194
+ private def update_commands
195
+ system_cmds = SYSTEM_COMMANDS.map { |c| c.merge(type: :system) }
196
+ @commands = system_cmds + @skill_commands
197
+ update_filtered_commands if @visible
198
+ end
199
+
200
+ # Update filtered commands based on current filter text
201
+ private def update_filtered_commands
202
+ if @filter_text.empty?
203
+ @filtered_commands = @commands
204
+ else
205
+ filter_lower = @filter_text.downcase
206
+ @filtered_commands = @commands.select do |cmd|
207
+ # Remove leading / for comparison
208
+ cmd_name = cmd[:command].sub(/^\//, "")
209
+ # Only match command name, not description
210
+ cmd_name.downcase.start_with?(filter_lower)
211
+ end
212
+ end
213
+ end
214
+
215
+ # Render a single command item
216
+ # @param cmd [Hash] Command hash with :command and :description
217
+ # @param selected [Boolean] Whether this item is selected
218
+ # @param width [Integer] Maximum width
219
+ # @return [String] Rendered item
220
+ private def render_command_item(cmd, selected, width)
221
+ # Calculate available space
222
+ available = width - 4 # Account for borders and padding
223
+
224
+ # Format command (e.g., "/clear")
225
+ command_text = cmd[:command]
226
+
227
+ # Format description
228
+ max_desc_length = available - command_text.length - 3 # 3 for spacing
229
+ description = truncate_text(cmd[:description], max_desc_length)
230
+
231
+ # Build line
232
+ if selected
233
+ # Highlighted selection
234
+ line = @pastel.on_blue(@pastel.white(" #{command_text} "))
235
+ line += @pastel.on_blue(@pastel.dim(" #{description}"))
236
+ # Pad to full width
237
+ content_length = command_text.length + description.length + 2
238
+ padding = " " * [available - content_length, 0].max
239
+ line += @pastel.on_blue(padding)
240
+ @pastel.dim("│") + line + @pastel.dim("│")
241
+ else
242
+ # Normal item
243
+ line = " #{@pastel.cyan(command_text)} #{@pastel.dim(description)}"
244
+ # Pad to full width
245
+ content_length = strip_ansi(line).length
246
+ padding = " " * [available - content_length, 0].max
247
+ @pastel.dim("│") + line + padding + @pastel.dim("│")
248
+ end
249
+ end
250
+
251
+ # Truncate text to maximum length
252
+ # @param text [String] Text to truncate
253
+ # @param max_length [Integer] Maximum length
254
+ # @return [String] Truncated text
255
+ private def truncate_text(text, max_length)
256
+ return "" if max_length <= 3
257
+ return text if text.length <= max_length
258
+
259
+ text[0...(max_length - 3)] + "..."
260
+ end
261
+
262
+ # Strip ANSI codes from text
263
+ # @param text [String] Text with ANSI codes
264
+ # @return [String] Plain text
265
+ private def strip_ansi(text)
266
+ text.gsub(/\e\[[0-9;]*m/, '')
267
+ end
268
+
269
+ # Position cursor at specific row and column
270
+ # @param row [Integer] Row position (0-indexed)
271
+ # @param col [Integer] Column position (0-indexed)
272
+ # @return [String] ANSI escape sequence
273
+ private def position_cursor(row, col)
274
+ "\e[#{row + 1};#{col + 1}H"
275
+ end
276
+
277
+ # Clear current line
278
+ # @return [String] ANSI escape sequence
279
+ private def clear_line
280
+ "\e[2K"
281
+ end
282
+
283
+ # Flush output to terminal
284
+ private def flush
285
+ $stdout.flush
286
+ end
287
+ end
288
+ end
289
+ end
290
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_component"
4
+
5
+ module Octo
6
+ module UI2
7
+ module Components
8
+ # CommonComponent renders common UI elements (progress, success, error, warning)
9
+ class CommonComponent < BaseComponent
10
+ # Render thinking indicator
11
+ # @return [String] Thinking indicator
12
+ def render_thinking
13
+ symbol = format_symbol(:thinking)
14
+ text = format_text("Thinking...", :thinking)
15
+ "#{symbol} #{text}"
16
+ end
17
+
18
+ # Render progress indicator (stopped state, gray)
19
+ # @param message [String] Progress message
20
+ # @return [String] Progress indicator
21
+ def render_progress(message)
22
+ symbol = format_symbol(:thinking)
23
+ text = format_text(message, :thinking)
24
+ "#{symbol} #{text}"
25
+ end
26
+
27
+ # Render working indicator (active state, yellow)
28
+ # @param message [String] Progress message
29
+ # @return [String] Working indicator
30
+ def render_working(message)
31
+ symbol = format_symbol(:working)
32
+ text = format_text(message, :working)
33
+ "#{symbol} #{text}"
34
+ end
35
+
36
+ # Render success message
37
+ # @param message [String] Success message
38
+ # @return [String] Success message
39
+ def render_success(message)
40
+ symbol = format_symbol(:success)
41
+ text = format_text(message, :success)
42
+ "#{symbol} #{text}"
43
+ end
44
+
45
+ # Render error message
46
+ # @param message [String] Error message
47
+ # @return [String] Error message
48
+ def render_error(message)
49
+ symbol = format_symbol(:error)
50
+ text = format_text(message, :error)
51
+ "#{symbol} #{text}"
52
+ end
53
+
54
+ # Render warning message
55
+ # @param message [String] Warning message
56
+ # @return [String] Warning message
57
+ def render_warning(message)
58
+ symbol = format_symbol(:warning)
59
+ text = format_text(message, :warning)
60
+ "#{symbol} #{text}"
61
+ end
62
+
63
+ # Render task completion summary
64
+ # @param iterations [Integer] Number of iterations
65
+ # @param duration [Float] Duration in seconds
66
+ # @param cache_tokens [Integer] Cache read tokens
67
+ # @param cache_requests [Integer] Total cache requests count
68
+ # @param cache_hits [Integer] Cache hit requests count
69
+ # @return [String] Formatted completion summary
70
+ def render_task_complete(iterations:, duration: nil, cache_tokens: nil, cache_requests: nil, cache_hits: nil)
71
+ lines = []
72
+ lines << ""
73
+ lines << @pastel.dim("─" * 60)
74
+ lines << render_success("Task Complete")
75
+ lines << ""
76
+
77
+ # Display each stat on a separate line
78
+ lines << " Iterations: #{iterations}"
79
+ lines << " Duration: #{duration.round(1)}s" if duration
80
+
81
+ # Display cache information if available
82
+ if cache_tokens && cache_tokens > 0
83
+ lines << " Cache Tokens: #{cache_tokens} tokens"
84
+ end
85
+
86
+ if cache_requests && cache_requests > 0
87
+ hit_rate = cache_hits > 0 ? ((cache_hits.to_f / cache_requests) * 100).round(1) : 0
88
+ lines << " Cache Requests: #{cache_requests} (#{cache_hits} hits, #{hit_rate}% hit rate)"
89
+ end
90
+
91
+ lines.join("\n")
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,226 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../line_editor"
4
+
5
+ module Octo
6
+ module UI2
7
+ module Components
8
+ # InlineInput provides inline input for confirmations and simple prompts
9
+ # Renders at the end of output area, not at fixed bottom position
10
+ class InlineInput
11
+ include LineEditor
12
+
13
+ attr_reader :prompt, :default_value
14
+
15
+ def initialize(prompt: "", default: nil)
16
+ initialize_line_editor
17
+ @prompt = prompt
18
+ @default_value = default
19
+ @active = false
20
+ @result_queue = nil
21
+ @paste_counter = 0
22
+ @paste_placeholders = {}
23
+ @continuation_prompt = "> " # Continuation prompt for wrapped lines
24
+ end
25
+
26
+ # Activate inline input and wait for user input
27
+ # @return [String] User input
28
+ def collect
29
+ @active = true
30
+ @result_queue = Queue.new
31
+ # Don't set default as initial text - start empty
32
+ @result_queue.pop
33
+ end
34
+
35
+ # Check if active
36
+ def active?
37
+ @active
38
+ end
39
+
40
+ # Handle keyboard input
41
+ # @param key [Symbol, String] Key input
42
+ # @return [Hash] Result with action
43
+ def handle_key(key)
44
+ return { action: nil } unless @active
45
+
46
+ case key
47
+ when Hash
48
+ if key[:type] == :rapid_input
49
+ # Handle multi-line paste with placeholder
50
+ pasted_text = key[:text]
51
+ pasted_lines = pasted_text.split(/\r\n|\r|\n/)
52
+
53
+ if pasted_lines.size > 1
54
+ # Multi-line paste - use placeholder
55
+ @paste_counter += 1
56
+ placeholder = "[##{@paste_counter} Paste Text]"
57
+ @paste_placeholders[placeholder] = pasted_text
58
+ insert_text(placeholder)
59
+ else
60
+ # Single line - insert directly
61
+ insert_text(pasted_text)
62
+ end
63
+ end
64
+ { action: :update }
65
+ when :enter
66
+ handle_enter
67
+ when :backspace
68
+ backspace
69
+ { action: :update }
70
+ when :delete
71
+ delete_char
72
+ { action: :update }
73
+ when :left_arrow, :ctrl_b
74
+ cursor_left
75
+ { action: :update }
76
+ when :right_arrow, :ctrl_f
77
+ cursor_right
78
+ { action: :update }
79
+ when :home, :ctrl_a
80
+ cursor_home
81
+ { action: :update }
82
+ when :end, :ctrl_e
83
+ cursor_end
84
+ { action: :update }
85
+ when :ctrl_k
86
+ kill_to_end
87
+ { action: :update }
88
+ when :ctrl_u
89
+ kill_to_start
90
+ { action: :update }
91
+ when :ctrl_w
92
+ kill_word
93
+ { action: :update }
94
+ when :shift_tab
95
+ handle_shift_tab
96
+ when :ctrl_o
97
+ handle_toggle_expand
98
+ when :ctrl_c
99
+ handle_cancel
100
+ when :escape
101
+ handle_cancel
102
+ else
103
+ if key.is_a?(String) && key.length >= 1 && key.ord >= 32
104
+ insert_char(key)
105
+ { action: :update }
106
+ else
107
+ { action: nil }
108
+ end
109
+ end
110
+ end
111
+
112
+ # Render inline input with prompt and cursor
113
+ # @return [String] Rendered line (may wrap to multiple lines)
114
+ def render
115
+ width = TTY::Screen.width
116
+ # Use effective content width (respecting MAX_CONTENT_WIDTH_RATIO)
117
+ content_width = effective_content_width(width)
118
+ prompt_width = calculate_display_width(strip_ansi_codes(@prompt))
119
+ available_width = content_width - prompt_width
120
+
121
+ # Get wrapped segments
122
+ wrapped_segments = wrap_line(@line, available_width)
123
+
124
+ # Build rendered output with cursor
125
+ output = ""
126
+
127
+ wrapped_segments.each_with_index do |segment, idx|
128
+ prefix = if idx == 0
129
+ @prompt
130
+ else
131
+ "> " # Continuation prompt indicator
132
+ end
133
+
134
+ # Render segment with cursor if needed
135
+ segment_text = render_line_segment_with_cursor(@line, segment[:start], segment[:end])
136
+
137
+ output += "#{prefix}#{segment_text}"
138
+ output += "\n" unless idx == wrapped_segments.size - 1
139
+ end
140
+
141
+ output
142
+ end
143
+
144
+ # Get cursor column position
145
+ # @return [Integer] Column position
146
+ def cursor_col
147
+ cursor_column(@prompt)
148
+ end
149
+
150
+ # Get cursor position for display (considering line wrapping and continuation prompt)
151
+ # @param width [Integer] Terminal width
152
+ # @return [Array<Integer>] Row and column position (0-indexed)
153
+ def cursor_position_for_display(width = TTY::Screen.width)
154
+ # Use effective content width (respecting MAX_CONTENT_WIDTH_RATIO)
155
+ content_width = effective_content_width(width)
156
+ cursor_position_with_wrap(@prompt, content_width, @continuation_prompt)
157
+ end
158
+
159
+ # Get the number of lines this input will occupy when rendered
160
+ # @param width [Integer] Terminal width
161
+ # @return [Integer] Number of lines
162
+ def line_count(width = TTY::Screen.width)
163
+ # Use effective content width (respecting MAX_CONTENT_WIDTH_RATIO)
164
+ content_width = effective_content_width(width)
165
+ prompt_width = calculate_display_width(strip_ansi_codes(@prompt))
166
+ available_width = content_width - prompt_width
167
+ return 1 if available_width <= 0
168
+
169
+ segments = wrap_line(@line, available_width)
170
+ segments.size
171
+ end
172
+
173
+ # Deactivate inline input
174
+ def deactivate
175
+ @active = false
176
+ @result_queue = nil
177
+ end
178
+
179
+
180
+ def handle_enter
181
+ result = expand_placeholders(current_line)
182
+ # If empty and has default, use default
183
+ result = @default_value.to_s if result.empty? && @default_value
184
+
185
+ queue = @result_queue
186
+ deactivate
187
+ queue&.push(result)
188
+
189
+ { action: :submit, result: result }
190
+ end
191
+
192
+ def expand_placeholders(text)
193
+ super(text, @paste_placeholders)
194
+ end
195
+
196
+ def handle_cancel
197
+ queue = @result_queue
198
+ deactivate
199
+ queue&.push(nil)
200
+
201
+ { action: :cancel }
202
+ end
203
+
204
+ def handle_shift_tab
205
+ # Auto-confirm as yes (or use default if it's true)
206
+ result = if @default_value == true || @default_value.to_s.downcase == "yes"
207
+ @default_value.to_s
208
+ else
209
+ "yes"
210
+ end
211
+
212
+ queue = @result_queue
213
+ deactivate
214
+ queue&.push(result)
215
+
216
+ { action: :toggle_mode }
217
+ end
218
+
219
+ private def handle_toggle_expand
220
+ # Toggle expansion of diff display
221
+ { action: :toggle_expand }
222
+ end
223
+ end
224
+ end
225
+ end
226
+ end