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,343 @@
1
+ // trash.js — Recently Deleted panel
2
+ //
3
+ // Top-level sidebar panel that lists files moved to trash by the agent
4
+ // across every project-scoped trash dir under ~/.octo/trash/.
5
+ //
6
+ // Each card shows the original path, project, size and deleted-at,
7
+ // plus Restore / Delete buttons. Bulk actions at the top:
8
+ // refresh, empty files older than 7 days, empty everything.
9
+ //
10
+ // Load order: after app.js modules (I18n, Modal), before app.js boot.
11
+
12
+ const Trash = (() => {
13
+ // ── Private state ────────────────────────────────────────────────────
14
+ let _files = [];
15
+ let _totals = { count: 0, size: 0 };
16
+ let _loading = false;
17
+ let _wired = false;
18
+
19
+ // ── Helpers ──────────────────────────────────────────────────────────
20
+
21
+ function $(id) { return document.getElementById(id); }
22
+
23
+ function escapeHtml(s) {
24
+ return String(s ?? "")
25
+ .replace(/&/g, "&amp;").replace(/</g, "&lt;")
26
+ .replace(/>/g, "&gt;").replace(/"/g, "&quot;");
27
+ }
28
+
29
+ function _t(key) {
30
+ return I18n.t ? I18n.t(key) : key;
31
+ }
32
+
33
+ function _humanBytes(n) {
34
+ if (!n || n < 0) return "0 B";
35
+ const units = ["B", "KB", "MB", "GB"];
36
+ let i = 0;
37
+ while (n >= 1024 && i < units.length - 1) { n /= 1024; i++; }
38
+ return (i === 0 ? n.toFixed(0) : n.toFixed(2)) + " " + units[i];
39
+ }
40
+
41
+ function _humanTime(iso) {
42
+ if (!iso) return "";
43
+ const d = new Date(iso);
44
+ if (isNaN(d.getTime())) return iso;
45
+ const now = new Date();
46
+ const ms = now - d;
47
+ const mins = Math.floor(ms / 60000);
48
+ const hours = Math.floor(ms / 3600000);
49
+ const days = Math.floor(ms / 86400000);
50
+ const zh = I18n.lang() === "zh";
51
+ if (mins < 1) return zh ? "刚刚" : "just now";
52
+ if (mins < 60) return zh ? `${mins} 分钟前` : `${mins}m ago`;
53
+ if (hours < 24) return zh ? `${hours} 小时前` : `${hours}h ago`;
54
+ if (days < 7) return zh ? `${days} 天前` : `${days}d ago`;
55
+ return d.toLocaleDateString();
56
+ }
57
+
58
+ // ── Data loading ─────────────────────────────────────────────────────
59
+
60
+ async function _load() {
61
+ if (_loading) return;
62
+ _loading = true;
63
+ const list = $("trash-list");
64
+ if (list) list.innerHTML =
65
+ `<div class="trash-loading">${_t("trash.loading")}</div>`;
66
+ try {
67
+ const res = await fetch("/api/trash");
68
+ const data = await res.json();
69
+ if (!res.ok) throw new Error(data.error || "Load failed");
70
+ _files = data.files || [];
71
+ _totals = { count: data.total_count || 0, size: data.total_size || 0 };
72
+ _render();
73
+ } catch (e) {
74
+ console.error("[Trash] load failed", e);
75
+ if (list) list.innerHTML =
76
+ `<div class="trash-empty trash-error">${escapeHtml(e.message)}</div>`;
77
+ } finally {
78
+ _loading = false;
79
+ }
80
+ }
81
+
82
+ function _render() {
83
+ const list = $("trash-list");
84
+ const summary = $("trash-summary");
85
+ const btnOld = $("btn-trash-empty-old");
86
+ const btnOrphans = $("btn-trash-empty-orphans");
87
+ const btnAll = $("btn-trash-empty-all");
88
+ if (!list) return;
89
+
90
+ const orphanCount = _files.filter(f => {
91
+ const root = f.project_root || "";
92
+ return /^\/(?:var\/folders|tmp|private\/var\/folders)\b/.test(root) ||
93
+ /\/d\d{8}-\d+-[a-z0-9]+(?:\/|$)/.test(root);
94
+ }).length;
95
+
96
+ if (summary) {
97
+ summary.textContent = _files.length
98
+ ? I18n.t("trash.summary", {
99
+ count: _totals.count,
100
+ size: _humanBytes(_totals.size)
101
+ }) + (orphanCount > 0 ? " • " + I18n.t("trash.summaryOrphans", { count: orphanCount }) : "")
102
+ : "";
103
+ }
104
+ if (btnOld) btnOld.disabled = _files.length === 0;
105
+ if (btnOrphans) btnOrphans.disabled = orphanCount === 0;
106
+ if (btnAll) btnAll.disabled = _files.length === 0;
107
+
108
+ if (_files.length === 0) {
109
+ list.innerHTML = `<div class="trash-empty">${_t("trash.empty")}</div>`;
110
+ return;
111
+ }
112
+
113
+ list.innerHTML = "";
114
+ _files.forEach(f => list.appendChild(_buildCard(f)));
115
+ }
116
+
117
+ function _buildCard(file) {
118
+ const card = document.createElement("div");
119
+ card.className = "trash-card";
120
+ card.dataset.project = file.project_root;
121
+ card.dataset.path = file.original_path;
122
+
123
+ const original = file.original_path || "";
124
+ const basename = original.split("/").pop() || original;
125
+ // Show last two path segments after basename to give agents context when
126
+ // many files share the same basename (very common: "package.json", "index.js").
127
+ const parts = original.split("/").filter(Boolean);
128
+ const shortPath = parts.length > 3
129
+ ? ".../" + parts.slice(-3).join("/")
130
+ : original;
131
+ const sizeStr = _humanBytes(file.file_size || 0);
132
+ const whenStr = _humanTime(file.deleted_at);
133
+ // Heuristic: if project_root starts with /var/folders or /tmp, or contains
134
+ // a tempdir-style name (d20260502-...), the original project is gone.
135
+ // We still show it, but mark it so the user can clean it up confidently.
136
+ const orphan = /^\/(?:var\/folders|tmp|private\/var\/folders)\b/.test(file.project_root || "") ||
137
+ /\/d\d{8}-\d+-[a-z0-9]+(?:\/|$)/.test(file.project_root || "");
138
+
139
+ card.innerHTML = `
140
+ <div class="trash-card-info">
141
+ <div class="trash-card-title" title="${escapeHtml(original)}">${escapeHtml(basename)}</div>
142
+ <div class="trash-card-path" title="${escapeHtml(original)}">${escapeHtml(shortPath)}</div>
143
+ <div class="trash-card-meta">
144
+ <span class="trash-project" title="${escapeHtml(file.project_root)}">
145
+ <svg xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
146
+ <path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
147
+ </svg>
148
+ ${escapeHtml(file.project_name || "")}
149
+ </span>
150
+ <span>${sizeStr}</span>
151
+ <span title="${escapeHtml(file.deleted_at || "")}">${escapeHtml(whenStr)}</span>
152
+ ${orphan ? `<span class="trash-missing" title="${_t("trash.orphanHint")}">⚠ ${_t("trash.orphan")}</span>` : ""}
153
+ </div>
154
+ </div>
155
+ <div class="trash-card-actions">
156
+ <button class="btn-trash-restore" title="${_t("trash.restore")}" ${orphan ? "disabled" : ""}>
157
+ <svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
158
+ <polyline points="1 4 1 10 7 10"/>
159
+ <path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/>
160
+ </svg>
161
+ ${_t("trash.restore")}
162
+ </button>
163
+ <button class="btn-trash-delete" title="${_t("trash.delete")}">
164
+ <svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
165
+ <polyline points="3 6 5 6 21 6"/>
166
+ <path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/>
167
+ <path d="M10 11v6"/><path d="M14 11v6"/>
168
+ </svg>
169
+ </button>
170
+ </div>`;
171
+
172
+ card.querySelector(".btn-trash-restore").addEventListener("click", () =>
173
+ _restoreOne(file, card));
174
+ card.querySelector(".btn-trash-delete").addEventListener("click", () =>
175
+ _deleteOne(file, card));
176
+
177
+ return card;
178
+ }
179
+
180
+ async function _restoreOne(file, card) {
181
+ const btn = card.querySelector(".btn-trash-restore");
182
+ btn.disabled = true;
183
+ try {
184
+ const res = await fetch("/api/trash/restore", {
185
+ method: "POST",
186
+ headers: { "Content-Type": "application/json" },
187
+ body: JSON.stringify({
188
+ project_root: file.project_root,
189
+ original_path: file.original_path
190
+ })
191
+ });
192
+ const data = await res.json();
193
+ if (!res.ok || !data.ok) {
194
+ alert(I18n.t("trash.restoreFail", {
195
+ msg: data.error || res.statusText
196
+ }));
197
+ } else {
198
+ // Remove card, update totals locally for instant feedback.
199
+ _files = _files.filter(f =>
200
+ !(f.project_root === file.project_root && f.original_path === file.original_path));
201
+ _totals = {
202
+ count: Math.max(0, _totals.count - 1),
203
+ size: Math.max(0, _totals.size - (file.file_size || 0))
204
+ };
205
+ _render();
206
+ }
207
+ } catch (e) {
208
+ alert(I18n.t("trash.restoreFail", { msg: e.message }));
209
+ } finally {
210
+ btn.disabled = false;
211
+ }
212
+ }
213
+
214
+ async function _deleteOne(file, card) {
215
+ const basename = (file.original_path || "").split("/").pop() || file.original_path;
216
+ const confirmed = await Modal.confirm(
217
+ I18n.t("trash.confirmDeleteOne", { name: basename })
218
+ );
219
+ if (!confirmed) return;
220
+
221
+ const url = "/api/trash?" + new URLSearchParams({
222
+ project: file.project_root,
223
+ file: file.original_path
224
+ }).toString();
225
+
226
+ try {
227
+ const res = await fetch(url, { method: "DELETE" });
228
+ const data = await res.json();
229
+ if (!res.ok || !data.ok) {
230
+ alert(data.error || res.statusText);
231
+ return;
232
+ }
233
+ _files = _files.filter(f =>
234
+ !(f.project_root === file.project_root && f.original_path === file.original_path));
235
+ _totals = {
236
+ count: Math.max(0, _totals.count - 1),
237
+ size: Math.max(0, _totals.size - (file.file_size || 0))
238
+ };
239
+ _render();
240
+ } catch (e) {
241
+ alert(e.message);
242
+ }
243
+ }
244
+
245
+ async function _emptyBulk(daysOld, confirmKey) {
246
+ const confirmed = await Modal.confirm(_t(confirmKey));
247
+ if (!confirmed) return;
248
+
249
+ const qs = new URLSearchParams();
250
+ qs.set("days_old", String(daysOld));
251
+ const url = "/api/trash?" + qs.toString();
252
+
253
+ try {
254
+ const res = await fetch(url, { method: "DELETE" });
255
+ const data = await res.json();
256
+ if (!res.ok || !data.ok) {
257
+ alert(data.error || res.statusText);
258
+ return;
259
+ }
260
+ if (data.deleted_count === 0 && daysOld > 0) {
261
+ alert(_t("trash.nothingOld"));
262
+ } else {
263
+ alert(I18n.t("trash.emptied", {
264
+ count: data.deleted_count || 0,
265
+ size: _humanBytes(data.freed_size || 0)
266
+ }));
267
+ }
268
+ await _load();
269
+ } catch (e) {
270
+ alert(e.message);
271
+ }
272
+ }
273
+
274
+ // Detects trash entries whose original project_root clearly no longer
275
+ // exists (test temp dirs under /var/folders, /tmp, or dir-format "dYYYYMMDD-...").
276
+ // The delete API does permanent deletion on a per-file basis.
277
+ async function _emptyOrphans() {
278
+ const orphans = _files.filter(f => {
279
+ const root = f.project_root || "";
280
+ return /^\/(?:var\/folders|tmp|private\/var\/folders)\b/.test(root) ||
281
+ /\/d\d{8}-\d+-[a-z0-9]+(?:\/|$)/.test(root);
282
+ });
283
+ if (orphans.length === 0) {
284
+ alert(_t("trash.noOrphans"));
285
+ return;
286
+ }
287
+ const confirmed = await Modal.confirm(
288
+ I18n.t("trash.confirmEmptyOrphans", { count: orphans.length })
289
+ );
290
+ if (!confirmed) return;
291
+
292
+ let deleted = 0, freed = 0, failed = 0;
293
+ for (const f of orphans) {
294
+ const url = "/api/trash?" + new URLSearchParams({
295
+ project: f.project_root,
296
+ file: f.original_path
297
+ }).toString();
298
+ try {
299
+ const r = await fetch(url, { method: "DELETE" });
300
+ const d = await r.json();
301
+ if (r.ok && d.ok) {
302
+ deleted += 1;
303
+ freed += d.freed_size || 0;
304
+ } else {
305
+ failed += 1;
306
+ }
307
+ } catch (_e) {
308
+ failed += 1;
309
+ }
310
+ }
311
+ alert(I18n.t("trash.orphansCleaned", {
312
+ count: deleted,
313
+ size: _humanBytes(freed),
314
+ failed: failed
315
+ }));
316
+ await _load();
317
+ }
318
+
319
+ function _wire() {
320
+ if (_wired) return;
321
+ _wired = true;
322
+ const btnRefresh = $("btn-trash-refresh");
323
+ const btnOld = $("btn-trash-empty-old");
324
+ const btnOrphans = $("btn-trash-empty-orphans");
325
+ const btnAll = $("btn-trash-empty-all");
326
+ if (btnRefresh) btnRefresh.addEventListener("click", () => _load());
327
+ if (btnOld) btnOld.addEventListener("click",
328
+ () => _emptyBulk(7, "trash.confirmEmptyOld"));
329
+ if (btnOrphans) btnOrphans.addEventListener("click", () => _emptyOrphans());
330
+ if (btnAll) btnAll.addEventListener("click",
331
+ () => _emptyBulk(0, "trash.confirmEmptyAll"));
332
+ }
333
+
334
+ // ── Public API ────────────────────────────────────────────────────────
335
+
336
+ return {
337
+ /** Called by Router when the trash panel becomes active. */
338
+ onPanelShow() {
339
+ _wire();
340
+ _load();
341
+ },
342
+ };
343
+ })();