Enhance dashboard functionality

Added an overview (landing page showing current and available config) and advanced stats windows, controlled by top-right menu

Stats calculation is no longer optional, but we use a trivial token estimator by default
This commit is contained in:
MischaPanch committed 2025-10-27 22:08:53 +01:00
1 parent 45407f7a2a
commit ad4fec9230
10 files changed
+1234 -385

No files matched your search

+3 -2
View File
@@ -12,7 +12,7 @@ from serena.constants import REPO_ROOT
from serena.tools import FindFileTool, FindReferencingSymbolsTool, GetSymbolsOverviewTool, SearchForPatternTool
if __name__ == "__main__":
agent = SerenaAgent(project=REPO_ROOT, serena_config=SerenaConfig(gui_log_window_enabled=False, web_dashboard=False))
agent = SerenaAgent(project=REPO_ROOT, serena_config=SerenaConfig(gui_log_window_enabled=False, web_dashboard=True))
# apply a tool
find_refs_tool = agent.get_tool(FindReferencingSymbolsTool)
@@ -21,6 +21,7 @@ if __name__ == "__main__":
overview_tool = agent.get_tool(GetSymbolsOverviewTool)
result = agent.execute_task(
lambda: overview_tool.apply("src/solidlsp/ls.py"),
lambda: overview_tool.apply_ex(relative_file_path="src/solidlsp/ls.py"),
)
pprint(json.loads(result))
# input("Press Enter to continue...")
+24 -27
View File
@@ -127,25 +127,9 @@ class SerenaAgent:
if self._gui_log_viewer is not None:
self._gui_log_viewer.set_tool_names(tool_names)
self._tool_usage_stats: ToolUsageStats | None = None
if self.serena_config.record_tool_usage_stats:
token_count_estimator = RegisteredTokenCountEstimator[self.serena_config.token_count_estimator]
log.info(f"Tool usage statistics recording is enabled with token count estimator: {token_count_estimator.name}.")
self._tool_usage_stats = ToolUsageStats(token_count_estimator)
# start the dashboard (web frontend), registering its log handler
if self.serena_config.web_dashboard:
self._dashboard_thread, port = SerenaDashboardAPI(
get_memory_log_handler(), tool_names, agent=self, tool_usage_stats=self._tool_usage_stats
).run_in_thread()
dashboard_url = f"http://127.0.0.1:{port}/dashboard/index.html"
log.info("Serena web dashboard started at %s", dashboard_url)
if self.serena_config.web_dashboard_open_on_launch:
# open the dashboard URL in the default web browser (using a separate process to control
# output redirection)
process = multiprocessing.Process(target=self._open_dashboard, args=(dashboard_url,))
process.start()
process.join(timeout=1)
token_count_estimator = RegisteredTokenCountEstimator[self.serena_config.token_count_estimator]
log.info(f"Will record tool usage statistics with token count estimator: {token_count_estimator.name}.")
self._tool_usage_stats = ToolUsageStats(token_count_estimator)
# log fundamental information
log.info(f"Starting Serena server (version={serena_version()}, process id={os.getpid()}, parent process id={os.getppid()})")
@@ -192,6 +176,22 @@ class SerenaAgent:
except Exception as e:
log.error(f"Error activating project '{project}' at startup: {e}", exc_info=e)
# start the dashboard (web frontend), registering its log handler
# should be the last thing to happen in the initialization since the dashboard
# may access various parts of the agent
if self.serena_config.web_dashboard:
self._dashboard_thread, port = SerenaDashboardAPI(
get_memory_log_handler(), tool_names, agent=self, tool_usage_stats=self._tool_usage_stats
).run_in_thread()
dashboard_url = f"http://127.0.0.1:{port}/dashboard/index.html"
log.info("Serena web dashboard started at %s", dashboard_url)
if self.serena_config.web_dashboard_open_on_launch:
# open the dashboard URL in the default web browser (using a separate process to control
# output redirection)
process = multiprocessing.Process(target=self._open_dashboard, args=(dashboard_url,))
process.start()
process.join(timeout=1)
def get_context(self) -> SerenaAgentContext:
return self._context
@@ -234,18 +234,15 @@ class SerenaAgent:
tool_inclusion_definitions.append(project.project_config)
return tool_inclusion_definitions
def record_tool_usage_if_enabled(self, input_kwargs: dict, tool_result: str | dict, tool: Tool) -> None:
def record_tool_usage(self, input_kwargs: dict, tool_result: str | dict, tool: Tool) -> None:
"""
Record the usage of a tool with the given input and output strings if tool usage statistics recording is enabled.
"""
tool_name = tool.get_name()
if self._tool_usage_stats is not None:
input_str = str(input_kwargs)
output_str = str(tool_result)
log.debug(f"Recording tool usage for tool '{tool_name}'")
self._tool_usage_stats.record_tool_usage(tool_name, input_str, output_str)
else:
log.debug(f"Tool usage statistics recording is disabled, not recording usage of '{tool_name}'.")
input_str = str(input_kwargs)
output_str = str(tool_result)
log.debug(f"Recording tool usage for tool '{tool_name}'")
self._tool_usage_stats.record_tool_usage(tool_name, input_str, output_str)
@staticmethod
def _open_dashboard(url: str) -> None:
+17 -1
View File
@@ -69,12 +69,26 @@ class AnthropicTokenCount(TokenCountEstimator):
return self._send_count_tokens_request(text).input_tokens
class CharCountEstimator(TokenCountEstimator):
"""
A naive character count estimator that estimates tokens based on character count.
"""
def __init__(self, avg_chars_per_token: int = 4):
self._avg_chars_per_token = avg_chars_per_token
def estimate_token_count(self, text: str) -> int:
# Assuming an average of 4 characters per token
return len(text) // self._avg_chars_per_token
_registered_token_estimator_instances_cache: dict[RegisteredTokenCountEstimator, TokenCountEstimator] = {}
class RegisteredTokenCountEstimator(Enum):
TIKTOKEN_GPT4O = "TIKTOKEN_GPT4O"
ANTHROPIC_CLAUDE_SONNET_4 = "ANTHROPIC_CLAUDE_SONNET_4"
CHAR_COUNT = "CHAR_COUNT"
@classmethod
def get_valid_names(cls) -> list[str]:
@@ -89,8 +103,10 @@ class RegisteredTokenCountEstimator(Enum):
return TiktokenCountEstimator(model_name="gpt-4o")
case RegisteredTokenCountEstimator.ANTHROPIC_CLAUDE_SONNET_4:
return AnthropicTokenCount(model_name="claude-sonnet-4-20250514")
case RegisteredTokenCountEstimator.CHAR_COUNT:
return CharCountEstimator(avg_chars_per_token=4)
case _:
raise ValueError(f"Unknown token count estimator: {self.value}")
raise ValueError(f"Unknown token count estimator: {self}")
def load_estimator(self) -> TokenCountEstimator:
estimator_instance = _registered_token_estimator_instances_cache.get(self)
+2 -5
View File
@@ -340,10 +340,8 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin):
"""
whether to apply JetBrains mode
"""
record_tool_usage_stats: bool = False
"""Whether to record tool usage statistics, they will be shown in the web dashboard if recording is active.
"""
token_count_estimator: str = RegisteredTokenCountEstimator.TIKTOKEN_GPT4O.name
token_count_estimator: str = RegisteredTokenCountEstimator.CHAR_COUNT.name
"""Only relevant if `record_tool_usage` is True; the name of the token count estimator to use for tool usage statistics.
See the `RegisteredTokenCountEstimator` enum for available options.
@@ -457,7 +455,6 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin):
instance.excluded_tools = loaded_commented_yaml.get("excluded_tools", [])
instance.included_optional_tools = loaded_commented_yaml.get("included_optional_tools", [])
instance.jetbrains = loaded_commented_yaml.get("jetbrains", False)
instance.record_tool_usage_stats = loaded_commented_yaml.get("record_tool_usage_stats", False)
instance.token_count_estimator = loaded_commented_yaml.get(
"token_count_estimator", RegisteredTokenCountEstimator.TIKTOKEN_GPT4O.name
)
+106
View File
@@ -39,6 +39,18 @@ class ResponseToolStats(BaseModel):
stats: dict[str, dict[str, int]]
class ResponseConfigOverview(BaseModel):
active_project: dict[str, str | None]
context: dict[str, str]
modes: list[dict[str, str]]
active_tools: list[str]
tool_stats_summary: dict[str, dict[str, int]]
registered_projects: list[dict[str, str | bool]]
available_tools: list[dict[str, str | bool]]
available_modes: list[dict[str, str | bool]]
available_contexts: list[dict[str, str | bool]]
class SerenaDashboardAPI:
log = logging.getLogger(__qualname__)
@@ -104,6 +116,11 @@ class SerenaDashboardAPI:
estimator_name = self._tool_usage_stats.token_estimator_name if self._tool_usage_stats else "unknown"
return {"token_count_estimator_name": estimator_name}
@self._app.route("/get_config_overview", methods=["GET"])
def get_config_overview() -> dict[str, Any]:
result = self._get_config_overview()
return result.model_dump()
@self._app.route("/shutdown", methods=["PUT"])
def shutdown() -> dict[str, str]:
self._shutdown()
@@ -129,6 +146,95 @@ class SerenaDashboardAPI:
if self._tool_usage_stats is not None:
self._tool_usage_stats.clear()
def _get_config_overview(self) -> ResponseConfigOverview:
from serena.config.context_mode import SerenaAgentContext, SerenaAgentMode
# Get active project info
project = self._agent.get_active_project()
active_project_name = project.project_name if project else None
project_info = {
"name": active_project_name,
"language": project.project_config.language.value if project else None,
}
# Get context info
context = self._agent.get_context()
context_info = {
"name": context.name,
"description": context.description,
}
# Get active modes
modes = self._agent.get_active_modes()
modes_info = [{"name": mode.name, "description": mode.description} for mode in modes]
active_mode_names = [mode.name for mode in modes]
# Get active tools
active_tools = self._agent.get_active_tool_names()
# Get registered projects
registered_projects: list[dict[str, str | bool]] = []
for proj in self._agent.serena_config.projects:
registered_projects.append(
{
"name": proj.project_name,
"path": str(proj.project_root),
"is_active": proj.project_name == active_project_name,
}
)
# Get all available tools (excluding active ones)
all_tool_names = sorted([tool.get_name_from_cls() for tool in self._agent._all_tools.values()])
available_tools: list[dict[str, str | bool]] = []
for tool_name in all_tool_names:
if tool_name not in active_tools:
available_tools.append(
{
"name": tool_name,
"is_active": False,
}
)
# Get all available modes
all_mode_names = SerenaAgentMode.list_registered_mode_names()
available_modes: list[dict[str, str | bool]] = []
for mode_name in all_mode_names:
available_modes.append(
{
"name": mode_name,
"is_active": mode_name in active_mode_names,
}
)
# Get all available contexts
all_context_names = SerenaAgentContext.list_registered_context_names()
available_contexts: list[dict[str, str | bool]] = []
for context_name in all_context_names:
available_contexts.append(
{
"name": context_name,
"is_active": context_name == context.name,
}
)
# Get basic tool stats (just num_calls for overview)
tool_stats_summary = {}
if self._tool_usage_stats is not None:
full_stats = self._tool_usage_stats.get_tool_stats_dict()
tool_stats_summary = {name: {"num_calls": stats["num_times_called"]} for name, stats in full_stats.items()}
return ResponseConfigOverview(
active_project=project_info,
context=context_info,
modes=modes_info,
active_tools=active_tools,
tool_stats_summary=tool_stats_summary,
registered_projects=registered_projects,
available_tools=available_tools,
available_modes=available_modes,
available_contexts=available_contexts,
)
def _shutdown(self) -> None:
log.info("Shutting down Serena")
if self._shutdown_callback:
@@ -0,0 +1,620 @@
:root {
/* Light theme variables */
--bg-primary: #f5f5f5;
--bg-secondary: #ffffff;
--text-primary: #000000;
--text-secondary: #333333;
--text-muted: #666666;
--border-color: #ddd;
--btn-primary: #eaa45d;
--btn-hover: #dca662;
--btn-disabled: #6c757d;
--shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
--tool-highlight: #ffff00;
--tool-highlight-text: #000000;
--log-debug: #808080;
--log-info: #000000;
--log-warning: #FF8C00;
--log-error: #FF0000;
--stats-header: #f8f9fa;
}
[data-theme="dark"] {
/* Dark theme variables */
--bg-primary: #1a1a1a;
--bg-secondary: #2d2d2d;
--text-primary: #ffffff;
--text-secondary: #e0e0e0;
--text-muted: #b0b0b0;
--border-color: #444;
--btn-primary: #eaa45d;
--btn-hover: #dca662;
--btn-disabled: #6c757d;
--shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
--tool-highlight: #ffd700;
--tool-highlight-text: #000000;
--log-debug: #808080;
--log-info: #ffffff;
--log-warning: #FF8C00;
--log-error: #FF0000;
--stats-header: #3a3a3a;
}
body {
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
margin: 0;
padding: 20px;
padding-top: 130px;
background-color: var(--bg-primary);
color: var(--text-primary);
transition: background-color 0.3s ease, color 0.3s ease;
}
.header {
position: fixed;
top: 0;
left: 0;
right: 0;
background-color: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
padding: 20px;
display: flex;
justify-content: space-between;
align-items: center;
z-index: 1000;
transition: background-color 0.3s ease, border-color 0.3s ease;
min-height: 70px;
box-shadow: var(--shadow);
}
.logo-container {
flex: 1;
}
.logo-container img {
max-width: 300px;
height: auto;
display: block;
}
.header-nav {
position: relative;
display: flex;
align-items: center;
gap: 10px;
}
.menu-button {
background-color: var(--bg-secondary);
color: var(--text-primary);
border: 1px solid var(--border-color);
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.3s ease, border-color 0.3s ease;
display: flex;
align-items: center;
gap: 8px;
}
.menu-button:hover {
background-color: var(--border-color);
}
.menu-dropdown {
position: absolute;
top: 45px;
right: 0;
background-color: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 4px;
box-shadow: var(--shadow);
min-width: 200px;
z-index: 999;
transition: background-color 0.3s ease, border-color 0.3s ease;
}
.menu-dropdown a {
display: block;
padding: 12px 20px;
color: var(--text-primary);
text-decoration: none;
transition: background-color 0.3s ease;
}
.menu-dropdown a:hover {
background-color: var(--border-color);
}
.menu-dropdown a.active {
background-color: var(--btn-primary);
color: white;
}
.menu-dropdown hr {
border: none;
border-top: 1px solid var(--border-color);
margin: 5px 0;
}
.page-view {
max-width: 1600px;
margin: 0 auto;
}
/* Overview Page Layout */
.overview-container {
display: grid;
grid-template-columns: 1fr 400px;
gap: 20px;
}
.overview-left {
min-width: 0;
}
.overview-right {
min-width: 0;
}
/* Overview Page Styles */
.config-section,
.basic-stats-section,
.projects-section {
background-color: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 5px;
padding: 20px;
margin-bottom: 20px;
transition: background-color 0.3s ease, border-color 0.3s ease;
}
.config-section h2,
.basic-stats-section h2 {
margin-top: 0;
color: var(--text-secondary);
}
/* Collapsible Headers */
.collapsible-header {
margin-top: 0;
margin-bottom: 0;
font-size: 18px;
color: var(--text-secondary);
cursor: pointer;
user-select: none;
display: flex;
justify-content: space-between;
align-items: center;
}
.collapsible-header:hover {
color: var(--text-primary);
}
.toggle-icon {
transition: transform 0.3s ease;
font-size: 14px;
}
.toggle-icon.expanded {
transform: rotate(-180deg);
}
.collapsible-content {
margin-top: 15px;
max-height: 400px;
overflow-y: auto;
}
.config-grid {
display: grid;
grid-template-columns: 180px 1fr;
gap: 12px;
margin-bottom: 20px;
}
.config-label {
font-weight: bold;
color: var(--text-secondary);
}
.config-value {
color: var(--text-primary);
}
.config-list {
list-style: none;
padding: 0;
margin: 0;
}
.config-list li {
padding: 4px 0;
}
.tools-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 8px;
margin-top: 10px;
}
.tool-item {
background-color: var(--bg-primary);
padding: 6px 10px;
border-radius: 3px;
font-size: 13px;
color: var(--text-primary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: default;
}
/* Projects List */
.project-item {
padding: 10px 12px;
margin: 5px 0;
border-radius: 4px;
background-color: var(--bg-primary);
border: 1px solid var(--border-color);
transition: background-color 0.2s ease;
}
.project-item:hover {
background-color: var(--border-color);
}
.project-item.active {
background-color: var(--btn-primary);
color: white;
border-color: var(--btn-primary);
}
.project-name {
font-weight: bold;
margin-bottom: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.project-path {
font-size: 11px;
color: var(--text-muted);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.project-item.active .project-path {
color: rgba(255, 255, 255, 0.8);
}
/* Generic Item Styles for Tools/Modes/Contexts */
.info-item {
padding: 8px 12px;
margin: 5px 0;
border-radius: 4px;
background-color: var(--bg-primary);
border: 1px solid var(--border-color);
transition: background-color 0.2s ease;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: default;
}
.info-item:hover {
background-color: var(--border-color);
}
.info-item.active {
background-color: var(--btn-primary);
color: white;
border-color: var(--btn-primary);
font-weight: bold;
}
/* Basic Stats Styles */
.basic-stats-section {
background-color: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 5px;
padding: 20px;
margin-bottom: 20px;
transition: background-color 0.3s ease, border-color 0.3s ease;
}
.basic-stats-section h2 {
margin-top: 0;
color: var(--text-secondary);
}
.stat-bar-container {
display: flex;
align-items: center;
margin: 8px 0;
gap: 12px;
}
.stat-tool-name {
min-width: 200px;
max-width: 200px;
font-weight: bold;
color: var(--text-secondary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: default;
}
.bar-wrapper {
flex: 1;
height: 24px;
background-color: var(--border-color);
border-radius: 3px;
overflow: hidden;
position: relative;
}
.bar {
height: 100%;
background-color: var(--btn-primary);
transition: width 0.5s ease;
border-radius: 3px;
}
.stat-count {
min-width: 60px;
text-align: right;
font-weight: bold;
color: var(--text-primary);
}
.no-stats-message {
text-align: center;
color: var(--text-muted);
font-style: italic;
padding: 20px;
}
/* Log Container Styles */
.log-container {
background-color: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 5px;
height: calc(100vh - 260px);
max-height: 700px;
overflow-y: auto;
overflow-x: auto;
padding: 10px;
white-space: pre-wrap;
font-size: 12px;
line-height: 1.4;
color: var(--text-primary);
transition: background-color 0.3s ease, border-color 0.3s ease, color 0.3s ease;
}
.controls {
position: sticky;
top: 90px;
z-index: 100;
background-color: var(--bg-primary);
padding: 10px 0;
margin-bottom: 10px;
text-align: center;
display: flex;
justify-content: center;
align-items: center;
gap: 10px;
flex-wrap: wrap;
transition: background-color 0.3s ease;
}
.btn {
background-color: var(--btn-primary);
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.3s ease;
}
.btn:hover {
background-color: var(--btn-hover);
}
.btn:disabled {
background-color: var(--btn-disabled);
cursor: not-allowed;
}
.theme-toggle {
display: flex;
align-items: center;
gap: 5px;
background-color: var(--bg-secondary);
color: var(--text-primary);
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 8px 16px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.3s ease, border-color 0.3s ease;
}
.theme-toggle:hover {
background-color: var(--border-color);
}
.theme-toggle span {
line-height: 1;
}
.log-debug {
color: var(--log-debug);
}
.log-info {
color: var(--log-info);
}
.log-warning {
color: var(--log-warning);
}
.log-error {
color: var(--log-error);
}
.log-default {
color: var(--log-info);
}
/* Tool name highlighting */
.tool-name {
background-color: var(--tool-highlight);
color: var(--tool-highlight-text);
font-weight: bold;
}
.loading {
text-align: center;
color: var(--text-muted);
font-style: italic;
}
.error-message {
color: var(--log-error);
text-align: center;
margin: 10px 0;
}
/* Advanced Stats Styles */
.charts-container {
display: flex;
flex-wrap: wrap;
gap: 15px;
justify-content: space-between;
max-width: 1400px;
margin: 0 auto;
}
.chart-group {
flex: 1;
min-width: 280px;
max-width: 320px;
text-align: center;
}
.chart-wide {
flex: 0 0 100%;
min-width: 100%;
margin-top: 10px;
}
.chart-group h3 {
margin: 0 0 10px 0;
color: var(--text-secondary);
}
.stats-summary {
margin: 0 auto;
border-collapse: collapse;
background: var(--bg-secondary);
border-radius: 5px;
overflow: hidden;
box-shadow: var(--shadow);
transition: background-color 0.3s ease, box-shadow 0.3s ease;
}
.stats-summary th,
.stats-summary td {
padding: 10px 20px;
text-align: left;
border-bottom: 1px solid var(--border-color);
color: var(--text-primary);
transition: border-color 0.3s ease, color 0.3s ease;
}
.stats-summary th {
background-color: var(--stats-header);
font-weight: bold;
transition: background-color 0.3s ease;
}
.stats-summary tr:last-child td {
border-bottom: none;
}
@media (max-width: 1024px) {
.overview-container {
grid-template-columns: 1fr;
}
.overview-right {
order: -1;
}
}
@media (max-width: 768px) {
body {
padding-top: 140px;
}
.header {
flex-direction: column;
gap: 10px;
padding: 10px 15px;
}
.logo-container {
width: 100%;
text-align: center;
}
.logo-container img {
max-width: 200px;
}
.header-nav {
width: 100%;
justify-content: center;
}
.charts-container {
flex-direction: column;
}
.chart-group,
.chart-wide {
min-width: auto;
max-width: none;
}
.controls {
flex-direction: column;
gap: 5px;
}
.config-grid {
grid-template-columns: 1fr;
}
.tools-grid {
grid-template-columns: 1fr;
}
.stat-bar-container {
flex-wrap: wrap;
}
.stat-tool-name {
min-width: 100%;
}
}
+359 -85
View File
@@ -30,7 +30,7 @@ class LogMessage {
}
escapeHtml (convertString) {
if (typeof convertString !== 'string') return convertString;
if (typeof convertString !== 'string') return convertString;
const patterns = {
'<' : '&lt;',
@@ -49,47 +49,322 @@ class Dashboard {
constructor() {
let self = this;
// Page state
this.currentPage = 'overview';
this.configData = null;
// Tool names and stats
this.toolNames = [];
this.currentMaxIdx = -1;
this.pollInterval = null;
this.configPollInterval = null;
this.failureCount = 0;
// jQuery elements
this.$logContainer = $('#log-container');
this.$errorContainer = $('#error-container');
this.$loadButton = $('#load-logs');
this.$shutdownButton = $('#shutdown');
this.$toggleStats = $('#toggle-stats');
this.$statsSection = $('#stats-section');
this.$refreshStats = $('#refresh-stats');
this.$clearStats = $('#clear-stats');
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');
// Chart references
this.countChart = null;
this.tokensChart = null;
this.inputChart = null;
this.outputChart = null;
// register event handlers
// Register event handlers
this.$loadButton.click(this.loadLogs.bind(this));
this.$shutdownButton.click(this.shutdown.bind(this));
this.$toggleStats.click(this.toggleStats.bind(this));
this.$menuShutdown.click(this.shutdown.bind(this));
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.$themeToggle.click(this.toggleTheme.bind(this));
// initialize theme
// 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();
}
});
// 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 the application
// Initialize the application
this.loadToolNames().then(function() {
// Load logs on page load after tool names are loaded
self.loadLogs();
// Start on overview page
self.loadConfigOverview();
self.startConfigPolling();
});
}
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();
} 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;
}
}
// ===== Config Overview Methods =====
loadConfigOverview() {
let self = this;
$.ajax({
url: '/get_config_overview',
type: 'GET',
success: function(response) {
self.configData = response;
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);
},
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>');
}
});
}
startConfigPolling() {
// Poll every 2 seconds for config updates
this.configPollInterval = setInterval(this.loadConfigOverview.bind(this), 2000);
}
displayConfig(config) {
// Check if tools section is currently expanded
const $existingContent = $('#tools-content');
const wasExpanded = $existingContent.is(':visible');
let html = '<div class="config-grid">';
// Project info
html += '<div class="config-label">Active Project:</div>';
html += '<div class="config-value">' + (config.active_project.name || 'None') + '</div>';
html += '<div class="config-label">Language:</div>';
html += '<div class="config-value">' + (config.active_project.language || 'N/A') + '</div>';
// Context info
html += '<div class="config-label">Context:</div>';
html += '<div class="config-value">' + config.context.name + '</div>';
// Modes info
html += '<div class="config-label">Active Modes:</div>';
html += '<div class="config-value">';
if (config.modes.length > 0) {
html += '<ul class="config-list">';
config.modes.forEach(function(mode) {
html += '<li>' + mode.name + '</li>';
});
html += '</ul>';
} else {
html += 'None';
}
html += '</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' + (wasExpanded ? ' expanded' : '') + '">▼</span>';
html += '</h3>';
html += '<div class="collapsible-content tools-grid" id="tools-content" style="' + (wasExpanded ? '' : 'display:none;') + ' margin-top: 10px;">';
config.active_tools.forEach(function(tool) {
html += '<div class="tool-item" title="' + tool + '">' + tool + '</div>';
});
html += '</div>';
html += '</div>';
this.$configDisplay.html(html);
// 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');
});
}
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.name + '">' + 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.name + '">' + context.name + '</div>';
});
this.$availableContextsDisplay.html(html);
}
// ===== Logs Methods =====
displayLogMessage(message) {
$('#log-container').append(new LogMessage(message, this.toolNames).$elem);
this.$logContainer.append(new LogMessage(message, this.toolNames).$elem);
}
loadToolNames() {
@@ -227,16 +502,7 @@ class Dashboard {
this.pollInterval = setInterval(this.pollForNewLogs.bind(this), 1000);
}
toggleStats() {
if (this.$statsSection.is(':visible')) {
this.$statsSection.hide();
this.$toggleStats.text('Show Stats');
} else {
this.$statsSection.show();
this.$toggleStats.text('Hide Stats');
this.loadStats();
}
}
// ===== Stats Methods =====
loadStats() {
let self = this;
@@ -252,7 +518,6 @@ class Dashboard {
});
}
clearStats() {
let self = this;
$.ajax({
@@ -292,12 +557,12 @@ class Dashboard {
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);
@@ -325,16 +590,16 @@ class Dashboard {
// Tool calls pie chart
this.countChart = new Chart(countCtx, {
type: 'pie',
data: {
labels: names,
datasets: [{
data: {
labels: names,
datasets: [{
data: counts,
backgroundColor: colors
}]
}]
},
options: {
plugins: {
legend: {
legend: {
display: true,
labels: {
color: textColor
@@ -353,16 +618,16 @@ class Dashboard {
// Input tokens pie chart
this.inputChart = new Chart(inputCtx, {
type: 'pie',
data: {
labels: names,
datasets: [{
data: {
labels: names,
datasets: [{
data: inputTokens,
backgroundColor: colors
}]
}]
},
options: {
plugins: {
legend: {
legend: {
display: true,
labels: {
color: textColor
@@ -381,16 +646,16 @@ class Dashboard {
// Output tokens pie chart
this.outputChart = new Chart(outputCtx, {
type: 'pie',
data: {
labels: names,
datasets: [{
data: {
labels: names,
datasets: [{
data: outputTokens,
backgroundColor: colors
}]
}]
},
options: {
plugins: {
legend: {
legend: {
display: true,
labels: {
color: textColor
@@ -409,11 +674,11 @@ class Dashboard {
// Combined input/output tokens bar chart
this.tokensChart = new Chart(tokensCtx, {
type: 'bar',
data: {
labels: names,
data: {
labels: names,
datasets: [
{
label: 'Input Tokens',
{
label: 'Input Tokens',
data: inputTokens,
backgroundColor: colors.map(color => color + '80'), // Semi-transparent
borderColor: colors,
@@ -421,8 +686,8 @@ class Dashboard {
borderSkipped: false,
yAxisID: 'y'
},
{
label: 'Output Tokens',
{
label: 'Output Tokens',
data: outputTokens,
backgroundColor: colors,
yAxisID: 'y1'
@@ -452,8 +717,8 @@ class Dashboard {
display: true,
position: 'left',
beginAtZero: true,
title: {
display: true,
title: {
display: true,
text: 'Input Tokens',
color: textColor
},
@@ -469,15 +734,15 @@ class Dashboard {
display: true,
position: 'right',
beginAtZero: true,
title: {
display: true,
title: {
display: true,
text: 'Output Tokens',
color: textColor
},
ticks: {
color: textColor
},
grid: {
grid: {
drawOnChartArea: false,
color: gridColor
}
@@ -508,10 +773,12 @@ class Dashboard {
$('#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);
@@ -519,7 +786,7 @@ class Dashboard {
// No manual preference, detect system color scheme
this.detectSystemTheme();
}
// Listen for system theme changes
this.setupSystemThemeListener();
}
@@ -534,7 +801,7 @@ class Dashboard {
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');
@@ -543,7 +810,7 @@ class Dashboard {
this.setTheme(newTheme);
}
};
// Add listener for system theme changes
if (mediaQuery.addEventListener) {
mediaQuery.addEventListener('change', handleSystemThemeChange);
@@ -556,7 +823,7 @@ class Dashboard {
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);
@@ -565,8 +832,8 @@ class Dashboard {
setTheme(theme) {
// Set the theme on the document element
document.documentElement.setAttribute('data-theme', theme);
// Update the toggle button
// Update the theme toggle button
if (theme === 'dark') {
this.$themeIcon.text('☀️');
this.$themeText.text('Light');
@@ -574,13 +841,13 @@ class Dashboard {
this.$themeIcon.text('🌙');
this.$themeText.text('Dark');
}
// Update the logo based on theme
this.updateLogo(theme);
// Save to localStorage
localStorage.setItem('serena-theme', theme);
// Update charts if they exist
this.updateChartsTheme();
}
@@ -600,43 +867,47 @@ class Dashboard {
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
const textColor = isDark ? '#ffffff' : '#000000';
const gridColor = isDark ? '#444' : '#ddd';
// Update existing charts
if (this.countChart) {
this.countChart.options.scales.x.ticks.color = textColor;
this.countChart.options.scales.y.ticks.color = textColor;
this.countChart.options.scales.x.grid.color = gridColor;
this.countChart.options.scales.y.grid.color = gridColor;
// 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.scales.x.ticks.color = textColor;
this.inputChart.options.scales.y.ticks.color = textColor;
this.inputChart.options.scales.x.grid.color = gridColor;
this.inputChart.options.scales.y.grid.color = gridColor;
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.scales.x.ticks.color = textColor;
this.outputChart.options.scales.y.ticks.color = textColor;
this.outputChart.options.scales.x.grid.color = gridColor;
this.outputChart.options.scales.y.grid.color = gridColor;
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) {
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();
}
}
// ===== Shutdown Method =====
shutdown() {
const self = this;
const _shutdown = function () {
@@ -658,5 +929,8 @@ class Dashboard {
} else {
console.log("Shutdown cancelled");
}
// Close menu
self.$menuDropdown.hide();
}
}
+96 -256
View File
@@ -8,271 +8,112 @@
<link rel="icon" type="image/png" sizes="16x16" href="serena-icon-16.png">
<link rel="icon" type="image/png" sizes="32x32" href="serena-icon-32.png">
<link rel="icon" type="image/png" sizes="48x48" href="serena-icon-48.png">
<link rel="stylesheet" href="dashboard.css">
<script src="jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2"></script>
<script src="dashboard.js"></script>
<style>
:root {
/* Light theme variables */
--bg-primary: #f5f5f5;
--bg-secondary: #ffffff;
--text-primary: #000000;
--text-secondary: #333333;
--text-muted: #666666;
--border-color: #ddd;
--btn-primary: #eaa45d;
--btn-hover: #dca662;
--btn-disabled: #6c757d;
--shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
--tool-highlight: #ffff00;
--tool-highlight-text: #000000;
--log-debug: #808080;
--log-info: #000000;
--log-warning: #FF8C00;
--log-error: #FF0000;
--stats-header: #f8f9fa;
}
[data-theme="dark"] {
/* Dark theme variables */
--bg-primary: #1a1a1a;
--bg-secondary: #2d2d2d;
--text-primary: #ffffff;
--text-secondary: #e0e0e0;
--text-muted: #b0b0b0;
--border-color: #444;
--btn-primary: #eaa45d;
--btn-hover: #dca662;
--btn-disabled: #6c757d;
--shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
--tool-highlight: #ffd700;
--tool-highlight-text: #000000;
--log-debug: #808080;
--log-info: #ffffff;
--log-warning: #FF8C00;
--log-error: #FF0000;
--stats-header: #3a3a3a;
}
body {
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
margin: 0;
padding: 20px;
background-color: var(--bg-primary);
color: var(--text-primary);
transition: background-color 0.3s ease, color 0.3s ease;
}
.header {
text-align: center;
margin-bottom: 20px;
}
.log-container {
background-color: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 5px;
height: 600px;
overflow-y: auto;
overflow-x: auto;
padding: 10px;
white-space: pre-wrap;
font-size: 12px;
line-height: 1.4;
color: var(--text-primary);
transition: background-color 0.3s ease, border-color 0.3s ease, color 0.3s ease;
}
.controls {
margin-bottom: 10px;
text-align: center;
display: flex;
justify-content: center;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.logo {
margin-bottom: 10px;
text-align: center;
}
.btn {
background-color: var(--btn-primary);
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.3s ease;
}
.btn:hover {
background-color: var(--btn-hover);
}
.btn:disabled {
background-color: var(--btn-disabled);
cursor: not-allowed;
}
.theme-toggle {
display: flex;
align-items: center;
gap: 5px;
background-color: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 6px 12px;
cursor: pointer;
transition: background-color 0.3s ease, border-color 0.3s ease;
}
.theme-toggle:hover {
background-color: var(--border-color);
}
.theme-toggle .icon {
font-size: 16px;
}
.log-debug {
color: var(--log-debug);
}
.log-info {
color: var(--log-info);
}
.log-warning {
color: var(--log-warning);
}
.log-error {
color: var(--log-error);
}
.log-default {
color: var(--log-info);
}
/* Tool name highlighting */
.tool-name {
background-color: var(--tool-highlight);
color: var(--tool-highlight-text);
font-weight: bold;
}
.loading {
text-align: center;
color: var(--text-muted);
font-style: italic;
}
.error-message {
color: var(--log-error);
text-align: center;
margin: 10px 0;
}
.charts-container {
display: flex;
flex-wrap: wrap;
gap: 15px;
justify-content: space-between;
max-width: 1400px;
margin: 0 auto;
}
.chart-group {
flex: 1;
min-width: 280px;
max-width: 320px;
text-align: center;
}
.chart-wide {
flex: 0 0 100%;
min-width: 100%;
margin-top: 10px;
}
.chart-group h3 {
margin: 0 0 10px 0;
color: var(--text-secondary);
}
.stats-summary {
margin: 0 auto;
border-collapse: collapse;
background: var(--bg-secondary);
border-radius: 5px;
overflow: hidden;
box-shadow: var(--shadow);
transition: background-color 0.3s ease, box-shadow 0.3s ease;
}
.stats-summary th,
.stats-summary td {
padding: 10px 20px;
text-align: left;
border-bottom: 1px solid var(--border-color);
color: var(--text-primary);
transition: border-color 0.3s ease, color 0.3s ease;
}
.stats-summary th {
background-color: var(--stats-header);
font-weight: bold;
transition: background-color 0.3s ease;
}
.stats-summary tr:last-child td {
border-bottom: none;
}
@media (max-width: 768px) {
.charts-container {
flex-direction: column;
}
.chart-group,
.chart-wide {
min-width: auto;
max-width: none;
}
.controls {
flex-direction: column;
gap: 5px;
}
}
</style>
</head>
<body>
<div class="header">
<img id="serena-logo" src="serena-logs.png" alt="Serena" style="max-width: 400px; height: auto;">
</div>
<header class="header">
<div class="logo-container">
<img id="serena-logo" src="serena-logs.png" alt="Serena">
</div>
<nav class="header-nav">
<button id="theme-toggle" class="theme-toggle">
<span id="theme-icon">🌙</span>
<span id="theme-text">Dark</span>
</button>
<button id="menu-toggle" class="menu-button">
<span>☰</span>
<span>Menu</span>
</button>
<div id="menu-dropdown" class="menu-dropdown" style="display:none">
<a href="#" data-page="overview" class="active">Overview</a>
<a href="#" data-page="logs">Logs</a>
<a href="#" data-page="stats">Advanced Stats</a>
<hr>
<a href="#" id="menu-shutdown">Shutdown Server</a>
</div>
</nav>
</header>
<div class="controls">
<button id="load-logs" class="btn">Reload Log</button>
<button id="shutdown" class="btn">Shutdown Server</button>
<button id="toggle-stats" class="btn">Show Stats</button>
<div id="theme-toggle" class="theme-toggle" title="Toggle theme">
<span class="icon" id="theme-icon">🌙</span>
<span id="theme-text">Dark</span>
<!-- Overview Page (Landing) -->
<div id="page-overview" class="page-view">
<div class="overview-container">
<div class="overview-left">
<section class="config-section">
<h2>Current Configuration</h2>
<div id="config-display">
<div class="loading">Loading configuration...</div>
</div>
</section>
<section class="basic-stats-section">
<h2>Tool Usage</h2>
<div id="basic-stats-display">
<div class="loading">Loading stats...</div>
</div>
</section>
</div>
<div class="overview-right">
<section class="projects-section">
<h2 class="collapsible-header" id="projects-header">
<span>Registered Projects</span>
<span class="toggle-icon">▼</span>
</h2>
<div id="projects-display" class="collapsible-content" style="display:none">
<div class="loading">Loading projects...</div>
</div>
</section>
<section class="projects-section">
<h2 class="collapsible-header" id="available-tools-header">
<span>Available Tools (Disabled)</span>
<span class="toggle-icon">▼</span>
</h2>
<div id="available-tools-display" class="collapsible-content" style="display:none">
<div class="loading">Loading tools...</div>
</div>
</section>
<section class="projects-section">
<h2 class="collapsible-header" id="available-modes-header">
<span>Available Modes</span>
<span class="toggle-icon">▼</span>
</h2>
<div id="available-modes-display" class="collapsible-content" style="display:none">
<div class="loading">Loading modes...</div>
</div>
</section>
<section class="projects-section">
<h2 class="collapsible-header" id="available-contexts-header">
<span>Available Contexts</span>
<span class="toggle-icon">▼</span>
</h2>
<div id="available-contexts-display" class="collapsible-content" style="display:none">
<div class="loading">Loading contexts...</div>
</div>
</section>
</div>
</div>
</div>
<div id="error-container"></div>
<div id="log-container" class="log-container"></div>
<!-- Logs Page -->
<div id="page-logs" class="page-view" style="display:none">
<div class="controls">
<button id="load-logs" class="btn">Reload Log</button>
</div>
<div id="error-container"></div>
<div id="log-container" class="log-container"></div>
</div>
<div id="stats-section" style="display:none; margin-top:20px;">
<div style="text-align:center; margin-bottom:20px;">
<!-- Advanced Stats Page -->
<div id="page-stats" class="page-view" style="display:none">
<div class="controls">
<button id="refresh-stats" class="btn">Refresh Stats</button>
<button id="clear-stats" class="btn">Clear Stats</button>
</div>
@@ -280,10 +121,9 @@
<div id="stats-summary" style="margin-bottom:20px; text-align:center;"></div>
<div id="estimator-name" style="text-align:center; margin-bottom:10px;"></div>
<div id="no-stats-message" style="text-align:center; color:var(--text-muted); font-style:italic; display:none;">
No tool stats collected. Have you enabled tool stats collection in the configuration?
No tool stats collected yet.
</div>
<div class="charts-container">
<div class="chart-group">
<h3>Tool Calls</h3>
@@ -311,4 +151,4 @@
</script>
</body>
</html>
</html>
@@ -61,16 +61,14 @@ default_max_tool_answer_chars: 150000
# Even though the value of the max_answer_chars can be changed when calling the tool, it may make sense to adjust this default
# through the global configuration.
record_tool_usage_stats: False
# whether to record tool usage statistics, they will be shown in the web dashboard if recording is active.
token_count_estimator: TIKTOKEN_GPT4O
# Only relevant if `record_tool_usage` is True; the name of the token count estimator to use for tool usage statistics.
token_count_estimator: CHAR_COUNT
# the name of the token count estimator to use for tool usage statistics.
# See the `RegisteredTokenCountEstimator` enum for available options.
#
# Note: some token estimators (like tiktoken) may require downloading data files
# on the first run, which can take some time and require internet access. Others, like the Anthropic ones, may require an API key
# and rate limits may apply.
# By default, a very naive character count estimator is used, which simply counts the number of characters.
# You can configure this to TIKTOKEN_GPT4 to use a local tiktoken-based estimator for GPT-4 (will download tiktoken
# data files on first run), or ANTHROPIC_CLAUDE_SONNET_4 which will use the (free of cost) Anthropic API to
# estimate the token count using the Claude Sonnet 4 tokenizer.
# MANAGED BY SERENA, KEEP AT THE BOTTOM OF THE YAML AND DON'T EDIT WITHOUT NEED
+1 -1
View File
@@ -260,7 +260,7 @@ class Tool(Component):
raise
# record tool usage
self.agent.record_tool_usage_if_enabled(kwargs, result, self)
self.agent.record_tool_usage(kwargs, result, self)
except Exception as e:
if not catch_exceptions: