ltcai 11.2.0 → 11.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (247) hide show
  1. package/README.md +46 -53
  2. package/docs/CHANGELOG.md +61 -0
  3. package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
  4. package/docs/DEVELOPMENT.md +1 -1
  5. package/docs/MULTI_AGENT_RUNTIME.md +1 -1
  6. package/docs/ONBOARDING.md +1 -1
  7. package/docs/OPERATIONS.md +6 -2
  8. package/docs/PERMISSION_MODE.md +1 -1
  9. package/docs/TRUST_MODEL.md +1 -1
  10. package/docs/WHY_LATTICE.md +1 -1
  11. package/docs/kg-schema.md +2 -2
  12. package/docs/v11.3.0_PLAN.md +202 -0
  13. package/docs/v11.4.0_RUST_FOUNDATION_PLAN.md +176 -0
  14. package/lattice_brain/__init__.py +1 -1
  15. package/lattice_brain/graph/_kg_common/__init__.py +287 -0
  16. package/lattice_brain/graph/_kg_common/extraction.py +516 -0
  17. package/lattice_brain/graph/_kg_common/relations.py +161 -0
  18. package/lattice_brain/graph/_kg_common/text.py +479 -0
  19. package/lattice_brain/graph/discovery_index/__init__.py +35 -0
  20. package/lattice_brain/graph/discovery_index/cleanup.py +182 -0
  21. package/lattice_brain/graph/discovery_index/extract.py +137 -0
  22. package/lattice_brain/graph/discovery_index/scan.py +411 -0
  23. package/lattice_brain/graph/discovery_index/upsert.py +495 -0
  24. package/lattice_brain/graph/projection/__init__.py +42 -0
  25. package/lattice_brain/graph/projection/curation.py +500 -0
  26. package/lattice_brain/graph/{projection.py → projection/v2_schema.py} +15 -477
  27. package/lattice_brain/graph/retrieval/__init__.py +54 -0
  28. package/lattice_brain/graph/retrieval/context.py +197 -0
  29. package/lattice_brain/graph/retrieval/graph_view.py +319 -0
  30. package/lattice_brain/graph/retrieval/hybrid.py +488 -0
  31. package/lattice_brain/graph/retrieval/maintenance.py +121 -0
  32. package/lattice_brain/graph/retrieval/signals.py +95 -0
  33. package/lattice_brain/graph/retrieval_vector/__init__.py +42 -0
  34. package/lattice_brain/graph/retrieval_vector/fingerprint.py +97 -0
  35. package/lattice_brain/graph/retrieval_vector/indexing.py +347 -0
  36. package/lattice_brain/graph/retrieval_vector/search.py +560 -0
  37. package/lattice_brain/graph/retrieval_vector/status.py +374 -0
  38. package/lattice_brain/ingestion/__init__.py +130 -0
  39. package/lattice_brain/ingestion/_contract.py +90 -0
  40. package/lattice_brain/ingestion/constants.py +127 -0
  41. package/lattice_brain/ingestion/folder_scan.py +57 -0
  42. package/lattice_brain/ingestion/folders.py +258 -0
  43. package/lattice_brain/ingestion/hashing.py +26 -0
  44. package/lattice_brain/ingestion/jobs_api.py +107 -0
  45. package/lattice_brain/ingestion/models.py +80 -0
  46. package/lattice_brain/ingestion/pipeline.py +486 -0
  47. package/lattice_brain/ingestion/quality.py +209 -0
  48. package/lattice_brain/ingestion/routing.py +295 -0
  49. package/lattice_brain/multimodal/__init__.py +164 -0
  50. package/lattice_brain/multimodal/audio.py +77 -0
  51. package/lattice_brain/multimodal/common.py +118 -0
  52. package/lattice_brain/multimodal/images.py +498 -0
  53. package/lattice_brain/multimodal/ports.py +169 -0
  54. package/lattice_brain/multimodal/video.py +410 -0
  55. package/lattice_brain/portability/__init__.py +90 -0
  56. package/lattice_brain/portability/_contract.py +42 -0
  57. package/lattice_brain/portability/backups.py +338 -0
  58. package/lattice_brain/portability/bundles.py +136 -0
  59. package/lattice_brain/portability/constants.py +93 -0
  60. package/lattice_brain/portability/fsops.py +138 -0
  61. package/lattice_brain/portability/service.py +41 -0
  62. package/lattice_brain/{portability.py → portability/sharing.py} +44 -677
  63. package/lattice_brain/runtime/__init__.py +1 -1
  64. package/lattice_brain/runtime/multi_agent.py +1 -1
  65. package/latticeai/__init__.py +1 -1
  66. package/latticeai/api/chronicle.py +63 -0
  67. package/latticeai/core/agent/__init__.py +93 -0
  68. package/latticeai/core/agent/_contract.py +79 -0
  69. package/latticeai/core/agent/context.py +57 -0
  70. package/latticeai/core/agent/deps.py +125 -0
  71. package/latticeai/core/agent/execution.py +622 -0
  72. package/latticeai/core/agent/planning.py +145 -0
  73. package/latticeai/core/agent/recovery.py +157 -0
  74. package/latticeai/core/agent/runtime.py +210 -0
  75. package/latticeai/core/agent/verification.py +231 -0
  76. package/latticeai/core/embedding_providers/__init__.py +151 -0
  77. package/latticeai/core/embedding_providers/base.py +199 -0
  78. package/latticeai/core/embedding_providers/captions.py +162 -0
  79. package/latticeai/core/embedding_providers/profiles.py +126 -0
  80. package/latticeai/core/embedding_providers/text.py +350 -0
  81. package/latticeai/core/embedding_providers/vision.py +352 -0
  82. package/latticeai/core/file_generation/__init__.py +115 -0
  83. package/latticeai/core/file_generation/bundles.py +76 -0
  84. package/latticeai/core/file_generation/extraction.py +154 -0
  85. package/latticeai/core/file_generation/inference.py +235 -0
  86. package/latticeai/core/file_generation/orchestration.py +152 -0
  87. package/latticeai/core/file_generation/prompting.py +117 -0
  88. package/latticeai/core/file_generation/repair.py +114 -0
  89. package/latticeai/core/file_generation/sanitize.py +61 -0
  90. package/latticeai/core/file_generation/validation.py +201 -0
  91. package/latticeai/core/legacy_compatibility.py +1 -1
  92. package/latticeai/core/marketplace.py +1 -1
  93. package/latticeai/core/messages.py +9 -0
  94. package/latticeai/core/workspace_os_constants.py +1 -1
  95. package/latticeai/integrations/telegram_bot/__init__.py +123 -0
  96. package/latticeai/integrations/telegram_bot/__main__.py +17 -0
  97. package/latticeai/integrations/telegram_bot/config.py +86 -0
  98. package/latticeai/integrations/telegram_bot/dispatch.py +311 -0
  99. package/latticeai/integrations/telegram_bot/flows.py +478 -0
  100. package/latticeai/integrations/telegram_bot/helpers.py +322 -0
  101. package/latticeai/integrations/telegram_bot/screens.py +394 -0
  102. package/latticeai/models/router/__init__.py +88 -0
  103. package/latticeai/models/router/_contract.py +66 -0
  104. package/latticeai/models/router/branding.py +56 -0
  105. package/latticeai/models/router/catalog.py +69 -0
  106. package/latticeai/models/router/documents.py +199 -0
  107. package/latticeai/models/router/errors.py +37 -0
  108. package/latticeai/models/router/generation.py +258 -0
  109. package/latticeai/models/router/loading.py +291 -0
  110. package/latticeai/models/router/local_models.py +85 -0
  111. package/latticeai/models/router/registry.py +147 -0
  112. package/latticeai/runtime/build_phases/__init__.py +82 -0
  113. package/latticeai/runtime/build_phases/features.py +407 -0
  114. package/latticeai/runtime/build_phases/foundation.py +555 -0
  115. package/latticeai/runtime/build_phases/web.py +492 -0
  116. package/latticeai/runtime/runtime_context.py +1 -0
  117. package/latticeai/services/architecture_readiness.py +48 -19
  118. package/latticeai/services/brain_intelligence/__init__.py +58 -0
  119. package/latticeai/services/brain_intelligence/_contract.py +71 -0
  120. package/latticeai/services/brain_intelligence/consistency.py +193 -0
  121. package/latticeai/services/brain_intelligence/constants.py +47 -0
  122. package/latticeai/services/brain_intelligence/digest.py +258 -0
  123. package/latticeai/services/brain_intelligence/health.py +331 -0
  124. package/latticeai/services/brain_intelligence/proposals.py +264 -0
  125. package/latticeai/services/brain_intelligence/sampling.py +84 -0
  126. package/latticeai/services/brain_intelligence/service.py +48 -0
  127. package/latticeai/services/chronicle.py +557 -0
  128. package/latticeai/services/memory_service/__init__.py +52 -0
  129. package/latticeai/services/memory_service/_contract.py +100 -0
  130. package/latticeai/services/memory_service/brief.py +431 -0
  131. package/latticeai/services/memory_service/constants.py +57 -0
  132. package/latticeai/services/memory_service/maintenance.py +138 -0
  133. package/latticeai/services/memory_service/manager.py +186 -0
  134. package/latticeai/services/memory_service/proof.py +136 -0
  135. package/latticeai/services/memory_service/recall.py +225 -0
  136. package/latticeai/services/memory_service/service.py +48 -0
  137. package/latticeai/services/memory_service/stores.py +110 -0
  138. package/latticeai/services/model_runtime/__init__.py +322 -0
  139. package/latticeai/services/model_runtime/cloud.py +87 -0
  140. package/latticeai/services/model_runtime/download.py +282 -0
  141. package/latticeai/services/model_runtime/engines.py +341 -0
  142. package/latticeai/services/model_runtime/loading.py +178 -0
  143. package/latticeai/services/model_runtime/service.py +129 -0
  144. package/latticeai/services/model_runtime/state.py +131 -0
  145. package/latticeai/services/model_runtime/status.py +255 -0
  146. package/latticeai/services/product_readiness.py +15 -7
  147. package/latticeai/setup/wizard/__init__.py +126 -0
  148. package/latticeai/setup/wizard/catalog.py +172 -0
  149. package/latticeai/setup/wizard/detect.py +323 -0
  150. package/latticeai/setup/wizard/install.py +348 -0
  151. package/latticeai/setup/wizard/paths.py +168 -0
  152. package/latticeai/setup/wizard/plans.py +74 -0
  153. package/latticeai/setup/wizard/recommend.py +320 -0
  154. package/package.json +6 -2
  155. package/scripts/bump_version.py +14 -0
  156. package/scripts/capture_release_evidence.mjs +33 -21
  157. package/scripts/check_current_release_docs.mjs +1 -1
  158. package/scripts/check_i18n_namespace_coverage.mjs +41 -4
  159. package/scripts/check_max_file_lines.mjs +102 -0
  160. package/scripts/check_release_evidence_bound.mjs +30 -15
  161. package/scripts/check_screenshot_pixel_delta.py +34 -4
  162. package/scripts/check_server_i18n.mjs +1 -0
  163. package/scripts/generate_rust_parity_fixtures.py +562 -0
  164. package/scripts/lib/mock_server_fingerprint.mjs +94 -0
  165. package/scripts/release_screen_claims.json +31 -2
  166. package/src-tauri/Cargo.lock +361 -3
  167. package/src-tauri/Cargo.toml +6 -1
  168. package/src-tauri/src/backend.rs +349 -0
  169. package/src-tauri/src/folder.rs +33 -0
  170. package/src-tauri/src/main.rs +97 -399
  171. package/src-tauri/tauri.conf.json +1 -1
  172. package/static/app/asset-manifest.json +41 -37
  173. package/static/app/assets/Act-yYpYnn0v.js +1 -0
  174. package/static/app/assets/AdminConsole-DL3Cr5pL.js +1 -0
  175. package/static/app/assets/{Brain-tuhI4sOC.js → Brain-C1HBN0Wf.js} +2 -2
  176. package/static/app/assets/BrainHome-DoXRhUUC.js +2 -0
  177. package/static/app/assets/BrainSignals-6yR6ir5t.js +1 -0
  178. package/static/app/assets/Capture-CFIRsFNE.js +1 -0
  179. package/static/app/assets/Chronicle-BZbEgiwN.js +1 -0
  180. package/static/app/assets/CommandPalette-D2pMxC2I.js +1 -0
  181. package/static/app/assets/Library-DwO3yZST.js +1 -0
  182. package/static/app/assets/{LivingBrain-DBwhto14.js → LivingBrain-Jn1GK0-S.js} +1 -1
  183. package/static/app/assets/ProductFlow-B-w1R4Oo.js +1 -0
  184. package/static/app/assets/ReviewCard-6B27X8Vg.js +3 -0
  185. package/static/app/assets/System-DW8F-2xL.js +1 -0
  186. package/static/app/assets/arrow-left-DXvKg9U6.js +1 -0
  187. package/static/app/assets/{bot-Cia42c2h.js → bot-IM_E_Y12.js} +1 -1
  188. package/static/app/assets/brain-Ci1CkWjM.js +1 -0
  189. package/static/app/assets/{button-2j2Ijzgq.js → button-COwyqfHM.js} +1 -1
  190. package/static/app/assets/circle-check-DfInj-qD.js +1 -0
  191. package/static/app/assets/{circle-pause-BEFeWpVW.js → circle-pause-DEM4A1Y5.js} +1 -1
  192. package/static/app/assets/{circle-play-ujXMcHxl.js → circle-play-C9djDuLd.js} +1 -1
  193. package/static/app/assets/{cpu-k4awryFq.js → cpu-DFdo1gw-.js} +1 -1
  194. package/static/app/assets/{download-DFbLJ_ig.js → download-SnJL6oqk.js} +1 -1
  195. package/static/app/assets/{folder-open-7y_b6xkM.js → folder-open-CqZeDkjE.js} +1 -1
  196. package/static/app/assets/{hard-drive-Bidh02Kr.js → hard-drive-j1jJXYYf.js} +1 -1
  197. package/static/app/assets/{index-DwDl9-8Y.css → index-BLPb5lmE.css} +1 -1
  198. package/static/app/assets/index-_u5iUHDr.js +10 -0
  199. package/static/app/assets/input-B0lPdRQZ.js +1 -0
  200. package/static/app/assets/link-2-CoFbooHS.js +1 -0
  201. package/static/app/assets/{permissionCopy-Bpb83Hx9.js → permissionCopy-BsyLxtao.js} +1 -1
  202. package/static/app/assets/primitives-DEbN-d6p.js +1 -0
  203. package/static/app/assets/search-BybIWPNd.js +1 -0
  204. package/static/app/assets/{share-2-BH1M-WNi.js → share-2-CVtZ_ewX.js} +1 -1
  205. package/static/app/assets/{shield-alert-BlKdBXcG.js → shield-alert-CBi2GNWM.js} +1 -1
  206. package/static/app/assets/{textarea-CCWbUfFB.js → textarea-DNMpB5ih.js} +1 -1
  207. package/static/app/assets/{useFocusTrap-YdHQ7pJ1.js → useFocusTrap-C83t3GXF.js} +1 -1
  208. package/static/app/assets/useMutation-DtbJDoyz.js +1 -0
  209. package/static/app/assets/{useQuery-CXQiwbVT.js → useQuery-Dcp1OChy.js} +1 -1
  210. package/static/app/assets/utils-BlZr7Pd4.js +4 -0
  211. package/static/app/assets/workspace-jJY4RuAV.js +1 -0
  212. package/static/app/index.html +4 -4
  213. package/static/sw.js +1 -1
  214. package/lattice_brain/graph/_kg_common.py +0 -1331
  215. package/lattice_brain/graph/discovery_index.py +0 -1141
  216. package/lattice_brain/graph/retrieval.py +0 -1120
  217. package/lattice_brain/graph/retrieval_vector.py +0 -1293
  218. package/lattice_brain/ingestion.py +0 -1525
  219. package/lattice_brain/multimodal.py +0 -1258
  220. package/latticeai/core/agent.py +0 -1465
  221. package/latticeai/core/embedding_providers.py +0 -1196
  222. package/latticeai/core/file_generation.py +0 -1047
  223. package/latticeai/integrations/telegram_bot.py +0 -1390
  224. package/latticeai/models/router.py +0 -1007
  225. package/latticeai/runtime/build_phases.py +0 -1450
  226. package/latticeai/services/brain_intelligence.py +0 -1083
  227. package/latticeai/services/memory_service.py +0 -1177
  228. package/latticeai/services/model_runtime.py +0 -1281
  229. package/latticeai/setup/wizard.py +0 -1310
  230. package/static/app/assets/Act-AWf0SAKp.js +0 -1
  231. package/static/app/assets/AdminConsole-D0u8Tiyj.js +0 -1
  232. package/static/app/assets/BrainHome-Ts7G_Ila.js +0 -2
  233. package/static/app/assets/BrainSignals-jMYgQ2Ar.js +0 -1
  234. package/static/app/assets/Capture-CqOSzyPr.js +0 -1
  235. package/static/app/assets/CommandPalette-DC0Bzh-I.js +0 -1
  236. package/static/app/assets/Library-CX-bbhmK.js +0 -1
  237. package/static/app/assets/ProductFlow-BHA2cfKI.js +0 -1
  238. package/static/app/assets/ReviewCard-BUhCKRNM.js +0 -3
  239. package/static/app/assets/System-Bu2t5hn1.js +0 -1
  240. package/static/app/assets/arrow-left-Dzwa5zRb.js +0 -1
  241. package/static/app/assets/brain-DJMoqrwx.js +0 -1
  242. package/static/app/assets/index-BpYkzcVm.js +0 -10
  243. package/static/app/assets/input-DSlJJxRs.js +0 -1
  244. package/static/app/assets/primitives-BCx6TvfG.js +0 -1
  245. package/static/app/assets/search-Cgy8cCFJ.js +0 -1
  246. package/static/app/assets/utils-zqPZJxdx.js +0 -4
  247. package/static/app/assets/workspace-DXTihhfU.js +0 -1
@@ -1,410 +1,125 @@
1
- use std::{
2
- env,
3
- fs::OpenOptions,
4
- path::PathBuf,
5
- process::{Child, Command, Stdio},
6
- sync::Mutex,
7
- };
8
-
9
- use serde::Serialize;
1
+ //! Lattice AI desktop shell.
2
+ //!
3
+ //! Everything this file used to do itself — resolving the worker command,
4
+ //! spawning it, probing a TCP socket to guess whether it was up, killing and
5
+ //! respawning on restart — moved into the `lattice-host` crate in 11.4.0 and is
6
+ //! now shared with the `lattice-host` binary, so the desktop and the CLI front
7
+ //! door cannot drift apart. What is left here is the part that is genuinely
8
+ //! Tauri's: five commands, one window, and the promise that closing the window
9
+ //! stops the worker.
10
+ //!
11
+ //! The five command names and their response shapes are unchanged — the
12
+ //! frontend (`frontend/src/api/base.ts`, `frontend/src/api/client.ts`) invokes
13
+ //! them by name and reads `backend_status` as a plain record.
14
+
15
+ mod backend;
16
+ mod folder;
17
+
18
+ use std::sync::Arc;
19
+
20
+ use backend::{BackendStatus, DesktopBackend};
10
21
  use tauri::{Manager, State};
11
22
 
12
- struct BackendState {
13
- origin: String,
14
- command: String,
15
- cwd: Option<String>,
16
- child: Mutex<Option<Child>>,
17
- last_error: Mutex<Option<String>>,
18
- }
19
-
20
- #[derive(Serialize)]
21
- struct BackendStatus {
22
- origin: String,
23
- command: String,
24
- cwd: Option<String>,
25
- running: bool,
26
- pid: Option<u32>,
27
- last_error: Option<String>,
28
- }
29
-
30
- struct BackendLaunch {
31
- command: String,
32
- program: String,
33
- args: Vec<String>,
34
- cwd: Option<PathBuf>,
35
- }
36
-
23
+ /// `backend_origin` → the origin every frontend request is sent to.
37
24
  #[tauri::command]
38
- fn backend_origin(state: State<'_, BackendState>) -> String {
39
- state.origin.clone()
25
+ fn backend_origin(state: State<'_, Arc<DesktopBackend>>) -> String {
26
+ state.origin().to_string()
40
27
  }
41
28
 
29
+ /// `backend_status` → a live snapshot, health probe included.
30
+ ///
31
+ /// Async, and therefore `Result`, because Tauri requires it of a command that
32
+ /// borrows state across an await. It never returns `Err`: the frontend keeps
33
+ /// receiving a resolved record exactly as before.
42
34
  #[tauri::command]
43
- fn backend_status(state: State<'_, BackendState>) -> BackendStatus {
44
- status_from_state(&state)
35
+ async fn backend_status(state: State<'_, Arc<DesktopBackend>>) -> Result<BackendStatus, String> {
36
+ Ok(state.status().await)
45
37
  }
46
38
 
39
+ /// `restart_backend` → stop, start, and report what happened.
47
40
  #[tauri::command]
48
- fn restart_backend(state: State<'_, BackendState>) -> BackendStatus {
49
- kill_backend(&state);
50
- let launch = backend_launch(&state.origin);
51
- match spawn_backend(&state.origin, &launch) {
52
- Ok(child) => {
53
- if let Ok(mut slot) = state.child.lock() {
54
- *slot = child;
55
- }
56
- set_error(&state, None);
57
- }
58
- Err(err) => set_error(&state, Some(err)),
59
- }
60
- status_from_state(&state)
41
+ async fn restart_backend(state: State<'_, Arc<DesktopBackend>>) -> Result<BackendStatus, String> {
42
+ Ok(state.restart().await)
61
43
  }
62
44
 
45
+ /// `shutdown_backend` → stop the worker and suppress restarts.
63
46
  #[tauri::command]
64
- fn shutdown_backend(state: State<'_, BackendState>) -> BackendStatus {
65
- kill_backend(&state);
66
- status_from_state(&state)
47
+ async fn shutdown_backend(state: State<'_, Arc<DesktopBackend>>) -> Result<BackendStatus, String> {
48
+ Ok(state.stop().await)
67
49
  }
68
50
 
51
+ /// `select_folder` → the native directory picker.
69
52
  #[tauri::command]
70
53
  fn select_folder() -> Option<String> {
71
- native_select_folder()
72
- }
73
-
74
- #[cfg(target_os = "macos")]
75
- fn native_select_folder() -> Option<String> {
76
- let output = Command::new("osascript")
77
- .args([
78
- "-e",
79
- r#"POSIX path of (choose folder with prompt "Choose a folder for Lattice AI")"#,
80
- ])
81
- .output()
82
- .ok()?;
83
- if !output.status.success() {
84
- return None;
85
- }
86
- let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
87
- if path.is_empty() {
88
- None
89
- } else {
90
- Some(path)
91
- }
92
- }
93
-
94
- #[cfg(not(target_os = "macos"))]
95
- fn native_select_folder() -> Option<String> {
96
- None
97
- }
98
-
99
- fn split_command(command: &str) -> Vec<String> {
100
- command.split_whitespace().map(|part| part.to_string()).collect()
101
- }
102
-
103
- fn command_in_path(name: &str) -> Option<String> {
104
- let mut dirs: Vec<PathBuf> = env::var_os("PATH")
105
- .map(|value| env::split_paths(&value).collect())
106
- .unwrap_or_default();
107
- dirs.extend([
108
- PathBuf::from("/opt/homebrew/bin"),
109
- PathBuf::from("/usr/local/bin"),
110
- PathBuf::from("/usr/bin"),
111
- PathBuf::from("/bin"),
112
- ]);
113
- for dir in dirs {
114
- let candidate = dir.join(name);
115
- if candidate.is_file() {
116
- return Some(candidate.to_string_lossy().to_string());
117
- }
118
- }
119
- None
54
+ folder::select_folder()
120
55
  }
121
56
 
122
- fn python_candidates() -> Vec<String> {
123
- let mut out = Vec::new();
124
- if let Ok(value) = env::var("LTCAI_PYTHON") {
125
- out.push(value);
126
- }
127
- for name in ["python3", "python"] {
128
- if let Some(path) = command_in_path(name) {
129
- out.push(path);
130
- }
131
- }
132
- out.extend([
133
- "/opt/homebrew/bin/python3".to_string(),
134
- "/usr/local/bin/python3".to_string(),
135
- "/usr/bin/python3".to_string(),
136
- ]);
137
- out.sort();
138
- out.dedup();
139
- out
140
- }
141
-
142
- fn module_importable(python: &str, module: &str) -> bool {
143
- Command::new(python)
144
- .args(["-c", &format!("import {module}")])
145
- .stdout(Stdio::null())
146
- .stderr(Stdio::null())
147
- .status()
148
- .map(|status| status.success())
149
- .unwrap_or(false)
150
- }
151
-
152
- fn resource_dir() -> Option<PathBuf> {
153
- let exe = env::current_exe().ok()?;
154
- let macos_dir = exe.parent()?;
155
- let contents_dir = macos_dir.parent()?;
156
- let resources = contents_dir.join("Resources");
157
- if resources.exists() {
158
- Some(resources)
159
- } else {
160
- None
161
- }
162
- }
163
-
164
- fn bundled_python_root() -> Option<PathBuf> {
165
- let resources = resource_dir()?;
166
- let up = resources.join("_up_");
167
- if up.join("latticeai").join("cli").join("entrypoint.py").is_file() {
168
- Some(up)
169
- } else if resources.join("latticeai").join("cli").join("entrypoint.py").is_file() {
170
- Some(resources)
171
- } else {
172
- None
173
- }
174
- }
175
-
176
- fn desktop_runtime_dir() -> Option<PathBuf> {
177
- let home = env::var("HOME").ok()?;
178
- let dir = PathBuf::from(home).join(".ltcai").join("desktop-runtime");
179
- let _ = std::fs::create_dir_all(&dir);
180
- Some(dir)
181
- }
182
-
183
- fn python_path_env(launch: &BackendLaunch) -> Option<String> {
184
- let mut paths: Vec<PathBuf> = Vec::new();
185
- if let Some(resources) = bundled_python_root() {
186
- paths.push(resources);
187
- }
188
- if let Some(cwd) = &launch.cwd {
189
- if !paths.iter().any(|path| path == cwd) {
190
- paths.push(cwd.clone());
191
- }
192
- }
193
- if let Some(existing) = env::var_os("PYTHONPATH") {
194
- paths.extend(env::split_paths(&existing));
195
- }
196
- env::join_paths(paths).ok().map(|value| value.to_string_lossy().to_string())
197
- }
198
-
199
- fn backend_launch(origin: &str) -> BackendLaunch {
200
- let port = origin.rsplit(':').next().unwrap_or("8765").to_string();
201
- if let Ok(command) = env::var("LATTICEAI_DESKTOP_BACKEND_CMD") {
202
- let parts = split_command(&command);
203
- if let Some(program) = parts.first() {
204
- return BackendLaunch {
205
- command,
206
- program: program.clone(),
207
- args: parts[1..].to_vec(),
208
- cwd: env::var("LATTICEAI_DESKTOP_BACKEND_CWD").ok().map(PathBuf::from),
209
- };
210
- }
211
- }
212
-
213
- for name in ["LTCAI", "ltcai"] {
214
- if let Some(program) = command_in_path(name) {
215
- return BackendLaunch {
216
- command: format!("{program} --host 127.0.0.1 --port {port}"),
217
- program,
218
- args: vec!["--host".into(), "127.0.0.1".into(), "--port".into(), port],
219
- cwd: None,
220
- };
221
- }
222
- }
223
-
224
- for python in python_candidates() {
225
- if module_importable(&python, "latticeai.cli.entrypoint") {
226
- return BackendLaunch {
227
- command: format!("{python} -m latticeai.cli.entrypoint --host 127.0.0.1 --port {port}"),
228
- program: python,
229
- args: vec![
230
- "-m".into(),
231
- "latticeai.cli.entrypoint".into(),
232
- "--host".into(),
233
- "127.0.0.1".into(),
234
- "--port".into(),
235
- port,
236
- ],
237
- cwd: None,
238
- };
57
+ /// Bring the worker up, then point the webview at `{origin}/app`.
58
+ ///
59
+ /// The navigation waits on the worker's own `GET /health` rather than on a TCP
60
+ /// connect: a bound socket only proves something is listening, and landing on
61
+ /// `/app` before the routes exist is what used to show a blank window.
62
+ fn boot(app: &tauri::App) {
63
+ let Some(window) = app.get_webview_window("main") else {
64
+ return;
65
+ };
66
+ let _ = window.set_title("Lattice AI");
67
+ let _ = window.show();
68
+ let _ = window.set_focus();
69
+
70
+ let shell = Arc::clone(&app.state::<Arc<DesktopBackend>>());
71
+ tauri::async_runtime::spawn(async move {
72
+ let status = shell.start().await;
73
+ if !status.healthy && !shell.supervised() {
74
+ // A supervised worker already had its health gate inside `start`;
75
+ // an adopted one (LATTICEAI_DESKTOP_BACKEND_ORIGIN) still needs it.
76
+ // Waiting in both places would double the worst case.
77
+ shell.wait_until_serving().await;
239
78
  }
240
- }
241
-
242
- if let Some(resources) = bundled_python_root() {
243
- if resources.join("latticeai").join("cli").join("entrypoint.py").is_file() {
244
- if let Some(python) = python_candidates().into_iter().next() {
245
- return BackendLaunch {
246
- command: format!("{python} -m latticeai.cli.entrypoint --host 127.0.0.1 --port {port}"),
247
- program: python,
248
- args: vec![
249
- "-m".into(),
250
- "latticeai.cli.entrypoint".into(),
251
- "--host".into(),
252
- "127.0.0.1".into(),
253
- "--port".into(),
254
- port,
255
- ],
256
- cwd: Some(resources),
257
- };
258
- }
79
+ // Navigate either way, as the shell always has: a window showing the
80
+ // browser's own "cannot connect" is more use than a window showing
81
+ // nothing while a worker that will never arrive is waited for.
82
+ if let Ok(url) = tauri::Url::parse(&shell.app_url()) {
83
+ let _ = window.navigate(url);
259
84
  }
260
- }
261
-
262
- BackendLaunch {
263
- command: "unavailable: LTCAI executable or importable latticeai.cli.entrypoint module not found".to_string(),
264
- program: String::new(),
265
- args: Vec::new(),
266
- cwd: None,
267
- }
268
- }
269
-
270
- fn set_error(state: &BackendState, err: Option<String>) {
271
- if let Ok(mut last) = state.last_error.lock() {
272
- *last = err;
85
+ });
86
+ }
87
+
88
+ /// Stop the worker from a synchronous Tauri callback (window close, app exit).
89
+ ///
90
+ /// `block_on` is safe here because these callbacks run on the event loop
91
+ /// thread, outside any async runtime — and the wait is what makes the shutdown
92
+ /// graceful: SIGTERM, then SIGKILL only if the worker ignores it, so the
93
+ /// worker gets to close its SQLite handles instead of being shot mid-write.
94
+ fn stop_worker<R: tauri::Runtime, M: Manager<R>>(manager: &M) {
95
+ if let Some(shell) = manager.try_state::<Arc<DesktopBackend>>() {
96
+ let shell = Arc::clone(&shell);
97
+ tauri::async_runtime::block_on(async move {
98
+ shell.stop().await;
99
+ });
273
100
  }
274
101
  }
275
102
 
276
- fn spawn_backend(origin: &str, launch: &BackendLaunch) -> Result<Option<Child>, String> {
277
- if env::var("LATTICEAI_DESKTOP_NO_BACKEND").is_ok() {
278
- return Ok(None);
279
- }
280
- if launch.program.is_empty() {
281
- return Err("Desktop backend unavailable: LTCAI executable or importable latticeai.cli.entrypoint module not found.".to_string());
282
- }
283
-
284
- let mut cmd = Command::new(&launch.program);
285
- cmd.args(&launch.args)
286
- .env("LATTICEAI_HOST", "127.0.0.1")
287
- .env("LATTICEAI_PORT", origin.rsplit(':').next().unwrap_or("8765"))
288
- .env("LATTICEAI_ENABLE_TELEGRAM", "false")
289
- .env("LATTICEAI_AUTOLOAD_MODELS", "false")
290
- .env("LATTICEAI_ALLOW_MODEL_DOWNLOADS", "false")
291
- .env("LATTICEAI_CORS_ALLOW_NETWORK", "false")
292
- .env("LATTICEAI_ENABLE_EXTERNAL_CONNECTORS", "false")
293
- .env("LATTICEAI_TUNNEL", "false")
294
- .env(
295
- "PATH",
296
- format!(
297
- "{}:{}",
298
- "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin",
299
- env::var("PATH").unwrap_or_default()
300
- ),
301
- );
302
- if let Some(runtime_dir) = desktop_runtime_dir() {
303
- cmd.env("LATTICEAI_AGENT_ROOT", runtime_dir.join("agent_workspace"));
304
- if launch.cwd.is_none() {
305
- cmd.current_dir(&runtime_dir);
306
- }
307
- }
308
- if let Some(python_path) = python_path_env(launch) {
309
- cmd.env("PYTHONPATH", python_path);
310
- }
311
- if let Some(cwd) = &launch.cwd {
312
- cmd.current_dir(cwd);
313
- }
314
- if let Ok(home) = env::var("HOME") {
315
- let log_dir = PathBuf::from(home).join(".ltcai");
316
- let _ = std::fs::create_dir_all(&log_dir);
317
- if let Ok(file) = OpenOptions::new().create(true).append(true).open(log_dir.join("desktop-sidecar.log")) {
318
- cmd.stdout(Stdio::from(file));
319
- } else {
320
- cmd.stdout(Stdio::null());
103
+ fn main() {
104
+ let shell = match DesktopBackend::resolve() {
105
+ Ok(shell) => Arc::new(shell),
106
+ Err(err) => {
107
+ eprintln!("lattice-ai-desktop: cannot prepare the backend: {err}");
108
+ std::process::exit(1);
321
109
  }
322
- if let Ok(file) = OpenOptions::new().create(true).append(true).open(log_dir.join("desktop-sidecar.err.log")) {
323
- cmd.stderr(Stdio::from(file));
110
+ };
111
+ eprintln!(
112
+ "lattice-ai-desktop: backend {} ({})",
113
+ shell.origin(),
114
+ if shell.supervised() {
115
+ "supervised here"
324
116
  } else {
325
- cmd.stderr(Stdio::null());
326
- }
327
- } else {
328
- cmd.stdout(Stdio::null()).stderr(Stdio::null());
329
- }
330
-
331
- cmd.spawn()
332
- .map(Some)
333
- .map_err(|err| format!("Failed to start desktop backend '{}': {}", launch.command, err))
334
- }
335
-
336
- fn kill_backend(state: &BackendState) {
337
- if let Ok(mut child) = state.child.lock() {
338
- if let Some(mut process) = child.take() {
339
- let _ = process.kill();
340
- let _ = process.wait();
341
- }
342
- }
343
- }
344
-
345
- fn status_from_state(state: &BackendState) -> BackendStatus {
346
- let mut running = false;
347
- let mut pid = None;
348
- if let Ok(mut child_slot) = state.child.lock() {
349
- if let Some(child) = child_slot.as_mut() {
350
- match child.try_wait() {
351
- Ok(Some(status)) => {
352
- set_error(state, Some(format!("Desktop backend exited with status {}", status)));
353
- *child_slot = None;
354
- }
355
- Ok(None) => {
356
- running = true;
357
- pid = Some(child.id());
358
- }
359
- Err(err) => set_error(state, Some(format!("Unable to inspect desktop backend: {}", err))),
360
- }
117
+ "external — nothing will be spawned"
361
118
  }
362
- }
363
- let last_error = state.last_error.lock().ok().and_then(|guard| guard.clone());
364
- BackendStatus {
365
- origin: state.origin.clone(),
366
- command: state.command.clone(),
367
- cwd: state.cwd.clone(),
368
- running,
369
- pid,
370
- last_error,
371
- }
372
- }
119
+ );
373
120
 
374
- fn wait_for_backend(origin: &str) {
375
- let host_port = origin
376
- .trim_start_matches("http://")
377
- .trim_start_matches("https://")
378
- .split('/')
379
- .next()
380
- .unwrap_or("127.0.0.1:8765")
381
- .to_string();
382
- for _ in 0..45 {
383
- if std::net::TcpStream::connect(&host_port).is_ok() {
384
- return;
385
- }
386
- std::thread::sleep(std::time::Duration::from_millis(500));
387
- }
388
- }
389
-
390
- fn main() {
391
- let origin = env::var("LATTICEAI_DESKTOP_BACKEND_ORIGIN")
392
- .unwrap_or_else(|_| "http://127.0.0.1:8765".to_string());
393
- let launch = backend_launch(&origin);
394
- let command = launch.command.clone();
395
- let cwd = launch.cwd.as_ref().map(|path| path.to_string_lossy().to_string());
396
- let (child, last_error) = match spawn_backend(&origin, &launch) {
397
- Ok(child) => (child, None),
398
- Err(err) => (None, Some(err)),
399
- };
400
121
  tauri::Builder::default()
401
- .manage(BackendState {
402
- origin,
403
- command,
404
- cwd,
405
- child: Mutex::new(child),
406
- last_error: Mutex::new(last_error),
407
- })
122
+ .manage(shell)
408
123
  .invoke_handler(tauri::generate_handler![
409
124
  backend_origin,
410
125
  backend_status,
@@ -413,27 +128,12 @@ fn main() {
413
128
  select_folder
414
129
  ])
415
130
  .setup(|app| {
416
- if let Some(window) = app.get_webview_window("main") {
417
- let _ = window.set_title("Lattice AI");
418
- let _ = window.show();
419
- let _ = window.set_focus();
420
- let origin = app.state::<BackendState>().origin.clone();
421
- let target = format!("{}/app", origin.trim_end_matches('/'));
422
- let window_for_nav = window.clone();
423
- std::thread::spawn(move || {
424
- wait_for_backend(&origin);
425
- if let Ok(url) = tauri::Url::parse(&target) {
426
- let _ = window_for_nav.navigate(url);
427
- }
428
- });
429
- }
131
+ boot(app);
430
132
  Ok(())
431
133
  })
432
134
  .on_window_event(|window, event| {
433
135
  if matches!(event, tauri::WindowEvent::CloseRequested { .. }) {
434
- if let Some(state) = window.try_state::<BackendState>() {
435
- kill_backend(&state);
436
- }
136
+ stop_worker(window);
437
137
  }
438
138
  })
439
139
  .build(tauri::generate_context!())
@@ -443,9 +143,7 @@ fn main() {
443
143
  event,
444
144
  tauri::RunEvent::ExitRequested { .. } | tauri::RunEvent::Exit
445
145
  ) {
446
- if let Some(state) = app_handle.try_state::<BackendState>() {
447
- kill_backend(&state);
448
- }
146
+ stop_worker(app_handle);
449
147
  }
450
148
  });
451
149
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://schema.tauri.app/config/2",
3
3
  "productName": "Lattice AI",
4
- "version": "11.2.0",
4
+ "version": "11.4.0",
5
5
  "identifier": "ai.lattice.desktop",
6
6
  "build": {
7
7
  "beforeDevCommand": "npm run frontend:dev",
@@ -1,45 +1,49 @@
1
1
  {
2
- "version": "11.2.0",
2
+ "version": "11.4.0",
3
3
  "generated_at": "vite",
4
4
  "entrypoints": {
5
- "app": "/static/app/assets/index-BpYkzcVm.js"
5
+ "app": "/static/app/assets/index-_u5iUHDr.js"
6
6
  },
7
7
  "assets": {
8
8
  "../node_modules/@tauri-apps/api/core.js": "/static/app/assets/core-CwxXejkd.js",
9
- "_BrainSignals-jMYgQ2Ar.js": "/static/app/assets/BrainSignals-jMYgQ2Ar.js",
10
- "_LivingBrain-DBwhto14.js": "/static/app/assets/LivingBrain-DBwhto14.js",
11
- "_ReviewCard-BUhCKRNM.js": "/static/app/assets/ReviewCard-BUhCKRNM.js",
12
- "_arrow-left-Dzwa5zRb.js": "/static/app/assets/arrow-left-Dzwa5zRb.js",
13
- "_bot-Cia42c2h.js": "/static/app/assets/bot-Cia42c2h.js",
14
- "_brain-DJMoqrwx.js": "/static/app/assets/brain-DJMoqrwx.js",
15
- "_button-2j2Ijzgq.js": "/static/app/assets/button-2j2Ijzgq.js",
16
- "_circle-pause-BEFeWpVW.js": "/static/app/assets/circle-pause-BEFeWpVW.js",
17
- "_circle-play-ujXMcHxl.js": "/static/app/assets/circle-play-ujXMcHxl.js",
18
- "_cpu-k4awryFq.js": "/static/app/assets/cpu-k4awryFq.js",
19
- "_download-DFbLJ_ig.js": "/static/app/assets/download-DFbLJ_ig.js",
20
- "_folder-open-7y_b6xkM.js": "/static/app/assets/folder-open-7y_b6xkM.js",
21
- "_hard-drive-Bidh02Kr.js": "/static/app/assets/hard-drive-Bidh02Kr.js",
22
- "_input-DSlJJxRs.js": "/static/app/assets/input-DSlJJxRs.js",
23
- "_permissionCopy-Bpb83Hx9.js": "/static/app/assets/permissionCopy-Bpb83Hx9.js",
24
- "_primitives-BCx6TvfG.js": "/static/app/assets/primitives-BCx6TvfG.js",
25
- "_search-Cgy8cCFJ.js": "/static/app/assets/search-Cgy8cCFJ.js",
26
- "_share-2-BH1M-WNi.js": "/static/app/assets/share-2-BH1M-WNi.js",
27
- "_shield-alert-BlKdBXcG.js": "/static/app/assets/shield-alert-BlKdBXcG.js",
28
- "_textarea-CCWbUfFB.js": "/static/app/assets/textarea-CCWbUfFB.js",
29
- "_useFocusTrap-YdHQ7pJ1.js": "/static/app/assets/useFocusTrap-YdHQ7pJ1.js",
30
- "_useQuery-CXQiwbVT.js": "/static/app/assets/useQuery-CXQiwbVT.js",
31
- "_utils-zqPZJxdx.js": "/static/app/assets/utils-zqPZJxdx.js",
32
- "_workspace-DXTihhfU.js": "/static/app/assets/workspace-DXTihhfU.js",
33
- "index.html": "/static/app/assets/index-BpYkzcVm.js",
34
- "assets/index-DwDl9-8Y.css": "/static/app/assets/index-DwDl9-8Y.css",
35
- "src/components/ProductFlow.tsx": "/static/app/assets/ProductFlow-BHA2cfKI.js",
36
- "src/features/admin/AdminConsole.tsx": "/static/app/assets/AdminConsole-D0u8Tiyj.js",
37
- "src/features/brain/BrainHome.tsx": "/static/app/assets/BrainHome-Ts7G_Ila.js",
38
- "src/features/command/CommandPalette.tsx": "/static/app/assets/CommandPalette-DC0Bzh-I.js",
39
- "src/pages/Act.tsx": "/static/app/assets/Act-AWf0SAKp.js",
40
- "src/pages/Brain.tsx": "/static/app/assets/Brain-tuhI4sOC.js",
41
- "src/pages/Capture.tsx": "/static/app/assets/Capture-CqOSzyPr.js",
42
- "src/pages/Library.tsx": "/static/app/assets/Library-CX-bbhmK.js",
43
- "src/pages/System.tsx": "/static/app/assets/System-Bu2t5hn1.js"
9
+ "_BrainSignals-6yR6ir5t.js": "/static/app/assets/BrainSignals-6yR6ir5t.js",
10
+ "_LivingBrain-Jn1GK0-S.js": "/static/app/assets/LivingBrain-Jn1GK0-S.js",
11
+ "_ReviewCard-6B27X8Vg.js": "/static/app/assets/ReviewCard-6B27X8Vg.js",
12
+ "_arrow-left-DXvKg9U6.js": "/static/app/assets/arrow-left-DXvKg9U6.js",
13
+ "_bot-IM_E_Y12.js": "/static/app/assets/bot-IM_E_Y12.js",
14
+ "_brain-Ci1CkWjM.js": "/static/app/assets/brain-Ci1CkWjM.js",
15
+ "_button-COwyqfHM.js": "/static/app/assets/button-COwyqfHM.js",
16
+ "_circle-check-DfInj-qD.js": "/static/app/assets/circle-check-DfInj-qD.js",
17
+ "_circle-pause-DEM4A1Y5.js": "/static/app/assets/circle-pause-DEM4A1Y5.js",
18
+ "_circle-play-C9djDuLd.js": "/static/app/assets/circle-play-C9djDuLd.js",
19
+ "_cpu-DFdo1gw-.js": "/static/app/assets/cpu-DFdo1gw-.js",
20
+ "_download-SnJL6oqk.js": "/static/app/assets/download-SnJL6oqk.js",
21
+ "_folder-open-CqZeDkjE.js": "/static/app/assets/folder-open-CqZeDkjE.js",
22
+ "_hard-drive-j1jJXYYf.js": "/static/app/assets/hard-drive-j1jJXYYf.js",
23
+ "_input-B0lPdRQZ.js": "/static/app/assets/input-B0lPdRQZ.js",
24
+ "_link-2-CoFbooHS.js": "/static/app/assets/link-2-CoFbooHS.js",
25
+ "_permissionCopy-BsyLxtao.js": "/static/app/assets/permissionCopy-BsyLxtao.js",
26
+ "_primitives-DEbN-d6p.js": "/static/app/assets/primitives-DEbN-d6p.js",
27
+ "_search-BybIWPNd.js": "/static/app/assets/search-BybIWPNd.js",
28
+ "_share-2-CVtZ_ewX.js": "/static/app/assets/share-2-CVtZ_ewX.js",
29
+ "_shield-alert-CBi2GNWM.js": "/static/app/assets/shield-alert-CBi2GNWM.js",
30
+ "_textarea-DNMpB5ih.js": "/static/app/assets/textarea-DNMpB5ih.js",
31
+ "_useFocusTrap-C83t3GXF.js": "/static/app/assets/useFocusTrap-C83t3GXF.js",
32
+ "_useMutation-DtbJDoyz.js": "/static/app/assets/useMutation-DtbJDoyz.js",
33
+ "_useQuery-Dcp1OChy.js": "/static/app/assets/useQuery-Dcp1OChy.js",
34
+ "_utils-BlZr7Pd4.js": "/static/app/assets/utils-BlZr7Pd4.js",
35
+ "_workspace-jJY4RuAV.js": "/static/app/assets/workspace-jJY4RuAV.js",
36
+ "index.html": "/static/app/assets/index-_u5iUHDr.js",
37
+ "assets/index-BLPb5lmE.css": "/static/app/assets/index-BLPb5lmE.css",
38
+ "src/components/ProductFlow.tsx": "/static/app/assets/ProductFlow-B-w1R4Oo.js",
39
+ "src/features/admin/AdminConsole.tsx": "/static/app/assets/AdminConsole-DL3Cr5pL.js",
40
+ "src/features/brain/BrainHome.tsx": "/static/app/assets/BrainHome-DoXRhUUC.js",
41
+ "src/features/command/CommandPalette.tsx": "/static/app/assets/CommandPalette-D2pMxC2I.js",
42
+ "src/pages/Act.tsx": "/static/app/assets/Act-yYpYnn0v.js",
43
+ "src/pages/Brain.tsx": "/static/app/assets/Brain-C1HBN0Wf.js",
44
+ "src/pages/Capture.tsx": "/static/app/assets/Capture-CFIRsFNE.js",
45
+ "src/pages/Chronicle.tsx": "/static/app/assets/Chronicle-BZbEgiwN.js",
46
+ "src/pages/Library.tsx": "/static/app/assets/Library-DwO3yZST.js",
47
+ "src/pages/System.tsx": "/static/app/assets/System-DW8F-2xL.js"
44
48
  }
45
49
  }