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
data/lib/octo/web/app.js
ADDED
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
// ── app.js — Main entry point ──────────────────────────────────────────────
|
|
2
|
+
//
|
|
3
|
+
// Coordinates WS, Sessions, Tasks, Skills and Settings modules.
|
|
4
|
+
// Handles WS event dispatch and wires up all DOM event listeners.
|
|
5
|
+
//
|
|
6
|
+
// Load order (in index.html):
|
|
7
|
+
// ws.js → sessions.js → tasks.js → skills.js → app.js
|
|
8
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
// ── DOM helper (shared by all modules loaded after this) ──────────────────
|
|
11
|
+
const $ = id => document.getElementById(id);
|
|
12
|
+
|
|
13
|
+
// ── Utilities (shared) ────────────────────────────────────────────────────
|
|
14
|
+
function escapeHtml(str) {
|
|
15
|
+
return String(str)
|
|
16
|
+
.replace(/&/g, "&")
|
|
17
|
+
.replace(/</g, "<")
|
|
18
|
+
.replace(/>/g, ">");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ── Router ────────────────────────────────────────────────────────────────
|
|
22
|
+
//
|
|
23
|
+
// Single source of truth for panel visibility and URL hash.
|
|
24
|
+
//
|
|
25
|
+
// Views:
|
|
26
|
+
// welcome → /#
|
|
27
|
+
// session/{id} → /#session/{id}
|
|
28
|
+
// tasks → /#tasks
|
|
29
|
+
// skills → /#skills
|
|
30
|
+
// settings → /#settings
|
|
31
|
+
//
|
|
32
|
+
// Usage:
|
|
33
|
+
// Router.navigate("session", { id: "abc123" })
|
|
34
|
+
// Router.navigate("tasks")
|
|
35
|
+
// Router.navigate("welcome")
|
|
36
|
+
//
|
|
37
|
+
// All panels must be listed in PANELS so they are hidden before the active
|
|
38
|
+
// one is shown. Modules must NOT touch panel display styles directly.
|
|
39
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
40
|
+
const PANELS = [
|
|
41
|
+
"setup-panel",
|
|
42
|
+
"onboard-panel",
|
|
43
|
+
"welcome",
|
|
44
|
+
"chat-panel",
|
|
45
|
+
"task-detail-panel",
|
|
46
|
+
"skills-panel",
|
|
47
|
+
"channels-panel",
|
|
48
|
+
"trash-panel",
|
|
49
|
+
"profile-panel",
|
|
50
|
+
"settings-panel",
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
const Router = (() => {
|
|
54
|
+
let _current = null; // current view name
|
|
55
|
+
let _params = {}; // current params (e.g. { id: "abc" } for session view)
|
|
56
|
+
let _skipNextHashChange = false; // prevent echo loop when we set hash ourselves
|
|
57
|
+
|
|
58
|
+
// Hide all panels.
|
|
59
|
+
function _hideAll() {
|
|
60
|
+
PANELS.forEach(p => {
|
|
61
|
+
const el = $(p);
|
|
62
|
+
if (el) el.style.display = "none";
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Update the URL hash without triggering a hashchange handler loop.
|
|
67
|
+
function _setHash(hash) {
|
|
68
|
+
_skipNextHashChange = true;
|
|
69
|
+
location.hash = hash;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Resolve a hash string into { view, params }.
|
|
73
|
+
function _parseHash(hash) {
|
|
74
|
+
const h = (hash || "").replace(/^#\/?/, "");
|
|
75
|
+
if (!h) return { view: "welcome", params: {} };
|
|
76
|
+
if (h === "tasks") return { view: "tasks", params: {} };
|
|
77
|
+
if (h === "skills") return { view: "skills", params: {} };
|
|
78
|
+
if (h === "channels") return { view: "channels", params: {} };
|
|
79
|
+
if (h === "trash") return { view: "trash", params: {} };
|
|
80
|
+
if (h === "profile") return { view: "profile", params: {} };
|
|
81
|
+
// Legacy: #memories redirects to #profile (memories are now merged into
|
|
82
|
+
// the profile panel). Kept so bookmarks / external links don't 404.
|
|
83
|
+
if (h === "memories") return { view: "profile", params: {} };
|
|
84
|
+
if (h === "settings") return { view: "settings", params: {} };
|
|
85
|
+
const m = h.match(/^session\/(.+)$/);
|
|
86
|
+
if (m) return { view: "session", params: { id: m[1] } };
|
|
87
|
+
return { view: "welcome", params: {} };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Sidebar items managed by Router (keyed by view name → element id).
|
|
91
|
+
// Router is the single authority for active highlight — modules must NOT
|
|
92
|
+
// add/remove the "active" class on these elements themselves.
|
|
93
|
+
const SIDEBAR_ITEMS = {
|
|
94
|
+
tasks: "tasks-sidebar-item",
|
|
95
|
+
skills: "skills-sidebar-item",
|
|
96
|
+
channels: "channels-sidebar-item",
|
|
97
|
+
trash: "trash-sidebar-item",
|
|
98
|
+
profile: "profile-sidebar-item",
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// Remove active highlight from all Router-managed sidebar items.
|
|
102
|
+
function _clearSidebarActive() {
|
|
103
|
+
Object.values(SIDEBAR_ITEMS).forEach(id => {
|
|
104
|
+
const el = $(id);
|
|
105
|
+
if (el) el.classList.remove("active");
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Core: apply a view change. Called both from navigate() and hashchange.
|
|
110
|
+
function _apply(view, params = {}) {
|
|
111
|
+
_current = view;
|
|
112
|
+
_params = params;
|
|
113
|
+
|
|
114
|
+
// Close sidebar on mobile when navigating to any view
|
|
115
|
+
_mobileCloseSidebar();
|
|
116
|
+
|
|
117
|
+
// ── Clean up previous state ──────────────────────────────────────────
|
|
118
|
+
if (Sessions.activeId) {
|
|
119
|
+
Sessions._cacheActiveAndDeselect();
|
|
120
|
+
}
|
|
121
|
+
Sessions.updateInfoBar(null); // hide info bar when leaving any session
|
|
122
|
+
// Clear all sidebar highlights and settings button active state
|
|
123
|
+
_clearSidebarActive();
|
|
124
|
+
const btnSettings = $("btn-settings");
|
|
125
|
+
if (btnSettings) btnSettings.classList.remove("active");
|
|
126
|
+
|
|
127
|
+
_hideAll();
|
|
128
|
+
|
|
129
|
+
// Reveal #app on first navigation — ensures the correct view (and language)
|
|
130
|
+
// is already in place before the user sees anything.
|
|
131
|
+
// #app covers sidebar + main, so data-i18n elements in the sidebar are also
|
|
132
|
+
// hidden until applyAll() has run (prevents flash of English sidebar labels).
|
|
133
|
+
const appEl = document.getElementById("app");
|
|
134
|
+
if (appEl && appEl.style.visibility === "hidden") {
|
|
135
|
+
I18n.applyAll(); // Translate all data-i18n elements before revealing
|
|
136
|
+
appEl.style.visibility = "";
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ── Activate target panel + sidebar highlight ────────────────────────
|
|
140
|
+
switch (view) {
|
|
141
|
+
|
|
142
|
+
case "session": {
|
|
143
|
+
const id = params.id;
|
|
144
|
+
const s = Sessions.find(id);
|
|
145
|
+
if (!s) {
|
|
146
|
+
// Session not found (e.g. deleted) — fall back to welcome
|
|
147
|
+
_apply("welcome");
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
_setHash(`session/${id}`);
|
|
151
|
+
$("chat-panel").style.display = "flex";
|
|
152
|
+
$("chat-panel").style.flexDirection = "column";
|
|
153
|
+
Sessions.updateChatHeader(s);
|
|
154
|
+
Sessions.updateStatusBar(s.status);
|
|
155
|
+
Sessions.updateInfoBar(s);
|
|
156
|
+
Sessions._restoreMessagesPublic(id);
|
|
157
|
+
Sessions._setActiveId(id);
|
|
158
|
+
// Immediately re-attach saved progress UI (timer + spinner) so it appears
|
|
159
|
+
// instantly without waiting for the async history fetch or WS replay.
|
|
160
|
+
Sessions._attachProgressUI(id);
|
|
161
|
+
WS.setSubscribedSession(id);
|
|
162
|
+
// Only disable send button until server confirms subscription
|
|
163
|
+
// Input field remains usable so user can type while waiting
|
|
164
|
+
$("btn-send").disabled = true;
|
|
165
|
+
WS.send({ type: "subscribe", session_id: id });
|
|
166
|
+
Sessions.renderList();
|
|
167
|
+
$("user-input").focus();
|
|
168
|
+
|
|
169
|
+
// Load session-scoped skill list (filtered by agent profile) for slash autocomplete
|
|
170
|
+
SkillAC.loadForSession(id);
|
|
171
|
+
|
|
172
|
+
// Always reload history on every switch (cache is not used)
|
|
173
|
+
Sessions.loadHistory(id);
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
case "tasks":
|
|
178
|
+
_setHash("tasks");
|
|
179
|
+
$("task-detail-panel").style.display = "flex";
|
|
180
|
+
Tasks.onPanelShow();
|
|
181
|
+
Sessions.renderList();
|
|
182
|
+
break;
|
|
183
|
+
|
|
184
|
+
case "skills":
|
|
185
|
+
_setHash("skills");
|
|
186
|
+
$("skills-panel").style.display = "flex";
|
|
187
|
+
Skills.onPanelShow();
|
|
188
|
+
Sessions.renderList();
|
|
189
|
+
break;
|
|
190
|
+
|
|
191
|
+
case "channels":
|
|
192
|
+
_setHash("channels");
|
|
193
|
+
$("channels-panel").style.display = "flex";
|
|
194
|
+
Channels.onPanelShow();
|
|
195
|
+
Sessions.renderList();
|
|
196
|
+
break;
|
|
197
|
+
|
|
198
|
+
case "trash":
|
|
199
|
+
_setHash("trash");
|
|
200
|
+
$("trash-panel").style.display = "flex";
|
|
201
|
+
Trash.onPanelShow();
|
|
202
|
+
Sessions.renderList();
|
|
203
|
+
break;
|
|
204
|
+
|
|
205
|
+
case "profile":
|
|
206
|
+
_setHash("profile");
|
|
207
|
+
$("profile-panel").style.display = "flex";
|
|
208
|
+
Profile.onPanelShow();
|
|
209
|
+
Sessions.renderList();
|
|
210
|
+
break;
|
|
211
|
+
|
|
212
|
+
case "settings":
|
|
213
|
+
_setHash("settings");
|
|
214
|
+
$("settings-panel").style.display = "";
|
|
215
|
+
if (btnSettings) btnSettings.classList.add("active");
|
|
216
|
+
Settings.open();
|
|
217
|
+
Sessions.renderList();
|
|
218
|
+
break;
|
|
219
|
+
|
|
220
|
+
case "setup":
|
|
221
|
+
// Full-screen mandatory setup (language + API key). No hash — keep URL clean.
|
|
222
|
+
$("setup-panel").style.display = "flex";
|
|
223
|
+
break;
|
|
224
|
+
|
|
225
|
+
case "onboard":
|
|
226
|
+
// Kept for compatibility; setup-panel is now used for first-run setup.
|
|
227
|
+
$("onboard-panel").style.display = "flex";
|
|
228
|
+
break;
|
|
229
|
+
|
|
230
|
+
default: // "welcome"
|
|
231
|
+
_setHash("");
|
|
232
|
+
$("welcome").style.display = "";
|
|
233
|
+
Sessions.renderList();
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Re-apply sidebar active highlight after all rendering is done.
|
|
238
|
+
// renderSection() rebuilds the DOM element, so we stamp active *after*.
|
|
239
|
+
_clearSidebarActive();
|
|
240
|
+
const activeItem = SIDEBAR_ITEMS[view];
|
|
241
|
+
if (activeItem) $(activeItem)?.classList.add("active");
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Listen for browser back/forward (or manual hash edits).
|
|
245
|
+
window.addEventListener("hashchange", () => {
|
|
246
|
+
if (_skipNextHashChange) {
|
|
247
|
+
_skipNextHashChange = false;
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const { view, params } = _parseHash(location.hash);
|
|
251
|
+
_apply(view, params);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
get current() { return _current; },
|
|
256
|
+
get params() { return _params; },
|
|
257
|
+
|
|
258
|
+
/** Navigate to a view. This is the only way panels should change. */
|
|
259
|
+
navigate(view, params = {}) {
|
|
260
|
+
_apply(view, params);
|
|
261
|
+
},
|
|
262
|
+
|
|
263
|
+
/** Restore state from current URL hash (called once on boot after data loads). */
|
|
264
|
+
restoreFromHash() {
|
|
265
|
+
const { view, params } = _parseHash(location.hash);
|
|
266
|
+
_apply(view, params);
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
})();
|
|
270
|
+
|
|
271
|
+
// ── Modal utility ─────────────────────────────────────────────────────────
|
|
272
|
+
const Modal = (() => {
|
|
273
|
+
/** Show a yes/no confirmation dialog. Returns a Promise<boolean>. */
|
|
274
|
+
function confirm(message) {
|
|
275
|
+
return new Promise(resolve => {
|
|
276
|
+
$("modal-message").textContent = message;
|
|
277
|
+
$("modal-overlay").style.display = "flex";
|
|
278
|
+
|
|
279
|
+
const cleanup = (result) => {
|
|
280
|
+
$("modal-overlay").style.display = "none";
|
|
281
|
+
$("modal-yes").onclick = null;
|
|
282
|
+
$("modal-no").onclick = null;
|
|
283
|
+
resolve(result);
|
|
284
|
+
};
|
|
285
|
+
$("modal-yes").onclick = () => cleanup(true);
|
|
286
|
+
$("modal-no").onclick = () => cleanup(false);
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** Show a text input prompt dialog. Returns a Promise<string|null>. */
|
|
291
|
+
function prompt(message, defaultValue = "") {
|
|
292
|
+
return new Promise(resolve => {
|
|
293
|
+
$("prompt-modal-message").textContent = message;
|
|
294
|
+
const input = $("prompt-modal-input");
|
|
295
|
+
input.value = defaultValue;
|
|
296
|
+
$("prompt-modal-overlay").style.display = "flex";
|
|
297
|
+
|
|
298
|
+
// Auto-focus and select all text
|
|
299
|
+
setTimeout(() => {
|
|
300
|
+
input.focus();
|
|
301
|
+
input.select();
|
|
302
|
+
}, 50);
|
|
303
|
+
|
|
304
|
+
const cleanup = (result) => {
|
|
305
|
+
$("prompt-modal-overlay").style.display = "none";
|
|
306
|
+
$("prompt-modal-ok").onclick = null;
|
|
307
|
+
$("prompt-modal-cancel").onclick = null;
|
|
308
|
+
input.onkeydown = null;
|
|
309
|
+
resolve(result);
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
$("prompt-modal-ok").onclick = () => cleanup(input.value.trim() || null);
|
|
313
|
+
$("prompt-modal-cancel").onclick = () => cleanup(null);
|
|
314
|
+
|
|
315
|
+
// Support Enter to confirm, Escape to cancel
|
|
316
|
+
input.onkeydown = (e) => {
|
|
317
|
+
if (e.key === "Enter") cleanup(input.value.trim() || null);
|
|
318
|
+
if (e.key === "Escape") cleanup(null);
|
|
319
|
+
};
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** Show a rename dialog. Returns a Promise<string|null>. */
|
|
324
|
+
function rename(currentName = "") {
|
|
325
|
+
return new Promise(resolve => {
|
|
326
|
+
const input = $("rename-modal-input");
|
|
327
|
+
input.value = currentName;
|
|
328
|
+
input.classList.remove("input-error");
|
|
329
|
+
$("rename-modal-overlay").style.display = "flex";
|
|
330
|
+
|
|
331
|
+
setTimeout(() => {
|
|
332
|
+
input.focus();
|
|
333
|
+
input.select();
|
|
334
|
+
}, 50);
|
|
335
|
+
|
|
336
|
+
const cleanup = (result) => {
|
|
337
|
+
$("rename-modal-overlay").style.display = "none";
|
|
338
|
+
$("rename-modal-save").onclick = null;
|
|
339
|
+
$("rename-modal-cancel").onclick = null;
|
|
340
|
+
$("rename-modal-overlay").onclick = null;
|
|
341
|
+
input.onkeydown = null;
|
|
342
|
+
input.oninput = null;
|
|
343
|
+
resolve(result);
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
const saveHandler = () => {
|
|
347
|
+
const newName = input.value.trim();
|
|
348
|
+
if (!newName) {
|
|
349
|
+
input.classList.add("input-error");
|
|
350
|
+
input.focus();
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
cleanup(newName === currentName ? null : newName);
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
input.oninput = () => input.classList.remove("input-error");
|
|
357
|
+
|
|
358
|
+
$("rename-modal-save").onclick = saveHandler;
|
|
359
|
+
$("rename-modal-cancel").onclick = () => cleanup(null);
|
|
360
|
+
|
|
361
|
+
input.onkeydown = (e) => {
|
|
362
|
+
if (e.key === "Enter") { e.preventDefault(); saveHandler(); }
|
|
363
|
+
if (e.key === "Escape") cleanup(null);
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
// Close on overlay click
|
|
367
|
+
$("rename-modal-overlay").onclick = (e) => {
|
|
368
|
+
if (e.target.id === "rename-modal-overlay") cleanup(null);
|
|
369
|
+
};
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return { confirm, prompt, rename };
|
|
374
|
+
})();
|
|
375
|
+
|
|
376
|
+
// ── Confirmation modal ────────────────────────────────────────────────────
|
|
377
|
+
function showConfirmModal(confId, message) {
|
|
378
|
+
$("modal-message").textContent = message;
|
|
379
|
+
$("modal-overlay").style.display = "flex";
|
|
380
|
+
|
|
381
|
+
const answer = result => {
|
|
382
|
+
$("modal-overlay").style.display = "none";
|
|
383
|
+
WS.send({ type: "confirmation", session_id: Sessions.activeId, id: confId, result });
|
|
384
|
+
};
|
|
385
|
+
$("modal-yes").onclick = () => answer("yes");
|
|
386
|
+
$("modal-no").onclick = () => answer("no");
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
// ── WS event dispatcher ───────────────────────────────────────────────────
|
|
391
|
+
// Moved to ws-dispatcher.js.
|
|
392
|
+
|
|
393
|
+
// ── Image & file attachments ──────────────────────────────────────────────
|
|
394
|
+
// Moved to sessions.js (Composer section — _initComposer() in Sessions.init()).
|
|
395
|
+
// All state (_pendingImages/_pendingFiles), helpers (_addAttachmentFile/etc.),
|
|
396
|
+
// preview rendering, and sendMessage() now live there as private members.
|
|
397
|
+
|
|
398
|
+
// ── DOM event listeners ───────────────────────────────────────────────────
|
|
399
|
+
// Sidebar toggle (with mobile overlay support)
|
|
400
|
+
function _isMobile() { return window.innerWidth <= 768; }
|
|
401
|
+
|
|
402
|
+
function _closeSidebar() {
|
|
403
|
+
$("sidebar").classList.add("hidden");
|
|
404
|
+
$("sidebar-overlay").classList.remove("active");
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function _openSidebar() {
|
|
408
|
+
$("sidebar").classList.remove("hidden");
|
|
409
|
+
if (_isMobile()) $("sidebar-overlay").classList.add("active");
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function _toggleSidebar() {
|
|
413
|
+
const isHidden = $("sidebar").classList.contains("hidden");
|
|
414
|
+
isHidden ? _openSidebar() : _closeSidebar();
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if ($("btn-toggle-sidebar")) {
|
|
418
|
+
$("btn-toggle-sidebar").addEventListener("click", _toggleSidebar);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Tap overlay to close sidebar on mobile
|
|
422
|
+
$("sidebar-overlay").addEventListener("click", _closeSidebar);
|
|
423
|
+
|
|
424
|
+
// On mobile: start with sidebar hidden
|
|
425
|
+
if (_isMobile()) _closeSidebar();
|
|
426
|
+
|
|
427
|
+
// On mobile: auto-close sidebar when switching sessions/pages
|
|
428
|
+
function _mobileCloseSidebar() {
|
|
429
|
+
if (_isMobile()) _closeSidebar();
|
|
430
|
+
}
|
|
431
|
+
// Expose for use in sessions.js (rename/delete dialogs need to close sidebar first)
|
|
432
|
+
window.mobileCloseSidebar = _mobileCloseSidebar;
|
|
433
|
+
|
|
434
|
+
// ── New session controls ───────────────────────────────────────────────────
|
|
435
|
+
// Moved to sessions.js (_initNewSessionControls, called from Sessions.init()).
|
|
436
|
+
|
|
437
|
+
// ── Session search bar ─────────────────────────────────────────────────────
|
|
438
|
+
// Moved to sessions.js (_initSearch in Sessions.init()).
|
|
439
|
+
|
|
440
|
+
// ── Theme / session-scoped message panel bindings ──────────────────────────
|
|
441
|
+
|
|
442
|
+
// Theme toggle in header
|
|
443
|
+
if ($("theme-toggle-header")) {
|
|
444
|
+
$("theme-toggle-header").addEventListener("click", () => Theme.toggle());
|
|
445
|
+
}
|
|
446
|
+
// btn-delete-session, #messages scroll-to-top (load history), and btn-interrupt
|
|
447
|
+
// moved to sessions.js (_initMessageHistory in Sessions.init()).
|
|
448
|
+
|
|
449
|
+
// btn-send, btn-attach, image-file-input change, input-area drag/drop, and
|
|
450
|
+
// user-input paste handlers moved to sessions.js (_initComposer).
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
// ── Skill autocomplete + composer bindings ───────────────────────────────
|
|
454
|
+
// Moved to skills.js (SkillAC IIFE, initialized from SkillAC.init()).
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
// ── Boot ──────────────────────────────────────────────────────────────────
|
|
458
|
+
Sidebar.init();
|
|
459
|
+
Settings.init();
|
|
460
|
+
Channels.init();
|
|
461
|
+
Sessions.init();
|
|
462
|
+
|
|
463
|
+
// Boot sequence:
|
|
464
|
+
// 1. Onboard check — first-run setup (key_setup / soul_setup)
|
|
465
|
+
// 2. Normal UI boot — WS + sessions + tasks + skills
|
|
466
|
+
//
|
|
467
|
+
// key_setup → hard block: shows full-screen setup-panel (language + API key).
|
|
468
|
+
// On success, setup-panel auto-launches /onboard session then boots UI.
|
|
469
|
+
// soul_setup → soft: auto-launches /onboard session and boots UI immediately.
|
|
470
|
+
// No blocking panel shown.
|
|
471
|
+
|
|
472
|
+
window.bootUI = async function() {
|
|
473
|
+
const { needsOnboard, phase } = await Onboard.check();
|
|
474
|
+
// key_setup blocks boot entirely; onboard.js calls _bootUI() when done.
|
|
475
|
+
if (needsOnboard && phase === "key_setup") return;
|
|
476
|
+
|
|
477
|
+
// Initialize skill autocomplete
|
|
478
|
+
SkillAC.init();
|
|
479
|
+
|
|
480
|
+
// soul_setup: Onboard.check() already launched the session and called _bootUI().
|
|
481
|
+
// For any other state, boot normally here.
|
|
482
|
+
if (!needsOnboard) {
|
|
483
|
+
// Auth already checked at app boot — safe to make API calls
|
|
484
|
+
WS.connect();
|
|
485
|
+
Tasks.load();
|
|
486
|
+
Skills.load();
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
(async () => {
|
|
491
|
+
// Auth check MUST run first — all API calls depend on it
|
|
492
|
+
const authOk = await Auth.check();
|
|
493
|
+
if (!authOk) {
|
|
494
|
+
// User cancelled auth prompt — stop boot
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
await window.bootUI();
|
|
499
|
+
})();
|
|
500
|
+
|
|
501
|
+
// ── Image Lightbox ────────────────────────────────────────────────────────────
|
|
502
|
+
// Global lightbox: click any .msg-image-thumb to open; click backdrop or ✕ or
|
|
503
|
+
// press ESC to close.
|
|
504
|
+
(function () {
|
|
505
|
+
let _overlay = null;
|
|
506
|
+
|
|
507
|
+
function _open(src, alt) {
|
|
508
|
+
if (_overlay) return;
|
|
509
|
+
_overlay = document.createElement("div");
|
|
510
|
+
_overlay.className = "img-lightbox";
|
|
511
|
+
_overlay.innerHTML =
|
|
512
|
+
`<span class="img-lightbox-close" title="Close">✕</span>` +
|
|
513
|
+
`<img src="${src}" alt="${alt || "image"}">`;
|
|
514
|
+
|
|
515
|
+
// Click on backdrop or ✕ → close
|
|
516
|
+
_overlay.addEventListener("click", function (e) {
|
|
517
|
+
if (e.target === _overlay || e.target.classList.contains("img-lightbox-close")) {
|
|
518
|
+
_close();
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
document.body.appendChild(_overlay);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function _close() {
|
|
526
|
+
if (_overlay) { _overlay.remove(); _overlay = null; }
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// ESC key closes lightbox
|
|
530
|
+
document.addEventListener("keydown", function (e) {
|
|
531
|
+
if (e.key === "Escape") _close();
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
// Event delegation: any click on .msg-image-thumb anywhere in the page
|
|
535
|
+
document.addEventListener("click", function (e) {
|
|
536
|
+
if (e.target.classList.contains("msg-image-thumb")) {
|
|
537
|
+
_open(e.target.src, e.target.alt);
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
})();
|
|
541
|
+
|
|
542
|
+
// Session Info Bar (model switcher + working-directory switcher) moved to sessions.js
|
|
543
|
+
|
|
Binary file
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
const Auth = (() => {
|
|
2
|
+
// ── Constants ──────────────────────────────────────────────────────────
|
|
3
|
+
const COOKIE_NAME = 'octo_access_key';
|
|
4
|
+
const STORAGE_KEY = 'octo_access_key';
|
|
5
|
+
const PROBE_ENDPOINT = '/api/sessions?limit=1';
|
|
6
|
+
const MAX_PROMPT_TRIES = 3;
|
|
7
|
+
|
|
8
|
+
const PROBE = Object.freeze({
|
|
9
|
+
OK: 'ok',
|
|
10
|
+
UNAUTHORIZED: 'unauthorized',
|
|
11
|
+
SERVER_ERR: 'server_error',
|
|
12
|
+
NETWORK_ERR: 'network_error',
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// ── Module state ───────────────────────────────────────────────────────
|
|
16
|
+
let _authCheckPromise = null;
|
|
17
|
+
let _authPassed = false;
|
|
18
|
+
|
|
19
|
+
// ── Storage helpers ────────────────────────────────────────────────────
|
|
20
|
+
const Cookie = {
|
|
21
|
+
set(key) {
|
|
22
|
+
const secure = location.protocol === 'https:' ? '; Secure' : '';
|
|
23
|
+
document.cookie =
|
|
24
|
+
`${COOKIE_NAME}=${encodeURIComponent(key)}; path=/; SameSite=Strict${secure}`;
|
|
25
|
+
},
|
|
26
|
+
clear() {
|
|
27
|
+
document.cookie =
|
|
28
|
+
`${COOKIE_NAME}=; path=/; max-age=0; SameSite=Strict`;
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function _getStoredKey() {
|
|
33
|
+
return (
|
|
34
|
+
localStorage.getItem(STORAGE_KEY) ||
|
|
35
|
+
new URLSearchParams(location.search).get('access_key') ||
|
|
36
|
+
null
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ── Auth probe ─────────────────────────────────────────────────────────
|
|
41
|
+
async function _probe() {
|
|
42
|
+
try {
|
|
43
|
+
const r = await fetch(PROBE_ENDPOINT);
|
|
44
|
+
if (r.ok) return PROBE.OK;
|
|
45
|
+
if (r.status === 401) return PROBE.UNAUTHORIZED;
|
|
46
|
+
return PROBE.SERVER_ERR;
|
|
47
|
+
} catch {
|
|
48
|
+
return PROBE.NETWORK_ERR;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ── Prompt helper ──────────────────────────────────────────────────────
|
|
53
|
+
async function _askUserForKey() {
|
|
54
|
+
const message = (typeof I18n !== 'undefined')
|
|
55
|
+
? I18n.t('auth.accessKeyRequired')
|
|
56
|
+
: 'Access key required:';
|
|
57
|
+
|
|
58
|
+
const el = document.getElementById('prompt-modal-input');
|
|
59
|
+
if (el) el.type = 'password';
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const input = (typeof Modal !== 'undefined' && Modal.prompt)
|
|
63
|
+
? await Modal.prompt(message)
|
|
64
|
+
: prompt(message);
|
|
65
|
+
return input?.trim() || null;
|
|
66
|
+
} finally {
|
|
67
|
+
if (el) el.type = 'text';
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── Core flow ──────────────────────────────────────────────────────────
|
|
72
|
+
async function _doCheck() {
|
|
73
|
+
const existing = _getStoredKey();
|
|
74
|
+
if (existing) Cookie.set(existing); // seed cookie before probe
|
|
75
|
+
|
|
76
|
+
const result = await _probe();
|
|
77
|
+
|
|
78
|
+
if (result === PROBE.OK) {
|
|
79
|
+
_authPassed = true;
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (result === PROBE.UNAUTHORIZED) {
|
|
84
|
+
Cookie.clear();
|
|
85
|
+
localStorage.removeItem(STORAGE_KEY);
|
|
86
|
+
return _promptAndRetry();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Server/network error — let app proceed
|
|
90
|
+
_authPassed = true;
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function _promptAndRetry() {
|
|
95
|
+
for (let attempt = 1; attempt <= MAX_PROMPT_TRIES; attempt++) {
|
|
96
|
+
const key = await _askUserForKey();
|
|
97
|
+
if (!key) {
|
|
98
|
+
_authPassed = false;
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
Cookie.set(key);
|
|
103
|
+
const result = await _probe();
|
|
104
|
+
|
|
105
|
+
if (result === PROBE.OK) {
|
|
106
|
+
localStorage.setItem(STORAGE_KEY, key); // persist only after success
|
|
107
|
+
_authPassed = true;
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (result !== PROBE.UNAUTHORIZED) {
|
|
112
|
+
_authPassed = true; // transient — proceed
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
Cookie.clear(); // wrong key → try again
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
_authPassed = false;
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ── Public API (compatible with the original ws.js/app.js usage) ───────
|
|
124
|
+
function check() {
|
|
125
|
+
if (!_authCheckPromise) _authCheckPromise = _doCheck();
|
|
126
|
+
return _authCheckPromise;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
check,
|
|
131
|
+
|
|
132
|
+
// Returns an Authorization header object, or {} if no key present.
|
|
133
|
+
getHeaders() {
|
|
134
|
+
const k = _getStoredKey();
|
|
135
|
+
return k ? { Authorization: `Bearer ${k}` } : {};
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
// Returns the raw key (or null). Used by ws.js for WebSocket URLs.
|
|
139
|
+
getKey: _getStoredKey,
|
|
140
|
+
|
|
141
|
+
// Clears auth state so check() will re-probe on next call.
|
|
142
|
+
reset() {
|
|
143
|
+
_authCheckPromise = null;
|
|
144
|
+
_authPassed = false;
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
// Read-only getter: `Auth.passed` (not a function call).
|
|
148
|
+
get passed() { return _authPassed; },
|
|
149
|
+
};
|
|
150
|
+
})();
|