octo-agent 0.11.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.clacky/skills/commit/SKILL.md +423 -0
- data/.clacky/skills/gem-release/SKILL.md +199 -0
- data/.clacky/skills/gem-release/scripts/release.sh +304 -0
- data/.clacky/skills/oss-upload/SKILL.md +47 -0
- data/.octorules +106 -0
- data/.rspec +3 -0
- data/.rubocop.yml +8 -0
- data/CHANGELOG.md +76 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/CONTRIBUTING.md +92 -0
- data/Dockerfile +28 -0
- data/LICENSE.txt +22 -0
- data/POSITIONING.md +46 -0
- data/README.md +134 -0
- data/README_CN.md +134 -0
- data/Rakefile +34 -0
- data/benchmark/fixtures/sample_project/Gemfile +3 -0
- data/benchmark/fixtures/sample_project/lib/api_handler.rb +32 -0
- data/benchmark/fixtures/sample_project/lib/order_calculator.rb +23 -0
- data/benchmark/fixtures/sample_project/lib/user_renderer.rb +20 -0
- data/benchmark/fixtures/sample_project/spec/order_calculator_spec.rb +20 -0
- data/benchmark/results/EVALUATION_REPORT.md +165 -0
- data/benchmark/results/baseline_20260511_174424.json +128 -0
- data/benchmark/results/report_20260511_175256.json +271 -0
- data/benchmark/results/report_20260511_175444.json +271 -0
- data/benchmark/results/treatment_20260511_175103.json +130 -0
- data/benchmark/runner.rb +441 -0
- data/bin/octo +7 -0
- data/docs/agent-first-ui-design.md +77 -0
- data/docs/billing-system.md +318 -0
- data/docs/channel-architecture.md +235 -0
- data/docs/engineering-article.md +343 -0
- data/docs/session-skill-invocation.md +69 -0
- data/docs/time_machine_design.md +247 -0
- data/docs/ui2-architecture.md +124 -0
- data/homebrew/README.md +96 -0
- data/homebrew/openocto.rb +24 -0
- data/lib/octo/agent/hook_manager.rb +61 -0
- data/lib/octo/agent/llm_caller.rb +800 -0
- data/lib/octo/agent/memory_updater.rb +246 -0
- data/lib/octo/agent/message_compressor.rb +225 -0
- data/lib/octo/agent/message_compressor_helper.rb +869 -0
- data/lib/octo/agent/next_message_suggester.rb +215 -0
- data/lib/octo/agent/session_serializer.rb +685 -0
- data/lib/octo/agent/skill_auto_creator.rb +114 -0
- data/lib/octo/agent/skill_evolution.rb +61 -0
- data/lib/octo/agent/skill_manager.rb +466 -0
- data/lib/octo/agent/skill_reflector.rb +89 -0
- data/lib/octo/agent/system_prompt_builder.rb +101 -0
- data/lib/octo/agent/time_machine.rb +214 -0
- data/lib/octo/agent/tool_executor.rb +454 -0
- data/lib/octo/agent/tool_registry.rb +150 -0
- data/lib/octo/agent.rb +2180 -0
- data/lib/octo/agent_config.rb +989 -0
- data/lib/octo/agent_profile.rb +112 -0
- data/lib/octo/anthropic_stream_aggregator.rb +137 -0
- data/lib/octo/background_task_registry.rb +324 -0
- data/lib/octo/banner.rb +34 -0
- data/lib/octo/bedrock_stream_aggregator.rb +137 -0
- data/lib/octo/block_font.rb +331 -0
- data/lib/octo/cli.rb +968 -0
- data/lib/octo/client.rb +623 -0
- data/lib/octo/default_agents/SOUL.md +3 -0
- data/lib/octo/default_agents/USER.md +1 -0
- data/lib/octo/default_agents/base_prompt.md +66 -0
- data/lib/octo/default_agents/coding/profile.yml +2 -0
- data/lib/octo/default_agents/coding/system_prompt.md +67 -0
- data/lib/octo/default_agents/general/profile.yml +2 -0
- data/lib/octo/default_agents/general/system_prompt.md +16 -0
- data/lib/octo/default_parsers/doc_parser.rb +69 -0
- data/lib/octo/default_parsers/docx_parser.rb +188 -0
- data/lib/octo/default_parsers/pdf_parser.rb +120 -0
- data/lib/octo/default_parsers/pdf_parser_ocr.py +103 -0
- data/lib/octo/default_parsers/pdf_parser_plumber.py +62 -0
- data/lib/octo/default_parsers/pptx_parser.rb +140 -0
- data/lib/octo/default_parsers/xlsx_parser.rb +121 -0
- data/lib/octo/default_skills/browser-setup/SKILL.md +426 -0
- data/lib/octo/default_skills/channel-manager/SKILL.md +623 -0
- data/lib/octo/default_skills/channel-manager/dingtalk_setup.rb +191 -0
- data/lib/octo/default_skills/channel-manager/discord_setup.rb +199 -0
- data/lib/octo/default_skills/channel-manager/feishu_setup.rb +574 -0
- data/lib/octo/default_skills/channel-manager/import_lark_skills.rb +97 -0
- data/lib/octo/default_skills/channel-manager/install_feishu_skills.rb +105 -0
- data/lib/octo/default_skills/channel-manager/weixin_setup.rb +274 -0
- data/lib/octo/default_skills/code-explorer/SKILL.md +36 -0
- data/lib/octo/default_skills/cron-task-creator/SKILL.md +257 -0
- data/lib/octo/default_skills/cron-task-creator/evals/evals.json +38 -0
- data/lib/octo/default_skills/onboard/SKILL.md +578 -0
- data/lib/octo/default_skills/onboard/scripts/import_external_skills.rb +413 -0
- data/lib/octo/default_skills/onboard/scripts/install_builtin_skills.rb +97 -0
- data/lib/octo/default_skills/persist-memory/SKILL.md +59 -0
- data/lib/octo/default_skills/personal-website/SKILL.md +113 -0
- data/lib/octo/default_skills/personal-website/publish.rb +235 -0
- data/lib/octo/default_skills/product-help/SKILL.md +123 -0
- data/lib/octo/default_skills/product-help/docs/agent-config.md +74 -0
- data/lib/octo/default_skills/product-help/docs/best-practices.md +49 -0
- data/lib/octo/default_skills/product-help/docs/browser-tool.md +53 -0
- data/lib/octo/default_skills/product-help/docs/built-in-skills.md +43 -0
- data/lib/octo/default_skills/product-help/docs/cli-reference.md +82 -0
- data/lib/octo/default_skills/product-help/docs/create-your-first-skill.md +47 -0
- data/lib/octo/default_skills/product-help/docs/faq.md +98 -0
- data/lib/octo/default_skills/product-help/docs/how-to-use-a-skill.md +58 -0
- data/lib/octo/default_skills/product-help/docs/installation.md +59 -0
- data/lib/octo/default_skills/product-help/docs/memory-system.md +61 -0
- data/lib/octo/default_skills/product-help/docs/octorules.md +62 -0
- data/lib/octo/default_skills/product-help/docs/session-management.md +63 -0
- data/lib/octo/default_skills/product-help/docs/skill-basics.md +55 -0
- data/lib/octo/default_skills/product-help/docs/skill-frontmatter.md +61 -0
- data/lib/octo/default_skills/product-help/docs/web-server.md +49 -0
- data/lib/octo/default_skills/product-help/docs/what-is-octo.md +37 -0
- data/lib/octo/default_skills/product-help/docs/windows-installation.md +36 -0
- data/lib/octo/default_skills/product-help/docs/writing-tips.md +53 -0
- data/lib/octo/default_skills/recall-memory/SKILL.md +65 -0
- data/lib/octo/default_skills/skill-add/SKILL.md +59 -0
- data/lib/octo/default_skills/skill-add/scripts/install_from_zip.rb +295 -0
- data/lib/octo/default_skills/skill-creator/SKILL.md +602 -0
- data/lib/octo/default_skills/skill-creator/agents/analyzer.md +274 -0
- data/lib/octo/default_skills/skill-creator/agents/comparator.md +202 -0
- data/lib/octo/default_skills/skill-creator/agents/grader.md +223 -0
- data/lib/octo/default_skills/skill-creator/eval-viewer/generate_review.py +471 -0
- data/lib/octo/default_skills/skill-creator/eval-viewer/viewer.html +1325 -0
- data/lib/octo/default_skills/skill-creator/references/schemas.md +430 -0
- data/lib/octo/default_skills/skill-creator/scripts/__init__.py +0 -0
- data/lib/octo/default_skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
- data/lib/octo/default_skills/skill-creator/scripts/generate_report.py +326 -0
- data/lib/octo/default_skills/skill-creator/scripts/improve_description.py +310 -0
- data/lib/octo/default_skills/skill-creator/scripts/quick_validate.py +103 -0
- data/lib/octo/default_skills/skill-creator/scripts/run_eval.py +317 -0
- data/lib/octo/default_skills/skill-creator/scripts/run_loop.py +331 -0
- data/lib/octo/default_skills/skill-creator/scripts/utils.py +47 -0
- data/lib/octo/default_skills/skill-creator/scripts/validate_skill_frontmatter.rb +143 -0
- data/lib/octo/idle_compression_timer.rb +115 -0
- data/lib/octo/json_ui_controller.rb +204 -0
- data/lib/octo/message_format/anthropic.rb +409 -0
- data/lib/octo/message_format/bedrock.rb +361 -0
- data/lib/octo/message_format/open_ai.rb +222 -0
- data/lib/octo/message_history.rb +373 -0
- data/lib/octo/openai_stream_aggregator.rb +130 -0
- data/lib/octo/plain_ui_controller.rb +166 -0
- data/lib/octo/providers.rb +534 -0
- data/lib/octo/server/browser_manager.rb +397 -0
- data/lib/octo/server/channel/adapters/base.rb +82 -0
- data/lib/octo/server/channel/adapters/dingtalk/adapter.rb +314 -0
- data/lib/octo/server/channel/adapters/dingtalk/api_client.rb +391 -0
- data/lib/octo/server/channel/adapters/dingtalk/stream_client.rb +203 -0
- data/lib/octo/server/channel/adapters/discord/adapter.rb +229 -0
- data/lib/octo/server/channel/adapters/discord/api_client.rb +107 -0
- data/lib/octo/server/channel/adapters/discord/gateway_client.rb +270 -0
- data/lib/octo/server/channel/adapters/feishu/adapter.rb +320 -0
- data/lib/octo/server/channel/adapters/feishu/bot.rb +478 -0
- data/lib/octo/server/channel/adapters/feishu/file_processor.rb +36 -0
- data/lib/octo/server/channel/adapters/feishu/message_parser.rb +129 -0
- data/lib/octo/server/channel/adapters/feishu/ws_client.rb +423 -0
- data/lib/octo/server/channel/adapters/telegram/adapter.rb +375 -0
- data/lib/octo/server/channel/adapters/telegram/api_client.rb +205 -0
- data/lib/octo/server/channel/adapters/wecom/adapter.rb +148 -0
- data/lib/octo/server/channel/adapters/wecom/media_downloader.rb +115 -0
- data/lib/octo/server/channel/adapters/wecom/ws_client.rb +395 -0
- data/lib/octo/server/channel/adapters/weixin/adapter.rb +692 -0
- data/lib/octo/server/channel/adapters/weixin/api_client.rb +402 -0
- data/lib/octo/server/channel/channel_config.rb +178 -0
- data/lib/octo/server/channel/channel_manager.rb +468 -0
- data/lib/octo/server/channel/channel_ui_controller.rb +224 -0
- data/lib/octo/server/channel.rb +33 -0
- data/lib/octo/server/discover.rb +77 -0
- data/lib/octo/server/epipe_safe_io.rb +105 -0
- data/lib/octo/server/http_server.rb +3554 -0
- data/lib/octo/server/scheduler.rb +317 -0
- data/lib/octo/server/server_master.rb +325 -0
- data/lib/octo/server/session_registry.rb +431 -0
- data/lib/octo/server/web_ui_controller.rb +487 -0
- data/lib/octo/session_manager.rb +385 -0
- data/lib/octo/skill.rb +466 -0
- data/lib/octo/skill_loader.rb +328 -0
- data/lib/octo/tools/base.rb +118 -0
- data/lib/octo/tools/browser.rb +625 -0
- data/lib/octo/tools/edit.rb +165 -0
- data/lib/octo/tools/file_reader.rb +549 -0
- data/lib/octo/tools/glob.rb +162 -0
- data/lib/octo/tools/grep.rb +356 -0
- data/lib/octo/tools/invoke_skill.rb +96 -0
- data/lib/octo/tools/list_tasks.rb +54 -0
- data/lib/octo/tools/redo_task.rb +41 -0
- data/lib/octo/tools/request_user_feedback.rb +84 -0
- data/lib/octo/tools/security.rb +333 -0
- data/lib/octo/tools/terminal/output_cleaner.rb +63 -0
- data/lib/octo/tools/terminal/persistent_session.rb +268 -0
- data/lib/octo/tools/terminal/safe_rm.sh +106 -0
- data/lib/octo/tools/terminal/session_manager.rb +213 -0
- data/lib/octo/tools/terminal.rb +1828 -0
- data/lib/octo/tools/todo_manager.rb +374 -0
- data/lib/octo/tools/trash_manager.rb +388 -0
- data/lib/octo/tools/undo_task.rb +35 -0
- data/lib/octo/tools/web_fetch.rb +242 -0
- data/lib/octo/tools/web_search.rb +260 -0
- data/lib/octo/tools/write.rb +77 -0
- data/lib/octo/ui2/block_font.rb +10 -0
- data/lib/octo/ui2/components/base_component.rb +163 -0
- data/lib/octo/ui2/components/command_suggestions.rb +290 -0
- data/lib/octo/ui2/components/common_component.rb +96 -0
- data/lib/octo/ui2/components/inline_input.rb +226 -0
- data/lib/octo/ui2/components/input_area.rb +1338 -0
- data/lib/octo/ui2/components/message_component.rb +99 -0
- data/lib/octo/ui2/components/modal_component.rb +419 -0
- data/lib/octo/ui2/components/todo_area.rb +149 -0
- data/lib/octo/ui2/components/tool_component.rb +107 -0
- data/lib/octo/ui2/components/welcome_banner.rb +139 -0
- data/lib/octo/ui2/layout_manager.rb +807 -0
- data/lib/octo/ui2/line_editor.rb +363 -0
- data/lib/octo/ui2/markdown_renderer.rb +100 -0
- data/lib/octo/ui2/output_buffer.rb +370 -0
- data/lib/octo/ui2/progress_handle.rb +362 -0
- data/lib/octo/ui2/progress_indicator.rb +55 -0
- data/lib/octo/ui2/screen_buffer.rb +273 -0
- data/lib/octo/ui2/terminal_detector.rb +119 -0
- data/lib/octo/ui2/theme_manager.rb +85 -0
- data/lib/octo/ui2/themes/base_theme.rb +105 -0
- data/lib/octo/ui2/themes/hacker_theme.rb +62 -0
- data/lib/octo/ui2/themes/minimal_theme.rb +56 -0
- data/lib/octo/ui2/thinking_verbs.rb +26 -0
- data/lib/octo/ui2/ui_controller.rb +1625 -0
- data/lib/octo/ui2/view_renderer.rb +177 -0
- data/lib/octo/ui2.rb +40 -0
- data/lib/octo/ui_interface.rb +154 -0
- data/lib/octo/utils/arguments_parser.rb +191 -0
- data/lib/octo/utils/browser_detector.rb +195 -0
- data/lib/octo/utils/encoding.rb +92 -0
- data/lib/octo/utils/environment_detector.rb +140 -0
- data/lib/octo/utils/file_ignore_helper.rb +170 -0
- data/lib/octo/utils/file_processor.rb +601 -0
- data/lib/octo/utils/gitignore_parser.rb +154 -0
- data/lib/octo/utils/limit_stack.rb +152 -0
- data/lib/octo/utils/logger.rb +124 -0
- data/lib/octo/utils/login_shell.rb +72 -0
- data/lib/octo/utils/model_pricing.rb +646 -0
- data/lib/octo/utils/parser_manager.rb +165 -0
- data/lib/octo/utils/path_helper.rb +15 -0
- data/lib/octo/utils/scripts_manager.rb +59 -0
- data/lib/octo/utils/string_matcher.rb +158 -0
- data/lib/octo/utils/trash_directory.rb +112 -0
- data/lib/octo/utils/workspace_rules.rb +46 -0
- data/lib/octo/version.rb +5 -0
- data/lib/octo/web/app.css +7141 -0
- data/lib/octo/web/app.js +543 -0
- data/lib/octo/web/apple-touch-icon.png +0 -0
- data/lib/octo/web/auth.js +150 -0
- data/lib/octo/web/channels.js +276 -0
- data/lib/octo/web/datepicker.js +205 -0
- data/lib/octo/web/favicon.png +0 -0
- data/lib/octo/web/i18n.js +1073 -0
- data/lib/octo/web/icon-512.png +0 -0
- data/lib/octo/web/icon-dark.svg +25 -0
- data/lib/octo/web/icon.svg +29 -0
- data/lib/octo/web/index.html +871 -0
- data/lib/octo/web/marked.min.js +69 -0
- data/lib/octo/web/onboard.js +491 -0
- data/lib/octo/web/profile.js +442 -0
- data/lib/octo/web/sessions.js +4421 -0
- data/lib/octo/web/settings.js +913 -0
- data/lib/octo/web/sidebar.js +32 -0
- data/lib/octo/web/skills.js +885 -0
- data/lib/octo/web/tasks.js +297 -0
- data/lib/octo/web/theme.js +105 -0
- data/lib/octo/web/trash.js +343 -0
- data/lib/octo/web/vendor/hljs/highlight.min.js +1244 -0
- data/lib/octo/web/vendor/hljs/hljs-theme.css +95 -0
- data/lib/octo/web/vendor/katex/auto-render.min.js +1 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/katex.min.css +1 -0
- data/lib/octo/web/vendor/katex/katex.min.js +1 -0
- data/lib/octo/web/version.js +449 -0
- data/lib/octo/web/weixin-qr.html +209 -0
- data/lib/octo/web/ws-dispatcher.js +357 -0
- data/lib/octo/web/ws.js +128 -0
- data/lib/octo.rb +145 -0
- data/scripts/build/build.sh +329 -0
- data/scripts/build/lib/apt.sh +56 -0
- data/scripts/build/lib/brew.sh +89 -0
- data/scripts/build/lib/colors.sh +17 -0
- data/scripts/build/lib/gem.sh +95 -0
- data/scripts/build/lib/mise.sh +125 -0
- data/scripts/build/lib/network.sh +157 -0
- data/scripts/build/lib/os.sh +57 -0
- data/scripts/build/lib/shell.sh +37 -0
- data/scripts/build/src/install.sh.cc +174 -0
- data/scripts/build/src/install_browser.sh.cc +101 -0
- data/scripts/build/src/install_full.sh.cc +290 -0
- data/scripts/build/src/install_rails_deps.sh.cc +145 -0
- data/scripts/build/src/install_system_deps.sh.cc +123 -0
- data/scripts/build/src/uninstall.sh.cc +101 -0
- data/scripts/install.ps1 +532 -0
- data/scripts/install.sh +567 -0
- data/scripts/install_browser.sh +479 -0
- data/scripts/install_full.sh +838 -0
- data/scripts/install_rails_deps.sh +746 -0
- data/scripts/install_system_deps.sh +518 -0
- data/scripts/uninstall.sh +287 -0
- data/sig/octo.rbs +4 -0
- metadata +614 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
// ── WS event dispatcher ───────────────────────────────────────────────────
|
|
2
|
+
//
|
|
3
|
+
// Consumes events emitted by WS (ws.js) and dispatches them to the right
|
|
4
|
+
// business module (Sessions, Tasks, Skills, Channels, Settings, Brand, ...).
|
|
5
|
+
//
|
|
6
|
+
// Kept as a separate file from ws.js on purpose:
|
|
7
|
+
// - ws.js is a pure transport layer (connect / send / subscribe / reconnect)
|
|
8
|
+
// - this file is the application-level router that knows about every
|
|
9
|
+
// business module. Mixing the two would force ws.js to depend on every
|
|
10
|
+
// other module, breaking layering.
|
|
11
|
+
//
|
|
12
|
+
// Depends on: WS (ws.js), Sessions, Tasks, Skills, Channels, Settings, Brand,
|
|
13
|
+
// Router, I18n, global $ / escapeHtml / showConfirmModal helpers.
|
|
14
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
15
|
+
(function() {
|
|
16
|
+
// Guard: restore hash routing only once after initial session_list arrives.
|
|
17
|
+
let _initialRestoreDone = false;
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
WS.onEvent(ev => {
|
|
21
|
+
console.log("[DEBUG] WS event received:", ev.type, ev);
|
|
22
|
+
switch (ev.type) {
|
|
23
|
+
|
|
24
|
+
// ── Internal WS lifecycle ──────────────────────────────────────────
|
|
25
|
+
case "_ws_connected": {
|
|
26
|
+
const banner = document.getElementById("offline-banner");
|
|
27
|
+
if (banner) banner.style.display = "none";
|
|
28
|
+
const hint = $("ws-disconnect-hint");
|
|
29
|
+
if (hint) hint.style.display = "none";
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
case "_ws_disconnected": {
|
|
34
|
+
const banner = document.getElementById("offline-banner");
|
|
35
|
+
if (banner) {
|
|
36
|
+
banner.textContent = I18n.t("offline.banner");
|
|
37
|
+
banner.style.display = "block";
|
|
38
|
+
}
|
|
39
|
+
// Do NOT force status bar to "idle" here — on a brief WS hiccup the
|
|
40
|
+
// agent may still be running, and reconnect will deliver a fresh
|
|
41
|
+
// session snapshot that patches the real status. Forcing idle here
|
|
42
|
+
// caused stuck UI after reconnect when the snapshot logic wasn't
|
|
43
|
+
// re-asserting status on every reconnect.
|
|
44
|
+
Sessions.clearAllProgress();
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ── Session list ───────────────────────────────────────────────────
|
|
49
|
+
case "session_list": {
|
|
50
|
+
Sessions.setAll(ev.sessions || [], !!ev.has_more, ev.cron_count || 0);
|
|
51
|
+
Sessions.renderList();
|
|
52
|
+
|
|
53
|
+
// Restore URL hash once on initial connect; ignore subsequent session_list events.
|
|
54
|
+
// Skip if we are already on a session view (e.g. onboard flow navigated there
|
|
55
|
+
// before WS connected) — restoreFromHash would wrongly redirect to "welcome"
|
|
56
|
+
// because there is no hash set during onboarding.
|
|
57
|
+
if (!_initialRestoreDone) {
|
|
58
|
+
_initialRestoreDone = true;
|
|
59
|
+
if (Router.current !== "session") {
|
|
60
|
+
Router.restoreFromHash();
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
// If active session was deleted, go to welcome
|
|
64
|
+
if (Sessions.activeId && !Sessions.find(Sessions.activeId)) {
|
|
65
|
+
Router.navigate("welcome");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── Session lifecycle ──────────────────────────────────────────────
|
|
72
|
+
case "subscribed": {
|
|
73
|
+
// Re-enable send button now that the server has confirmed the subscription.
|
|
74
|
+
$("btn-send").disabled = false;
|
|
75
|
+
$("user-input").focus();
|
|
76
|
+
// If this session was created by Tasks.run(), fire the agent now that
|
|
77
|
+
// we're guaranteed to receive its broadcasts.
|
|
78
|
+
const pendingId = Sessions.takePendingRunTask();
|
|
79
|
+
if (pendingId && pendingId === ev.session_id) {
|
|
80
|
+
WS.send({ type: "run_task", session_id: pendingId });
|
|
81
|
+
}
|
|
82
|
+
// If a slash-command was queued (e.g. /onboard from first-boot flow),
|
|
83
|
+
// send it now — after restoreFromHash has settled — so appendMsg won't be wiped.
|
|
84
|
+
const pendingMsg = Sessions.takePendingMessage();
|
|
85
|
+
if (pendingMsg && pendingMsg.session_id === ev.session_id) {
|
|
86
|
+
Sessions.appendMsg("user", escapeHtml(pendingMsg.content), { time: new Date() });
|
|
87
|
+
WS.send({ type: "message", session_id: pendingMsg.session_id, content: pendingMsg.content });
|
|
88
|
+
}
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
case "session_update": {
|
|
93
|
+
// Two shapes arrive under this type:
|
|
94
|
+
// (1) Full session object from http_server broadcast_session_update:
|
|
95
|
+
// { type, session: { id, name, status, total_tasks, ... } }
|
|
96
|
+
// (2) Partial real-time update from web_ui_controller (tasks/status):
|
|
97
|
+
// { type, session_id, tasks?, status? }
|
|
98
|
+
let sid, patch;
|
|
99
|
+
if (ev.session) {
|
|
100
|
+
// Shape (1): full session — use as-is
|
|
101
|
+
sid = ev.session.id;
|
|
102
|
+
patch = ev.session;
|
|
103
|
+
} else {
|
|
104
|
+
// Shape (2): partial update — build patch from top-level fields
|
|
105
|
+
sid = ev.session_id;
|
|
106
|
+
patch = {};
|
|
107
|
+
if (ev.tasks !== undefined) patch.total_tasks = ev.tasks;
|
|
108
|
+
if (ev.status !== undefined) patch.status = ev.status;
|
|
109
|
+
// Latency pushed by Agent after each LLM call (see update_sessionbar).
|
|
110
|
+
// Stored under latest_latency — same field name the HTTP /api/sessions
|
|
111
|
+
// list returns, so updateInfoBar doesn't need to branch on the source.
|
|
112
|
+
if (ev.latency !== undefined) patch.latest_latency = ev.latency;
|
|
113
|
+
}
|
|
114
|
+
if (!sid) break;
|
|
115
|
+
Sessions.patch(sid, patch);
|
|
116
|
+
Sessions.renderList();
|
|
117
|
+
if (sid === Sessions.activeId) {
|
|
118
|
+
const current = Sessions.find(sid);
|
|
119
|
+
if (patch.status !== undefined) Sessions.updateStatusBar(patch.status);
|
|
120
|
+
Sessions.updateInfoBar(current);
|
|
121
|
+
// Update chat title/subtitle in case session was renamed or working_dir changed
|
|
122
|
+
Sessions.updateChatHeader(current);
|
|
123
|
+
}
|
|
124
|
+
// When a session finishes, refresh tasks and skills, and clear any progress state
|
|
125
|
+
if (patch.status === "idle") {
|
|
126
|
+
Tasks.load();
|
|
127
|
+
Skills.load();
|
|
128
|
+
// Clear progress state for this session (even if not currently active)
|
|
129
|
+
Sessions.clearProgress(sid);
|
|
130
|
+
}
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
case "session_renamed": {
|
|
135
|
+
Sessions.patch(ev.session_id, { name: ev.name });
|
|
136
|
+
Sessions.renderList();
|
|
137
|
+
// Title is now shown only in the sidebar; chat-header element was removed.
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
case "session_deleted":
|
|
142
|
+
Sessions.remove(ev.session_id);
|
|
143
|
+
if (ev.session_id === Sessions.activeId) Router.navigate("welcome");
|
|
144
|
+
Sessions.renderList();
|
|
145
|
+
break;
|
|
146
|
+
|
|
147
|
+
// ── Chat messages ──────────────────────────────────────────────────
|
|
148
|
+
case "history_user_message":
|
|
149
|
+
// During history replay, HistoryCollector sends events via HTTP — they
|
|
150
|
+
// never hit the WS dispatcher. Events arriving here are always live
|
|
151
|
+
// (agent just drained a queued message from the inbox). Remove the
|
|
152
|
+
// ghost bubble and render the real user message.
|
|
153
|
+
// Only remove the first pending ghost — subsequent ghosts belong
|
|
154
|
+
// to other queued messages that haven't been drained yet.
|
|
155
|
+
if (ev.session_id !== Sessions.activeId) break;
|
|
156
|
+
const firstGhost = document.querySelector(".msg-pending");
|
|
157
|
+
if (firstGhost) firstGhost.remove();
|
|
158
|
+
let userHtml = "";
|
|
159
|
+
if (Array.isArray(ev.images) && ev.images.length > 0) {
|
|
160
|
+
userHtml += ev.images.map(src => {
|
|
161
|
+
if (typeof src !== "string" || !src) return "";
|
|
162
|
+
if (src.startsWith("pdf:")) {
|
|
163
|
+
const fname = src.slice(4);
|
|
164
|
+
const lower = fname.toLowerCase();
|
|
165
|
+
const ext = (fname.split(".").pop() || "file").toUpperCase();
|
|
166
|
+
const displayExt = lower.endsWith(".tar.gz") ? "TAR.GZ" : ext;
|
|
167
|
+
const icon = ext === "PDF" ? "📄" :
|
|
168
|
+
(ext === "ZIP" || ext === "GZ" || ext === "TGZ" || ext === "TAR" ||
|
|
169
|
+
ext === "RAR" || ext === "7Z" || lower.endsWith(".tar.gz")) ? "🗜️" :
|
|
170
|
+
(ext === "DOC" || ext === "DOCX") ? "📝" :
|
|
171
|
+
(ext === "XLS" || ext === "XLSX" || ext === "CSV") ? "📊" :
|
|
172
|
+
(ext === "PPT" || ext === "PPTX") ? "📋" :
|
|
173
|
+
(ext === "MD" || ext === "MARKDOWN") ? "📝" :
|
|
174
|
+
(ext === "TXT" || ext === "LOG") ? "📄" : "📎";
|
|
175
|
+
return `<span class="msg-pdf-badge">` +
|
|
176
|
+
`<span class="msg-pdf-badge-icon">${icon}</span>` +
|
|
177
|
+
`<span class="msg-pdf-badge-info">` +
|
|
178
|
+
`<span class="msg-pdf-badge-name">${escapeHtml(fname)}</span>` +
|
|
179
|
+
`<span class="msg-pdf-badge-type">${escapeHtml(displayExt)}</span>` +
|
|
180
|
+
`</span>` +
|
|
181
|
+
`</span>`;
|
|
182
|
+
}
|
|
183
|
+
if (src.startsWith("expired:")) {
|
|
184
|
+
const fname = src.slice(8);
|
|
185
|
+
return `<span class="msg-pdf-badge msg-image-expired">` +
|
|
186
|
+
`<span class="msg-pdf-badge-icon">🖼️</span>` +
|
|
187
|
+
`<span class="msg-pdf-badge-info">` +
|
|
188
|
+
`<span class="msg-pdf-badge-name">${escapeHtml(fname || "image")}</span>` +
|
|
189
|
+
`<span class="msg-pdf-badge-type">${I18n.t("chat.image_expired") || "Expired"}</span>` +
|
|
190
|
+
`</span>` +
|
|
191
|
+
`</span>`;
|
|
192
|
+
}
|
|
193
|
+
return `<img src="${escapeHtml(src)}" alt="image" class="msg-image-thumb">`;
|
|
194
|
+
}).join("");
|
|
195
|
+
if (ev.content) userHtml += "<br>";
|
|
196
|
+
}
|
|
197
|
+
userHtml += escapeHtml(ev.content || "");
|
|
198
|
+
Sessions.appendMsg("user", userHtml, { time: ev.created_at });
|
|
199
|
+
break;
|
|
200
|
+
|
|
201
|
+
case "pending_user_messages":
|
|
202
|
+
// Replayed on WebSocket (re)subscribe — messages still in the
|
|
203
|
+
// inbox queue. Render them as pending ghost bubbles that the
|
|
204
|
+
// regular history_user_message handler will replace when the
|
|
205
|
+
// agent drains each one.
|
|
206
|
+
if (ev.session_id !== Sessions.activeId) break;
|
|
207
|
+
Sessions.renderPendingMessages(ev.messages || []);
|
|
208
|
+
break;
|
|
209
|
+
|
|
210
|
+
case "assistant_message":
|
|
211
|
+
if (ev.session_id !== Sessions.activeId) break;
|
|
212
|
+
Sessions.clearProgress();
|
|
213
|
+
Sessions.appendMsg("assistant", ev.content);
|
|
214
|
+
break;
|
|
215
|
+
|
|
216
|
+
case "tool_call":
|
|
217
|
+
if (ev.session_id !== Sessions.activeId) break;
|
|
218
|
+
Sessions.clearProgress();
|
|
219
|
+
Sessions.appendToolCall(ev.name, ev.args, ev.summary);
|
|
220
|
+
break;
|
|
221
|
+
|
|
222
|
+
case "tool_result":
|
|
223
|
+
if (ev.session_id !== Sessions.activeId) break;
|
|
224
|
+
Sessions.appendToolResult(ev.result, ev.ui_payload);
|
|
225
|
+
break;
|
|
226
|
+
|
|
227
|
+
case "tool_stdout":
|
|
228
|
+
if (ev.session_id !== Sessions.activeId) break;
|
|
229
|
+
Sessions.appendToolStdout(ev.lines);
|
|
230
|
+
break;
|
|
231
|
+
|
|
232
|
+
case "diff":
|
|
233
|
+
if (ev.session_id !== Sessions.activeId) break;
|
|
234
|
+
Sessions.appendDiff(ev.diff, ev.truncated);
|
|
235
|
+
break;
|
|
236
|
+
|
|
237
|
+
case "tool_error":
|
|
238
|
+
if (ev.session_id !== Sessions.activeId) break;
|
|
239
|
+
Sessions.appendMsg("info", `⚠ Tool error: ${escapeHtml(ev.error)}`);
|
|
240
|
+
break;
|
|
241
|
+
|
|
242
|
+
case "token_usage":
|
|
243
|
+
if (ev.session_id !== Sessions.activeId) break;
|
|
244
|
+
Sessions.appendTokenUsage(ev);
|
|
245
|
+
break;
|
|
246
|
+
|
|
247
|
+
case "progress":
|
|
248
|
+
if (ev.session_id !== Sessions.activeId) break;
|
|
249
|
+
if (ev.phase === "active" || ev.status === "start") {
|
|
250
|
+
const progress_type = ev.progress_type || "thinking";
|
|
251
|
+
const metadata = ev.metadata || {};
|
|
252
|
+
Sessions.showProgress(ev.message, progress_type, metadata, ev.started_at || null);
|
|
253
|
+
// A new turn started — any cached suggestion from the previous turn
|
|
254
|
+
// is stale.
|
|
255
|
+
Sessions.clearInputSuggestion();
|
|
256
|
+
} else {
|
|
257
|
+
Sessions.clearProgress(ev.message);
|
|
258
|
+
}
|
|
259
|
+
break;
|
|
260
|
+
|
|
261
|
+
case "complete":
|
|
262
|
+
if (ev.session_id !== Sessions.activeId) break;
|
|
263
|
+
Sessions.clearProgress();
|
|
264
|
+
Sessions.collapseToolGroup();
|
|
265
|
+
{
|
|
266
|
+
let mainLine = I18n.t("chat.done", { n: ev.iterations });
|
|
267
|
+
if (typeof ev.duration === "number" && ev.duration > 0) {
|
|
268
|
+
mainLine += I18n.t("chat.done.duration", { duration: ev.duration.toFixed(1) });
|
|
269
|
+
}
|
|
270
|
+
let cacheLine = null;
|
|
271
|
+
const cs = ev.cache_stats;
|
|
272
|
+
const total = cs && (cs.total_requests || cs["total_requests"]);
|
|
273
|
+
const hits = cs && (cs.cache_hit_requests || cs["cache_hit_requests"]);
|
|
274
|
+
const cachedTokens = cs && (cs.cache_read_input_tokens || cs["cache_read_input_tokens"]);
|
|
275
|
+
if (total && total > 0 && cachedTokens && cachedTokens > 0) {
|
|
276
|
+
const rate = ((hits / total) * 100).toFixed(1);
|
|
277
|
+
const tokensFmt = cachedTokens >= 1000
|
|
278
|
+
? `${(cachedTokens / 1000).toFixed(1)}k`
|
|
279
|
+
: `${cachedTokens}`;
|
|
280
|
+
cacheLine = I18n.t("chat.done.cache", {
|
|
281
|
+
rate, hits, total: total, tokens: tokensFmt
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
Sessions.appendInfo(`✓ ${mainLine}`, cacheLine);
|
|
285
|
+
}
|
|
286
|
+
break;
|
|
287
|
+
|
|
288
|
+
case "next_message_suggestion":
|
|
289
|
+
// Backend predicted the user's next message — render as ghost text
|
|
290
|
+
// in the composer. Only apply when the suggestion is for the session
|
|
291
|
+
// the user is currently looking at; suggestions for background
|
|
292
|
+
// sessions are dropped (they'd be stale by the time the user switches).
|
|
293
|
+
if (ev.session_id !== Sessions.activeId) break;
|
|
294
|
+
Sessions.setInputSuggestion(ev.text);
|
|
295
|
+
break;
|
|
296
|
+
|
|
297
|
+
case "request_feedback":
|
|
298
|
+
if (ev.session_id !== Sessions.activeId) break;
|
|
299
|
+
Sessions.showFeedbackRequest(ev.question, ev.context, ev.options);
|
|
300
|
+
break;
|
|
301
|
+
|
|
302
|
+
case "request_confirmation":
|
|
303
|
+
if (ev.session_id !== Sessions.activeId) break;
|
|
304
|
+
showConfirmModal(ev.id, ev.message);
|
|
305
|
+
break;
|
|
306
|
+
|
|
307
|
+
case "interrupted":
|
|
308
|
+
if (ev.session_id !== Sessions.activeId) break;
|
|
309
|
+
Sessions.clearProgress();
|
|
310
|
+
Sessions.collapseToolGroup();
|
|
311
|
+
Sessions.appendInfo(I18n.t("chat.interrupted"));
|
|
312
|
+
break;
|
|
313
|
+
|
|
314
|
+
// ── Info / errors ──────────────────────────────────────────────────
|
|
315
|
+
case "info":
|
|
316
|
+
Sessions.appendInfo(ev.message);
|
|
317
|
+
break;
|
|
318
|
+
|
|
319
|
+
case "background_tasks_update":
|
|
320
|
+
if (ev.session_id !== Sessions.activeId) break;
|
|
321
|
+
Sessions.updateBgTasksBadge(ev.running || 0, ev.tasks || []);
|
|
322
|
+
break;
|
|
323
|
+
|
|
324
|
+
case "background_task_notice":
|
|
325
|
+
if (ev.session_id !== Sessions.activeId) break;
|
|
326
|
+
Sessions.appendBgTaskNotice(ev.command, ev.handle_id, ev.status);
|
|
327
|
+
break;
|
|
328
|
+
|
|
329
|
+
case "user_message_queue_status":
|
|
330
|
+
// Agent broadcast: N user messages are sitting in the inbox waiting
|
|
331
|
+
// to be drained at the next iteration boundary. Show/hide the
|
|
332
|
+
// "{{n}} messages waiting" hint above the input bar.
|
|
333
|
+
if (ev.session_id !== Sessions.activeId) break;
|
|
334
|
+
Sessions.setInputQueueHint(ev.pending || 0);
|
|
335
|
+
break;
|
|
336
|
+
|
|
337
|
+
case "warning":
|
|
338
|
+
// Optimize retry messages for better UX
|
|
339
|
+
const friendlyWarning = _transformRetryWarning(ev.message);
|
|
340
|
+
if (friendlyWarning) {
|
|
341
|
+
Sessions.appendInfo(friendlyWarning);
|
|
342
|
+
}
|
|
343
|
+
break;
|
|
344
|
+
|
|
345
|
+
case "success":
|
|
346
|
+
Sessions.appendMsg("success", "✓ " + escapeHtml(ev.message));
|
|
347
|
+
break;
|
|
348
|
+
|
|
349
|
+
case "error":
|
|
350
|
+
if (!ev.session_id || ev.session_id === Sessions.activeId)
|
|
351
|
+
Sessions.appendMsg("error", escapeHtml(ev.message));
|
|
352
|
+
break;
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
})();
|
data/lib/octo/web/ws.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// ── WS — WebSocket connection manager ─────────────────────────────────────
|
|
2
|
+
//
|
|
3
|
+
// Responsibilities:
|
|
4
|
+
// - Connect / reconnect with exponential backoff
|
|
5
|
+
// - Queue outbound messages while disconnected, flush on reconnect
|
|
6
|
+
// - Track active session subscription; restore it after reconnect
|
|
7
|
+
// - Dispatch inbound server events to registered handlers
|
|
8
|
+
//
|
|
9
|
+
// Usage:
|
|
10
|
+
// WS.onEvent(handler) // register an event handler
|
|
11
|
+
// WS.send({ type: "..." }) // send (queued if not yet connected)
|
|
12
|
+
// WS.connect() // start connection
|
|
13
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
const WS = (() => {
|
|
16
|
+
// ── Private state ──────────────────────────────────────────────────────
|
|
17
|
+
let _socket = null;
|
|
18
|
+
let _ready = false;
|
|
19
|
+
let _retryDelay = 1000; // ms; doubles on each failure, cap 30s
|
|
20
|
+
let _retryTimer = null;
|
|
21
|
+
let _queue = []; // messages queued while disconnected
|
|
22
|
+
let _handlers = []; // event handler callbacks
|
|
23
|
+
let _subscribedId = null; // currently subscribed session id
|
|
24
|
+
|
|
25
|
+
const MAX_DELAY = 30_000;
|
|
26
|
+
|
|
27
|
+
// ── Private helpers ────────────────────────────────────────────────────
|
|
28
|
+
function _dispatch(event) {
|
|
29
|
+
_handlers.forEach(fn => {
|
|
30
|
+
try { fn(event); } catch (e) { console.error("[WS] handler error", e); }
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function _flushQueue() {
|
|
35
|
+
const pending = _queue.splice(0);
|
|
36
|
+
pending.forEach(msg => {
|
|
37
|
+
try { _socket.send(JSON.stringify(msg)); }
|
|
38
|
+
catch (e) { _queue.unshift(msg); } // re-queue on send error
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function _onOpen() {
|
|
43
|
+
_ready = true;
|
|
44
|
+
_retryDelay = 1000; // reset backoff on successful connect
|
|
45
|
+
|
|
46
|
+
// Always request fresh session list on (re)connect
|
|
47
|
+
_socket.send(JSON.stringify({ type: "list_sessions" }));
|
|
48
|
+
|
|
49
|
+
// Restore subscription if we had one before disconnect
|
|
50
|
+
if (_subscribedId) {
|
|
51
|
+
_socket.send(JSON.stringify({ type: "subscribe", session_id: _subscribedId }));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
_flushQueue();
|
|
55
|
+
_dispatch({ type: "_ws_connected" });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function _onMessage(e) {
|
|
59
|
+
let event;
|
|
60
|
+
try { event = JSON.parse(e.data); }
|
|
61
|
+
catch (ex) { console.error("[WS] parse error", ex); return; }
|
|
62
|
+
_dispatch(event);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function _onClose(e) {
|
|
66
|
+
// 1006 = abnormal closure (server rejected handshake, likely 401)
|
|
67
|
+
if (e.code === 1006 && !Auth.passed) {
|
|
68
|
+
Auth.reset();
|
|
69
|
+
Auth.check();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
_ready = false;
|
|
73
|
+
_socket = null;
|
|
74
|
+
console.warn(`[WS] closed — retry in ${_retryDelay}ms`);
|
|
75
|
+
_dispatch({ type: "_ws_disconnected" });
|
|
76
|
+
|
|
77
|
+
_retryTimer = setTimeout(() => {
|
|
78
|
+
_retryDelay = Math.min(_retryDelay * 2, MAX_DELAY);
|
|
79
|
+
_connect();
|
|
80
|
+
}, _retryDelay);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function _onError(err) {
|
|
84
|
+
console.error("[WS] error", err);
|
|
85
|
+
// onclose will fire automatically after onerror
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function _connect() {
|
|
89
|
+
if (_retryTimer) { clearTimeout(_retryTimer); _retryTimer = null; }
|
|
90
|
+
if (_socket && (_socket.readyState === WebSocket.OPEN ||
|
|
91
|
+
_socket.readyState === WebSocket.CONNECTING)) return;
|
|
92
|
+
|
|
93
|
+
const accessKey = Auth.getKey();
|
|
94
|
+
const protocol = location.protocol === "https:" ? "wss:" : "ws:";
|
|
95
|
+
const url = accessKey
|
|
96
|
+
? `${protocol}//${location.host}/ws?access_key=${encodeURIComponent(accessKey)}`
|
|
97
|
+
: `${protocol}//${location.host}/ws`;
|
|
98
|
+
_socket = new WebSocket(url);
|
|
99
|
+
_socket.onopen = _onOpen;
|
|
100
|
+
_socket.onmessage = _onMessage;
|
|
101
|
+
_socket.onclose = _onClose;
|
|
102
|
+
_socket.onerror = _onError;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ── Public API ─────────────────────────────────────────────────────────
|
|
106
|
+
return {
|
|
107
|
+
/** Register a handler for all server events. */
|
|
108
|
+
onEvent(fn) { _handlers.push(fn); },
|
|
109
|
+
|
|
110
|
+
/** Send a message. If not connected, queue it for later. */
|
|
111
|
+
send(obj) {
|
|
112
|
+
if (_ready && _socket) {
|
|
113
|
+
try { _socket.send(JSON.stringify(obj)); return; }
|
|
114
|
+
catch (e) { /* fall through to queue */ }
|
|
115
|
+
}
|
|
116
|
+
_queue.push(obj);
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
/** Track which session is currently subscribed (used for reconnect). */
|
|
120
|
+
setSubscribedSession(id) { _subscribedId = id; },
|
|
121
|
+
|
|
122
|
+
/** Start the connection. Call once on boot. */
|
|
123
|
+
connect: _connect,
|
|
124
|
+
|
|
125
|
+
/** True when the socket is open and ready. */
|
|
126
|
+
get ready() { return _ready; },
|
|
127
|
+
};
|
|
128
|
+
})();
|
data/lib/octo.rb
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# ── Global encoding defaults ──────────────────────────────────────────────────
|
|
4
|
+
# Force UTF-8 as the default external/internal encoding for all IO operations
|
|
5
|
+
# (File.read, Open3.capture3, HTTP bodies, etc.) so that binary-encoded strings
|
|
6
|
+
# from external processes or network I/O never cause "invalid byte sequence in
|
|
7
|
+
# UTF-8" errors on Ruby 2.6+.
|
|
8
|
+
# Binary-specific operations (File.binread, IO#read with "b" mode, .b) are
|
|
9
|
+
# unaffected — they always bypass this setting.
|
|
10
|
+
Encoding.default_external = Encoding::UTF_8
|
|
11
|
+
Encoding.default_internal = Encoding::UTF_8
|
|
12
|
+
|
|
13
|
+
# ── Ruby < 2.7 polyfills ──────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
# Enumerable#filter_map was added in Ruby 2.7.
|
|
16
|
+
if RUBY_VERSION < "2.7"
|
|
17
|
+
module Enumerable
|
|
18
|
+
def filter_map(&block)
|
|
19
|
+
return to_enum(:filter_map) unless block
|
|
20
|
+
|
|
21
|
+
each_with_object([]) do |item, result|
|
|
22
|
+
mapped = block.call(item)
|
|
23
|
+
result << mapped if mapped
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# File.absolute_path? was added in Ruby 2.7.
|
|
30
|
+
# Polyfill: a path is absolute if it starts with "/" (Unix) or a drive letter (Windows).
|
|
31
|
+
unless File.respond_to?(:absolute_path?)
|
|
32
|
+
def File.absolute_path?(path)
|
|
33
|
+
File.expand_path(path) == path.to_s
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# URI.encode_uri_component was added in Ruby 3.2.
|
|
38
|
+
# CGI.escape encodes spaces as '+'; replace with '%20' to match URI encoding.
|
|
39
|
+
require "uri"
|
|
40
|
+
require "cgi"
|
|
41
|
+
unless URI.respond_to?(:encode_uri_component)
|
|
42
|
+
def URI.encode_uri_component(str)
|
|
43
|
+
CGI.escape(str.to_s).gsub("+", "%20")
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# YAML.safe_load with permitted_classes: keyword was added in Psych 4 (Ruby 3.1).
|
|
48
|
+
# On older Ruby, the second positional argument serves the same purpose.
|
|
49
|
+
# This helper provides a unified interface across Ruby versions.
|
|
50
|
+
module YAMLCompat
|
|
51
|
+
def self.safe_load(yaml_string, permitted_classes: [])
|
|
52
|
+
if Psych::VERSION >= "4.0"
|
|
53
|
+
YAML.safe_load(yaml_string, permitted_classes: permitted_classes)
|
|
54
|
+
else
|
|
55
|
+
YAML.safe_load(yaml_string, permitted_classes)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def self.load_file(path, permitted_classes: [])
|
|
60
|
+
safe_load(File.read(path), permitted_classes: permitted_classes)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
require_relative "octo/version"
|
|
65
|
+
require_relative "octo/message_format/anthropic"
|
|
66
|
+
require_relative "octo/message_format/open_ai"
|
|
67
|
+
require_relative "octo/message_format/bedrock"
|
|
68
|
+
require_relative "octo/bedrock_stream_aggregator"
|
|
69
|
+
require_relative "octo/openai_stream_aggregator"
|
|
70
|
+
require_relative "octo/anthropic_stream_aggregator"
|
|
71
|
+
require_relative "octo/client"
|
|
72
|
+
require_relative "octo/skill"
|
|
73
|
+
require_relative "octo/skill_loader"
|
|
74
|
+
|
|
75
|
+
# Agent system
|
|
76
|
+
require_relative "octo/message_history"
|
|
77
|
+
require_relative "octo/agent_config"
|
|
78
|
+
require_relative "octo/agent_profile"
|
|
79
|
+
require_relative "octo/providers"
|
|
80
|
+
require_relative "octo/session_manager"
|
|
81
|
+
require_relative "octo/idle_compression_timer"
|
|
82
|
+
|
|
83
|
+
# Agent modules
|
|
84
|
+
require_relative "octo/agent/message_compressor"
|
|
85
|
+
require_relative "octo/agent/hook_manager"
|
|
86
|
+
require_relative "octo/agent/tool_registry"
|
|
87
|
+
|
|
88
|
+
# UI modules
|
|
89
|
+
require_relative "octo/ui2/thinking_verbs"
|
|
90
|
+
require_relative "octo/ui2/progress_indicator"
|
|
91
|
+
|
|
92
|
+
# Utils
|
|
93
|
+
require_relative "octo/utils/logger"
|
|
94
|
+
require_relative "octo/utils/encoding"
|
|
95
|
+
require_relative "octo/utils/environment_detector"
|
|
96
|
+
require_relative "octo/utils/browser_detector"
|
|
97
|
+
require_relative "octo/utils/scripts_manager"
|
|
98
|
+
require_relative "octo/utils/model_pricing"
|
|
99
|
+
require_relative "octo/utils/gitignore_parser"
|
|
100
|
+
require_relative "octo/utils/limit_stack"
|
|
101
|
+
require_relative "octo/utils/path_helper"
|
|
102
|
+
require_relative "octo/utils/file_ignore_helper"
|
|
103
|
+
require_relative "octo/utils/string_matcher"
|
|
104
|
+
require_relative "octo/utils/login_shell"
|
|
105
|
+
require_relative "octo/tools/base"
|
|
106
|
+
require_relative "octo/utils/file_processor"
|
|
107
|
+
|
|
108
|
+
require_relative "octo/tools/security"
|
|
109
|
+
require_relative "octo/tools/file_reader"
|
|
110
|
+
require_relative "octo/tools/write"
|
|
111
|
+
require_relative "octo/tools/edit"
|
|
112
|
+
require_relative "octo/tools/glob"
|
|
113
|
+
require_relative "octo/tools/grep"
|
|
114
|
+
require_relative "octo/tools/web_search"
|
|
115
|
+
require_relative "octo/tools/web_fetch"
|
|
116
|
+
require_relative "octo/tools/todo_manager"
|
|
117
|
+
require_relative "octo/tools/trash_manager"
|
|
118
|
+
require_relative "octo/tools/request_user_feedback"
|
|
119
|
+
require_relative "octo/tools/invoke_skill"
|
|
120
|
+
require_relative "octo/tools/undo_task"
|
|
121
|
+
require_relative "octo/tools/redo_task"
|
|
122
|
+
require_relative "octo/tools/list_tasks"
|
|
123
|
+
require_relative "octo/tools/browser"
|
|
124
|
+
require_relative "octo/tools/terminal"
|
|
125
|
+
require_relative "octo/agent"
|
|
126
|
+
|
|
127
|
+
require_relative "octo/server/session_registry"
|
|
128
|
+
require_relative "octo/server/web_ui_controller"
|
|
129
|
+
require_relative "octo/server/browser_manager"
|
|
130
|
+
require_relative "octo/cli"
|
|
131
|
+
|
|
132
|
+
module Octo
|
|
133
|
+
class AgentInterrupted < Exception; end # Inherit from Exception to bypass rescue StandardError
|
|
134
|
+
class AgentError < StandardError; end
|
|
135
|
+
class BadRequestError < AgentError; end # 400 errors — our request was malformed, history should be rolled back
|
|
136
|
+
class RetryableError < StandardError; end # Transient errors that should be retried (5xx, HTML response, rate limit)
|
|
137
|
+
# Upstream (model/router like OpenRouter/Bedrock) returned finish_reason="stop" together with
|
|
138
|
+
# one or more tool_calls whose `arguments` JSON was truncated (empty, "{}" placeholder, or
|
|
139
|
+
# otherwise unparseable). Subclass of RetryableError so it flows through the existing
|
|
140
|
+
# retry/fallback pipeline in LlmCaller#call_llm.
|
|
141
|
+
class UpstreamTruncatedError < RetryableError; end
|
|
142
|
+
class ToolCallError < AgentError; end # Raised when tool call fails due to invalid parameters
|
|
143
|
+
class BrowserNotReachableError < AgentError; end # Chrome/Edge not running or remote debugging disabled
|
|
144
|
+
# BrowserManager singleton: Octo::BrowserManager.instance
|
|
145
|
+
end
|