This is page 14 of 17. Use http://codebase.md/oraios/serena?lines=false&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
class LogMessage {
constructor(message, toolNames) {
message = this.escapeHtml(message);
const logLevel = this.determineLogLevel(message);
const highlightedMessage = this.highlightToolNames(message, toolNames);
this.$elem = $('<div>').addClass('log-' + logLevel).html(highlightedMessage + '\n');
}
determineLogLevel(message) {
if (message.startsWith('DEBUG')) {
return 'debug';
} else if (message.startsWith('INFO')) {
return 'info';
} else if (message.startsWith('WARNING')) {
return 'warning';
} else if (message.startsWith('ERROR')) {
return 'error';
} else {
return 'default';
}
}
highlightToolNames(message, toolNames) {
let highlightedMessage = message;
toolNames.forEach(function (toolName) {
const regex = new RegExp('\\b' + toolName + '\\b', 'gi');
highlightedMessage = highlightedMessage.replace(regex, '<span class="tool-name">' + toolName + '</span>');
});
return highlightedMessage;
}
escapeHtml(convertString) {
if (typeof convertString !== 'string') return convertString;
const patterns = {
'<': '<', '>': '>', '&': '&', '"': '"', '\'': ''', '`': '`'
};
return convertString.replace(/[<>&"'`]/g, match => patterns[match]);
};
}
function updateThemeAwareImage($img, theme=null) {
if (!theme) {
const isDarkMode = $('html').data("theme") == 'dark';
theme = isDarkMode ? 'dark' : 'light';
}
console.log("updating theme-aware image to theme:", theme);
const newSrc = $img.data('src-' + theme);
if (newSrc) {
$img.attr('src', newSrc);
}
}
class BannerRotation {
constructor() {
this.platinumIndex = 0;
this.goldIndex = 0;
this.platinumTimer = null;
this.goldTimer = null;
this.platinumInterval = 15000;
this.goldInterval = 15000;
this.init();
}
init() {
let self = this;
this.loadBanners(function() {
self.startPlatinumRotation();
self.startGoldRotation();
});
}
loadBanners(onSuccess) {
$.ajax({
url: 'https://oraios-software.de/serena-banners/manifest.php',
type: 'GET',
success: function (response) {
console.log('Banners loaded:', response);
function fillBanners($container, banners, className) {
$.each(banners, function (index, banner) {
let $img = $('<img src="' + banner.image + '" alt="' + banner.alt + '" class="banner-image">');
if (banner.image_dark) {
$img.addClass('theme-aware-img');
$img.attr('data-src-dark', banner.image_dark);
$img.attr('data-src-light', banner.image);
updateThemeAwareImage($img);
}
let $anchor = $('<a href="' + banner.link + '" target="_blank"></a>');
$anchor.append($img);
let $banner = $('<div class="' + className + '-slide" data-banner="' + (index + 1) + '"></div>');
$banner.append($anchor);
if (index === 0) {
$banner.addClass('active');
}
if (banner.border) {
$img.addClass('banner-border');
}
$container.append($banner);
});
}
fillBanners($('#gold-banners'), response.gold, 'gold-banner');
fillBanners($('#platinum-banners'), response.platinum, 'platinum-banner');
onSuccess();
},
error: function (xhr, status, error) {
console.error('Error loading banners:', error);
}
});
}
startPlatinumRotation() {
const self = this;
this.platinumTimer = setInterval(() => {
self.rotatePlatinum('next');
}, this.platinumInterval);
}
startGoldRotation() {
const self = this;
this.goldTimer = setInterval(() => {
self.rotateGold('next');
}, this.goldInterval);
}
rotatePlatinum(direction) {
const $slides = $('.platinum-banner-slide');
const total = $slides.length;
if (total === 0) return;
// Remove active class from current slide
$slides.eq(this.platinumIndex).removeClass('active');
// Calculate next index
if (direction === 'next') {
this.platinumIndex = (this.platinumIndex + 1) % total;
} else {
this.platinumIndex = (this.platinumIndex - 1 + total) % total;
}
// Add active class to new slide
$slides.eq(this.platinumIndex).addClass('active');
// Reset timer
clearInterval(this.platinumTimer);
this.startPlatinumRotation();
}
rotateGold(direction) {
const $groups = $('.gold-banner-slide');
const total = $groups.length;
if (total === 0) return;
// Remove active class from current group
$groups.eq(this.goldIndex).removeClass('active');
// Calculate next index
if (direction === 'next') {
this.goldIndex = (this.goldIndex + 1) % total;
} else {
this.goldIndex = (this.goldIndex - 1 + total) % total;
}
// Add active class to new group
$groups.eq(this.goldIndex).addClass('active');
// Reset timer
clearInterval(this.goldTimer);
this.startGoldRotation();
}
}
class Dashboard {
constructor() {
let self = this;
// Page state
this.currentPage = 'overview';
this.configData = null;
this.lastConfigDataJson = null; // Cache for comparison
this.jetbrainsMode = false;
this.activeProjectName = null;
this.languageToRemove = null;
this.currentMemoryName = null;
this.originalMemoryContent = null;
this.memoryContentDirty = false;
this.memoryToDelete = null;
this.isAddingLanguage = false;
this.waitingForConfigPollingResult = false;
this.waitingForExecutionsPollingResult = false;
this.originalSerenaConfigContent = null;
this.serenaConfigContentDirty = false;
// Execution tracking
this.cancelledExecutions = [];
this.executionToCancel = null;
// Tool names and stats
this.toolNames = [];
this.currentMaxIdx = -1;
this.pollInterval = null;
this.configPollInterval = null;
this.executionsPollInterval = null;
this.heartbeatFailureCount = 0;
// jQuery elements
this.$logContainer = $('#log-container');
this.$errorContainer = $('#error-container');
this.$copyLogsBtn = $('#copy-logs-btn');
this.$menuToggle = $('#menu-toggle');
this.$menuDropdown = $('#menu-dropdown');
this.$menuShutdown = $('#menu-shutdown');
this.$themeToggle = $('#theme-toggle');
this.$themeIcon = $('#theme-icon');
this.$themeText = $('#theme-text');
this.$configDisplay = $('#config-display');
this.$basicStatsDisplay = $('#basic-stats-display');
this.$statsSection = $('#stats-section');
this.$refreshStats = $('#refresh-stats');
this.$clearStats = $('#clear-stats');
this.$projectsDisplay = $('#projects-display');
this.$projectsHeader = $('#projects-header');
this.$availableToolsDisplay = $('#available-tools-display');
this.$availableModesDisplay = $('#available-modes-display');
this.$availableContextsDisplay = $('#available-contexts-display');
this.$addLanguageModal = $('#add-language-modal');
this.$modalLanguageSelect = $('#modal-language-select');
this.$modalProjectName = $('#modal-project-name');
this.$modalAddBtn = $('#modal-add-btn');
this.$modalCancelBtn = $('#modal-cancel-btn');
this.$modalClose = $('.modal-close');
this.$removeLanguageModal = $('#remove-language-modal');
this.$removeLanguageName = $('#remove-language-name');
this.$removeModalOkBtn = $('#remove-modal-ok-btn');
this.$removeModalCancelBtn = $('#remove-modal-cancel-btn');
this.$modalCloseRemove = $('.modal-close-remove');
this.$editMemoryModal = $('#edit-memory-modal');
this.$editMemoryName = $('#edit-memory-name');
this.$editMemoryContent = $('#edit-memory-content');
this.$editMemorySaveBtn = $('#edit-memory-save-btn');
this.$editMemoryCancelBtn = $('#edit-memory-cancel-btn');
this.$modalCloseEditMemory = $('.modal-close-edit-memory');
this.$deleteMemoryModal = $('#delete-memory-modal');
this.$deleteMemoryName = $('#delete-memory-name');
this.$deleteMemoryOkBtn = $('#delete-memory-ok-btn');
this.$deleteMemoryCancelBtn = $('#delete-memory-cancel-btn');
this.$modalCloseDeleteMemory = $('.modal-close-delete-memory');
this.$createMemoryModal = $('#create-memory-modal');
this.$createMemoryProjectName = $('#create-memory-project-name');
this.$createMemoryNameInput = $('#create-memory-name-input');
this.$createMemoryCreateBtn = $('#create-memory-create-btn');
this.$createMemoryCancelBtn = $('#create-memory-cancel-btn');
this.$modalCloseCreateMemory = $('.modal-close-create-memory');
this.$activeExecutionQueueDisplay = $('#active-executions-display');
this.$lastExecutionDisplay = $('#last-execution-display');
this.$cancelledExecutionsDisplay = $('#cancelled-executions-display');
this.$cancelExecutionModal = $('#cancel-execution-modal');
this.$cancelExecutionOkBtn = $('#cancel-execution-ok-btn');
this.$cancelExecutionCancelBtn = $('#cancel-execution-cancel-btn');
this.$modalCloseCancelExecution = $('.modal-close-cancel-execution');
this.$editSerenaConfigModal = $('#edit-serena-config-modal');
this.$editSerenaConfigContent = $('#edit-serena-config-content');
this.$editSerenaConfigSaveBtn = $('#edit-serena-config-save-btn');
this.$editSerenaConfigCancelBtn = $('#edit-serena-config-cancel-btn');
this.$modalCloseEditSerenaConfig = $('.modal-close-edit-serena-config');
// Chart references
this.countChart = null;
this.tokensChart = null;
this.inputChart = null;
this.outputChart = null;
// Register event handlers
this.$copyLogsBtn.click(this.copyLogs.bind(this));
this.$menuShutdown.click(function (e) {
e.preventDefault();
self.shutdown();
});
this.$menuToggle.click(this.toggleMenu.bind(this));
this.$themeToggle.click(this.toggleTheme.bind(this));
this.$refreshStats.click(this.loadStats.bind(this));
this.$clearStats.click(this.clearStats.bind(this));
this.$modalAddBtn.click(this.addLanguageFromModal.bind(this));
this.$modalCancelBtn.click(this.closeLanguageModal.bind(this));
this.$modalClose.click(this.closeLanguageModal.bind(this));
this.$removeModalOkBtn.click(this.confirmRemoveLanguageOk.bind(this));
this.$removeModalCancelBtn.click(this.closeRemoveLanguageModal.bind(this));
this.$modalCloseRemove.click(this.closeRemoveLanguageModal.bind(this));
this.$editMemorySaveBtn.click(this.saveMemoryFromModal.bind(this));
this.$editMemoryCancelBtn.click(this.closeEditMemoryModal.bind(this));
this.$modalCloseEditMemory.click(this.closeEditMemoryModal.bind(this));
this.$editMemoryContent.on('input', this.trackMemoryChanges.bind(this));
this.$deleteMemoryOkBtn.click(this.confirmDeleteMemoryOk.bind(this));
this.$deleteMemoryCancelBtn.click(this.closeDeleteMemoryModal.bind(this));
this.$modalCloseDeleteMemory.click(this.closeDeleteMemoryModal.bind(this));
this.$createMemoryCreateBtn.click(this.createMemoryFromModal.bind(this));
this.$createMemoryCancelBtn.click(this.closeCreateMemoryModal.bind(this));
this.$modalCloseCreateMemory.click(this.closeCreateMemoryModal.bind(this));
this.$createMemoryNameInput.keypress(function (e) {
if (e.which === 13) { // Enter key
e.preventDefault();
self.createMemoryFromModal();
}
});
this.$cancelExecutionOkBtn.click(this.confirmCancelExecutionOk.bind(this));
this.$cancelExecutionCancelBtn.click(this.closeCancelExecutionModal.bind(this));
this.$modalCloseCancelExecution.click(this.closeCancelExecutionModal.bind(this));
this.$editSerenaConfigSaveBtn.click(this.saveSerenaConfigFromModal.bind(this));
this.$editSerenaConfigCancelBtn.click(this.closeEditSerenaConfigModal.bind(this));
this.$modalCloseEditSerenaConfig.click(this.closeEditSerenaConfigModal.bind(this));
// Page navigation
$('[data-page]').click(function (e) {
e.preventDefault();
const page = $(this).data('page');
self.navigateToPage(page);
});
// Close menu when clicking outside
$(document).click(function (e) {
if (!$(e.target).closest('.header-nav').length) {
self.$menuDropdown.hide();
}
});
// Close modals when clicking outside
this.$addLanguageModal.click(function (e) {
if ($(e.target).hasClass('modal')) {
self.closeLanguageModal();
}
});
this.$removeLanguageModal.click(function (e) {
if ($(e.target).hasClass('modal')) {
self.closeRemoveLanguageModal();
}
});
this.$editMemoryModal.click(function (e) {
if ($(e.target).hasClass('modal')) {
self.closeEditMemoryModal();
}
});
this.$deleteMemoryModal.click(function (e) {
if ($(e.target).hasClass('modal')) {
self.closeDeleteMemoryModal();
}
});
this.$createMemoryModal.click(function (e) {
if ($(e.target).hasClass('modal')) {
self.closeCreateMemoryModal();
}
});
this.$editSerenaConfigModal.click(function (e) {
if ($(e.target).hasClass('modal')) {
self.closeEditSerenaConfigModal();
}
});
// Collapsible sections
$('.collapsible-header').click(function () {
const $header = $(this);
const $content = $header.next('.collapsible-content');
const $icon = $header.find('.toggle-icon');
$content.slideToggle(300);
$icon.toggleClass('expanded');
});
// Initialize theme
this.initializeTheme();
// Initialize banner rotation
this.bannerRotation = new BannerRotation();
// Add ESC key handler for closing modals
$(document).keydown(function (e) {
if (e.key === 'Escape' || e.keyCode === 27) {
if (self.$addLanguageModal.is(':visible')) {
self.closeLanguageModal();
} else if (self.$removeLanguageModal.is(':visible')) {
self.closeRemoveLanguageModal();
} else if (self.$editMemoryModal.is(':visible')) {
self.closeEditMemoryModal();
} else if (self.$deleteMemoryModal.is(':visible')) {
self.closeDeleteMemoryModal();
} else if (self.$createMemoryModal.is(':visible')) {
self.closeCreateMemoryModal();
}
}
});
// Initialize the application
this.loadToolNames().then(function () {
// Start on overview page
self.loadConfigOverview();
self.startConfigPolling();
self.startExecutionsPolling();
});
// Initialize heartbeat interval
setInterval(this.heartbeat.bind(this), 250);
}
heartbeat() {
let self = this;
$.ajax({
url: '/heartbeat',
type: 'GET',
success: function (response) {
self.heartbeatFailureCount = 0;
},
error: function (xhr, status, error) {
self.heartbeatFailureCount++;
console.error('Heartbeat failure; count = ', self.heartbeatFailureCount);
if (self.heartbeatFailureCount >= 1) {
console.log('Server appears to be down, closing tab');
window.close();
}
},
});
}
toggleMenu() {
this.$menuDropdown.toggle();
}
navigateToPage(page) {
// Hide menu
this.$menuDropdown.hide();
// Hide all pages
$('.page-view').hide();
// Show selected page
$('#page-' + page).show();
// Update menu active state
$('[data-page]').removeClass('active');
$('[data-page="' + page + '"]').addClass('active');
// Update current page
this.currentPage = page;
// Stop all polling
this.stopPolling();
// Start appropriate polling for the page
if (page === 'overview') {
this.loadConfigOverview();
this.startConfigPolling();
this.startExecutionsPolling();
} else if (page === 'logs') {
this.loadLogs();
} else if (page === 'stats') {
this.loadStats();
}
}
stopPolling() {
if (this.pollInterval) {
clearInterval(this.pollInterval);
this.pollInterval = null;
}
if (this.configPollInterval) {
clearInterval(this.configPollInterval);
this.configPollInterval = null;
}
if (this.executionsPollInterval) {
clearInterval(this.executionsPollInterval);
this.executionsPollInterval = null;
}
}
// ===== Config Overview Methods =====
loadConfigOverview() {
if (this.waitingForConfigPollingResult) {
console.log('Still waiting for previous config poll result, skipping this poll');
return;
}
this.waitingForConfigPollingResult = true;
console.log('Polling for config overview...');
let self = this;
$.ajax({
url: '/get_config_overview',
type: 'GET',
success: function (response) {
// Check if the config data has actually changed
const currentConfigJson = JSON.stringify(response);
const hasChanged = self.lastConfigDataJson !== currentConfigJson;
if (hasChanged) {
console.log('Config has changed, updating display');
self.lastConfigDataJson = currentConfigJson;
self.configData = response;
self.jetbrainsMode = response.jetbrains_mode;
self.activeProjectName = response.active_project.name;
self.displayConfig(response);
self.displayBasicStats(response.tool_stats_summary);
self.displayProjects(response.registered_projects);
self.displayAvailableTools(response.available_tools);
self.displayAvailableModes(response.available_modes);
self.displayAvailableContexts(response.available_contexts);
} else {
console.log('Config unchanged, skipping display update');
}
}, error: function (xhr, status, error) {
console.error('Error loading config overview:', error);
self.$configDisplay.html('<div class="error-message">Error loading configuration</div>');
self.$basicStatsDisplay.html('<div class="error-message">Error loading stats</div>');
self.$projectsDisplay.html('<div class="error-message">Error loading projects</div>');
self.$availableToolsDisplay.html('<div class="error-message">Error loading tools</div>');
self.$availableModesDisplay.html('<div class="error-message">Error loading modes</div>');
self.$availableContextsDisplay.html('<div class="error-message">Error loading contexts</div>');
}, complete: function () {
self.waitingForConfigPollingResult = false;
}
});
}
startConfigPolling() {
this.configPollInterval = setInterval(this.loadConfigOverview.bind(this), 1000);
}
startExecutionsPolling() {
// Poll every 1 second for executions (independent of config polling)
// This ensures stuck executions can still be cancelled even if config polling is blocked
this.loadExecutions()
this.executionsPollInterval = setInterval(() => {
this.loadQueuedExecutions();
this.loadLastExecution();
}, 1000);
}
displayConfig(config) {
try {
// Check if tools and memories sections are currently expanded
const $existingToolsContent = $('#tools-content');
const $existingMemoriesContent = $('#memories-content');
const wasToolsExpanded = $existingToolsContent.is(':visible');
const wasMemoriesExpanded = $existingMemoriesContent.is(':visible');
let html = '<div class="config-grid">';
// Project info
html += '<div class="config-label">Active Project:</div>';
if (config.active_project.name && config.active_project.path) {
const configPath = config.active_project.path + '/.serena/project.yml';
html += '<div class="config-value"><span title="Project configuration in ' + configPath + '">' + config.active_project.name + '</span></div>';
} else {
html += '<div class="config-value">' + (config.active_project.name || 'None') + '</div>';
}
html += '<div class="config-label">Languages:</div>';
if (this.jetbrainsMode) {
html += '<div class="config-value">Using JetBrains backend</div>';
} else {
html += '<div class="config-value">';
if (config.languages && config.languages.length > 0) {
html += '<div class="languages-container">';
config.languages.forEach(function (language, index) {
const isRemovable = config.languages.length > 1;
html += '<div class="language-badge' + (isRemovable ? ' removable' : '') + '">';
html += language;
if (isRemovable) {
html += '<span class="language-remove" data-language="' + language + '">×</span>';
}
html += '</div>';
});
// Add the "Add Language" button inline with language badges (only if active project exists)
if (config.active_project && config.active_project.name) {
// TODO: address after refactoring, it's not awesome to keep depending on state
if (this.isAddingLanguage) {
html += '<div id="add-language-spinner" class="language-spinner">';
} else {
html += '<button id="add-language-btn" class="btn language-add-btn">+ Add Language</button>';
html += '<div id="add-language-spinner" class="language-spinner" style="display:none;">';
}
html += '<div class="spinner"></div>';
html += '</div>';
}
html += '</div>';
} else {
html += 'N/A';
}
html += '</div>';
}
// Context info
html += '<div class="config-label">Context:</div>';
html += '<div class="config-value"><span title="' + config.context.path + '">' + config.context.name + '</span></div>';
// Modes info
html += '<div class="config-label">Active Modes:</div>';
html += '<div class="config-value">';
if (config.modes.length > 0) {
const modeSpans = config.modes.map(function (mode) {
return '<span title="' + mode.path + '">' + mode.name + '</span>';
});
html += modeSpans.join(', ');
} else {
html += 'None';
}
html += '</div>';
// File Encoding info
html += '<div class="config-label">File Encoding:</div>';
html += '<div class="config-value">' + (config.encoding || 'N/A') + '</div>';
html += '</div>';
// Active tools - collapsible
html += '<div style="margin-top: 20px;">';
html += '<h3 class="collapsible-header" id="tools-header" style="font-size: 16px; margin: 0;">';
html += '<span>Active Tools (' + config.active_tools.length + ')</span>';
html += '<span class="toggle-icon' + (wasToolsExpanded ? ' expanded' : '') + '">▼</span>';
html += '</h3>';
html += '<div class="collapsible-content tools-grid" id="tools-content" style="' + (wasToolsExpanded ? '' : 'display:none;') + ' margin-top: 10px;">';
config.active_tools.forEach(function (tool) {
html += '<div class="tool-item" title="' + tool + '">' + tool + '</div>';
});
html += '</div>';
html += '</div>';
// Available memories - collapsible (show if memories exist or if project exists)
if (config.active_project && config.active_project.name) {
html += '<div style="margin-top: 20px;">';
html += '<h3 class="collapsible-header" id="memories-header" style="font-size: 16px; margin: 0;">';
const memoryCount = (config.available_memories && config.available_memories.length) || 0;
html += '<span>Available Memories (' + memoryCount + ')</span>';
html += '<span class="toggle-icon' + (wasMemoriesExpanded ? ' expanded' : '') + '">▼</span>';
html += '</h3>';
html += '<div class="collapsible-content memories-container" id="memories-content" style="' + (wasMemoriesExpanded ? '' : 'display:none;') + ' margin-top: 10px;">';
if (config.available_memories && config.available_memories.length > 0) {
config.available_memories.forEach(function (memory) {
html += '<div class="memory-item removable" data-memory="' + memory + '">';
html += memory;
html += '<span class="memory-remove" data-memory="' + memory + '">×</span>';
html += '</div>';
});
}
// Add Create Memory button
html += '<button id="create-memory-btn" class="memory-add-btn">+ Add Memory</button>';
html += '</div>';
html += '</div>';
}
// Configuration help link and edit config button
html += '<div style="margin-top: 15px; display: flex; gap: 10px; align-items: center;">';
html += '<div style="flex: 1; padding: 10px; background: var(--bg-secondary); border-radius: 4px; font-size: 13px; border: 1px solid var(--border-color);">';
html += '<span style="color: var(--text-muted);">📖</span> ';
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>';
html += '</div>';
html += '<button id="edit-serena-config-btn" class="btn language-add-btn" style="white-space: nowrap; padding: 10px; ">Edit Global Serena Config</button>';
html += '</div>';
this.$configDisplay.html(html);
// Attach event handlers for the dynamically created add language button
$('#add-language-btn').click(this.openLanguageModal.bind(this));
// Attach event handler for edit serena config button
$('#edit-serena-config-btn').click(this.openEditSerenaConfigModal.bind(this));
// Attach event handlers for language remove buttons
const self = this;
$('.language-remove').click(function (e) {
e.preventDefault();
e.stopPropagation();
const language = $(this).data('language');
self.confirmRemoveLanguage(language);
});
// Attach event handlers for memory items
$('.memory-item').click(function (e) {
e.preventDefault();
const memoryName = $(this).data('memory');
self.openEditMemoryModal(memoryName);
});
// Attach event handlers for memory remove buttons
$('.memory-remove').click(function (e) {
e.preventDefault();
e.stopPropagation();
const memoryName = $(this).data('memory');
self.confirmDeleteMemory(memoryName);
});
// Attach event handler for create memory button
$('#create-memory-btn').click(this.openCreateMemoryModal.bind(this));
// Re-attach collapsible handler for the newly created tools header
$('#tools-header').click(function () {
const $header = $(this);
const $content = $('#tools-content');
const $icon = $header.find('.toggle-icon');
$content.slideToggle(300);
$icon.toggleClass('expanded');
});
// Re-attach collapsible handler for the newly created memories header
$('#memories-header').click(function () {
const $header = $(this);
const $content = $('#memories-content');
const $icon = $header.find('.toggle-icon');
$content.slideToggle(300);
$icon.toggleClass('expanded');
});
} catch (error) {
console.error('Error in displayConfig:', error);
this.$configDisplay.html('<div class="error-message">Error displaying configuration: ' + error.message + '</div>');
}
}
displayBasicStats(stats) {
if (Object.keys(stats).length === 0) {
this.$basicStatsDisplay.html('<div class="no-stats-message">No tool usage stats collected yet.</div>');
return;
}
// Sort tools by call count (descending)
const sortedTools = Object.keys(stats).sort((a, b) => {
return stats[b].num_calls - stats[a].num_calls;
});
const maxCalls = Math.max(...sortedTools.map(tool => stats[tool].num_calls));
let html = '';
sortedTools.forEach(function (toolName) {
const count = stats[toolName].num_calls;
const percentage = maxCalls > 0 ? (count / maxCalls * 100) : 0;
html += '<div class="stat-bar-container">';
html += '<div class="stat-tool-name" title="' + toolName + '">' + toolName + '</div>';
html += '<div class="bar-wrapper">';
html += '<div class="bar" style="width: ' + percentage + '%"></div>';
html += '</div>';
html += '<div class="stat-count">' + count + '</div>';
html += '</div>';
});
this.$basicStatsDisplay.html(html);
}
displayProjects(projects) {
if (!projects || projects.length === 0) {
this.$projectsDisplay.html('<div class="no-stats-message">No projects registered.</div>');
return;
}
let html = '';
projects.forEach(function (project) {
const activeClass = project.is_active ? ' active' : '';
html += '<div class="project-item' + activeClass + '">';
html += '<div class="project-name" title="' + project.name + '">' + project.name + '</div>';
html += '<div class="project-path" title="' + project.path + '">' + project.path + '</div>';
html += '</div>';
});
this.$projectsDisplay.html(html);
}
displayAvailableTools(tools) {
if (!tools || tools.length === 0) {
this.$availableToolsDisplay.html('<div class="no-stats-message">All tools are active.</div>');
return;
}
let html = '';
tools.forEach(function (tool) {
html += '<div class="info-item" title="' + tool.name + '">' + tool.name + '</div>';
});
this.$availableToolsDisplay.html(html);
}
displayAvailableModes(modes) {
if (!modes || modes.length === 0) {
this.$availableModesDisplay.html('<div class="no-stats-message">No modes available.</div>');
return;
}
let html = '';
modes.forEach(function (mode) {
const activeClass = mode.is_active ? ' active' : '';
html += '<div class="info-item' + activeClass + '" title="' + mode.path + '">' + mode.name + '</div>';
});
this.$availableModesDisplay.html(html);
}
displayAvailableContexts(contexts) {
if (!contexts || contexts.length === 0) {
this.$availableContextsDisplay.html('<div class="no-stats-message">No contexts available.</div>');
return;
}
let html = '';
contexts.forEach(function (context) {
const activeClass = context.is_active ? ' active' : '';
html += '<div class="info-item' + activeClass + '" title="' + context.path + '">' + context.name + '</div>';
});
this.$availableContextsDisplay.html(html);
}
// ===== Executions Methods =====
loadQueuedExecutions() {
let self = this;
$.ajax({
url: '/queued_task_executions', type: 'GET', success: function (response) {
if (response.status === 'success') {
self.displayActiveExecutionsQueue(response.queued_executions || []);
} else {
console.error('Error loading executions:', response.message);
}
}, error: function (xhr, status, error) {
console.error('Error loading executions:', error);
self.$activeExecutionQueueDisplay.html('<div class="error-message">Error loading executions</div>');
}
});
}
loadLastExecution() {
let self = this;
$.ajax({
url: '/last_execution', type: 'GET', success: function (response) {
if (response.status === 'success') {
if (response.last_execution !== null && response.last_execution.logged) {
self.displayLastExecution(response.last_execution);
}
} else {
console.error('Error loading last execution:', response.message);
}
}, error: function (xhr, status, error) {
console.error('Error loading last execution:', error);
self.$lastExecutionDisplay.html('<div class="error-message">Error loading last execution</div>');
}
});
}
loadExecutions() {
if (this.waitingForExecutionsPollingResult) {
console.log('Still waiting for previous executions poll result, skipping this poll');
} else {
this.waitingForExecutionsPollingResult = true;
console.log('Polling for executions...');
this.loadQueuedExecutions();
this.loadLastExecution();
}
}
displayActiveExecutionsQueue(executions) {
if (!executions || executions.length === 0) {
return;
}
let html = '<div class="execution-list">';
let self = this;
executions.forEach(function (execution) {
const isRunning = execution.is_running;
const logged = execution.logged;
if (!logged) {
return; // Skip unlogged executions
}
let itemClass = 'execution-item';
if (isRunning) {
itemClass += ' running';
}
// Escape JSON for HTML attribute - replace single quotes and use HTML entities
const executionJson = JSON.stringify(execution).replace(/'/g, ''');
html += '<div class="' + itemClass + '" data-task-id="' + execution.task_id + '" data-execution=\'' + executionJson + '\'>';
if (isRunning) {
html += '<div class="execution-spinner"></div>';
}
html += '<div class="execution-name">' + self.escapeHtml(execution.name) + '</div>';
if (isRunning) {
html += '<div class="execution-meta">#' + execution.task_id + '</div>';
} else {
html += '<div class="execution-meta">queued · #' + execution.task_id + '</div>';
}
html += '<button class="execution-cancel-btn" data-task-id="' + execution.task_id + '" data-is-running="' + isRunning + '">✕</button>';
html += '</div>';
});
html += '</div>';
this.$activeExecutionQueueDisplay.html(html);
// Attach event handlers for cancel buttons
$('.execution-cancel-btn').click(function (e) {
e.preventDefault();
console.log('Cancel button clicked');
const $item = $(this).closest('.execution-item');
console.log('Found item:', $item.length);
const executionDataStr = $item.attr('data-execution');
console.log('Execution data string:', executionDataStr);
if (executionDataStr) {
// Unescape HTML entities
const unescapedStr = executionDataStr.replace(/'/g, "'");
const executionData = JSON.parse(unescapedStr);
console.log('Parsed execution data:', executionData);
self.confirmCancelExecution(executionData);
} else {
console.error('No execution data found on element');
}
});
// Update cancelled executions display
this.displayCancelledExecutions(executions);
}
displayLastExecution(execution) {
if (!execution) {
this.$lastExecutionDisplay.html('<div class="no-stats-message">No executions yet.</div>');
return;
}
const isSuccess = execution.finished_successfully;
let html = '<div class="last-execution-container' + (isSuccess ? '' : ' error') + '">';
html += '<div class="last-execution-icon-container">';
html += isSuccess ? '✓' : '✕';
html += '</div>';
html += '<div class="last-execution-body">';
html += '<div class="last-execution-status">' + (isSuccess ? 'Succeeded' : 'Failed') + '</div>';
html += '<div class="last-execution-name">' + this.escapeHtml(execution.name) + '</div>';
html += '</div>';
html += '<div class="execution-meta">#' + execution.task_id + '</div>';
html += '</div>';
this.$lastExecutionDisplay.html(html);
}
displayCancelledExecutions() {
let self = this;
const cancelledExecs = self.cancelledExecutions
if (cancelledExecs.length === 0) {
// Hide the cancelled executions section
$('.executions-section').eq(2).hide();
return;
}
// Show the cancelled executions section
$('.executions-section').eq(2).show();
let html = '<div class="execution-list">';
cancelledExecs.forEach(function (execution) {
const isAbandoned = execution.is_running;
html += '<div class="execution-item ' + (isAbandoned ? 'abandoned' : 'cancelled') + '">';
html += '<div class="execution-icon ' + (isAbandoned ? 'abandoned' : 'cancelled') + '">';
html += isAbandoned ? '!' : '✕';
html += '</div>';
html += '<div class="execution-name">' + self.escapeHtml(execution.name) + '</div>';
html += '<div class="execution-meta">' + (isAbandoned ? 'abandoned · ' : '') + '#' + execution.task_id + '</div>';
html += '</div>';
});
html += '</div>';
this.$cancelledExecutionsDisplay.html(html);
}
confirmCancelExecution(executionData) {
console.log('confirmCancelExecution called with:', executionData);
this.executionToCancel = executionData;
if (executionData.is_running) {
// Show modal for running executions
console.log('Showing modal for running execution');
this.$cancelExecutionModal.fadeIn(200);
} else {
// Directly cancel queued executions
console.log('Directly cancelling queued execution');
this.cancelExecution(executionData);
}
}
confirmCancelExecutionOk() {
if (this.executionToCancel) {
this.cancelExecution(this.executionToCancel);
}
this.closeCancelExecutionModal();
}
cancelExecution(executionData) {
const self = this;
console.log('cancelExecution called with full execution data:', executionData);
console.log('Attempting to cancel task:', executionData.task_id);
// Call backend API to cancel the task
$.ajax({
url: '/cancel_task_execution', type: 'POST', contentType: 'application/json', data: JSON.stringify({
task_id: executionData.task_id
}), success: function (response) {
console.log('Cancel task response:', response);
if (response.status === 'error') {
console.error('Backend returned error status:', response.message);
alert('Error cancelling task: ' + response.message);
return;
}
if (response.status === 'success') {
if (response.was_cancelled) {
console.log('Task ' + executionData.task_id + ' was successfully cancelled');
// Add to cancelled list (only managed in JS, not persisted)
const alreadyCancelled = self.cancelledExecutions.some(function (exec) {
return exec.task_id === executionData.task_id;
});
if (!alreadyCancelled) {
console.log('Adding execution to cancelled list:', executionData);
self.cancelledExecutions.push(executionData);
console.log('Cancelled executions array now contains:', self.cancelledExecutions);
} else {
console.log('Execution already in cancelled list');
}
} else {
console.log('Task ' + executionData.task_id + ' could not be cancelled (may have already completed). ' + response.message);
}
// Refresh display regardless
self.loadQueuedExecutions();
} else {
console.error('Unexpected response status:', response.status);
alert('Unexpected response from server');
}
}, error: function (xhr, status, error) {
console.error('AJAX error cancelling task:');
console.error(' Status:', status);
console.error(' Error:', error);
console.error(' XHR:', xhr);
console.error(' Response:', xhr.responseText);
let errorMessage = error;
if (xhr.responseJSON && xhr.responseJSON.message) {
errorMessage = xhr.responseJSON.message;
} else if (xhr.responseText) {
errorMessage = xhr.responseText;
}
alert('Error cancelling task: ' + errorMessage);
}
});
}
closeCancelExecutionModal() {
this.$cancelExecutionModal.fadeOut(200);
this.executionToCancel = null;
}
escapeHtml(text) {
if (typeof text !== 'string') return text;
const patterns = {
'<': '<', '>': '>', '&': '&', '"': '"', "'": ''', '`': '`'
};
return text.replace(/[<>&"'`]/g, match => patterns[match]);
}
// ===== Logs Methods =====
displayLogMessage(message) {
this.$logContainer.append(new LogMessage(message, this.toolNames).$elem);
}
loadToolNames() {
let self = this;
return $.ajax({
url: '/get_tool_names', type: 'GET', success: function (response) {
self.toolNames = response.tool_names || [];
console.log('Loaded tool names:', self.toolNames);
}, error: function (xhr, status, error) {
console.error('Error loading tool names:', error);
}
});
}
updateTitle(activeProject) {
document.title = activeProject ? `${activeProject} – Serena Dashboard` : 'Serena Dashboard';
}
copyLogs() {
const logText = this.$logContainer.text();
if (!logText) {
alert('No logs to copy');
return;
}
// Use the Clipboard API to copy text
navigator.clipboard.writeText(logText).then(() => {
// Visual feedback - temporarily change icon to grey checkmark
const originalHtml = this.$copyLogsBtn.html();
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>';
this.$copyLogsBtn.html(checkmarkSvg);
setTimeout(() => {
this.$copyLogsBtn.html(originalHtml);
}, 1500);
}).catch(err => {
console.error('Failed to copy logs:', err);
alert('Failed to copy logs to clipboard');
});
}
loadLogs() {
console.log("Loading logs");
let self = this;
self.$errorContainer.empty();
// Make API call
$.ajax({
url: '/get_log_messages', type: 'POST', contentType: 'application/json', data: JSON.stringify({
start_idx: 0
}), success: function (response) {
// Clear existing logs
self.$logContainer.empty();
// Update max_idx
self.currentMaxIdx = response.max_idx || -1;
// Display each log message
if (response.messages && response.messages.length > 0) {
response.messages.forEach(function (message) {
self.displayLogMessage(message);
});
// Auto-scroll to bottom
const logContainer = $('#log-container')[0];
logContainer.scrollTop = logContainer.scrollHeight;
} else {
$('#log-container').html('<div class="loading">No log messages found.</div>');
}
self.updateTitle(response.active_project);
// Start periodic polling for new logs
self.startPeriodicPolling();
}, error: function (xhr, status, error) {
console.error('Error loading logs:', error);
self.$errorContainer.html('<div class="error-message">Error loading logs: ' + (xhr.responseJSON ? xhr.responseJSON.detail : error) + '</div>');
}
});
}
pollForNewLogs() {
let self = this;
console.log("Polling logs", this.currentMaxIdx);
$.ajax({
url: '/get_log_messages',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({
start_idx: self.currentMaxIdx + 1
}),
success: function (response) {
// Only append new messages if we have any
if (response.messages && response.messages.length > 0) {
let wasAtBottom = false;
const logContainer = $('#log-container')[0];
// Check if user was at the bottom before adding new logs
if (logContainer.scrollHeight > 0) {
wasAtBottom = (logContainer.scrollTop + logContainer.clientHeight) >= (logContainer.scrollHeight - 10);
}
// Append new messages
response.messages.forEach(function (message) {
self.displayLogMessage(message);
});
// Update max_idx
self.currentMaxIdx = response.max_idx || self.currentMaxIdx;
// Auto-scroll to bottom if user was already at bottom
if (wasAtBottom) {
logContainer.scrollTop = logContainer.scrollHeight;
}
} else {
// Update max_idx even if no new messages
self.currentMaxIdx = response.max_idx || self.currentMaxIdx;
}
// Update window title with active project
self.updateTitle(response.active_project);
}
});
}
startPeriodicPolling() {
// Clear any existing interval
if (this.pollInterval) {
clearInterval(this.pollInterval);
}
// Start polling every second (1000ms)
this.pollInterval = setInterval(this.pollForNewLogs.bind(this), 1000);
}
// ===== Stats Methods =====
loadStats() {
let self = this;
$.when($.ajax({url: '/get_tool_stats', type: 'GET'}), $.ajax({
url: '/get_token_count_estimator_name',
type: 'GET'
})).done(function (statsResp, estimatorResp) {
const stats = statsResp[0].stats;
const tokenCountEstimatorName = estimatorResp[0].token_count_estimator_name;
self.displayStats(stats, tokenCountEstimatorName);
}).fail(function () {
console.error('Error loading stats or estimator name');
});
}
clearStats() {
let self = this;
$.ajax({
url: '/clear_tool_stats', type: 'POST', success: function () {
self.loadStats();
}, error: function (xhr, status, error) {
console.error('Error clearing stats:', error);
}
});
}
displayStats(stats, tokenCountEstimatorName) {
const names = Object.keys(stats);
// If no stats collected
if (names.length === 0) {
// hide summary, charts, estimator name
$('#stats-summary').hide();
$('#estimator-name').hide();
$('.charts-container').hide();
// show no-stats message
$('#no-stats-message').show();
return;
} else {
// Ensure everything is visible
$('#estimator-name').show();
$('#stats-summary').show();
$('.charts-container').show();
$('#no-stats-message').hide();
}
$('#estimator-name').html(`<strong>Token count estimator:</strong> ${tokenCountEstimatorName}`);
const counts = names.map(n => stats[n].num_times_called);
const inputTokens = names.map(n => stats[n].input_tokens);
const outputTokens = names.map(n => stats[n].output_tokens);
const totalTokens = names.map(n => stats[n].input_tokens + stats[n].output_tokens);
// Calculate totals for summary table
const totalCalls = counts.reduce((sum, count) => sum + count, 0);
const totalInputTokens = inputTokens.reduce((sum, tokens) => sum + tokens, 0);
const totalOutputTokens = outputTokens.reduce((sum, tokens) => sum + tokens, 0);
// Generate consistent colors for tools
const colors = this.generateColors(names.length);
const countCtx = document.getElementById('count-chart');
const tokensCtx = document.getElementById('tokens-chart');
const inputCtx = document.getElementById('input-chart');
const outputCtx = document.getElementById('output-chart');
if (this.countChart) this.countChart.destroy();
if (this.tokensChart) this.tokensChart.destroy();
if (this.inputChart) this.inputChart.destroy();
if (this.outputChart) this.outputChart.destroy();
// Update summary table
this.updateSummaryTable(totalCalls, totalInputTokens, totalOutputTokens);
// Register datalabels plugin
Chart.register(ChartDataLabels);
// Get theme-aware colors
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
const textColor = isDark ? '#ffffff' : '#000000';
const gridColor = isDark ? '#444' : '#ddd';
// Tool calls pie chart
this.countChart = new Chart(countCtx, {
type: 'pie', data: {
labels: names, datasets: [{
data: counts, backgroundColor: colors
}]
}, options: {
plugins: {
legend: {
display: true, labels: {
color: textColor
}
}, datalabels: {
display: true, color: 'white', font: {weight: 'bold'}, formatter: (value) => value
}
}
}
});
// Input tokens pie chart
this.inputChart = new Chart(inputCtx, {
type: 'pie', data: {
labels: names, datasets: [{
data: inputTokens, backgroundColor: colors
}]
}, options: {
plugins: {
legend: {
display: true, labels: {
color: textColor
}
}, datalabels: {
display: true, color: 'white', font: {weight: 'bold'}, formatter: (value) => value
}
}
}
});
// Output tokens pie chart
this.outputChart = new Chart(outputCtx, {
type: 'pie', data: {
labels: names, datasets: [{
data: outputTokens, backgroundColor: colors
}]
}, options: {
plugins: {
legend: {
display: true, labels: {
color: textColor
}
}, datalabels: {
display: true, color: 'white', font: {weight: 'bold'}, formatter: (value) => value
}
}
}
});
// Combined input/output tokens bar chart
this.tokensChart = new Chart(tokensCtx, {
type: 'bar', data: {
labels: names, datasets: [{
label: 'Input Tokens', data: inputTokens, backgroundColor: colors.map(color => color + '80'), // Semi-transparent
borderColor: colors, borderWidth: 2, borderSkipped: false, yAxisID: 'y'
}, {
label: 'Output Tokens', data: outputTokens, backgroundColor: colors, yAxisID: 'y1'
}]
}, options: {
responsive: true, plugins: {
legend: {
labels: {
color: textColor
}
}
}, scales: {
x: {
ticks: {
color: textColor
}, grid: {
color: gridColor
}
}, y: {
type: 'linear', display: true, position: 'left', beginAtZero: true, title: {
display: true, text: 'Input Tokens', color: textColor
}, ticks: {
color: textColor
}, grid: {
color: gridColor
}
}, y1: {
type: 'linear', display: true, position: 'right', beginAtZero: true, title: {
display: true, text: 'Output Tokens', color: textColor
}, ticks: {
color: textColor
}, grid: {
drawOnChartArea: false, color: gridColor
}
}
}
}
});
}
generateColors(count) {
const colors = ['#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF', '#FF9F40', '#FF6384', '#C9CBCF', '#4BC0C0', '#FF6384'];
return Array.from({length: count}, (_, i) => colors[i % colors.length]);
}
updateSummaryTable(totalCalls, totalInputTokens, totalOutputTokens) {
const tableHtml = `
<table class="stats-summary">
<tr><th>Metric</th><th>Total</th></tr>
<tr><td>Tool Calls</td><td>${totalCalls}</td></tr>
<tr><td>Input Tokens</td><td>${totalInputTokens}</td></tr>
<tr><td>Output Tokens</td><td>${totalOutputTokens}</td></tr>
<tr><td>Total Tokens</td><td>${totalInputTokens + totalOutputTokens}</td></tr>
</table>
`;
$('#stats-summary').html(tableHtml);
}
// ===== Theme Methods =====
initializeTheme() {
// Check if user has manually set a theme preference
const savedTheme = localStorage.getItem('serena-theme');
if (savedTheme) {
// User has manually set a preference, use it
this.setTheme(savedTheme);
} else {
// No manual preference, detect system color scheme
this.detectSystemTheme();
}
// Listen for system theme changes
this.setupSystemThemeListener();
}
detectSystemTheme() {
// Check if system prefers dark mode
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const theme = prefersDark ? 'dark' : 'light';
this.setTheme(theme);
}
setupSystemThemeListener() {
// Listen for changes in system color scheme
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleSystemThemeChange = (e) => {
// Only auto-switch if user hasn't manually set a preference
const savedTheme = localStorage.getItem('serena-theme');
if (!savedTheme) {
const newTheme = e.matches ? 'dark' : 'light';
this.setTheme(newTheme);
}
};
// Add listener for system theme changes
if (mediaQuery.addEventListener) {
mediaQuery.addEventListener('change', handleSystemThemeChange);
} else {
// Fallback for older browsers
mediaQuery.addListener(handleSystemThemeChange);
}
}
toggleTheme() {
const currentTheme = document.documentElement.getAttribute('data-theme') || 'light';
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
// When user manually toggles, save their preference
localStorage.setItem('serena-theme', newTheme);
this.setTheme(newTheme);
}
/**
* @param theme {'light' | 'dark'}
*/
setTheme(theme) {
// Set the theme on the document element
document.documentElement.setAttribute('data-theme', theme);
// Update the theme toggle button
if (theme === 'dark') {
this.$themeIcon.text('☀️');
this.$themeText.text('Light');
} else {
this.$themeIcon.text('🌙');
this.$themeText.text('Dark');
}
// Update theme-aware images
$(".theme-aware-img").each(function() {
const $img = $(this);
updateThemeAwareImage($img, theme);
});
// Save to localStorage
localStorage.setItem('serena-theme', theme);
// Update charts if they exist
this.updateChartsTheme();
}
updateChartsTheme() {
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
const textColor = isDark ? '#ffffff' : '#000000';
const gridColor = isDark ? '#444' : '#ddd';
// Update existing charts if they exist and have the scales property
if (this.countChart && this.countChart.options.plugins) {
if (this.countChart.options.plugins.legend) {
this.countChart.options.plugins.legend.labels.color = textColor;
}
this.countChart.update();
}
if (this.inputChart && this.inputChart.options.plugins) {
if (this.inputChart.options.plugins.legend) {
this.inputChart.options.plugins.legend.labels.color = textColor;
}
this.inputChart.update();
}
if (this.outputChart && this.outputChart.options.plugins) {
if (this.outputChart.options.plugins.legend) {
this.outputChart.options.plugins.legend.labels.color = textColor;
}
this.outputChart.update();
}
if (this.tokensChart && this.tokensChart.options.scales) {
this.tokensChart.options.scales.x.ticks.color = textColor;
this.tokensChart.options.scales.y.ticks.color = textColor;
this.tokensChart.options.scales.y1.ticks.color = textColor;
this.tokensChart.options.scales.x.grid.color = gridColor;
this.tokensChart.options.scales.y.grid.color = gridColor;
this.tokensChart.options.scales.y1.grid.color = gridColor;
this.tokensChart.options.scales.y.title.color = textColor;
this.tokensChart.options.scales.y1.title.color = textColor;
if (this.tokensChart.options.plugins && this.tokensChart.options.plugins.legend) {
this.tokensChart.options.plugins.legend.labels.color = textColor;
}
this.tokensChart.update();
}
}
// ===== Language Management Methods =====
confirmRemoveLanguage(language) {
// Store the language to remove
this.languageToRemove = language;
// Set language name in modal
this.$removeLanguageName.text(language);
// Show modal
this.$removeLanguageModal.fadeIn(200);
}
closeRemoveLanguageModal() {
this.$removeLanguageModal.fadeOut(200);
this.languageToRemove = null;
}
confirmRemoveLanguageOk() {
if (this.languageToRemove) {
this.removeLanguage(this.languageToRemove);
this.closeRemoveLanguageModal();
}
}
removeLanguage(language) {
const self = this;
$.ajax({
url: '/remove_language', type: 'POST', contentType: 'application/json', data: JSON.stringify({
language: language
}), success: function (response) {
if (response.status === 'success') {
// Reload config to show updated language list
self.loadConfigOverview();
} else {
alert('Error removing language ' + language + ": " + response.message);
}
}, error: function (xhr, status, error) {
console.error('Error removing language:', error);
alert('Error removing language: ' + (xhr.responseJSON ? xhr.responseJSON.message : error));
}
});
}
openLanguageModal() {
// Set project name in modal
this.$modalProjectName.text(this.activeProjectName || 'Unknown');
// Load available languages into modal dropdown
this.loadAvailableLanguages();
// Show modal
this.$addLanguageModal.fadeIn(200);
}
closeLanguageModal() {
this.$addLanguageModal.fadeOut(200);
this.$modalLanguageSelect.empty();
this.$modalAddBtn.prop('disabled', false).text('Add Language');
}
loadAvailableLanguages() {
let self = this;
$.ajax({
url: '/get_available_languages', type: 'GET', success: function (response) {
const languages = response.languages || [];
// Clear all existing options
self.$modalLanguageSelect.empty();
if (languages.length === 0) {
// Show message if no languages available
self.$modalLanguageSelect.append($('<option>').val('').text('No languages available to add'));
self.$modalAddBtn.prop('disabled', true);
} else {
// Add language options
languages.forEach(function (language) {
self.$modalLanguageSelect.append($('<option>').val(language).text(language));
});
self.$modalAddBtn.prop('disabled', false);
}
}, error: function (xhr, status, error) {
console.error('Error loading available languages:', error);
}
});
}
addLanguageFromModal() {
const selectedLanguage = this.$modalLanguageSelect.val();
if (!selectedLanguage) {
alert('No language selected or no languages available to add');
return;
}
const self = this;
// Close modal immediately
self.closeLanguageModal();
// Hide the inline add language button and show spinner
$('#add-language-btn').hide();
$('#add-language-spinner').show();
self.isAddingLanguage = true;
$.ajax({
url: '/add_language', type: 'POST', contentType: 'application/json', data: JSON.stringify({
language: selectedLanguage
}), success: function (response) {
if (response.status === 'success') {
console.log("Language added successfully");
} else {
alert('Error adding language ' + selectedLanguage + ": " + response.message);
// Restore button visibility on error
$('#add-language-btn').show();
$('#add-language-spinner').hide();
}
}, error: function (xhr, status, error) {
console.error('Error adding language:', error);
alert('Error adding language: ' + (xhr.responseJSON ? xhr.responseJSON.message : error));
// Restore button visibility on error
$('#add-language-btn').show();
$('#add-language-spinner').hide();
}, complete: function () {
self.isAddingLanguage = false;
self.loadConfigOverview();
}
});
}
// ===== Memory Editing Methods =====
openEditMemoryModal(memoryName) {
const self = this;
this.currentMemoryName = memoryName;
this.memoryContentDirty = false;
// Set memory name in modal
this.$editMemoryName.text(memoryName);
// Load memory content
$.ajax({
url: '/get_memory', type: 'POST', contentType: 'application/json', data: JSON.stringify({
memory_name: memoryName
}), success: function (response) {
if (response.status === 'error') {
alert('Error: ' + response.message);
return;
}
self.originalMemoryContent = response.content;
self.$editMemoryContent.val(response.content);
self.memoryContentDirty = false;
self.$editMemoryModal.fadeIn(200);
}, error: function (xhr, status, error) {
console.error('Error loading memory:', error);
alert('Error loading memory: ' + (xhr.responseJSON ? xhr.responseJSON.message : error));
}
});
}
closeEditMemoryModal() {
// Check if there are unsaved changes
if (this.memoryContentDirty) {
if (!confirm('You have unsaved changes. Are you sure you want to close?')) {
return;
}
}
this.$editMemoryModal.fadeOut(200);
this.currentMemoryName = null;
this.originalMemoryContent = null;
this.memoryContentDirty = false;
}
trackMemoryChanges() {
const currentContent = this.$editMemoryContent.val();
this.memoryContentDirty = (currentContent !== this.originalMemoryContent);
}
saveMemoryFromModal() {
const self = this;
const memoryName = this.currentMemoryName;
const content = this.$editMemoryContent.val();
if (!memoryName) {
alert('No memory selected');
return;
}
// Disable button during request
self.$editMemorySaveBtn.prop('disabled', true).text('Saving...');
$.ajax({
url: '/save_memory', type: 'POST', contentType: 'application/json', data: JSON.stringify({
memory_name: memoryName, content: content
}), success: function (response) {
if (response.status === 'success') {
// Update original content and reset dirty flag
self.originalMemoryContent = content;
self.memoryContentDirty = false;
// Close modal
self.$editMemoryModal.fadeOut(200);
self.currentMemoryName = null;
} else {
alert('Error: ' + response.message);
}
}, error: function (xhr, status, error) {
console.error('Error saving memory:', error);
alert('Error saving memory: ' + (xhr.responseJSON ? xhr.responseJSON.message : error));
}, complete: function () {
// Re-enable button
self.$editMemorySaveBtn.prop('disabled', false).text('Save');
}
});
}
confirmDeleteMemory(memoryName) {
// Set memory name to delete
this.memoryToDelete = memoryName;
// Set memory name in modal
this.$deleteMemoryName.text(memoryName);
// Show modal
this.$deleteMemoryModal.fadeIn(200);
}
closeDeleteMemoryModal() {
this.$deleteMemoryModal.fadeOut(200);
this.memoryToDelete = null;
}
confirmDeleteMemoryOk() {
if (this.memoryToDelete) {
this.deleteMemory(this.memoryToDelete);
this.closeDeleteMemoryModal();
}
}
deleteMemory(memoryName) {
const self = this;
$.ajax({
url: '/delete_memory', type: 'POST', contentType: 'application/json', data: JSON.stringify({
memory_name: memoryName
}), success: function (response) {
if (response.status === 'success') {
// Reload config to show updated memory list
self.loadConfigOverview();
} else {
alert('Error: ' + response.message);
}
}, error: function (xhr, status, error) {
console.error('Error deleting memory:', error);
alert('Error deleting memory: ' + (xhr.responseJSON ? xhr.responseJSON.message : error));
}
});
}
openCreateMemoryModal() {
// Set project name in modal
this.$createMemoryProjectName.text(this.activeProjectName || 'Unknown');
// Clear the input field
this.$createMemoryNameInput.val('');
// Show modal
this.$createMemoryModal.fadeIn(200);
// Focus on the input field
setTimeout(() => {
this.$createMemoryNameInput.focus();
}, 250);
}
closeCreateMemoryModal() {
this.$createMemoryModal.fadeOut(200);
this.$createMemoryNameInput.val('');
this.$createMemoryCreateBtn.prop('disabled', false).text('Create');
}
createMemoryFromModal() {
const memoryName = this.$createMemoryNameInput.val().trim();
if (!memoryName) {
alert('Please enter a memory name');
return;
}
// Validate memory name (alphanumeric and underscores only)
if (!/^[a-zA-Z0-9_]+$/.test(memoryName)) {
alert('Memory name can only contain letters, numbers, and underscores');
return;
}
const self = this;
// Disable button during request
self.$createMemoryCreateBtn.prop('disabled', true).text('Creating...');
$.ajax({
url: '/save_memory', type: 'POST', contentType: 'application/json', data: JSON.stringify({
memory_name: memoryName, content: ''
}), success: function (response) {
if (response.status === 'success') {
// Close the create modal
self.closeCreateMemoryModal();
// Reload config to show the new memory
self.loadConfigOverview();
// Open the edit modal for the newly created memory
setTimeout(() => {
self.openEditMemoryModal(memoryName);
}, 500);
} else {
alert('Error: ' + response.message);
self.$createMemoryCreateBtn.prop('disabled', false).text('Create');
}
}, error: function (xhr, status, error) {
console.error('Error creating memory:', error);
alert('Error creating memory: ' + (xhr.responseJSON ? xhr.responseJSON.message : error));
self.$createMemoryCreateBtn.prop('disabled', false).text('Create');
}
});
}
// ===== Serena Config Editing Methods =====
openEditSerenaConfigModal() {
const self = this;
this.serenaConfigContentDirty = false;
// Load serena config content
$.ajax({
url: '/get_serena_config', type: 'GET', success: function (response) {
if (response.status === 'error') {
alert('Error: ' + response.message);
return;
}
self.originalSerenaConfigContent = response.content;
self.$editSerenaConfigContent.val(response.content);
self.serenaConfigContentDirty = false;
self.$editSerenaConfigModal.fadeIn(200);
}, error: function (xhr, status, error) {
console.error('Error loading serena config:', error);
alert('Error loading serena config: ' + (xhr.responseJSON ? xhr.responseJSON.message : error));
}
});
// Track changes to config content
this.$editSerenaConfigContent.off('input').on('input', function () {
const currentContent = self.$editSerenaConfigContent.val();
self.serenaConfigContentDirty = (currentContent !== self.originalSerenaConfigContent);
});
}
closeEditSerenaConfigModal() {
// Check if there are unsaved changes
if (this.serenaConfigContentDirty) {
if (!confirm('You have unsaved changes. Are you sure you want to close?')) {
return;
}
}
this.$editSerenaConfigModal.fadeOut(200);
this.originalSerenaConfigContent = null;
this.serenaConfigContentDirty = false;
}
saveSerenaConfigFromModal() {
const self = this;
const content = this.$editSerenaConfigContent.val();
// Disable button during request
self.$editSerenaConfigSaveBtn.prop('disabled', true).text('Saving...');
$.ajax({
url: '/save_serena_config', type: 'POST', contentType: 'application/json', data: JSON.stringify({
content: content
}), success: function (response) {
if (response.status === 'success') {
// Update original content and reset dirty flag
self.originalSerenaConfigContent = content;
self.serenaConfigContentDirty = false;
// Close modal
self.$editSerenaConfigModal.fadeOut(200);
alert('Configuration saved successfully. Please restart Serena for changes to take effect.');
} else {
alert('Error: ' + response.message);
}
}, error: function (xhr, status, error) {
console.error('Error saving serena config:', error);
alert('Error saving serena config: ' + (xhr.responseJSON ? xhr.responseJSON.message : error));
}, complete: function () {
// Re-enable button
self.$editSerenaConfigSaveBtn.prop('disabled', false).text('Save');
}
});
}
// ===== Shutdown Method =====
shutdown() {
const self = this;
const _shutdown = function () {
console.log("Triggering shutdown");
$.ajax({
url: '/shutdown', type: "PUT", contentType: 'application/json',
});
self.$errorContainer.html('<div class="error-message">Shutting down ...</div>')
setTimeout(function () {
window.close();
}, 1000);
}
// ask for confirmation using a dialog
if (confirm("This will fully terminate the Serena server.")) {
_shutdown();
} else {
console.log("Shutdown cancelled");
}
// Close menu
self.$menuDropdown.hide();
}
}
```