From 43082af3e104599df22c44e891338f9ddc1809a5 Mon Sep 17 00:00:00 2001 From: Aji Anaz Date: Thu, 9 Jul 2026 22:11:18 +0700 Subject: [PATCH 1/3] fix(docs): default doc_list limit to 1000 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit uteke-serve /doc/list defaults limit to 5 (shared memory pagination default). The doc tree fetches the full flat list client-side to build the hierarchy, so omitting limit silently capped it at 5 docs. Side effect: parent folders whose children fell outside the first 5 results had an empty childrenCache, so isFolder evaluated false and no expand/collapse chevron rendered — the tree could not be toggled. Default to 1000 in the Tauri command so the installed uteke 0.7.0 works without waiting for the upstream default fix (uteke #634). --- src-tauri/src/commands.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 0fb20d7..b1055fd 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -2783,6 +2783,9 @@ pub async fn doc_list( if !client.is_available().await { return Err(CommandError::Uteke("uteke-serve not reachable".into())); } + // uteke-serve defaults /doc/list limit to 5 (shared with memory pagination). + // The document tree needs the full set client-side, so default high here. + let limit = Some(limit.unwrap_or(1000)); let docs = client .doc_list(limit, roots_only, parent.as_deref()) .await From 215bcb9194a0e93eaf2bf83750a2bed384739e72 Mon Sep 17 00:00:00 2001 From: Aji Anaz Date: Thu, 9 Jul 2026 22:24:42 +0700 Subject: [PATCH 2/3] fix(docs): require uteke >= 0.7.1 for documents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bump MIN_UTEKE_FOR_DOCS from 0.7.0 to 0.7.1. Symptom: deleting a document failed with "uteke error: server returned 400 Bad Request". Root cause: uteke 0.7.0 has no /doc/delete route — document delete/move landed in 0.7.1. The old gate (0.7.0) let Corin run against an incomplete server and surface a confusing 400. Raising the gate to 0.7.1 makes require_uteke_version reject 0.7.0 up front with a clear message, and the Documents view renders its upgrade banner so the user runs uteke upgrade (0.7.1 is released) and gets the full doc feature set. Comments referencing the old 0.7.0 floor updated across lib/commands/ uteke_client. Added a docs_gate_requires_0_7_1 test. --- src-tauri/src/commands.rs | 6 +++--- src-tauri/src/lib.rs | 17 ++++++++++++++--- src-tauri/src/uteke_client.rs | 2 +- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index b1055fd..4d87ca2 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -116,7 +116,7 @@ pub struct AppState { pub uteke_client: Option, /// Cached installed uteke version ("X.Y.Z"), probed once at startup via /// `uteke --version`. `None` if the CLI couldn't be probed. Used to gate - /// features that require a minimum server version (e.g. Documents → 0.7.0). + /// features that require a minimum server version (e.g. Documents → 0.7.1). pub uteke_version: Option, } @@ -2669,7 +2669,7 @@ pub async fn disconnect_connection( // // Since uteke v0.7.0 (#614) documents are global — unique slugs across all // namespaces. `namespace` is no longer accepted. Every command gates on -// `MIN_UTEKE_FOR_DOCS` ("0.7.0") so older servers never get ambiguous +// `MIN_UTEKE_FOR_DOCS` ("0.7.1") so older servers never get ambiguous // global-slug requests. Frontend reads `uteke_version_status` to render an // upgrade banner; `uteke_self_update` runs `uteke upgrade` and re-detects. @@ -2678,7 +2678,7 @@ pub async fn disconnect_connection( pub struct VersionStatus { /// Detected `X.Y.Z`, or `None` if the `uteke` CLI couldn't be probed. pub current: Option, - /// Minimum uteke version that supports global documents ("0.7.0"). + /// Minimum uteke version that supports global documents ("0.7.1"). pub required: String, /// `true` iff `current >= required` (false when current is unknown). pub supported: bool, diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index c7933a3..e8f964f 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -195,8 +195,10 @@ fn find_uteke_serve() -> Option { None } -/// Minimum uteke version required for the global Documents feature (v0.7.0, #614). -pub(crate) const MIN_UTEKE_FOR_DOCS: &str = "0.7.0"; +/// Minimum uteke version required for the global Documents feature. +/// v0.7.0 introduced global docs (#614), but document delete/move landed in +/// v0.7.1 — require it so the full doc feature set is available. +pub(crate) const MIN_UTEKE_FOR_DOCS: &str = "0.7.1"; /// Find the `uteke` CLI binary in PATH or ~/.local/bin. pub(crate) fn find_uteke_cli() -> Option { @@ -510,7 +512,7 @@ pub fn run() { } // Cache the installed uteke version for feature - // gating (e.g. Documents requires >= 0.7.0). + // gating (e.g. Documents requires >= 0.7.1). s.uteke_version = detect_uteke_version(); } Err(e) => { @@ -602,6 +604,15 @@ mod tests { assert!(version_meets("1.0.0", "0.7.0")); } + #[test] + fn docs_gate_requires_0_7_1() { + // Document delete/move landed in 0.7.1; 0.7.0 must be rejected so the + // UI prompts for an upgrade instead of hitting a missing /doc/delete. + assert!(!version_meets("0.7.0", MIN_UTEKE_FOR_DOCS)); + assert!(version_meets("0.7.1", MIN_UTEKE_FOR_DOCS)); + assert!(version_meets("0.8.0", MIN_UTEKE_FOR_DOCS)); + } + #[test] fn version_meets_lower_rejected() { assert!(!version_meets("0.6.7", "0.7.0")); diff --git a/src-tauri/src/uteke_client.rs b/src-tauri/src/uteke_client.rs index b2137f2..cbfa03d 100644 --- a/src-tauri/src/uteke_client.rs +++ b/src-tauri/src/uteke_client.rs @@ -931,7 +931,7 @@ impl UtekeClient { // // Since uteke v0.7.0 (#614) documents are GLOBAL — unique slugs across all // namespaces. These methods send no `namespace`. Callers MUST gate access - // on `crate::MIN_UTEKE_FOR_DOCS` ("0.7.0") so older servers (where docs + // on `crate::MIN_UTEKE_FOR_DOCS` ("0.7.1") so older servers (where docs // were namespace-scoped) never receive ambiguous global-slug requests. /// List documents via POST /doc/list. From 57b024a4a5fc1c7059603d7ac82d760e74d0b226 Mon Sep 17 00:00:00 2001 From: Aji Anaz Date: Thu, 9 Jul 2026 22:37:11 +0700 Subject: [PATCH 3/3] fix(docs): remote-aware uteke version gating MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The documents version gate read AppState::uteke_version, which is the LOCAL 'uteke --version' CLI output. When Corin connects to a remote uteke-serve (https/host URL), the local CLI version is irrelevant — the server may be older or newer — yet the gate compared against the CLI and falsely told remote users to 'run uteke upgrade'. Now: - Track AppState::uteke_remote (set from is_remote_url at startup). - resolve_uteke_version() probes the remote server's self-reported version via GET /health (uteke >= health-version patch) and falls back to the cached CLI version for local servers. - require_uteke_version / uteke_version_status treat a remote server with an UNKNOWN version leniently (allow through, no upgrade banner) — remote servers are user-managed and surface real errors directly. Local servers keep the strict gate. - uteke_self_update skips the local CLI upgrade for remote servers and just re-probes the server version. Depends on upstream /health version field (uteke #636) for precise remote detection; without it, remote-unknown is handled leniently so remote users are no longer blocked. --- src-tauri/src/commands.rs | 91 ++++++++++++++++++++++++++++++----- src-tauri/src/lib.rs | 7 +++ src-tauri/src/uteke_client.rs | 21 ++++++++ 3 files changed, 106 insertions(+), 13 deletions(-) diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 4d87ca2..dd04aaa 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -114,10 +114,17 @@ pub struct AppState { /// HTTP client for uteke-serve (all memory CRUD + graph operations). /// None if server is not running. pub uteke_client: Option, - /// Cached installed uteke version ("X.Y.Z"), probed once at startup via - /// `uteke --version`. `None` if the CLI couldn't be probed. Used to gate - /// features that require a minimum server version (e.g. Documents → 0.7.1). + /// Cached installed uteke version ("X.Y.Z"), probed once at startup. + /// For local servers this is the `uteke --version` CLI output; for remote + /// servers it is the version reported by the server's `/health` (if any). + /// `None` if unknown. Used to gate features that require a minimum server + /// version (e.g. Documents → 0.7.1). pub uteke_version: Option, + /// `true` when Corin talks to a remote uteke-serve (https/host URL). + /// Remote servers are user-managed, so an unknown version is treated + /// leniently (no upgrade banner, no hard gate) — the server's own + /// responses surface any real incompatibility. + pub uteke_remote: bool, } // --------------------------------------------------------------------------- @@ -2684,40 +2691,83 @@ pub struct VersionStatus { pub supported: bool, } -/// Reject the call unless the cached uteke version meets `min`. +/// Resolve the effective uteke version for gating. /// -/// Reads `AppState::uteke_version` (set once at startup). Returns a clear, -/// upgrade-instructing message when unsupported or when the version is unknown. +/// - **Remote server**: probe the server's self-reported version via +/// `/health` (authoritative for a user-managed server). The local CLI +/// version is irrelevant — it may be older or newer than the server. +/// - **Local server**: use the cached `uteke --version` output. +async fn resolve_uteke_version( + state: &tauri::State<'_, Arc>>, +) -> (bool, Option) { + let (is_remote, client, cached) = { + let s = state.lock().await; + ( + s.uteke_remote, + s.uteke_client.clone(), + s.uteke_version.clone(), + ) + }; + if is_remote { + if let Some(client) = client + && client.is_available().await + { + let v = client.server_version().await; + return (true, v); + } + // Remote unreachable during this probe — fall back to whatever we know. + (true, cached) + } else { + (false, cached) + } +} + +/// Reject the call unless the effective uteke version meets `min`. +/// +/// For **remote** servers with an unknown version (e.g. an older server +/// that doesn't report version in `/health`) the call is allowed through — +/// remote servers are user-managed, and the server's own response surfaces +/// any real incompatibility (e.g. a 404/400 for a missing route). async fn require_uteke_version( state: &tauri::State<'_, Arc>>, min: &str, ) -> Result<(), CommandError> { - let current = state.lock().await.uteke_version.clone(); + let (is_remote, current) = resolve_uteke_version(state).await; let ok = current .as_ref() .map(|c| crate::version_meets(c, min)) - .unwrap_or(false); + // Remote + unknown version → don't block (user-managed server). + // Local + unknown version → block (we can't confirm capability). + .unwrap_or(is_remote); if ok { Ok(()) } else { + let hint = if is_remote { + "upgrade the uteke-serve on the remote host" + } else { + "run 'uteke upgrade' or use the Update button in Documents" + }; Err(CommandError::Uteke(format!( - "uteke {min} or newer is required for documents (current: {}). \ - Run 'uteke upgrade' or use the Update button in Documents.", + "uteke {min} or newer is required for documents (current: {}). Please {hint}.", current.as_deref().unwrap_or("unknown") ))) } } -/// Report the installed uteke version and whether it supports Documents. +/// Report the effective uteke version and whether it supports Documents. +/// +/// Drives the Documents upgrade banner. For remote servers with an unknown +/// version, `supported` is `true` so users are not nagged to upgrade a server +/// they manage (and which may well be newer than the local CLI). #[tauri::command] pub async fn uteke_version_status( state: tauri::State<'_, Arc>>, ) -> Result { - let current = state.lock().await.uteke_version.clone(); + let (is_remote, current) = resolve_uteke_version(&state).await; let supported = current .as_ref() .map(|c| crate::version_meets(c, crate::MIN_UTEKE_FOR_DOCS)) - .unwrap_or(false); + .unwrap_or(is_remote); Ok(VersionStatus { current, required: crate::MIN_UTEKE_FOR_DOCS.to_string(), @@ -2733,6 +2783,21 @@ pub async fn uteke_version_status( pub async fn uteke_self_update( state: tauri::State<'_, Arc>>, ) -> Result { + let is_remote = state.lock().await.uteke_remote; + if is_remote { + // Upgrading the local CLI won't affect a user-managed remote server. + // Just re-probe the server's version so the banner can refresh. + let (_, current) = resolve_uteke_version(&state).await; + let supported = current + .as_ref() + .map(|c| crate::version_meets(c, crate::MIN_UTEKE_FOR_DOCS)) + .unwrap_or(true); + return Ok(VersionStatus { + current, + required: crate::MIN_UTEKE_FOR_DOCS.to_string(), + supported, + }); + } let uteke = crate::find_uteke_cli().ok_or_else(|| { CommandError::Uteke("uteke CLI not found — install via the setup script".into()) })?; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index e8f964f..c292ac1 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -511,8 +511,15 @@ pub fn run() { s.uteke_client = Some(client); } + // Cache whether we're talking to a user-managed remote + // server (vs. a locally spawned uteke-serve). + s.uteke_remote = config::is_remote_url(&server_url); + // Cache the installed uteke version for feature // gating (e.g. Documents requires >= 0.7.1). + // For remote servers the authoritative version is + // probed live from /health in the gating commands; + // the local CLI version is only a fallback there. s.uteke_version = detect_uteke_version(); } Err(e) => { diff --git a/src-tauri/src/uteke_client.rs b/src-tauri/src/uteke_client.rs index cbfa03d..f5d99b0 100644 --- a/src-tauri/src/uteke_client.rs +++ b/src-tauri/src/uteke_client.rs @@ -286,6 +286,27 @@ impl UtekeClient { .unwrap_or(false) } + /// Probe the server's self-reported version via `/health`. + /// + /// Returns `Some("X.Y.Z")` when the server includes a `version` field + /// (uteke ≥ the /health-version patch), otherwise `None`. Used to gate + /// features for remote servers where the local `uteke` CLI version is + /// irrelevant — the server may be newer or older than the CLI. + pub async fn server_version(&self) -> Option { + let resp = self + .authed(self.client.get(format!("{}/health", self.base_url))) + .send() + .await + .ok()?; + if !resp.status().is_success() { + return None; + } + let body: serde_json::Value = resp.json().await.ok()?; + body.get("version") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()) + } + /// Semantic recall (vector + FTS5 hybrid search via RRF). pub async fn recall( &self,