This is page 18 of 21. Use http://codebase.md/oraios/serena?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .devcontainer
│ └── devcontainer.json
├── .dockerignore
├── .env.example
├── .github
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE
│ │ ├── config.yml
│ │ ├── feature_request.md
│ │ └── issue--bug--performance-problem--question-.md
│ └── workflows
│ ├── codespell.yml
│ ├── docker.yml
│ ├── docs.yaml
│ ├── junie.yml
│ ├── publish.yml
│ └── pytest.yml
├── .gitignore
├── .serena
│ ├── .gitignore
│ ├── memories
│ │ ├── adding_new_language_support_guide.md
│ │ ├── serena_core_concepts_and_architecture.md
│ │ ├── serena_repository_structure.md
│ │ └── suggested_commands.md
│ └── project.yml
├── .vscode
│ └── settings.json
├── CHANGELOG.md
├── CLAUDE.md
├── compose.yaml
├── CONTRIBUTING.md
├── docker_build_and_run.sh
├── DOCKER.md
├── Dockerfile
├── docs
│ ├── _config.yml
│ ├── _static
│ │ └── images
│ │ └── jetbrains-marketplace-button.png
│ ├── .gitignore
│ ├── 01-about
│ │ ├── 000_intro.md
│ │ ├── 010_llm-integration.md
│ │ ├── 020_programming-languages.md
│ │ ├── 030_serena-in-action.md
│ │ ├── 035_tools.md
│ │ ├── 040_comparison-to-other-agents.md
│ │ └── 050_acknowledgements.md
│ ├── 02-usage
│ │ ├── 000_intro.md
│ │ ├── 010_prerequisites.md
│ │ ├── 020_running.md
│ │ ├── 025_jetbrains_plugin.md
│ │ ├── 030_clients.md
│ │ ├── 040_workflow.md
│ │ ├── 050_configuration.md
│ │ ├── 060_dashboard.md
│ │ ├── 070_security.md
│ │ └── 999_additional-usage.md
│ ├── 03-special-guides
│ │ ├── 000_intro.md
│ │ ├── custom_agent.md
│ │ ├── groovy_setup_guide_for_serena.md
│ │ ├── scala_setup_guide_for_serena.md
│ │ └── serena_on_chatgpt.md
│ ├── autogen_rst.py
│ ├── create_toc.py
│ └── index.md
├── flake.lock
├── flake.nix
├── lessons_learned.md
├── LICENSE
├── llms-install.md
├── pyproject.toml
├── README.md
├── repo_dir_sync.py
├── resources
│ ├── jetbrains-marketplace-button.cdr
│ ├── serena-icons.cdr
│ ├── serena-logo-dark-mode.svg
│ ├── serena-logo.cdr
│ ├── serena-logo.svg
│ └── vscode_sponsor_logo.png
├── roadmap.md
├── scripts
│ ├── agno_agent.py
│ ├── demo_run_tools.py
│ ├── gen_prompt_factory.py
│ ├── mcp_server.py
│ ├── print_mode_context_options.py
│ ├── print_tool_overview.py
│ └── profile_tool_call.py
├── src
│ ├── interprompt
│ │ ├── __init__.py
│ │ ├── .syncCommitId.remote
│ │ ├── .syncCommitId.this
│ │ ├── jinja_template.py
│ │ ├── multilang_prompt.py
│ │ ├── prompt_factory.py
│ │ └── util
│ │ ├── __init__.py
│ │ └── class_decorators.py
│ ├── README.md
│ ├── serena
│ │ ├── __init__.py
│ │ ├── agent.py
│ │ ├── agno.py
│ │ ├── analytics.py
│ │ ├── cli.py
│ │ ├── code_editor.py
│ │ ├── config
│ │ │ ├── __init__.py
│ │ │ ├── context_mode.py
│ │ │ └── serena_config.py
│ │ ├── constants.py
│ │ ├── dashboard.py
│ │ ├── generated
│ │ │ └── generated_prompt_factory.py
│ │ ├── gui_log_viewer.py
│ │ ├── ls_manager.py
│ │ ├── mcp.py
│ │ ├── project.py
│ │ ├── prompt_factory.py
│ │ ├── resources
│ │ │ ├── config
│ │ │ │ ├── contexts
│ │ │ │ │ ├── agent.yml
│ │ │ │ │ ├── chatgpt.yml
│ │ │ │ │ ├── claude-code.yml
│ │ │ │ │ ├── codex.yml
│ │ │ │ │ ├── context.template.yml
│ │ │ │ │ ├── desktop-app.yml
│ │ │ │ │ ├── ide.yml
│ │ │ │ │ └── oaicompat-agent.yml
│ │ │ │ ├── internal_modes
│ │ │ │ │ └── jetbrains.yml
│ │ │ │ ├── modes
│ │ │ │ │ ├── editing.yml
│ │ │ │ │ ├── interactive.yml
│ │ │ │ │ ├── mode.template.yml
│ │ │ │ │ ├── no-memories.yml
│ │ │ │ │ ├── no-onboarding.yml
│ │ │ │ │ ├── onboarding.yml
│ │ │ │ │ ├── one-shot.yml
│ │ │ │ │ └── planning.yml
│ │ │ │ └── prompt_templates
│ │ │ │ ├── simple_tool_outputs.yml
│ │ │ │ └── system_prompt.yml
│ │ │ ├── dashboard
│ │ │ │ ├── dashboard.css
│ │ │ │ ├── dashboard.js
│ │ │ │ ├── index.html
│ │ │ │ ├── jquery.min.js
│ │ │ │ ├── serena-icon-16.png
│ │ │ │ ├── serena-icon-32.png
│ │ │ │ ├── serena-icon-48.png
│ │ │ │ ├── serena-logo-dark-mode.svg
│ │ │ │ ├── serena-logo.svg
│ │ │ │ ├── serena-logs-dark-mode.png
│ │ │ │ └── serena-logs.png
│ │ │ ├── project.template.yml
│ │ │ └── serena_config.template.yml
│ │ ├── symbol.py
│ │ ├── task_executor.py
│ │ ├── text_utils.py
│ │ ├── tools
│ │ │ ├── __init__.py
│ │ │ ├── cmd_tools.py
│ │ │ ├── config_tools.py
│ │ │ ├── file_tools.py
│ │ │ ├── jetbrains_plugin_client.py
│ │ │ ├── jetbrains_tools.py
│ │ │ ├── memory_tools.py
│ │ │ ├── symbol_tools.py
│ │ │ ├── tools_base.py
│ │ │ └── workflow_tools.py
│ │ └── util
│ │ ├── class_decorators.py
│ │ ├── cli_util.py
│ │ ├── exception.py
│ │ ├── file_system.py
│ │ ├── general.py
│ │ ├── git.py
│ │ ├── gui.py
│ │ ├── inspection.py
│ │ ├── logging.py
│ │ ├── shell.py
│ │ └── thread.py
│ └── solidlsp
│ ├── __init__.py
│ ├── .gitignore
│ ├── language_servers
│ │ ├── al_language_server.py
│ │ ├── bash_language_server.py
│ │ ├── clangd_language_server.py
│ │ ├── clojure_lsp.py
│ │ ├── common.py
│ │ ├── csharp_language_server.py
│ │ ├── dart_language_server.py
│ │ ├── eclipse_jdtls.py
│ │ ├── elixir_tools
│ │ │ ├── __init__.py
│ │ │ ├── elixir_tools.py
│ │ │ └── README.md
│ │ ├── elm_language_server.py
│ │ ├── erlang_language_server.py
│ │ ├── fortran_language_server.py
│ │ ├── fsharp_language_server.py
│ │ ├── gopls.py
│ │ ├── groovy_language_server.py
│ │ ├── haskell_language_server.py
│ │ ├── intelephense.py
│ │ ├── jedi_server.py
│ │ ├── julia_server.py
│ │ ├── kotlin_language_server.py
│ │ ├── lua_ls.py
│ │ ├── marksman.py
│ │ ├── matlab_language_server.py
│ │ ├── nixd_ls.py
│ │ ├── omnisharp
│ │ │ ├── initialize_params.json
│ │ │ ├── runtime_dependencies.json
│ │ │ └── workspace_did_change_configuration.json
│ │ ├── omnisharp.py
│ │ ├── pascal_server.py
│ │ ├── perl_language_server.py
│ │ ├── powershell_language_server.py
│ │ ├── pyright_server.py
│ │ ├── r_language_server.py
│ │ ├── regal_server.py
│ │ ├── ruby_lsp.py
│ │ ├── rust_analyzer.py
│ │ ├── scala_language_server.py
│ │ ├── solargraph.py
│ │ ├── sourcekit_lsp.py
│ │ ├── taplo_server.py
│ │ ├── terraform_ls.py
│ │ ├── typescript_language_server.py
│ │ ├── vts_language_server.py
│ │ ├── vue_language_server.py
│ │ ├── yaml_language_server.py
│ │ └── zls.py
│ ├── ls_config.py
│ ├── ls_exceptions.py
│ ├── ls_handler.py
│ ├── ls_request.py
│ ├── ls_types.py
│ ├── ls_utils.py
│ ├── ls.py
│ ├── lsp_protocol_handler
│ │ ├── lsp_constants.py
│ │ ├── lsp_requests.py
│ │ ├── lsp_types.py
│ │ └── server.py
│ ├── settings.py
│ └── util
│ ├── cache.py
│ ├── subprocess_util.py
│ └── zip.py
├── sync.py
├── test
│ ├── __init__.py
│ ├── conftest.py
│ ├── resources
│ │ └── repos
│ │ ├── al
│ │ │ └── test_repo
│ │ │ ├── app.json
│ │ │ └── src
│ │ │ ├── Codeunits
│ │ │ │ ├── CustomerMgt.Codeunit.al
│ │ │ │ └── PaymentProcessorImpl.Codeunit.al
│ │ │ ├── Enums
│ │ │ │ └── CustomerType.Enum.al
│ │ │ ├── Interfaces
│ │ │ │ └── IPaymentProcessor.Interface.al
│ │ │ ├── Pages
│ │ │ │ ├── CustomerCard.Page.al
│ │ │ │ └── CustomerList.Page.al
│ │ │ ├── TableExtensions
│ │ │ │ └── Item.TableExt.al
│ │ │ └── Tables
│ │ │ └── Customer.Table.al
│ │ ├── bash
│ │ │ └── test_repo
│ │ │ ├── config.sh
│ │ │ ├── main.sh
│ │ │ └── utils.sh
│ │ ├── clojure
│ │ │ └── test_repo
│ │ │ ├── deps.edn
│ │ │ └── src
│ │ │ └── test_app
│ │ │ ├── core.clj
│ │ │ └── utils.clj
│ │ ├── csharp
│ │ │ └── test_repo
│ │ │ ├── .gitignore
│ │ │ ├── Models
│ │ │ │ └── Person.cs
│ │ │ ├── Program.cs
│ │ │ ├── serena.sln
│ │ │ └── TestProject.csproj
│ │ ├── dart
│ │ │ └── test_repo
│ │ │ ├── .gitignore
│ │ │ ├── lib
│ │ │ │ ├── helper.dart
│ │ │ │ ├── main.dart
│ │ │ │ └── models.dart
│ │ │ └── pubspec.yaml
│ │ ├── elixir
│ │ │ └── test_repo
│ │ │ ├── .gitignore
│ │ │ ├── lib
│ │ │ │ ├── examples.ex
│ │ │ │ ├── ignored_dir
│ │ │ │ │ └── ignored_module.ex
│ │ │ │ ├── models.ex
│ │ │ │ ├── services.ex
│ │ │ │ ├── test_repo.ex
│ │ │ │ └── utils.ex
│ │ │ ├── mix.exs
│ │ │ ├── mix.lock
│ │ │ ├── scripts
│ │ │ │ └── build_script.ex
│ │ │ └── test
│ │ │ ├── models_test.exs
│ │ │ └── test_repo_test.exs
│ │ ├── elm
│ │ │ └── test_repo
│ │ │ ├── elm.json
│ │ │ ├── Main.elm
│ │ │ └── Utils.elm
│ │ ├── erlang
│ │ │ └── test_repo
│ │ │ ├── hello.erl
│ │ │ ├── ignored_dir
│ │ │ │ └── ignored_module.erl
│ │ │ ├── include
│ │ │ │ ├── records.hrl
│ │ │ │ └── types.hrl
│ │ │ ├── math_utils.erl
│ │ │ ├── rebar.config
│ │ │ ├── src
│ │ │ │ ├── app.erl
│ │ │ │ ├── models.erl
│ │ │ │ ├── services.erl
│ │ │ │ └── utils.erl
│ │ │ └── test
│ │ │ ├── models_tests.erl
│ │ │ └── utils_tests.erl
│ │ ├── fortran
│ │ │ └── test_repo
│ │ │ ├── main.f90
│ │ │ └── modules
│ │ │ ├── geometry.f90
│ │ │ └── math_utils.f90
│ │ ├── fsharp
│ │ │ └── test_repo
│ │ │ ├── .gitignore
│ │ │ ├── Calculator.fs
│ │ │ ├── Models
│ │ │ │ └── Person.fs
│ │ │ ├── Program.fs
│ │ │ ├── README.md
│ │ │ └── TestProject.fsproj
│ │ ├── go
│ │ │ └── test_repo
│ │ │ └── main.go
│ │ ├── groovy
│ │ │ └── test_repo
│ │ │ ├── .gitignore
│ │ │ ├── build.gradle
│ │ │ └── src
│ │ │ └── main
│ │ │ └── groovy
│ │ │ └── com
│ │ │ └── example
│ │ │ ├── Main.groovy
│ │ │ ├── Model.groovy
│ │ │ ├── ModelUser.groovy
│ │ │ └── Utils.groovy
│ │ ├── haskell
│ │ │ └── test_repo
│ │ │ ├── app
│ │ │ │ └── Main.hs
│ │ │ ├── haskell-test-repo.cabal
│ │ │ ├── package.yaml
│ │ │ ├── src
│ │ │ │ ├── Calculator.hs
│ │ │ │ └── Helper.hs
│ │ │ └── stack.yaml
│ │ ├── java
│ │ │ └── test_repo
│ │ │ ├── pom.xml
│ │ │ └── src
│ │ │ └── main
│ │ │ └── java
│ │ │ └── test_repo
│ │ │ ├── Main.java
│ │ │ ├── Model.java
│ │ │ ├── ModelUser.java
│ │ │ └── Utils.java
│ │ ├── julia
│ │ │ └── test_repo
│ │ │ ├── lib
│ │ │ │ └── helper.jl
│ │ │ └── main.jl
│ │ ├── kotlin
│ │ │ └── test_repo
│ │ │ ├── .gitignore
│ │ │ ├── build.gradle.kts
│ │ │ └── src
│ │ │ └── main
│ │ │ └── kotlin
│ │ │ └── test_repo
│ │ │ ├── Main.kt
│ │ │ ├── Model.kt
│ │ │ ├── ModelUser.kt
│ │ │ └── Utils.kt
│ │ ├── lua
│ │ │ └── test_repo
│ │ │ ├── .gitignore
│ │ │ ├── main.lua
│ │ │ ├── src
│ │ │ │ ├── calculator.lua
│ │ │ │ └── utils.lua
│ │ │ └── tests
│ │ │ └── test_calculator.lua
│ │ ├── markdown
│ │ │ └── test_repo
│ │ │ ├── api.md
│ │ │ ├── CONTRIBUTING.md
│ │ │ ├── guide.md
│ │ │ └── README.md
│ │ ├── matlab
│ │ │ └── test_repo
│ │ │ ├── Calculator.m
│ │ │ └── main.m
│ │ ├── nix
│ │ │ └── test_repo
│ │ │ ├── .gitignore
│ │ │ ├── default.nix
│ │ │ ├── flake.nix
│ │ │ ├── lib
│ │ │ │ └── utils.nix
│ │ │ ├── modules
│ │ │ │ └── example.nix
│ │ │ └── scripts
│ │ │ └── hello.sh
│ │ ├── pascal
│ │ │ └── test_repo
│ │ │ ├── .gitignore
│ │ │ ├── lib
│ │ │ │ └── helper.pas
│ │ │ └── main.pas
│ │ ├── perl
│ │ │ └── test_repo
│ │ │ ├── helper.pl
│ │ │ └── main.pl
│ │ ├── php
│ │ │ └── test_repo
│ │ │ ├── helper.php
│ │ │ ├── index.php
│ │ │ └── simple_var.php
│ │ ├── powershell
│ │ │ └── test_repo
│ │ │ ├── main.ps1
│ │ │ ├── PowerShellEditorServices.json
│ │ │ └── utils.ps1
│ │ ├── python
│ │ │ └── test_repo
│ │ │ ├── .gitignore
│ │ │ ├── custom_test
│ │ │ │ ├── __init__.py
│ │ │ │ └── advanced_features.py
│ │ │ ├── examples
│ │ │ │ ├── __init__.py
│ │ │ │ └── user_management.py
│ │ │ ├── ignore_this_dir_with_postfix
│ │ │ │ └── ignored_module.py
│ │ │ ├── scripts
│ │ │ │ ├── __init__.py
│ │ │ │ └── run_app.py
│ │ │ └── test_repo
│ │ │ ├── __init__.py
│ │ │ ├── complex_types.py
│ │ │ ├── models.py
│ │ │ ├── name_collisions.py
│ │ │ ├── nested_base.py
│ │ │ ├── nested.py
│ │ │ ├── overloaded.py
│ │ │ ├── services.py
│ │ │ ├── utils.py
│ │ │ └── variables.py
│ │ ├── r
│ │ │ └── test_repo
│ │ │ ├── .Rbuildignore
│ │ │ ├── DESCRIPTION
│ │ │ ├── examples
│ │ │ │ └── analysis.R
│ │ │ ├── NAMESPACE
│ │ │ └── R
│ │ │ ├── models.R
│ │ │ └── utils.R
│ │ ├── rego
│ │ │ └── test_repo
│ │ │ ├── policies
│ │ │ │ ├── authz.rego
│ │ │ │ └── validation.rego
│ │ │ └── utils
│ │ │ └── helpers.rego
│ │ ├── ruby
│ │ │ └── test_repo
│ │ │ ├── .solargraph.yml
│ │ │ ├── examples
│ │ │ │ └── user_management.rb
│ │ │ ├── lib.rb
│ │ │ ├── main.rb
│ │ │ ├── models.rb
│ │ │ ├── nested.rb
│ │ │ ├── services.rb
│ │ │ └── variables.rb
│ │ ├── rust
│ │ │ ├── test_repo
│ │ │ │ ├── Cargo.lock
│ │ │ │ ├── Cargo.toml
│ │ │ │ └── src
│ │ │ │ ├── lib.rs
│ │ │ │ └── main.rs
│ │ │ └── test_repo_2024
│ │ │ ├── Cargo.lock
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── lib.rs
│ │ │ └── main.rs
│ │ ├── scala
│ │ │ ├── build.sbt
│ │ │ ├── project
│ │ │ │ ├── build.properties
│ │ │ │ ├── metals.sbt
│ │ │ │ └── plugins.sbt
│ │ │ └── src
│ │ │ └── main
│ │ │ └── scala
│ │ │ └── com
│ │ │ └── example
│ │ │ ├── Main.scala
│ │ │ └── Utils.scala
│ │ ├── swift
│ │ │ └── test_repo
│ │ │ ├── Package.swift
│ │ │ └── src
│ │ │ ├── main.swift
│ │ │ └── utils.swift
│ │ ├── terraform
│ │ │ └── test_repo
│ │ │ ├── data.tf
│ │ │ ├── main.tf
│ │ │ ├── outputs.tf
│ │ │ └── variables.tf
│ │ ├── toml
│ │ │ └── test_repo
│ │ │ ├── Cargo.toml
│ │ │ ├── config.toml
│ │ │ └── pyproject.toml
│ │ ├── typescript
│ │ │ └── test_repo
│ │ │ ├── .serena
│ │ │ │ └── project.yml
│ │ │ ├── index.ts
│ │ │ ├── tsconfig.json
│ │ │ ├── use_helper.ts
│ │ │ └── ws_manager.js
│ │ ├── vue
│ │ │ └── test_repo
│ │ │ ├── .gitignore
│ │ │ ├── index.html
│ │ │ ├── package.json
│ │ │ ├── src
│ │ │ │ ├── App.vue
│ │ │ │ ├── components
│ │ │ │ │ ├── CalculatorButton.vue
│ │ │ │ │ ├── CalculatorDisplay.vue
│ │ │ │ │ └── CalculatorInput.vue
│ │ │ │ ├── composables
│ │ │ │ │ ├── useFormatter.ts
│ │ │ │ │ └── useTheme.ts
│ │ │ │ ├── main.ts
│ │ │ │ ├── stores
│ │ │ │ │ └── calculator.ts
│ │ │ │ └── types
│ │ │ │ └── index.ts
│ │ │ ├── tsconfig.json
│ │ │ ├── tsconfig.node.json
│ │ │ └── vite.config.ts
│ │ ├── yaml
│ │ │ └── test_repo
│ │ │ ├── config.yaml
│ │ │ ├── data.yaml
│ │ │ └── services.yml
│ │ └── zig
│ │ └── test_repo
│ │ ├── .gitignore
│ │ ├── build.zig
│ │ ├── src
│ │ │ ├── calculator.zig
│ │ │ ├── main.zig
│ │ │ └── math_utils.zig
│ │ └── zls.json
│ ├── serena
│ │ ├── __init__.py
│ │ ├── __snapshots__
│ │ │ └── test_symbol_editing.ambr
│ │ ├── config
│ │ │ ├── __init__.py
│ │ │ └── test_serena_config.py
│ │ ├── test_cli_project_commands.py
│ │ ├── test_edit_marker.py
│ │ ├── test_mcp.py
│ │ ├── test_serena_agent.py
│ │ ├── test_symbol_editing.py
│ │ ├── test_symbol.py
│ │ ├── test_task_executor.py
│ │ ├── test_text_utils.py
│ │ ├── test_tool_parameter_types.py
│ │ └── util
│ │ ├── test_exception.py
│ │ └── test_file_system.py
│ └── solidlsp
│ ├── al
│ │ └── test_al_basic.py
│ ├── bash
│ │ ├── __init__.py
│ │ └── test_bash_basic.py
│ ├── clojure
│ │ ├── __init__.py
│ │ └── test_clojure_basic.py
│ ├── csharp
│ │ └── test_csharp_basic.py
│ ├── dart
│ │ ├── __init__.py
│ │ └── test_dart_basic.py
│ ├── elixir
│ │ ├── __init__.py
│ │ ├── conftest.py
│ │ ├── test_elixir_basic.py
│ │ ├── test_elixir_ignored_dirs.py
│ │ ├── test_elixir_integration.py
│ │ └── test_elixir_symbol_retrieval.py
│ ├── elm
│ │ └── test_elm_basic.py
│ ├── erlang
│ │ ├── __init__.py
│ │ ├── conftest.py
│ │ ├── test_erlang_basic.py
│ │ ├── test_erlang_ignored_dirs.py
│ │ └── test_erlang_symbol_retrieval.py
│ ├── fortran
│ │ ├── __init__.py
│ │ └── test_fortran_basic.py
│ ├── fsharp
│ │ └── test_fsharp_basic.py
│ ├── go
│ │ └── test_go_basic.py
│ ├── groovy
│ │ └── test_groovy_basic.py
│ ├── haskell
│ │ ├── __init__.py
│ │ └── test_haskell_basic.py
│ ├── java
│ │ └── test_java_basic.py
│ ├── julia
│ │ └── test_julia_basic.py
│ ├── kotlin
│ │ └── test_kotlin_basic.py
│ ├── lua
│ │ └── test_lua_basic.py
│ ├── markdown
│ │ ├── __init__.py
│ │ └── test_markdown_basic.py
│ ├── matlab
│ │ ├── __init__.py
│ │ └── test_matlab_basic.py
│ ├── nix
│ │ └── test_nix_basic.py
│ ├── pascal
│ │ ├── __init__.py
│ │ └── test_pascal_basic.py
│ ├── perl
│ │ └── test_perl_basic.py
│ ├── php
│ │ └── test_php_basic.py
│ ├── powershell
│ │ ├── __init__.py
│ │ └── test_powershell_basic.py
│ ├── python
│ │ ├── test_python_basic.py
│ │ ├── test_retrieval_with_ignored_dirs.py
│ │ └── test_symbol_retrieval.py
│ ├── r
│ │ ├── __init__.py
│ │ └── test_r_basic.py
│ ├── rego
│ │ └── test_rego_basic.py
│ ├── ruby
│ │ ├── test_ruby_basic.py
│ │ └── test_ruby_symbol_retrieval.py
│ ├── rust
│ │ ├── test_rust_2024_edition.py
│ │ ├── test_rust_analyzer_detection.py
│ │ └── test_rust_basic.py
│ ├── scala
│ │ └── test_scala_language_server.py
│ ├── swift
│ │ └── test_swift_basic.py
│ ├── terraform
│ │ └── test_terraform_basic.py
│ ├── test_lsp_protocol_handler_server.py
│ ├── toml
│ │ ├── __init__.py
│ │ ├── test_toml_basic.py
│ │ ├── test_toml_edge_cases.py
│ │ ├── test_toml_ignored_dirs.py
│ │ └── test_toml_symbol_retrieval.py
│ ├── typescript
│ │ └── test_typescript_basic.py
│ ├── util
│ │ └── test_zip.py
│ ├── vue
│ │ ├── __init__.py
│ │ ├── test_vue_basic.py
│ │ ├── test_vue_error_cases.py
│ │ ├── test_vue_rename.py
│ │ └── test_vue_symbol_retrieval.py
│ ├── yaml_ls
│ │ ├── __init__.py
│ │ └── test_yaml_basic.py
│ └── zig
│ └── test_zig_basic.py
└── uv.lock
```
# Files
--------------------------------------------------------------------------------
/src/serena/resources/dashboard/dashboard.js:
--------------------------------------------------------------------------------
```javascript
1 | class LogMessage {
2 | constructor(message, toolNames) {
3 | message = this.escapeHtml(message);
4 | const logLevel = this.determineLogLevel(message);
5 | const highlightedMessage = this.highlightToolNames(message, toolNames);
6 | this.$elem = $('<div>').addClass('log-' + logLevel).html(highlightedMessage + '\n');
7 | }
8 |
9 | determineLogLevel(message) {
10 | if (message.startsWith('DEBUG')) {
11 | return 'debug';
12 | } else if (message.startsWith('INFO')) {
13 | return 'info';
14 | } else if (message.startsWith('WARNING')) {
15 | return 'warning';
16 | } else if (message.startsWith('ERROR')) {
17 | return 'error';
18 | } else {
19 | return 'default';
20 | }
21 | }
22 |
23 | highlightToolNames(message, toolNames) {
24 | let highlightedMessage = message;
25 | toolNames.forEach(function (toolName) {
26 | const regex = new RegExp('\\b' + toolName + '\\b', 'gi');
27 | highlightedMessage = highlightedMessage.replace(regex, '<span class="tool-name">' + toolName + '</span>');
28 | });
29 | return highlightedMessage;
30 | }
31 |
32 | escapeHtml(convertString) {
33 | if (typeof convertString !== 'string') return convertString;
34 |
35 | const patterns = {
36 | '<': '<', '>': '>', '&': '&', '"': '"', '\'': ''', '`': '`'
37 | };
38 |
39 | return convertString.replace(/[<>&"'`]/g, match => patterns[match]);
40 | };
41 | }
42 |
43 | function updateThemeAwareImage($img, theme=null) {
44 | if (!theme) {
45 | const isDarkMode = $('html').data("theme") == 'dark';
46 | theme = isDarkMode ? 'dark' : 'light';
47 | }
48 | console.log("updating theme-aware image to theme:", theme);
49 | const newSrc = $img.data('src-' + theme);
50 | if (newSrc) {
51 | $img.attr('src', newSrc);
52 | }
53 | }
54 |
55 | class BannerRotation {
56 | constructor() {
57 | this.platinumIndex = 0;
58 | this.goldIndex = 0;
59 | this.platinumTimer = null;
60 | this.goldTimer = null;
61 | this.platinumInterval = 15000;
62 | this.goldInterval = 15000;
63 |
64 | this.init();
65 | }
66 |
67 | init() {
68 | let self = this;
69 | this.loadBanners(function() {
70 | self.startPlatinumRotation();
71 | self.startGoldRotation();
72 | });
73 | }
74 |
75 | loadBanners(onSuccess) {
76 | $.ajax({
77 | url: 'https://oraios-software.de/serena-banners/manifest.php',
78 | type: 'GET',
79 | success: function (response) {
80 | console.log('Banners loaded:', response);
81 |
82 | function fillBanners($container, banners, className) {
83 | $.each(banners, function (index, banner) {
84 | let $img = $('<img src="' + banner.image + '" alt="' + banner.alt + '" class="banner-image">');
85 | if (banner.image_dark) {
86 | $img.addClass('theme-aware-img');
87 | $img.attr('data-src-dark', banner.image_dark);
88 | $img.attr('data-src-light', banner.image);
89 | updateThemeAwareImage($img);
90 | }
91 | let $anchor = $('<a href="' + banner.link + '" target="_blank"></a>');
92 | $anchor.append($img);
93 | let $banner = $('<div class="' + className + '-slide" data-banner="' + (index + 1) + '"></div>');
94 | $banner.append($anchor);
95 | if (index === 0) {
96 | $banner.addClass('active');
97 | }
98 | if (banner.border) {
99 | $img.addClass('banner-border');
100 | }
101 | $container.append($banner);
102 | });
103 | }
104 |
105 | fillBanners($('#gold-banners'), response.gold, 'gold-banner');
106 | fillBanners($('#platinum-banners'), response.platinum, 'platinum-banner');
107 | onSuccess();
108 | },
109 | error: function (xhr, status, error) {
110 | console.error('Error loading banners:', error);
111 | }
112 | });
113 | }
114 |
115 | startPlatinumRotation() {
116 | const self = this;
117 | this.platinumTimer = setInterval(() => {
118 | self.rotatePlatinum('next');
119 | }, this.platinumInterval);
120 | }
121 |
122 | startGoldRotation() {
123 | const self = this;
124 | this.goldTimer = setInterval(() => {
125 | self.rotateGold('next');
126 | }, this.goldInterval);
127 | }
128 |
129 | rotatePlatinum(direction) {
130 | const $slides = $('.platinum-banner-slide');
131 | const total = $slides.length;
132 |
133 | if (total === 0) return;
134 |
135 | // Remove active class from current slide
136 | $slides.eq(this.platinumIndex).removeClass('active');
137 |
138 | // Calculate next index
139 | if (direction === 'next') {
140 | this.platinumIndex = (this.platinumIndex + 1) % total;
141 | } else {
142 | this.platinumIndex = (this.platinumIndex - 1 + total) % total;
143 | }
144 |
145 | // Add active class to new slide
146 | $slides.eq(this.platinumIndex).addClass('active');
147 |
148 | // Reset timer
149 | clearInterval(this.platinumTimer);
150 | this.startPlatinumRotation();
151 | }
152 |
153 | rotateGold(direction) {
154 | const $groups = $('.gold-banner-slide');
155 | const total = $groups.length;
156 |
157 | if (total === 0) return;
158 |
159 | // Remove active class from current group
160 | $groups.eq(this.goldIndex).removeClass('active');
161 |
162 | // Calculate next index
163 | if (direction === 'next') {
164 | this.goldIndex = (this.goldIndex + 1) % total;
165 | } else {
166 | this.goldIndex = (this.goldIndex - 1 + total) % total;
167 | }
168 |
169 | // Add active class to new group
170 | $groups.eq(this.goldIndex).addClass('active');
171 |
172 | // Reset timer
173 | clearInterval(this.goldTimer);
174 | this.startGoldRotation();
175 | }
176 | }
177 |
178 | class Dashboard {
179 | constructor() {
180 | let self = this;
181 |
182 | // Page state
183 | this.currentPage = 'overview';
184 | this.configData = null;
185 | this.lastConfigDataJson = null; // Cache for comparison
186 | this.jetbrainsMode = false;
187 | this.activeProjectName = null;
188 | this.languageToRemove = null;
189 | this.currentMemoryName = null;
190 | this.originalMemoryContent = null;
191 | this.memoryContentDirty = false;
192 | this.memoryToDelete = null;
193 | this.isAddingLanguage = false;
194 | this.waitingForConfigPollingResult = false;
195 | this.waitingForExecutionsPollingResult = false;
196 | this.originalSerenaConfigContent = null;
197 | this.serenaConfigContentDirty = false;
198 |
199 | // Execution tracking
200 | this.cancelledExecutions = [];
201 | this.executionToCancel = null;
202 |
203 | // Tool names and stats
204 | this.toolNames = [];
205 | this.currentMaxIdx = -1;
206 | this.pollInterval = null;
207 | this.configPollInterval = null;
208 | this.executionsPollInterval = null;
209 | this.heartbeatFailureCount = 0;
210 |
211 | // jQuery elements
212 | this.$logContainer = $('#log-container');
213 | this.$errorContainer = $('#error-container');
214 | this.$copyLogsBtn = $('#copy-logs-btn');
215 | this.$menuToggle = $('#menu-toggle');
216 | this.$menuDropdown = $('#menu-dropdown');
217 | this.$menuShutdown = $('#menu-shutdown');
218 | this.$themeToggle = $('#theme-toggle');
219 | this.$themeIcon = $('#theme-icon');
220 | this.$themeText = $('#theme-text');
221 | this.$configDisplay = $('#config-display');
222 | this.$basicStatsDisplay = $('#basic-stats-display');
223 | this.$statsSection = $('#stats-section');
224 | this.$refreshStats = $('#refresh-stats');
225 | this.$clearStats = $('#clear-stats');
226 | this.$projectsDisplay = $('#projects-display');
227 | this.$projectsHeader = $('#projects-header');
228 | this.$availableToolsDisplay = $('#available-tools-display');
229 | this.$availableModesDisplay = $('#available-modes-display');
230 | this.$availableContextsDisplay = $('#available-contexts-display');
231 | this.$addLanguageModal = $('#add-language-modal');
232 | this.$modalLanguageSelect = $('#modal-language-select');
233 | this.$modalProjectName = $('#modal-project-name');
234 | this.$modalAddBtn = $('#modal-add-btn');
235 | this.$modalCancelBtn = $('#modal-cancel-btn');
236 | this.$modalClose = $('.modal-close');
237 | this.$removeLanguageModal = $('#remove-language-modal');
238 | this.$removeLanguageName = $('#remove-language-name');
239 | this.$removeModalOkBtn = $('#remove-modal-ok-btn');
240 | this.$removeModalCancelBtn = $('#remove-modal-cancel-btn');
241 | this.$modalCloseRemove = $('.modal-close-remove');
242 | this.$editMemoryModal = $('#edit-memory-modal');
243 | this.$editMemoryName = $('#edit-memory-name');
244 | this.$editMemoryContent = $('#edit-memory-content');
245 | this.$editMemorySaveBtn = $('#edit-memory-save-btn');
246 | this.$editMemoryCancelBtn = $('#edit-memory-cancel-btn');
247 | this.$modalCloseEditMemory = $('.modal-close-edit-memory');
248 | this.$deleteMemoryModal = $('#delete-memory-modal');
249 | this.$deleteMemoryName = $('#delete-memory-name');
250 | this.$deleteMemoryOkBtn = $('#delete-memory-ok-btn');
251 | this.$deleteMemoryCancelBtn = $('#delete-memory-cancel-btn');
252 | this.$modalCloseDeleteMemory = $('.modal-close-delete-memory');
253 | this.$createMemoryModal = $('#create-memory-modal');
254 | this.$createMemoryProjectName = $('#create-memory-project-name');
255 | this.$createMemoryNameInput = $('#create-memory-name-input');
256 | this.$createMemoryCreateBtn = $('#create-memory-create-btn');
257 | this.$createMemoryCancelBtn = $('#create-memory-cancel-btn');
258 | this.$modalCloseCreateMemory = $('.modal-close-create-memory');
259 | this.$activeExecutionQueueDisplay = $('#active-executions-display');
260 | this.$lastExecutionDisplay = $('#last-execution-display');
261 | this.$cancelledExecutionsDisplay = $('#cancelled-executions-display');
262 | this.$cancelExecutionModal = $('#cancel-execution-modal');
263 | this.$cancelExecutionOkBtn = $('#cancel-execution-ok-btn');
264 | this.$cancelExecutionCancelBtn = $('#cancel-execution-cancel-btn');
265 | this.$modalCloseCancelExecution = $('.modal-close-cancel-execution');
266 | this.$editSerenaConfigModal = $('#edit-serena-config-modal');
267 | this.$editSerenaConfigContent = $('#edit-serena-config-content');
268 | this.$editSerenaConfigSaveBtn = $('#edit-serena-config-save-btn');
269 | this.$editSerenaConfigCancelBtn = $('#edit-serena-config-cancel-btn');
270 | this.$modalCloseEditSerenaConfig = $('.modal-close-edit-serena-config');
271 |
272 | // Chart references
273 | this.countChart = null;
274 | this.tokensChart = null;
275 | this.inputChart = null;
276 | this.outputChart = null;
277 |
278 | // Register event handlers
279 | this.$copyLogsBtn.click(this.copyLogs.bind(this));
280 | this.$menuShutdown.click(function (e) {
281 | e.preventDefault();
282 | self.shutdown();
283 | });
284 | this.$menuToggle.click(this.toggleMenu.bind(this));
285 | this.$themeToggle.click(this.toggleTheme.bind(this));
286 | this.$refreshStats.click(this.loadStats.bind(this));
287 | this.$clearStats.click(this.clearStats.bind(this));
288 | this.$modalAddBtn.click(this.addLanguageFromModal.bind(this));
289 | this.$modalCancelBtn.click(this.closeLanguageModal.bind(this));
290 | this.$modalClose.click(this.closeLanguageModal.bind(this));
291 | this.$removeModalOkBtn.click(this.confirmRemoveLanguageOk.bind(this));
292 | this.$removeModalCancelBtn.click(this.closeRemoveLanguageModal.bind(this));
293 | this.$modalCloseRemove.click(this.closeRemoveLanguageModal.bind(this));
294 | this.$editMemorySaveBtn.click(this.saveMemoryFromModal.bind(this));
295 | this.$editMemoryCancelBtn.click(this.closeEditMemoryModal.bind(this));
296 | this.$modalCloseEditMemory.click(this.closeEditMemoryModal.bind(this));
297 | this.$editMemoryContent.on('input', this.trackMemoryChanges.bind(this));
298 | this.$deleteMemoryOkBtn.click(this.confirmDeleteMemoryOk.bind(this));
299 | this.$deleteMemoryCancelBtn.click(this.closeDeleteMemoryModal.bind(this));
300 | this.$modalCloseDeleteMemory.click(this.closeDeleteMemoryModal.bind(this));
301 | this.$createMemoryCreateBtn.click(this.createMemoryFromModal.bind(this));
302 | this.$createMemoryCancelBtn.click(this.closeCreateMemoryModal.bind(this));
303 | this.$modalCloseCreateMemory.click(this.closeCreateMemoryModal.bind(this));
304 | this.$createMemoryNameInput.keypress(function (e) {
305 | if (e.which === 13) { // Enter key
306 | e.preventDefault();
307 | self.createMemoryFromModal();
308 | }
309 | });
310 | this.$cancelExecutionOkBtn.click(this.confirmCancelExecutionOk.bind(this));
311 | this.$cancelExecutionCancelBtn.click(this.closeCancelExecutionModal.bind(this));
312 | this.$modalCloseCancelExecution.click(this.closeCancelExecutionModal.bind(this));
313 | this.$editSerenaConfigSaveBtn.click(this.saveSerenaConfigFromModal.bind(this));
314 | this.$editSerenaConfigCancelBtn.click(this.closeEditSerenaConfigModal.bind(this));
315 | this.$modalCloseEditSerenaConfig.click(this.closeEditSerenaConfigModal.bind(this));
316 |
317 | // Page navigation
318 | $('[data-page]').click(function (e) {
319 | e.preventDefault();
320 | const page = $(this).data('page');
321 | self.navigateToPage(page);
322 | });
323 |
324 | // Close menu when clicking outside
325 | $(document).click(function (e) {
326 | if (!$(e.target).closest('.header-nav').length) {
327 | self.$menuDropdown.hide();
328 | }
329 | });
330 |
331 | // Close modals when clicking outside
332 | this.$addLanguageModal.click(function (e) {
333 | if ($(e.target).hasClass('modal')) {
334 | self.closeLanguageModal();
335 | }
336 | });
337 |
338 | this.$removeLanguageModal.click(function (e) {
339 | if ($(e.target).hasClass('modal')) {
340 | self.closeRemoveLanguageModal();
341 | }
342 | });
343 |
344 | this.$editMemoryModal.click(function (e) {
345 | if ($(e.target).hasClass('modal')) {
346 | self.closeEditMemoryModal();
347 | }
348 | });
349 |
350 | this.$deleteMemoryModal.click(function (e) {
351 | if ($(e.target).hasClass('modal')) {
352 | self.closeDeleteMemoryModal();
353 | }
354 | });
355 |
356 | this.$createMemoryModal.click(function (e) {
357 | if ($(e.target).hasClass('modal')) {
358 | self.closeCreateMemoryModal();
359 | }
360 | });
361 |
362 | this.$editSerenaConfigModal.click(function (e) {
363 | if ($(e.target).hasClass('modal')) {
364 | self.closeEditSerenaConfigModal();
365 | }
366 | });
367 |
368 | // Collapsible sections
369 | $('.collapsible-header').click(function () {
370 | const $header = $(this);
371 | const $content = $header.next('.collapsible-content');
372 | const $icon = $header.find('.toggle-icon');
373 |
374 | $content.slideToggle(300);
375 | $icon.toggleClass('expanded');
376 | });
377 |
378 | // Initialize theme
379 | this.initializeTheme();
380 |
381 | // Initialize banner rotation
382 | this.bannerRotation = new BannerRotation();
383 |
384 | // Add ESC key handler for closing modals
385 | $(document).keydown(function (e) {
386 | if (e.key === 'Escape' || e.keyCode === 27) {
387 | if (self.$addLanguageModal.is(':visible')) {
388 | self.closeLanguageModal();
389 | } else if (self.$removeLanguageModal.is(':visible')) {
390 | self.closeRemoveLanguageModal();
391 | } else if (self.$editMemoryModal.is(':visible')) {
392 | self.closeEditMemoryModal();
393 | } else if (self.$deleteMemoryModal.is(':visible')) {
394 | self.closeDeleteMemoryModal();
395 | } else if (self.$createMemoryModal.is(':visible')) {
396 | self.closeCreateMemoryModal();
397 | }
398 | }
399 | });
400 |
401 | // Initialize the application
402 | this.loadToolNames().then(function () {
403 | // Start on overview page
404 | self.loadConfigOverview();
405 | self.startConfigPolling();
406 | self.startExecutionsPolling();
407 | });
408 | // Initialize heartbeat interval
409 | setInterval(this.heartbeat.bind(this), 250);
410 | }
411 |
412 | heartbeat() {
413 | let self = this;
414 | $.ajax({
415 | url: '/heartbeat',
416 | type: 'GET',
417 | success: function (response) {
418 | self.heartbeatFailureCount = 0;
419 | },
420 | error: function (xhr, status, error) {
421 | self.heartbeatFailureCount++;
422 | console.error('Heartbeat failure; count = ', self.heartbeatFailureCount);
423 | if (self.heartbeatFailureCount >= 1) {
424 | console.log('Server appears to be down, closing tab');
425 | window.close();
426 | }
427 | },
428 | });
429 | }
430 |
431 | toggleMenu() {
432 | this.$menuDropdown.toggle();
433 | }
434 |
435 | navigateToPage(page) {
436 | // Hide menu
437 | this.$menuDropdown.hide();
438 |
439 | // Hide all pages
440 | $('.page-view').hide();
441 |
442 | // Show selected page
443 | $('#page-' + page).show();
444 |
445 | // Update menu active state
446 | $('[data-page]').removeClass('active');
447 | $('[data-page="' + page + '"]').addClass('active');
448 |
449 | // Update current page
450 | this.currentPage = page;
451 |
452 | // Stop all polling
453 | this.stopPolling();
454 |
455 | // Start appropriate polling for the page
456 | if (page === 'overview') {
457 | this.loadConfigOverview();
458 | this.startConfigPolling();
459 | this.startExecutionsPolling();
460 | } else if (page === 'logs') {
461 | this.loadLogs();
462 | } else if (page === 'stats') {
463 | this.loadStats();
464 | }
465 | }
466 |
467 | stopPolling() {
468 | if (this.pollInterval) {
469 | clearInterval(this.pollInterval);
470 | this.pollInterval = null;
471 | }
472 | if (this.configPollInterval) {
473 | clearInterval(this.configPollInterval);
474 | this.configPollInterval = null;
475 | }
476 | if (this.executionsPollInterval) {
477 | clearInterval(this.executionsPollInterval);
478 | this.executionsPollInterval = null;
479 | }
480 | }
481 |
482 | // ===== Config Overview Methods =====
483 |
484 | loadConfigOverview() {
485 | if (this.waitingForConfigPollingResult) {
486 | console.log('Still waiting for previous config poll result, skipping this poll');
487 | return;
488 | }
489 | this.waitingForConfigPollingResult = true;
490 | console.log('Polling for config overview...');
491 | let self = this;
492 | $.ajax({
493 | url: '/get_config_overview',
494 | type: 'GET',
495 | success: function (response) {
496 | // Check if the config data has actually changed
497 | const currentConfigJson = JSON.stringify(response);
498 | const hasChanged = self.lastConfigDataJson !== currentConfigJson;
499 |
500 | if (hasChanged) {
501 | console.log('Config has changed, updating display');
502 | self.lastConfigDataJson = currentConfigJson;
503 | self.configData = response;
504 | self.jetbrainsMode = response.jetbrains_mode;
505 | self.activeProjectName = response.active_project.name;
506 | self.displayConfig(response);
507 | self.displayBasicStats(response.tool_stats_summary);
508 | self.displayProjects(response.registered_projects);
509 | self.displayAvailableTools(response.available_tools);
510 | self.displayAvailableModes(response.available_modes);
511 | self.displayAvailableContexts(response.available_contexts);
512 | } else {
513 | console.log('Config unchanged, skipping display update');
514 | }
515 | }, error: function (xhr, status, error) {
516 | console.error('Error loading config overview:', error);
517 | self.$configDisplay.html('<div class="error-message">Error loading configuration</div>');
518 | self.$basicStatsDisplay.html('<div class="error-message">Error loading stats</div>');
519 | self.$projectsDisplay.html('<div class="error-message">Error loading projects</div>');
520 | self.$availableToolsDisplay.html('<div class="error-message">Error loading tools</div>');
521 | self.$availableModesDisplay.html('<div class="error-message">Error loading modes</div>');
522 | self.$availableContextsDisplay.html('<div class="error-message">Error loading contexts</div>');
523 | }, complete: function () {
524 | self.waitingForConfigPollingResult = false;
525 | }
526 | });
527 | }
528 |
529 | startConfigPolling() {
530 | this.configPollInterval = setInterval(this.loadConfigOverview.bind(this), 1000);
531 | }
532 |
533 | startExecutionsPolling() {
534 | // Poll every 1 second for executions (independent of config polling)
535 | // This ensures stuck executions can still be cancelled even if config polling is blocked
536 | this.loadExecutions()
537 | this.executionsPollInterval = setInterval(() => {
538 | this.loadQueuedExecutions();
539 | this.loadLastExecution();
540 | }, 1000);
541 | }
542 |
543 | displayConfig(config) {
544 | try {
545 | // Check if tools and memories sections are currently expanded
546 | const $existingToolsContent = $('#tools-content');
547 | const $existingMemoriesContent = $('#memories-content');
548 | const wasToolsExpanded = $existingToolsContent.is(':visible');
549 | const wasMemoriesExpanded = $existingMemoriesContent.is(':visible');
550 |
551 | let html = '<div class="config-grid">';
552 |
553 | // Project info
554 | html += '<div class="config-label">Active Project:</div>';
555 | if (config.active_project.name && config.active_project.path) {
556 | const configPath = config.active_project.path + '/.serena/project.yml';
557 | html += '<div class="config-value"><span title="Project configuration in ' + configPath + '">' + config.active_project.name + '</span></div>';
558 | } else {
559 | html += '<div class="config-value">' + (config.active_project.name || 'None') + '</div>';
560 | }
561 |
562 | html += '<div class="config-label">Languages:</div>';
563 | if (this.jetbrainsMode) {
564 | html += '<div class="config-value">Using JetBrains backend</div>';
565 | } else {
566 | html += '<div class="config-value">';
567 | if (config.languages && config.languages.length > 0) {
568 | html += '<div class="languages-container">';
569 | config.languages.forEach(function (language, index) {
570 | const isRemovable = config.languages.length > 1;
571 | html += '<div class="language-badge' + (isRemovable ? ' removable' : '') + '">';
572 | html += language;
573 | if (isRemovable) {
574 | html += '<span class="language-remove" data-language="' + language + '">×</span>';
575 | }
576 | html += '</div>';
577 | });
578 | // Add the "Add Language" button inline with language badges (only if active project exists)
579 | if (config.active_project && config.active_project.name) {
580 | // TODO: address after refactoring, it's not awesome to keep depending on state
581 | if (this.isAddingLanguage) {
582 | html += '<div id="add-language-spinner" class="language-spinner">';
583 | } else {
584 | html += '<button id="add-language-btn" class="btn language-add-btn">+ Add Language</button>';
585 | html += '<div id="add-language-spinner" class="language-spinner" style="display:none;">';
586 | }
587 | html += '<div class="spinner"></div>';
588 | html += '</div>';
589 | }
590 | html += '</div>';
591 | } else {
592 | html += 'N/A';
593 | }
594 | html += '</div>';
595 | }
596 |
597 | // Context info
598 | html += '<div class="config-label">Context:</div>';
599 | html += '<div class="config-value"><span title="' + config.context.path + '">' + config.context.name + '</span></div>';
600 |
601 | // Modes info
602 | html += '<div class="config-label">Active Modes:</div>';
603 | html += '<div class="config-value">';
604 | if (config.modes.length > 0) {
605 | const modeSpans = config.modes.map(function (mode) {
606 | return '<span title="' + mode.path + '">' + mode.name + '</span>';
607 | });
608 | html += modeSpans.join(', ');
609 | } else {
610 | html += 'None';
611 | }
612 | html += '</div>';
613 |
614 | // File Encoding info
615 | html += '<div class="config-label">File Encoding:</div>';
616 | html += '<div class="config-value">' + (config.encoding || 'N/A') + '</div>';
617 |
618 | html += '</div>';
619 |
620 | // Active tools - collapsible
621 | html += '<div style="margin-top: 20px;">';
622 | html += '<h3 class="collapsible-header" id="tools-header" style="font-size: 16px; margin: 0;">';
623 | html += '<span>Active Tools (' + config.active_tools.length + ')</span>';
624 | html += '<span class="toggle-icon' + (wasToolsExpanded ? ' expanded' : '') + '">▼</span>';
625 | html += '</h3>';
626 | html += '<div class="collapsible-content tools-grid" id="tools-content" style="' + (wasToolsExpanded ? '' : 'display:none;') + ' margin-top: 10px;">';
627 | config.active_tools.forEach(function (tool) {
628 | html += '<div class="tool-item" title="' + tool + '">' + tool + '</div>';
629 | });
630 | html += '</div>';
631 | html += '</div>';
632 |
633 | // Available memories - collapsible (show if memories exist or if project exists)
634 | if (config.active_project && config.active_project.name) {
635 | html += '<div style="margin-top: 20px;">';
636 | html += '<h3 class="collapsible-header" id="memories-header" style="font-size: 16px; margin: 0;">';
637 | const memoryCount = (config.available_memories && config.available_memories.length) || 0;
638 | html += '<span>Available Memories (' + memoryCount + ')</span>';
639 | html += '<span class="toggle-icon' + (wasMemoriesExpanded ? ' expanded' : '') + '">▼</span>';
640 | html += '</h3>';
641 | html += '<div class="collapsible-content memories-container" id="memories-content" style="' + (wasMemoriesExpanded ? '' : 'display:none;') + ' margin-top: 10px;">';
642 | if (config.available_memories && config.available_memories.length > 0) {
643 | config.available_memories.forEach(function (memory) {
644 | html += '<div class="memory-item removable" data-memory="' + memory + '">';
645 | html += memory;
646 | html += '<span class="memory-remove" data-memory="' + memory + '">×</span>';
647 | html += '</div>';
648 | });
649 | }
650 | // Add Create Memory button
651 | html += '<button id="create-memory-btn" class="memory-add-btn">+ Add Memory</button>';
652 | html += '</div>';
653 | html += '</div>';
654 | }
655 |
656 | // Configuration help link and edit config button
657 | html += '<div style="margin-top: 15px; display: flex; gap: 10px; align-items: center;">';
658 | html += '<div style="flex: 1; padding: 10px; background: var(--bg-secondary); border-radius: 4px; font-size: 13px; border: 1px solid var(--border-color);">';
659 | html += '<span style="color: var(--text-muted);">📖</span> ';
660 | html += '<a href="https://oraios.github.io/serena/02-usage/050_configuration.html" target="_blank" rel="noopener noreferrer" style="color: var(--btn-primary); text-decoration: none; font-weight: 500;">View Configuration Guide</a>';
661 | html += '</div>';
662 | html += '<button id="edit-serena-config-btn" class="btn language-add-btn" style="white-space: nowrap; padding: 10px; ">Edit Global Serena Config</button>';
663 | html += '</div>';
664 |
665 | this.$configDisplay.html(html);
666 |
667 | // Attach event handlers for the dynamically created add language button
668 | $('#add-language-btn').click(this.openLanguageModal.bind(this));
669 |
670 | // Attach event handler for edit serena config button
671 | $('#edit-serena-config-btn').click(this.openEditSerenaConfigModal.bind(this));
672 |
673 | // Attach event handlers for language remove buttons
674 | const self = this;
675 | $('.language-remove').click(function (e) {
676 | e.preventDefault();
677 | e.stopPropagation();
678 | const language = $(this).data('language');
679 | self.confirmRemoveLanguage(language);
680 | });
681 |
682 | // Attach event handlers for memory items
683 | $('.memory-item').click(function (e) {
684 | e.preventDefault();
685 | const memoryName = $(this).data('memory');
686 | self.openEditMemoryModal(memoryName);
687 | });
688 |
689 | // Attach event handlers for memory remove buttons
690 | $('.memory-remove').click(function (e) {
691 | e.preventDefault();
692 | e.stopPropagation();
693 | const memoryName = $(this).data('memory');
694 | self.confirmDeleteMemory(memoryName);
695 | });
696 |
697 | // Attach event handler for create memory button
698 | $('#create-memory-btn').click(this.openCreateMemoryModal.bind(this));
699 |
700 | // Re-attach collapsible handler for the newly created tools header
701 | $('#tools-header').click(function () {
702 | const $header = $(this);
703 | const $content = $('#tools-content');
704 | const $icon = $header.find('.toggle-icon');
705 |
706 | $content.slideToggle(300);
707 | $icon.toggleClass('expanded');
708 | });
709 |
710 | // Re-attach collapsible handler for the newly created memories header
711 | $('#memories-header').click(function () {
712 | const $header = $(this);
713 | const $content = $('#memories-content');
714 | const $icon = $header.find('.toggle-icon');
715 |
716 | $content.slideToggle(300);
717 | $icon.toggleClass('expanded');
718 | });
719 | } catch (error) {
720 | console.error('Error in displayConfig:', error);
721 | this.$configDisplay.html('<div class="error-message">Error displaying configuration: ' + error.message + '</div>');
722 | }
723 | }
724 |
725 | displayBasicStats(stats) {
726 | if (Object.keys(stats).length === 0) {
727 | this.$basicStatsDisplay.html('<div class="no-stats-message">No tool usage stats collected yet.</div>');
728 | return;
729 | }
730 |
731 | // Sort tools by call count (descending)
732 | const sortedTools = Object.keys(stats).sort((a, b) => {
733 | return stats[b].num_calls - stats[a].num_calls;
734 | });
735 |
736 | const maxCalls = Math.max(...sortedTools.map(tool => stats[tool].num_calls));
737 |
738 | let html = '';
739 | sortedTools.forEach(function (toolName) {
740 | const count = stats[toolName].num_calls;
741 | const percentage = maxCalls > 0 ? (count / maxCalls * 100) : 0;
742 |
743 | html += '<div class="stat-bar-container">';
744 | html += '<div class="stat-tool-name" title="' + toolName + '">' + toolName + '</div>';
745 | html += '<div class="bar-wrapper">';
746 | html += '<div class="bar" style="width: ' + percentage + '%"></div>';
747 | html += '</div>';
748 | html += '<div class="stat-count">' + count + '</div>';
749 | html += '</div>';
750 | });
751 |
752 | this.$basicStatsDisplay.html(html);
753 | }
754 |
755 | displayProjects(projects) {
756 | if (!projects || projects.length === 0) {
757 | this.$projectsDisplay.html('<div class="no-stats-message">No projects registered.</div>');
758 | return;
759 | }
760 |
761 | let html = '';
762 | projects.forEach(function (project) {
763 | const activeClass = project.is_active ? ' active' : '';
764 | html += '<div class="project-item' + activeClass + '">';
765 | html += '<div class="project-name" title="' + project.name + '">' + project.name + '</div>';
766 | html += '<div class="project-path" title="' + project.path + '">' + project.path + '</div>';
767 | html += '</div>';
768 | });
769 |
770 | this.$projectsDisplay.html(html);
771 | }
772 |
773 | displayAvailableTools(tools) {
774 | if (!tools || tools.length === 0) {
775 | this.$availableToolsDisplay.html('<div class="no-stats-message">All tools are active.</div>');
776 | return;
777 | }
778 |
779 | let html = '';
780 | tools.forEach(function (tool) {
781 | html += '<div class="info-item" title="' + tool.name + '">' + tool.name + '</div>';
782 | });
783 |
784 | this.$availableToolsDisplay.html(html);
785 | }
786 |
787 | displayAvailableModes(modes) {
788 | if (!modes || modes.length === 0) {
789 | this.$availableModesDisplay.html('<div class="no-stats-message">No modes available.</div>');
790 | return;
791 | }
792 |
793 | let html = '';
794 | modes.forEach(function (mode) {
795 | const activeClass = mode.is_active ? ' active' : '';
796 | html += '<div class="info-item' + activeClass + '" title="' + mode.path + '">' + mode.name + '</div>';
797 | });
798 |
799 | this.$availableModesDisplay.html(html);
800 | }
801 |
802 | displayAvailableContexts(contexts) {
803 | if (!contexts || contexts.length === 0) {
804 | this.$availableContextsDisplay.html('<div class="no-stats-message">No contexts available.</div>');
805 | return;
806 | }
807 |
808 | let html = '';
809 | contexts.forEach(function (context) {
810 | const activeClass = context.is_active ? ' active' : '';
811 | html += '<div class="info-item' + activeClass + '" title="' + context.path + '">' + context.name + '</div>';
812 | });
813 |
814 | this.$availableContextsDisplay.html(html);
815 | }
816 |
817 | // ===== Executions Methods =====
818 |
819 | loadQueuedExecutions() {
820 | let self = this;
821 | $.ajax({
822 | url: '/queued_task_executions', type: 'GET', success: function (response) {
823 | if (response.status === 'success') {
824 | self.displayActiveExecutionsQueue(response.queued_executions || []);
825 | } else {
826 | console.error('Error loading executions:', response.message);
827 | }
828 | }, error: function (xhr, status, error) {
829 | console.error('Error loading executions:', error);
830 | self.$activeExecutionQueueDisplay.html('<div class="error-message">Error loading executions</div>');
831 | }
832 | });
833 | }
834 |
835 | loadLastExecution() {
836 | let self = this;
837 | $.ajax({
838 | url: '/last_execution', type: 'GET', success: function (response) {
839 | if (response.status === 'success') {
840 | if (response.last_execution !== null && response.last_execution.logged) {
841 | self.displayLastExecution(response.last_execution);
842 | }
843 | } else {
844 | console.error('Error loading last execution:', response.message);
845 | }
846 | }, error: function (xhr, status, error) {
847 | console.error('Error loading last execution:', error);
848 | self.$lastExecutionDisplay.html('<div class="error-message">Error loading last execution</div>');
849 | }
850 | });
851 | }
852 |
853 | loadExecutions() {
854 | if (this.waitingForExecutionsPollingResult) {
855 | console.log('Still waiting for previous executions poll result, skipping this poll');
856 | } else {
857 | this.waitingForExecutionsPollingResult = true;
858 | console.log('Polling for executions...');
859 | this.loadQueuedExecutions();
860 | this.loadLastExecution();
861 | }
862 | }
863 |
864 | displayActiveExecutionsQueue(executions) {
865 | if (!executions || executions.length === 0) {
866 | return;
867 | }
868 |
869 | let html = '<div class="execution-list">';
870 | let self = this;
871 |
872 | executions.forEach(function (execution) {
873 | const isRunning = execution.is_running;
874 | const logged = execution.logged;
875 |
876 | if (!logged) {
877 | return; // Skip unlogged executions
878 | }
879 |
880 | let itemClass = 'execution-item';
881 | if (isRunning) {
882 | itemClass += ' running';
883 | }
884 |
885 | // Escape JSON for HTML attribute - replace single quotes and use HTML entities
886 | const executionJson = JSON.stringify(execution).replace(/'/g, ''');
887 |
888 | html += '<div class="' + itemClass + '" data-task-id="' + execution.task_id + '" data-execution=\'' + executionJson + '\'>';
889 |
890 | if (isRunning) {
891 | html += '<div class="execution-spinner"></div>';
892 | }
893 |
894 | html += '<div class="execution-name">' + self.escapeHtml(execution.name) + '</div>';
895 |
896 | if (isRunning) {
897 | html += '<div class="execution-meta">#' + execution.task_id + '</div>';
898 | } else {
899 | html += '<div class="execution-meta">queued · #' + execution.task_id + '</div>';
900 | }
901 |
902 | html += '<button class="execution-cancel-btn" data-task-id="' + execution.task_id + '" data-is-running="' + isRunning + '">✕</button>';
903 | html += '</div>';
904 | });
905 |
906 | html += '</div>';
907 | this.$activeExecutionQueueDisplay.html(html);
908 |
909 | // Attach event handlers for cancel buttons
910 | $('.execution-cancel-btn').click(function (e) {
911 | e.preventDefault();
912 | console.log('Cancel button clicked');
913 | const $item = $(this).closest('.execution-item');
914 | console.log('Found item:', $item.length);
915 | const executionDataStr = $item.attr('data-execution');
916 | console.log('Execution data string:', executionDataStr);
917 | if (executionDataStr) {
918 | // Unescape HTML entities
919 | const unescapedStr = executionDataStr.replace(/'/g, "'");
920 | const executionData = JSON.parse(unescapedStr);
921 | console.log('Parsed execution data:', executionData);
922 | self.confirmCancelExecution(executionData);
923 | } else {
924 | console.error('No execution data found on element');
925 | }
926 | });
927 |
928 | // Update cancelled executions display
929 | this.displayCancelledExecutions(executions);
930 | }
931 |
932 | displayLastExecution(execution) {
933 | if (!execution) {
934 | this.$lastExecutionDisplay.html('<div class="no-stats-message">No executions yet.</div>');
935 | return;
936 | }
937 |
938 | const isSuccess = execution.finished_successfully;
939 | let html = '<div class="last-execution-container' + (isSuccess ? '' : ' error') + '">';
940 |
941 | html += '<div class="last-execution-icon-container">';
942 | html += isSuccess ? '✓' : '✕';
943 | html += '</div>';
944 |
945 | html += '<div class="last-execution-body">';
946 | html += '<div class="last-execution-status">' + (isSuccess ? 'Succeeded' : 'Failed') + '</div>';
947 | html += '<div class="last-execution-name">' + this.escapeHtml(execution.name) + '</div>';
948 | html += '</div>';
949 |
950 | html += '<div class="execution-meta">#' + execution.task_id + '</div>';
951 | html += '</div>';
952 |
953 | this.$lastExecutionDisplay.html(html);
954 | }
955 |
956 | displayCancelledExecutions() {
957 | let self = this;
958 | const cancelledExecs = self.cancelledExecutions
959 |
960 | if (cancelledExecs.length === 0) {
961 | // Hide the cancelled executions section
962 | $('.executions-section').eq(2).hide();
963 | return;
964 | }
965 |
966 | // Show the cancelled executions section
967 | $('.executions-section').eq(2).show();
968 |
969 | let html = '<div class="execution-list">';
970 |
971 | cancelledExecs.forEach(function (execution) {
972 | const isAbandoned = execution.is_running;
973 |
974 | html += '<div class="execution-item ' + (isAbandoned ? 'abandoned' : 'cancelled') + '">';
975 | html += '<div class="execution-icon ' + (isAbandoned ? 'abandoned' : 'cancelled') + '">';
976 | html += isAbandoned ? '!' : '✕';
977 | html += '</div>';
978 | html += '<div class="execution-name">' + self.escapeHtml(execution.name) + '</div>';
979 | html += '<div class="execution-meta">' + (isAbandoned ? 'abandoned · ' : '') + '#' + execution.task_id + '</div>';
980 | html += '</div>';
981 | });
982 |
983 | html += '</div>';
984 | this.$cancelledExecutionsDisplay.html(html);
985 | }
986 |
987 | confirmCancelExecution(executionData) {
988 | console.log('confirmCancelExecution called with:', executionData);
989 | this.executionToCancel = executionData;
990 |
991 | if (executionData.is_running) {
992 | // Show modal for running executions
993 | console.log('Showing modal for running execution');
994 | this.$cancelExecutionModal.fadeIn(200);
995 | } else {
996 | // Directly cancel queued executions
997 | console.log('Directly cancelling queued execution');
998 | this.cancelExecution(executionData);
999 | }
1000 | }
1001 |
1002 | confirmCancelExecutionOk() {
1003 | if (this.executionToCancel) {
1004 | this.cancelExecution(this.executionToCancel);
1005 | }
1006 | this.closeCancelExecutionModal();
1007 | }
1008 |
1009 | cancelExecution(executionData) {
1010 | const self = this;
1011 |
1012 | console.log('cancelExecution called with full execution data:', executionData);
1013 | console.log('Attempting to cancel task:', executionData.task_id);
1014 |
1015 | // Call backend API to cancel the task
1016 | $.ajax({
1017 | url: '/cancel_task_execution', type: 'POST', contentType: 'application/json', data: JSON.stringify({
1018 | task_id: executionData.task_id
1019 | }), success: function (response) {
1020 | console.log('Cancel task response:', response);
1021 |
1022 | if (response.status === 'error') {
1023 | console.error('Backend returned error status:', response.message);
1024 | alert('Error cancelling task: ' + response.message);
1025 | return;
1026 | }
1027 |
1028 | if (response.status === 'success') {
1029 | if (response.was_cancelled) {
1030 | console.log('Task ' + executionData.task_id + ' was successfully cancelled');
1031 | // Add to cancelled list (only managed in JS, not persisted)
1032 | const alreadyCancelled = self.cancelledExecutions.some(function (exec) {
1033 | return exec.task_id === executionData.task_id;
1034 | });
1035 | if (!alreadyCancelled) {
1036 | console.log('Adding execution to cancelled list:', executionData);
1037 | self.cancelledExecutions.push(executionData);
1038 | console.log('Cancelled executions array now contains:', self.cancelledExecutions);
1039 | } else {
1040 | console.log('Execution already in cancelled list');
1041 | }
1042 | } else {
1043 | console.log('Task ' + executionData.task_id + ' could not be cancelled (may have already completed). ' + response.message);
1044 | }
1045 | // Refresh display regardless
1046 | self.loadQueuedExecutions();
1047 | } else {
1048 | console.error('Unexpected response status:', response.status);
1049 | alert('Unexpected response from server');
1050 | }
1051 | }, error: function (xhr, status, error) {
1052 | console.error('AJAX error cancelling task:');
1053 | console.error(' Status:', status);
1054 | console.error(' Error:', error);
1055 | console.error(' XHR:', xhr);
1056 | console.error(' Response:', xhr.responseText);
1057 |
1058 | let errorMessage = error;
1059 | if (xhr.responseJSON && xhr.responseJSON.message) {
1060 | errorMessage = xhr.responseJSON.message;
1061 | } else if (xhr.responseText) {
1062 | errorMessage = xhr.responseText;
1063 | }
1064 |
1065 | alert('Error cancelling task: ' + errorMessage);
1066 | }
1067 | });
1068 | }
1069 |
1070 | closeCancelExecutionModal() {
1071 | this.$cancelExecutionModal.fadeOut(200);
1072 | this.executionToCancel = null;
1073 | }
1074 |
1075 | escapeHtml(text) {
1076 | if (typeof text !== 'string') return text;
1077 |
1078 | const patterns = {
1079 | '<': '<', '>': '>', '&': '&', '"': '"', "'": ''', '`': '`'
1080 | };
1081 |
1082 | return text.replace(/[<>&"'`]/g, match => patterns[match]);
1083 | }
1084 |
1085 | // ===== Logs Methods =====
1086 |
1087 | displayLogMessage(message) {
1088 | this.$logContainer.append(new LogMessage(message, this.toolNames).$elem);
1089 | }
1090 |
1091 | loadToolNames() {
1092 | let self = this;
1093 | return $.ajax({
1094 | url: '/get_tool_names', type: 'GET', success: function (response) {
1095 | self.toolNames = response.tool_names || [];
1096 | console.log('Loaded tool names:', self.toolNames);
1097 | }, error: function (xhr, status, error) {
1098 | console.error('Error loading tool names:', error);
1099 | }
1100 | });
1101 | }
1102 |
1103 | updateTitle(activeProject) {
1104 | document.title = activeProject ? `${activeProject} – Serena Dashboard` : 'Serena Dashboard';
1105 | }
1106 |
1107 | copyLogs() {
1108 | const logText = this.$logContainer.text();
1109 |
1110 | if (!logText) {
1111 | alert('No logs to copy');
1112 | return;
1113 | }
1114 |
1115 | // Use the Clipboard API to copy text
1116 | navigator.clipboard.writeText(logText).then(() => {
1117 | // Visual feedback - temporarily change icon to grey checkmark
1118 | const originalHtml = this.$copyLogsBtn.html();
1119 | const checkmarkSvg = '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#888" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg><span class="copy-logs-text">copy logs</span>';
1120 | this.$copyLogsBtn.html(checkmarkSvg);
1121 |
1122 | setTimeout(() => {
1123 | this.$copyLogsBtn.html(originalHtml);
1124 | }, 1500);
1125 | }).catch(err => {
1126 | console.error('Failed to copy logs:', err);
1127 | alert('Failed to copy logs to clipboard');
1128 | });
1129 | }
1130 |
1131 | loadLogs() {
1132 | console.log("Loading logs");
1133 | let self = this;
1134 |
1135 | self.$errorContainer.empty();
1136 |
1137 | // Make API call
1138 | $.ajax({
1139 | url: '/get_log_messages', type: 'POST', contentType: 'application/json', data: JSON.stringify({
1140 | start_idx: 0
1141 | }), success: function (response) {
1142 | // Clear existing logs
1143 | self.$logContainer.empty();
1144 |
1145 | // Update max_idx
1146 | self.currentMaxIdx = response.max_idx || -1;
1147 |
1148 | // Display each log message
1149 | if (response.messages && response.messages.length > 0) {
1150 | response.messages.forEach(function (message) {
1151 | self.displayLogMessage(message);
1152 | });
1153 |
1154 | // Auto-scroll to bottom
1155 | const logContainer = $('#log-container')[0];
1156 | logContainer.scrollTop = logContainer.scrollHeight;
1157 | } else {
1158 | $('#log-container').html('<div class="loading">No log messages found.</div>');
1159 | }
1160 |
1161 | self.updateTitle(response.active_project);
1162 |
1163 | // Start periodic polling for new logs
1164 | self.startPeriodicPolling();
1165 | }, error: function (xhr, status, error) {
1166 | console.error('Error loading logs:', error);
1167 | self.$errorContainer.html('<div class="error-message">Error loading logs: ' + (xhr.responseJSON ? xhr.responseJSON.detail : error) + '</div>');
1168 | }
1169 | });
1170 | }
1171 |
1172 | pollForNewLogs() {
1173 | let self = this;
1174 | console.log("Polling logs", this.currentMaxIdx);
1175 | $.ajax({
1176 | url: '/get_log_messages',
1177 | type: 'POST',
1178 | contentType: 'application/json',
1179 | data: JSON.stringify({
1180 | start_idx: self.currentMaxIdx + 1
1181 | }),
1182 | success: function (response) {
1183 | // Only append new messages if we have any
1184 | if (response.messages && response.messages.length > 0) {
1185 | let wasAtBottom = false;
1186 | const logContainer = $('#log-container')[0];
1187 |
1188 | // Check if user was at the bottom before adding new logs
1189 | if (logContainer.scrollHeight > 0) {
1190 | wasAtBottom = (logContainer.scrollTop + logContainer.clientHeight) >= (logContainer.scrollHeight - 10);
1191 | }
1192 |
1193 | // Append new messages
1194 | response.messages.forEach(function (message) {
1195 | self.displayLogMessage(message);
1196 | });
1197 |
1198 | // Update max_idx
1199 | self.currentMaxIdx = response.max_idx || self.currentMaxIdx;
1200 |
1201 | // Auto-scroll to bottom if user was already at bottom
1202 | if (wasAtBottom) {
1203 | logContainer.scrollTop = logContainer.scrollHeight;
1204 | }
1205 | } else {
1206 | // Update max_idx even if no new messages
1207 | self.currentMaxIdx = response.max_idx || self.currentMaxIdx;
1208 | }
1209 |
1210 | // Update window title with active project
1211 | self.updateTitle(response.active_project);
1212 | }
1213 | });
1214 | }
1215 |
1216 | startPeriodicPolling() {
1217 | // Clear any existing interval
1218 | if (this.pollInterval) {
1219 | clearInterval(this.pollInterval);
1220 | }
1221 |
1222 | // Start polling every second (1000ms)
1223 | this.pollInterval = setInterval(this.pollForNewLogs.bind(this), 1000);
1224 | }
1225 |
1226 | // ===== Stats Methods =====
1227 |
1228 | loadStats() {
1229 | let self = this;
1230 | $.when($.ajax({url: '/get_tool_stats', type: 'GET'}), $.ajax({
1231 | url: '/get_token_count_estimator_name',
1232 | type: 'GET'
1233 | })).done(function (statsResp, estimatorResp) {
1234 | const stats = statsResp[0].stats;
1235 | const tokenCountEstimatorName = estimatorResp[0].token_count_estimator_name;
1236 | self.displayStats(stats, tokenCountEstimatorName);
1237 | }).fail(function () {
1238 | console.error('Error loading stats or estimator name');
1239 | });
1240 | }
1241 |
1242 | clearStats() {
1243 | let self = this;
1244 | $.ajax({
1245 | url: '/clear_tool_stats', type: 'POST', success: function () {
1246 | self.loadStats();
1247 | }, error: function (xhr, status, error) {
1248 | console.error('Error clearing stats:', error);
1249 | }
1250 | });
1251 | }
1252 |
1253 | displayStats(stats, tokenCountEstimatorName) {
1254 | const names = Object.keys(stats);
1255 | // If no stats collected
1256 | if (names.length === 0) {
1257 | // hide summary, charts, estimator name
1258 | $('#stats-summary').hide();
1259 | $('#estimator-name').hide();
1260 | $('.charts-container').hide();
1261 | // show no-stats message
1262 | $('#no-stats-message').show();
1263 | return;
1264 | } else {
1265 | // Ensure everything is visible
1266 | $('#estimator-name').show();
1267 | $('#stats-summary').show();
1268 | $('.charts-container').show();
1269 | $('#no-stats-message').hide();
1270 | }
1271 |
1272 | $('#estimator-name').html(`<strong>Token count estimator:</strong> ${tokenCountEstimatorName}`);
1273 |
1274 | const counts = names.map(n => stats[n].num_times_called);
1275 | const inputTokens = names.map(n => stats[n].input_tokens);
1276 | const outputTokens = names.map(n => stats[n].output_tokens);
1277 | const totalTokens = names.map(n => stats[n].input_tokens + stats[n].output_tokens);
1278 |
1279 | // Calculate totals for summary table
1280 | const totalCalls = counts.reduce((sum, count) => sum + count, 0);
1281 | const totalInputTokens = inputTokens.reduce((sum, tokens) => sum + tokens, 0);
1282 | const totalOutputTokens = outputTokens.reduce((sum, tokens) => sum + tokens, 0);
1283 |
1284 | // Generate consistent colors for tools
1285 | const colors = this.generateColors(names.length);
1286 |
1287 | const countCtx = document.getElementById('count-chart');
1288 | const tokensCtx = document.getElementById('tokens-chart');
1289 | const inputCtx = document.getElementById('input-chart');
1290 | const outputCtx = document.getElementById('output-chart');
1291 |
1292 | if (this.countChart) this.countChart.destroy();
1293 | if (this.tokensChart) this.tokensChart.destroy();
1294 | if (this.inputChart) this.inputChart.destroy();
1295 | if (this.outputChart) this.outputChart.destroy();
1296 |
1297 | // Update summary table
1298 | this.updateSummaryTable(totalCalls, totalInputTokens, totalOutputTokens);
1299 |
1300 | // Register datalabels plugin
1301 | Chart.register(ChartDataLabels);
1302 |
1303 | // Get theme-aware colors
1304 | const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
1305 | const textColor = isDark ? '#ffffff' : '#000000';
1306 | const gridColor = isDark ? '#444' : '#ddd';
1307 |
1308 | // Tool calls pie chart
1309 | this.countChart = new Chart(countCtx, {
1310 | type: 'pie', data: {
1311 | labels: names, datasets: [{
1312 | data: counts, backgroundColor: colors
1313 | }]
1314 | }, options: {
1315 | plugins: {
1316 | legend: {
1317 | display: true, labels: {
1318 | color: textColor
1319 | }
1320 | }, datalabels: {
1321 | display: true, color: 'white', font: {weight: 'bold'}, formatter: (value) => value
1322 | }
1323 | }
1324 | }
1325 | });
1326 |
1327 | // Input tokens pie chart
1328 | this.inputChart = new Chart(inputCtx, {
1329 | type: 'pie', data: {
1330 | labels: names, datasets: [{
1331 | data: inputTokens, backgroundColor: colors
1332 | }]
1333 | }, options: {
1334 | plugins: {
1335 | legend: {
1336 | display: true, labels: {
1337 | color: textColor
1338 | }
1339 | }, datalabels: {
1340 | display: true, color: 'white', font: {weight: 'bold'}, formatter: (value) => value
1341 | }
1342 | }
1343 | }
1344 | });
1345 |
1346 | // Output tokens pie chart
1347 | this.outputChart = new Chart(outputCtx, {
1348 | type: 'pie', data: {
1349 | labels: names, datasets: [{
1350 | data: outputTokens, backgroundColor: colors
1351 | }]
1352 | }, options: {
1353 | plugins: {
1354 | legend: {
1355 | display: true, labels: {
1356 | color: textColor
1357 | }
1358 | }, datalabels: {
1359 | display: true, color: 'white', font: {weight: 'bold'}, formatter: (value) => value
1360 | }
1361 | }
1362 | }
1363 | });
1364 |
1365 | // Combined input/output tokens bar chart
1366 | this.tokensChart = new Chart(tokensCtx, {
1367 | type: 'bar', data: {
1368 | labels: names, datasets: [{
1369 | label: 'Input Tokens', data: inputTokens, backgroundColor: colors.map(color => color + '80'), // Semi-transparent
1370 | borderColor: colors, borderWidth: 2, borderSkipped: false, yAxisID: 'y'
1371 | }, {
1372 | label: 'Output Tokens', data: outputTokens, backgroundColor: colors, yAxisID: 'y1'
1373 | }]
1374 | }, options: {
1375 | responsive: true, plugins: {
1376 | legend: {
1377 | labels: {
1378 | color: textColor
1379 | }
1380 | }
1381 | }, scales: {
1382 | x: {
1383 | ticks: {
1384 | color: textColor
1385 | }, grid: {
1386 | color: gridColor
1387 | }
1388 | }, y: {
1389 | type: 'linear', display: true, position: 'left', beginAtZero: true, title: {
1390 | display: true, text: 'Input Tokens', color: textColor
1391 | }, ticks: {
1392 | color: textColor
1393 | }, grid: {
1394 | color: gridColor
1395 | }
1396 | }, y1: {
1397 | type: 'linear', display: true, position: 'right', beginAtZero: true, title: {
1398 | display: true, text: 'Output Tokens', color: textColor
1399 | }, ticks: {
1400 | color: textColor
1401 | }, grid: {
1402 | drawOnChartArea: false, color: gridColor
1403 | }
1404 | }
1405 | }
1406 | }
1407 | });
1408 | }
1409 |
1410 | generateColors(count) {
1411 | const colors = ['#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF', '#FF9F40', '#FF6384', '#C9CBCF', '#4BC0C0', '#FF6384'];
1412 | return Array.from({length: count}, (_, i) => colors[i % colors.length]);
1413 | }
1414 |
1415 | updateSummaryTable(totalCalls, totalInputTokens, totalOutputTokens) {
1416 | const tableHtml = `
1417 | <table class="stats-summary">
1418 | <tr><th>Metric</th><th>Total</th></tr>
1419 | <tr><td>Tool Calls</td><td>${totalCalls}</td></tr>
1420 | <tr><td>Input Tokens</td><td>${totalInputTokens}</td></tr>
1421 | <tr><td>Output Tokens</td><td>${totalOutputTokens}</td></tr>
1422 | <tr><td>Total Tokens</td><td>${totalInputTokens + totalOutputTokens}</td></tr>
1423 | </table>
1424 | `;
1425 | $('#stats-summary').html(tableHtml);
1426 | }
1427 |
1428 | // ===== Theme Methods =====
1429 |
1430 | initializeTheme() {
1431 | // Check if user has manually set a theme preference
1432 | const savedTheme = localStorage.getItem('serena-theme');
1433 |
1434 | if (savedTheme) {
1435 | // User has manually set a preference, use it
1436 | this.setTheme(savedTheme);
1437 | } else {
1438 | // No manual preference, detect system color scheme
1439 | this.detectSystemTheme();
1440 | }
1441 |
1442 | // Listen for system theme changes
1443 | this.setupSystemThemeListener();
1444 | }
1445 |
1446 | detectSystemTheme() {
1447 | // Check if system prefers dark mode
1448 | const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
1449 | const theme = prefersDark ? 'dark' : 'light';
1450 | this.setTheme(theme);
1451 | }
1452 |
1453 | setupSystemThemeListener() {
1454 | // Listen for changes in system color scheme
1455 | const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
1456 |
1457 | const handleSystemThemeChange = (e) => {
1458 | // Only auto-switch if user hasn't manually set a preference
1459 | const savedTheme = localStorage.getItem('serena-theme');
1460 | if (!savedTheme) {
1461 | const newTheme = e.matches ? 'dark' : 'light';
1462 | this.setTheme(newTheme);
1463 | }
1464 | };
1465 |
1466 | // Add listener for system theme changes
1467 | if (mediaQuery.addEventListener) {
1468 | mediaQuery.addEventListener('change', handleSystemThemeChange);
1469 | } else {
1470 | // Fallback for older browsers
1471 | mediaQuery.addListener(handleSystemThemeChange);
1472 | }
1473 | }
1474 |
1475 | toggleTheme() {
1476 | const currentTheme = document.documentElement.getAttribute('data-theme') || 'light';
1477 | const newTheme = currentTheme === 'light' ? 'dark' : 'light';
1478 |
1479 | // When user manually toggles, save their preference
1480 | localStorage.setItem('serena-theme', newTheme);
1481 | this.setTheme(newTheme);
1482 | }
1483 |
1484 | /**
1485 | * @param theme {'light' | 'dark'}
1486 | */
1487 | setTheme(theme) {
1488 | // Set the theme on the document element
1489 | document.documentElement.setAttribute('data-theme', theme);
1490 |
1491 | // Update the theme toggle button
1492 | if (theme === 'dark') {
1493 | this.$themeIcon.text('☀️');
1494 | this.$themeText.text('Light');
1495 | } else {
1496 | this.$themeIcon.text('🌙');
1497 | this.$themeText.text('Dark');
1498 | }
1499 |
1500 | // Update theme-aware images
1501 | $(".theme-aware-img").each(function() {
1502 | const $img = $(this);
1503 | updateThemeAwareImage($img, theme);
1504 | });
1505 |
1506 | // Save to localStorage
1507 | localStorage.setItem('serena-theme', theme);
1508 |
1509 | // Update charts if they exist
1510 | this.updateChartsTheme();
1511 | }
1512 |
1513 | updateChartsTheme() {
1514 | const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
1515 | const textColor = isDark ? '#ffffff' : '#000000';
1516 | const gridColor = isDark ? '#444' : '#ddd';
1517 |
1518 | // Update existing charts if they exist and have the scales property
1519 | if (this.countChart && this.countChart.options.plugins) {
1520 | if (this.countChart.options.plugins.legend) {
1521 | this.countChart.options.plugins.legend.labels.color = textColor;
1522 | }
1523 | this.countChart.update();
1524 | }
1525 |
1526 | if (this.inputChart && this.inputChart.options.plugins) {
1527 | if (this.inputChart.options.plugins.legend) {
1528 | this.inputChart.options.plugins.legend.labels.color = textColor;
1529 | }
1530 | this.inputChart.update();
1531 | }
1532 |
1533 | if (this.outputChart && this.outputChart.options.plugins) {
1534 | if (this.outputChart.options.plugins.legend) {
1535 | this.outputChart.options.plugins.legend.labels.color = textColor;
1536 | }
1537 | this.outputChart.update();
1538 | }
1539 |
1540 | if (this.tokensChart && this.tokensChart.options.scales) {
1541 | this.tokensChart.options.scales.x.ticks.color = textColor;
1542 | this.tokensChart.options.scales.y.ticks.color = textColor;
1543 | this.tokensChart.options.scales.y1.ticks.color = textColor;
1544 | this.tokensChart.options.scales.x.grid.color = gridColor;
1545 | this.tokensChart.options.scales.y.grid.color = gridColor;
1546 | this.tokensChart.options.scales.y1.grid.color = gridColor;
1547 | this.tokensChart.options.scales.y.title.color = textColor;
1548 | this.tokensChart.options.scales.y1.title.color = textColor;
1549 | if (this.tokensChart.options.plugins && this.tokensChart.options.plugins.legend) {
1550 | this.tokensChart.options.plugins.legend.labels.color = textColor;
1551 | }
1552 | this.tokensChart.update();
1553 | }
1554 | }
1555 |
1556 | // ===== Language Management Methods =====
1557 |
1558 | confirmRemoveLanguage(language) {
1559 | // Store the language to remove
1560 | this.languageToRemove = language;
1561 |
1562 | // Set language name in modal
1563 | this.$removeLanguageName.text(language);
1564 |
1565 | // Show modal
1566 | this.$removeLanguageModal.fadeIn(200);
1567 | }
1568 |
1569 | closeRemoveLanguageModal() {
1570 | this.$removeLanguageModal.fadeOut(200);
1571 | this.languageToRemove = null;
1572 | }
1573 |
1574 | confirmRemoveLanguageOk() {
1575 | if (this.languageToRemove) {
1576 | this.removeLanguage(this.languageToRemove);
1577 | this.closeRemoveLanguageModal();
1578 | }
1579 | }
1580 |
1581 | removeLanguage(language) {
1582 | const self = this;
1583 |
1584 | $.ajax({
1585 | url: '/remove_language', type: 'POST', contentType: 'application/json', data: JSON.stringify({
1586 | language: language
1587 | }), success: function (response) {
1588 | if (response.status === 'success') {
1589 | // Reload config to show updated language list
1590 | self.loadConfigOverview();
1591 | } else {
1592 | alert('Error removing language ' + language + ": " + response.message);
1593 | }
1594 | }, error: function (xhr, status, error) {
1595 | console.error('Error removing language:', error);
1596 | alert('Error removing language: ' + (xhr.responseJSON ? xhr.responseJSON.message : error));
1597 | }
1598 | });
1599 | }
1600 |
1601 | openLanguageModal() {
1602 | // Set project name in modal
1603 | this.$modalProjectName.text(this.activeProjectName || 'Unknown');
1604 |
1605 | // Load available languages into modal dropdown
1606 | this.loadAvailableLanguages();
1607 |
1608 | // Show modal
1609 | this.$addLanguageModal.fadeIn(200);
1610 | }
1611 |
1612 | closeLanguageModal() {
1613 | this.$addLanguageModal.fadeOut(200);
1614 | this.$modalLanguageSelect.empty();
1615 | this.$modalAddBtn.prop('disabled', false).text('Add Language');
1616 | }
1617 |
1618 | loadAvailableLanguages() {
1619 | let self = this;
1620 | $.ajax({
1621 | url: '/get_available_languages', type: 'GET', success: function (response) {
1622 | const languages = response.languages || [];
1623 | // Clear all existing options
1624 | self.$modalLanguageSelect.empty();
1625 |
1626 | if (languages.length === 0) {
1627 | // Show message if no languages available
1628 | self.$modalLanguageSelect.append($('<option>').val('').text('No languages available to add'));
1629 | self.$modalAddBtn.prop('disabled', true);
1630 | } else {
1631 | // Add language options
1632 | languages.forEach(function (language) {
1633 | self.$modalLanguageSelect.append($('<option>').val(language).text(language));
1634 | });
1635 | self.$modalAddBtn.prop('disabled', false);
1636 | }
1637 | }, error: function (xhr, status, error) {
1638 | console.error('Error loading available languages:', error);
1639 | }
1640 | });
1641 | }
1642 |
1643 | addLanguageFromModal() {
1644 | const selectedLanguage = this.$modalLanguageSelect.val();
1645 | if (!selectedLanguage) {
1646 | alert('No language selected or no languages available to add');
1647 | return;
1648 | }
1649 |
1650 | const self = this;
1651 |
1652 | // Close modal immediately
1653 | self.closeLanguageModal();
1654 |
1655 | // Hide the inline add language button and show spinner
1656 | $('#add-language-btn').hide();
1657 | $('#add-language-spinner').show();
1658 | self.isAddingLanguage = true;
1659 |
1660 | $.ajax({
1661 | url: '/add_language', type: 'POST', contentType: 'application/json', data: JSON.stringify({
1662 | language: selectedLanguage
1663 | }), success: function (response) {
1664 | if (response.status === 'success') {
1665 | console.log("Language added successfully");
1666 | } else {
1667 | alert('Error adding language ' + selectedLanguage + ": " + response.message);
1668 | // Restore button visibility on error
1669 | $('#add-language-btn').show();
1670 | $('#add-language-spinner').hide();
1671 | }
1672 | }, error: function (xhr, status, error) {
1673 | console.error('Error adding language:', error);
1674 | alert('Error adding language: ' + (xhr.responseJSON ? xhr.responseJSON.message : error));
1675 | // Restore button visibility on error
1676 | $('#add-language-btn').show();
1677 | $('#add-language-spinner').hide();
1678 | }, complete: function () {
1679 | self.isAddingLanguage = false;
1680 | self.loadConfigOverview();
1681 | }
1682 | });
1683 | }
1684 |
1685 | // ===== Memory Editing Methods =====
1686 |
1687 | openEditMemoryModal(memoryName) {
1688 | const self = this;
1689 | this.currentMemoryName = memoryName;
1690 | this.memoryContentDirty = false;
1691 |
1692 | // Set memory name in modal
1693 | this.$editMemoryName.text(memoryName);
1694 |
1695 | // Load memory content
1696 | $.ajax({
1697 | url: '/get_memory', type: 'POST', contentType: 'application/json', data: JSON.stringify({
1698 | memory_name: memoryName
1699 | }), success: function (response) {
1700 | if (response.status === 'error') {
1701 | alert('Error: ' + response.message);
1702 | return;
1703 | }
1704 | self.originalMemoryContent = response.content;
1705 | self.$editMemoryContent.val(response.content);
1706 | self.memoryContentDirty = false;
1707 | self.$editMemoryModal.fadeIn(200);
1708 | }, error: function (xhr, status, error) {
1709 | console.error('Error loading memory:', error);
1710 | alert('Error loading memory: ' + (xhr.responseJSON ? xhr.responseJSON.message : error));
1711 | }
1712 | });
1713 | }
1714 |
1715 | closeEditMemoryModal() {
1716 | // Check if there are unsaved changes
1717 | if (this.memoryContentDirty) {
1718 | if (!confirm('You have unsaved changes. Are you sure you want to close?')) {
1719 | return;
1720 | }
1721 | }
1722 |
1723 | this.$editMemoryModal.fadeOut(200);
1724 | this.currentMemoryName = null;
1725 | this.originalMemoryContent = null;
1726 | this.memoryContentDirty = false;
1727 | }
1728 |
1729 | trackMemoryChanges() {
1730 | const currentContent = this.$editMemoryContent.val();
1731 | this.memoryContentDirty = (currentContent !== this.originalMemoryContent);
1732 | }
1733 |
1734 | saveMemoryFromModal() {
1735 | const self = this;
1736 | const memoryName = this.currentMemoryName;
1737 | const content = this.$editMemoryContent.val();
1738 |
1739 | if (!memoryName) {
1740 | alert('No memory selected');
1741 | return;
1742 | }
1743 |
1744 | // Disable button during request
1745 | self.$editMemorySaveBtn.prop('disabled', true).text('Saving...');
1746 |
1747 | $.ajax({
1748 | url: '/save_memory', type: 'POST', contentType: 'application/json', data: JSON.stringify({
1749 | memory_name: memoryName, content: content
1750 | }), success: function (response) {
1751 | if (response.status === 'success') {
1752 | // Update original content and reset dirty flag
1753 | self.originalMemoryContent = content;
1754 | self.memoryContentDirty = false;
1755 | // Close modal
1756 | self.$editMemoryModal.fadeOut(200);
1757 | self.currentMemoryName = null;
1758 | } else {
1759 | alert('Error: ' + response.message);
1760 | }
1761 | }, error: function (xhr, status, error) {
1762 | console.error('Error saving memory:', error);
1763 | alert('Error saving memory: ' + (xhr.responseJSON ? xhr.responseJSON.message : error));
1764 | }, complete: function () {
1765 | // Re-enable button
1766 | self.$editMemorySaveBtn.prop('disabled', false).text('Save');
1767 | }
1768 | });
1769 | }
1770 |
1771 | confirmDeleteMemory(memoryName) {
1772 | // Set memory name to delete
1773 | this.memoryToDelete = memoryName;
1774 |
1775 | // Set memory name in modal
1776 | this.$deleteMemoryName.text(memoryName);
1777 |
1778 | // Show modal
1779 | this.$deleteMemoryModal.fadeIn(200);
1780 | }
1781 |
1782 | closeDeleteMemoryModal() {
1783 | this.$deleteMemoryModal.fadeOut(200);
1784 | this.memoryToDelete = null;
1785 | }
1786 |
1787 | confirmDeleteMemoryOk() {
1788 | if (this.memoryToDelete) {
1789 | this.deleteMemory(this.memoryToDelete);
1790 | this.closeDeleteMemoryModal();
1791 | }
1792 | }
1793 |
1794 | deleteMemory(memoryName) {
1795 | const self = this;
1796 |
1797 | $.ajax({
1798 | url: '/delete_memory', type: 'POST', contentType: 'application/json', data: JSON.stringify({
1799 | memory_name: memoryName
1800 | }), success: function (response) {
1801 | if (response.status === 'success') {
1802 | // Reload config to show updated memory list
1803 | self.loadConfigOverview();
1804 | } else {
1805 | alert('Error: ' + response.message);
1806 | }
1807 | }, error: function (xhr, status, error) {
1808 | console.error('Error deleting memory:', error);
1809 | alert('Error deleting memory: ' + (xhr.responseJSON ? xhr.responseJSON.message : error));
1810 | }
1811 | });
1812 | }
1813 |
1814 | openCreateMemoryModal() {
1815 | // Set project name in modal
1816 | this.$createMemoryProjectName.text(this.activeProjectName || 'Unknown');
1817 |
1818 | // Clear the input field
1819 | this.$createMemoryNameInput.val('');
1820 |
1821 | // Show modal
1822 | this.$createMemoryModal.fadeIn(200);
1823 |
1824 | // Focus on the input field
1825 | setTimeout(() => {
1826 | this.$createMemoryNameInput.focus();
1827 | }, 250);
1828 | }
1829 |
1830 | closeCreateMemoryModal() {
1831 | this.$createMemoryModal.fadeOut(200);
1832 | this.$createMemoryNameInput.val('');
1833 | this.$createMemoryCreateBtn.prop('disabled', false).text('Create');
1834 | }
1835 |
1836 | createMemoryFromModal() {
1837 | const memoryName = this.$createMemoryNameInput.val().trim();
1838 |
1839 | if (!memoryName) {
1840 | alert('Please enter a memory name');
1841 | return;
1842 | }
1843 |
1844 | // Validate memory name (alphanumeric and underscores only)
1845 | if (!/^[a-zA-Z0-9_]+$/.test(memoryName)) {
1846 | alert('Memory name can only contain letters, numbers, and underscores');
1847 | return;
1848 | }
1849 |
1850 | const self = this;
1851 |
1852 | // Disable button during request
1853 | self.$createMemoryCreateBtn.prop('disabled', true).text('Creating...');
1854 |
1855 | $.ajax({
1856 | url: '/save_memory', type: 'POST', contentType: 'application/json', data: JSON.stringify({
1857 | memory_name: memoryName, content: ''
1858 | }), success: function (response) {
1859 | if (response.status === 'success') {
1860 | // Close the create modal
1861 | self.closeCreateMemoryModal();
1862 | // Reload config to show the new memory
1863 | self.loadConfigOverview();
1864 | // Open the edit modal for the newly created memory
1865 | setTimeout(() => {
1866 | self.openEditMemoryModal(memoryName);
1867 | }, 500);
1868 | } else {
1869 | alert('Error: ' + response.message);
1870 | self.$createMemoryCreateBtn.prop('disabled', false).text('Create');
1871 | }
1872 | }, error: function (xhr, status, error) {
1873 | console.error('Error creating memory:', error);
1874 | alert('Error creating memory: ' + (xhr.responseJSON ? xhr.responseJSON.message : error));
1875 | self.$createMemoryCreateBtn.prop('disabled', false).text('Create');
1876 | }
1877 | });
1878 | }
1879 |
1880 | // ===== Serena Config Editing Methods =====
1881 |
1882 | openEditSerenaConfigModal() {
1883 | const self = this;
1884 | this.serenaConfigContentDirty = false;
1885 |
1886 | // Load serena config content
1887 | $.ajax({
1888 | url: '/get_serena_config', type: 'GET', success: function (response) {
1889 | if (response.status === 'error') {
1890 | alert('Error: ' + response.message);
1891 | return;
1892 | }
1893 | self.originalSerenaConfigContent = response.content;
1894 | self.$editSerenaConfigContent.val(response.content);
1895 | self.serenaConfigContentDirty = false;
1896 | self.$editSerenaConfigModal.fadeIn(200);
1897 | }, error: function (xhr, status, error) {
1898 | console.error('Error loading serena config:', error);
1899 | alert('Error loading serena config: ' + (xhr.responseJSON ? xhr.responseJSON.message : error));
1900 | }
1901 | });
1902 |
1903 | // Track changes to config content
1904 | this.$editSerenaConfigContent.off('input').on('input', function () {
1905 | const currentContent = self.$editSerenaConfigContent.val();
1906 | self.serenaConfigContentDirty = (currentContent !== self.originalSerenaConfigContent);
1907 | });
1908 | }
1909 |
1910 | closeEditSerenaConfigModal() {
1911 | // Check if there are unsaved changes
1912 | if (this.serenaConfigContentDirty) {
1913 | if (!confirm('You have unsaved changes. Are you sure you want to close?')) {
1914 | return;
1915 | }
1916 | }
1917 |
1918 | this.$editSerenaConfigModal.fadeOut(200);
1919 | this.originalSerenaConfigContent = null;
1920 | this.serenaConfigContentDirty = false;
1921 | }
1922 |
1923 | saveSerenaConfigFromModal() {
1924 | const self = this;
1925 | const content = this.$editSerenaConfigContent.val();
1926 |
1927 | // Disable button during request
1928 | self.$editSerenaConfigSaveBtn.prop('disabled', true).text('Saving...');
1929 |
1930 | $.ajax({
1931 | url: '/save_serena_config', type: 'POST', contentType: 'application/json', data: JSON.stringify({
1932 | content: content
1933 | }), success: function (response) {
1934 | if (response.status === 'success') {
1935 | // Update original content and reset dirty flag
1936 | self.originalSerenaConfigContent = content;
1937 | self.serenaConfigContentDirty = false;
1938 | // Close modal
1939 | self.$editSerenaConfigModal.fadeOut(200);
1940 | alert('Configuration saved successfully. Please restart Serena for changes to take effect.');
1941 | } else {
1942 | alert('Error: ' + response.message);
1943 | }
1944 | }, error: function (xhr, status, error) {
1945 | console.error('Error saving serena config:', error);
1946 | alert('Error saving serena config: ' + (xhr.responseJSON ? xhr.responseJSON.message : error));
1947 | }, complete: function () {
1948 | // Re-enable button
1949 | self.$editSerenaConfigSaveBtn.prop('disabled', false).text('Save');
1950 | }
1951 | });
1952 | }
1953 |
1954 | // ===== Shutdown Method =====
1955 |
1956 | shutdown() {
1957 | const self = this;
1958 | const _shutdown = function () {
1959 | console.log("Triggering shutdown");
1960 | $.ajax({
1961 | url: '/shutdown', type: "PUT", contentType: 'application/json',
1962 | });
1963 | self.$errorContainer.html('<div class="error-message">Shutting down ...</div>')
1964 | setTimeout(function () {
1965 | window.close();
1966 | }, 1000);
1967 | }
1968 |
1969 | // ask for confirmation using a dialog
1970 | if (confirm("This will fully terminate the Serena server.")) {
1971 | _shutdown();
1972 | } else {
1973 | console.log("Shutdown cancelled");
1974 | }
1975 |
1976 | // Close menu
1977 | self.$menuDropdown.hide();
1978 | }
1979 | }
1980 |
```