From c54f739354150f68414fc923ce42e6683ba7967b Mon Sep 17 00:00:00 2001 From: Nige Date: Thu, 12 Mar 2026 22:27:50 +0000 Subject: [PATCH] fix: resolve bug_bash UX/logging issues (#1054 #1055 #1058) (#1072) * fix(web,db): improve auth UX + reduce naive timestamp log noise * fix(clippy): keep memory test modules at end of file --- src/channels/web/static/app.js | 20 +++- src/db/libsql/mod.rs | 22 +++-- src/tools/builtin/memory.rs | 176 +++++++++++++++------------------ 3 files changed, 111 insertions(+), 107 deletions(-) diff --git a/src/channels/web/static/app.js b/src/channels/web/static/app.js index 5a55051d..b151840f 100644 --- a/src/channels/web/static/app.js +++ b/src/channels/web/static/app.js @@ -375,6 +375,9 @@ function connectSSE() { removeAuthCard(data.extension_name); closeConfigureModal(); showToast(data.message, data.success ? 'success' : 'error'); + if (shouldShowChannelConnectedMessage(data.extension_name, data.success)) { + addMessage('system', 'Telegram is now connected. You can message me there and I can send you notifications.'); + } // Refresh extensions list so status indicators update if (currentTab === 'extensions') loadExtensions(); enableChatInput(); @@ -1001,6 +1004,21 @@ function finalizeActivityGroup() { _activeToolCards = {}; } +function humanizeToolName(rawName) { + if (!rawName) return ''; + return String(rawName) + .replace(/[_-]+/g, ' ') + .replace(/([a-z0-9])([A-Z])/g, '$1 $2') + .replace(/^tool([a-zA-Z])/, 'tool $1') + .replace(/\s+/g, ' ') + .trim(); +} + +function shouldShowChannelConnectedMessage(extensionName, success) { + if (!success || !extensionName) return false; + return String(extensionName).toLowerCase().includes('telegram'); +} + function showApproval(data) { // Avoid duplicate cards on reconnect/history refresh. const existing = document.querySelector('.approval-card[data-request-id="' + CSS.escape(data.request_id) + '"]'); @@ -1018,7 +1036,7 @@ function showApproval(data) { const toolName = document.createElement('div'); toolName.className = 'approval-tool-name'; - toolName.textContent = data.tool_name; + toolName.textContent = humanizeToolName(data.tool_name); card.appendChild(toolName); if (data.description) { diff --git a/src/db/libsql/mod.rs b/src/db/libsql/mod.rs index a5c48f3f..dcc5a8b5 100644 --- a/src/db/libsql/mod.rs +++ b/src/db/libsql/mod.rs @@ -16,6 +16,7 @@ mod workspace; use std::path::Path; use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; use async_trait::async_trait; use chrono::{DateTime, NaiveDateTime, Utc}; @@ -32,6 +33,8 @@ use crate::workspace::MemoryDocument; use crate::db::libsql_migrations; +static NAIVE_TIMESTAMP_LOGGED: AtomicBool = AtomicBool::new(false); + /// Explicit column list for routines table (matches positional access in `row_to_routine_libsql`). pub(crate) const ROUTINE_COLUMNS: &str = "\ id, name, description, user_id, enabled, \ @@ -163,24 +166,27 @@ impl LibSqlBackend { /// /// Returns an error if none of the formats match. pub(crate) fn parse_timestamp(s: &str) -> Result, String> { + let log_naive_timestamp_once = || { + if !NAIVE_TIMESTAMP_LOGGED.swap(true, Ordering::Relaxed) { + tracing::debug!( + timestamp = %s, + "parsed naive timestamp without timezone; assuming UTC for backward compatibility" + ); + } + }; + // RFC 3339 (our canonical write format) if let Ok(dt) = DateTime::parse_from_rfc3339(s) { return Ok(dt.with_timezone(&Utc)); } // Naive with fractional seconds (legacy or SQLite datetime() output) if let Ok(ndt) = NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S%.f") { - tracing::debug!( - timestamp = %s, - "parsed naive timestamp without timezone; assuming UTC for backward compatibility" - ); + log_naive_timestamp_once(); return Ok(ndt.and_utc()); } // Naive without fractional seconds (legacy format) if let Ok(ndt) = NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S") { - tracing::debug!( - timestamp = %s, - "parsed naive timestamp without timezone; assuming UTC for backward compatibility" - ); + log_naive_timestamp_once(); return Ok(ndt.and_utc()); } Err(format!("unparseable timestamp: {:?}", s)) diff --git a/src/tools/builtin/memory.rs b/src/tools/builtin/memory.rs index c8f5178f..f1f84684 100644 --- a/src/tools/builtin/memory.rs +++ b/src/tools/builtin/memory.rs @@ -540,107 +540,9 @@ impl Tool for MemoryTreeTool { } #[cfg(test)] -mod path_routing_tests { - use super::looks_like_filesystem_path; - - #[test] - fn detects_filesystem_paths() { - assert!(looks_like_filesystem_path("/Users/nige/file.md")); - assert!(looks_like_filesystem_path("C:\\Users\\nige\\file.md")); - assert!(looks_like_filesystem_path("D:/work/file.md")); - assert!(looks_like_filesystem_path("~/notes.md")); - } - - #[test] - fn allows_workspace_memory_paths() { - assert!(!looks_like_filesystem_path("MEMORY.md")); - assert!(!looks_like_filesystem_path("daily/2026-03-11.md")); - assert!(!looks_like_filesystem_path("projects/alpha/notes.md")); - } -} - -#[cfg(all(test, feature = "postgres"))] mod tests { use super::*; - fn make_test_workspace() -> Arc { - Arc::new(Workspace::new( - "test_user", - deadpool_postgres::Pool::builder(deadpool_postgres::Manager::new( - tokio_postgres::Config::new(), - tokio_postgres::NoTls, - )) - .build() - .unwrap(), - )) - } - - #[test] - fn test_memory_search_schema() { - let workspace = make_test_workspace(); - let tool = MemorySearchTool::new(workspace); - - assert_eq!(tool.name(), "memory_search"); - assert!(!tool.requires_sanitization()); - - let schema = tool.parameters_schema(); - assert!(schema["properties"]["query"].is_object()); - assert!( - schema["required"] - .as_array() - .unwrap() - .contains(&"query".into()) - ); - } - - #[test] - fn test_memory_write_schema() { - let workspace = make_test_workspace(); - let tool = MemoryWriteTool::new(workspace); - - assert_eq!(tool.name(), "memory_write"); - - let schema = tool.parameters_schema(); - assert!(schema["properties"]["content"].is_object()); - assert!(schema["properties"]["target"].is_object()); - assert!(schema["properties"]["append"].is_object()); - } - - #[test] - fn test_memory_read_schema() { - let workspace = make_test_workspace(); - let tool = MemoryReadTool::new(workspace); - - assert_eq!(tool.name(), "memory_read"); - - let schema = tool.parameters_schema(); - assert!(schema["properties"]["path"].is_object()); - assert!( - schema["required"] - .as_array() - .unwrap() - .contains(&"path".into()) - ); - } - - #[test] - fn test_memory_tree_schema() { - let workspace = make_test_workspace(); - let tool = MemoryTreeTool::new(workspace); - - assert_eq!(tool.name(), "memory_tree"); - - let schema = tool.parameters_schema(); - assert!(schema["properties"]["path"].is_object()); - assert!(schema["properties"]["depth"].is_object()); - assert_eq!(schema["properties"]["depth"]["default"], 1); - } -} - -#[cfg(test)] -mod path_routing_tests { - use super::looks_like_filesystem_path; - #[test] fn detects_filesystem_paths() { assert!(looks_like_filesystem_path("/Users/nige/file.md")); @@ -655,4 +557,82 @@ mod path_routing_tests { assert!(!looks_like_filesystem_path("daily/2026-03-11.md")); assert!(!looks_like_filesystem_path("projects/alpha/notes.md")); } + + #[cfg(feature = "postgres")] + mod postgres_schema_tests { + use super::*; + + fn make_test_workspace() -> Arc { + Arc::new(Workspace::new( + "test_user", + deadpool_postgres::Pool::builder(deadpool_postgres::Manager::new( + tokio_postgres::Config::new(), + tokio_postgres::NoTls, + )) + .build() + .unwrap(), + )) + } + + #[test] + fn test_memory_search_schema() { + let workspace = make_test_workspace(); + let tool = MemorySearchTool::new(workspace); + + assert_eq!(tool.name(), "memory_search"); + assert!(!tool.requires_sanitization()); + + let schema = tool.parameters_schema(); + assert!(schema["properties"]["query"].is_object()); + assert!( + schema["required"] + .as_array() + .unwrap() + .contains(&"query".into()) + ); + } + + #[test] + fn test_memory_write_schema() { + let workspace = make_test_workspace(); + let tool = MemoryWriteTool::new(workspace); + + assert_eq!(tool.name(), "memory_write"); + + let schema = tool.parameters_schema(); + assert!(schema["properties"]["content"].is_object()); + assert!(schema["properties"]["target"].is_object()); + assert!(schema["properties"]["append"].is_object()); + } + + #[test] + fn test_memory_read_schema() { + let workspace = make_test_workspace(); + let tool = MemoryReadTool::new(workspace); + + assert_eq!(tool.name(), "memory_read"); + + let schema = tool.parameters_schema(); + assert!(schema["properties"]["path"].is_object()); + assert!( + schema["required"] + .as_array() + .unwrap() + .contains(&"path".into()) + ); + } + + #[test] + fn test_memory_tree_schema() { + let workspace = make_test_workspace(); + let tool = MemoryTreeTool::new(workspace); + + assert_eq!(tool.name(), "memory_tree"); + + let schema = tool.parameters_schema(); + assert!(schema["properties"]["path"].is_object()); + assert!(schema["properties"]["depth"].is_object()); + assert_eq!(schema["properties"]["depth"]["default"], 1); + } + } }