#
tokens: 29916/50000 1/410 files (page 18/21)
lines: on (toggle) GitHub
raw markdown copy reset
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 |             '<': '&lt;', '>': '&gt;', '&': '&amp;', '"': '&quot;', '\'': '&#x27;', '`': '&#x60;'
  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 + '">&times;</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 + '">&times;</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, '&#39;');
 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(/&#39;/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 |             '<': '&lt;', '>': '&gt;', '&': '&amp;', '"': '&quot;', "'": '&#x27;', '`': '&#x60;'
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 | 
```
Page 18/21FirstPrevNextLast