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
@@ -0,0 +1,349 @@
1
+ //! What the desktop shell asks of `lattice-host`.
2
+ //!
3
+ //! Through 11.3.0 this file's job was done inline in `main.rs`: four hand-rolled
4
+ //! command-resolution rules, a `Command::spawn`, a TCP-connect probe, and a
5
+ //! restart that killed the child and spawned another one. All four now live in
6
+ //! the `lattice-host` supervisor, which the `lattice-host` binary uses too — so
7
+ //! the desktop and the CLI front door cannot drift apart, and the desktop
8
+ //! inherits an HTTP `/health` gate, crash restart with backoff, graceful
9
+ //! SIGTERM shutdown, and a port that is scanned rather than assumed.
10
+ //!
11
+ //! What this module adds on top is the two things only the desktop has: the
12
+ //! `LATTICEAI_DESKTOP_BACKEND_ORIGIN` override (front an existing worker,
13
+ //! spawn nothing) and the `LATTICEAI_DESKTOP_NO_BACKEND` kill switch.
14
+
15
+ use std::sync::Arc;
16
+ use std::time::Duration;
17
+
18
+ use lattice_host::supervisor::{
19
+ find_free_port, wait_for_health, HostProbe, Supervisor, SupervisorConfig, SupervisorError,
20
+ SystemProbe, WorkerStatus, DEFAULT_PORT, DEFAULT_SCAN_ATTEMPTS,
21
+ };
22
+ use serde::Serialize;
23
+
24
+ /// Point the shell at an already-running worker. Nothing is spawned, and this
25
+ /// exact string is what `backend_origin` returns and the webview navigates to.
26
+ pub const ORIGIN_ENV: &str = "LATTICEAI_DESKTOP_BACKEND_ORIGIN";
27
+ /// Kill switch: open the window with no worker at all.
28
+ pub const NO_BACKEND_ENV: &str = "LATTICEAI_DESKTOP_NO_BACKEND";
29
+ /// An explicit worker port, honoured verbatim.
30
+ pub const PORT_ENV: &str = "LATTICEAI_PORT";
31
+
32
+ /// The desktop shell's view of the worker.
33
+ ///
34
+ /// The first six fields are the contract `latticeApi.desktopBackendStatus`
35
+ /// has consumed since the first shell and are unchanged in name, type and
36
+ /// meaning. The rest are additive: facts the ad-hoc shell could not know and
37
+ /// the supervisor does.
38
+ #[derive(Debug, Clone, Serialize)]
39
+ pub struct BackendStatus {
40
+ /// Where the frontend should send its requests.
41
+ pub origin: String,
42
+ /// The resolved command line, or why there is not one.
43
+ pub command: String,
44
+ /// Working directory of the worker, when the resolution rule set one.
45
+ pub cwd: Option<String>,
46
+ /// Whether a worker process is alive right now.
47
+ pub running: bool,
48
+ /// Its OS process id, when running.
49
+ pub pid: Option<u32>,
50
+ /// The last failure observed, if any.
51
+ pub last_error: Option<String>,
52
+ /// Whether `GET /health` answered 2xx on the most recent probe.
53
+ pub healthy: bool,
54
+ /// The chosen port — authoritative, not the configured preference.
55
+ pub port: u16,
56
+ /// `false` when the shell only fronts a worker it did not start.
57
+ pub supervised: bool,
58
+ /// How many times the supervisor restarted the worker after a crash.
59
+ pub restarts: u32,
60
+ /// Seconds since the current worker process started.
61
+ pub uptime_seconds: Option<u64>,
62
+ }
63
+
64
+ /// The worker port: an explicit `LATTICEAI_PORT` verbatim, else 4825 scanning
65
+ /// upward.
66
+ ///
67
+ /// An explicit port is an instruction, not a preference — if it is busy the
68
+ /// shell must fail loudly on it rather than quietly answer somewhere else,
69
+ /// because the whole point of setting it is that something else knows the
70
+ /// number.
71
+ pub fn resolve_port(probe: &dyn HostProbe) -> u16 {
72
+ let pinned = probe
73
+ .env_var(PORT_ENV)
74
+ .and_then(|raw| raw.trim().parse::<u16>().ok())
75
+ .filter(|port| *port != 0);
76
+ match pinned {
77
+ Some(port) => port,
78
+ None => find_free_port(DEFAULT_PORT, DEFAULT_SCAN_ATTEMPTS, &[]).unwrap_or(DEFAULT_PORT),
79
+ }
80
+ }
81
+
82
+ /// The port named by an origin, when it names one.
83
+ pub fn origin_port(origin: &str) -> Option<u16> {
84
+ let authority = origin
85
+ .split_once("://")
86
+ .map(|(_, rest)| rest)
87
+ .unwrap_or(origin)
88
+ .split('/')
89
+ .next()
90
+ .unwrap_or_default();
91
+ authority
92
+ .rsplit_once(':')
93
+ .and_then(|(_, port)| port.trim().parse::<u16>().ok())
94
+ .filter(|port| *port != 0)
95
+ }
96
+
97
+ /// The supervisor plus the origin every surface is told about.
98
+ pub struct DesktopBackend {
99
+ origin: String,
100
+ supervisor: Supervisor,
101
+ supervised: bool,
102
+ /// The kill switch was set: there is deliberately no worker to wait for.
103
+ disabled: bool,
104
+ health_deadline: Duration,
105
+ }
106
+
107
+ impl DesktopBackend {
108
+ /// Resolve against the real machine.
109
+ pub fn resolve() -> Result<Self, SupervisorError> {
110
+ Self::with_probe(Arc::new(SystemProbe))
111
+ }
112
+
113
+ /// Resolve against an injected probe.
114
+ pub fn with_probe(probe: Arc<dyn HostProbe>) -> Result<Self, SupervisorError> {
115
+ let override_origin = probe
116
+ .env_var(ORIGIN_ENV)
117
+ .map(|value| value.trim().to_string())
118
+ .filter(|value| !value.is_empty());
119
+ let disabled = probe.env_var(NO_BACKEND_ENV).is_some();
120
+ let port = override_origin
121
+ .as_deref()
122
+ .and_then(origin_port)
123
+ .unwrap_or_else(|| resolve_port(&*probe));
124
+
125
+ // Either override means "do not start anything": one because a worker
126
+ // already exists, the other because the operator asked for none. Only
127
+ // the second means there is nothing to wait for, so an explicit origin
128
+ // wins over the kill switch when both are set.
129
+ let override_origin_absent = override_origin.is_none();
130
+ let supervised = override_origin_absent && !disabled;
131
+ let mut config = SupervisorConfig::new(port).with_system_dirs(&*probe);
132
+ config.supervise = supervised;
133
+ let health_deadline = config.health_deadline;
134
+ let supervisor = Supervisor::with_probe(config, Arc::clone(&probe))?;
135
+ let origin = override_origin.unwrap_or_else(|| supervisor.worker_origin());
136
+ Ok(Self {
137
+ origin,
138
+ supervisor,
139
+ supervised,
140
+ disabled: disabled && override_origin_absent,
141
+ health_deadline,
142
+ })
143
+ }
144
+
145
+ /// Where the frontend and the webview should go.
146
+ pub fn origin(&self) -> &str {
147
+ &self.origin
148
+ }
149
+
150
+ /// `{origin}/app` — the URL the window navigates to once the worker answers.
151
+ pub fn app_url(&self) -> String {
152
+ format!("{}/app", self.origin.trim_end_matches('/'))
153
+ }
154
+
155
+ /// Whether this shell owns the worker process.
156
+ pub fn supervised(&self) -> bool {
157
+ self.supervised
158
+ }
159
+
160
+ /// Start the worker (or, in override mode, adopt the one already running).
161
+ pub async fn start(&self) -> BackendStatus {
162
+ let outcome = self.supervisor.start().await;
163
+ if let Err(err) = &outcome {
164
+ // Not fatal: the supervisor keeps retrying in the background, and
165
+ // the window must open either way so the person can read why.
166
+ eprintln!("lattice-ai-desktop: worker did not come up: {err}");
167
+ }
168
+ self.status().await
169
+ }
170
+
171
+ /// Wait for `GET {origin}/health`, so the webview never lands on a page the
172
+ /// worker has not started serving yet.
173
+ ///
174
+ /// Replaces the old TCP-connect probe, which proved only that *something*
175
+ /// had bound the port. With the kill switch on there is nothing to wait for
176
+ /// and this returns immediately.
177
+ pub async fn wait_until_serving(&self) -> bool {
178
+ if self.disabled {
179
+ return false;
180
+ }
181
+ wait_for_health(
182
+ &self.supervisor.client(),
183
+ &self.origin,
184
+ Duration::from_millis(250),
185
+ self.health_deadline,
186
+ )
187
+ .await
188
+ .is_ok()
189
+ }
190
+
191
+ /// A live snapshot, including a fresh health probe.
192
+ pub async fn status(&self) -> BackendStatus {
193
+ let healthy = self.supervisor.probe_health().await;
194
+ let mut status = self.snapshot();
195
+ status.healthy = healthy;
196
+ status
197
+ }
198
+
199
+ /// Stop the worker and suppress restarts.
200
+ pub async fn stop(&self) -> BackendStatus {
201
+ let status = self.supervisor.stop().await;
202
+ self.render(&status)
203
+ }
204
+
205
+ /// Stop then start again, preserving the restart counter.
206
+ pub async fn restart(&self) -> BackendStatus {
207
+ if let Err(err) = self.supervisor.restart().await {
208
+ eprintln!("lattice-ai-desktop: restart failed: {err}");
209
+ }
210
+ self.status().await
211
+ }
212
+
213
+ fn snapshot(&self) -> BackendStatus {
214
+ self.render(&self.supervisor.status())
215
+ }
216
+
217
+ fn render(&self, status: &WorkerStatus) -> BackendStatus {
218
+ BackendStatus {
219
+ origin: self.origin.clone(),
220
+ command: self.command_text(status),
221
+ cwd: status.cwd.clone(),
222
+ running: status.running,
223
+ pid: status.pid,
224
+ last_error: status.last_error.clone(),
225
+ healthy: status.healthy,
226
+ port: status.port,
227
+ supervised: status.supervised,
228
+ restarts: status.restarts,
229
+ uptime_seconds: status.uptime_seconds,
230
+ }
231
+ }
232
+
233
+ /// `command` has always been a plain string, never null, so an unresolved
234
+ /// worker says *why* rather than disappearing from the panel.
235
+ fn command_text(&self, status: &WorkerStatus) -> String {
236
+ if let Some(command) = &status.command {
237
+ return command.clone();
238
+ }
239
+ if !self.supervised {
240
+ return format!("external worker at {} (not started here)", self.origin);
241
+ }
242
+ match &status.last_error {
243
+ Some(error) => format!("unavailable: {error}"),
244
+ None => "not resolved yet".to_string(),
245
+ }
246
+ }
247
+ }
248
+
249
+ #[cfg(test)]
250
+ mod tests {
251
+ use super::*;
252
+ use lattice_host::supervisor::StaticProbe;
253
+
254
+ #[test]
255
+ fn an_origin_names_its_port_or_admits_it_does_not() {
256
+ assert_eq!(origin_port("http://127.0.0.1:8765"), Some(8765));
257
+ assert_eq!(origin_port("http://127.0.0.1:8765/app"), Some(8765));
258
+ assert_eq!(origin_port("https://[::1]:4825"), Some(4825));
259
+ assert_eq!(origin_port("127.0.0.1:4899"), Some(4899));
260
+ assert_eq!(origin_port("http://localhost"), None);
261
+ assert_eq!(origin_port("http://[::1]"), None);
262
+ assert_eq!(origin_port("http://127.0.0.1:0"), None);
263
+ assert_eq!(origin_port(""), None);
264
+ }
265
+
266
+ #[test]
267
+ fn an_explicit_port_is_honoured_verbatim() {
268
+ let probe = StaticProbe::new().with_env(PORT_ENV, " 8765 ");
269
+ assert_eq!(resolve_port(&probe), 8765);
270
+ }
271
+
272
+ #[test]
273
+ fn without_a_pinned_port_the_scan_starts_at_the_unified_default() {
274
+ let port = resolve_port(&StaticProbe::new());
275
+ assert!(
276
+ (DEFAULT_PORT..DEFAULT_PORT + DEFAULT_SCAN_ATTEMPTS).contains(&port),
277
+ "expected a port scanned upward from {DEFAULT_PORT}, got {port}"
278
+ );
279
+ }
280
+
281
+ #[test]
282
+ fn the_origin_override_pins_the_origin_and_spawns_nothing() {
283
+ let probe = StaticProbe::new().with_env(ORIGIN_ENV, "http://127.0.0.1:9100/");
284
+ let backend = DesktopBackend::with_probe(Arc::new(probe)).expect("backend");
285
+ assert_eq!(backend.origin(), "http://127.0.0.1:9100/");
286
+ assert_eq!(backend.app_url(), "http://127.0.0.1:9100/app");
287
+ assert!(!backend.supervised());
288
+ }
289
+
290
+ #[test]
291
+ fn the_kill_switch_spawns_nothing_but_still_names_an_origin() {
292
+ let probe = StaticProbe::new().with_env(NO_BACKEND_ENV, "1");
293
+ let backend = DesktopBackend::with_probe(Arc::new(probe)).expect("backend");
294
+ assert!(!backend.supervised());
295
+ assert!(
296
+ backend.disabled,
297
+ "nothing will ever answer, so nothing waits"
298
+ );
299
+ assert!(backend.origin().starts_with("http://127.0.0.1:"));
300
+ }
301
+
302
+ #[test]
303
+ fn an_explicit_origin_outranks_the_kill_switch() {
304
+ let probe = StaticProbe::new()
305
+ .with_env(NO_BACKEND_ENV, "1")
306
+ .with_env(ORIGIN_ENV, "http://127.0.0.1:9100");
307
+ let backend = DesktopBackend::with_probe(Arc::new(probe)).expect("backend");
308
+ assert!(!backend.supervised(), "still nothing is spawned");
309
+ assert!(
310
+ !backend.disabled,
311
+ "a worker was named, so the shell waits for it"
312
+ );
313
+ }
314
+
315
+ #[test]
316
+ fn the_default_shell_supervises_its_own_worker() {
317
+ let backend = DesktopBackend::with_probe(Arc::new(StaticProbe::new())).expect("backend");
318
+ assert!(backend.supervised());
319
+ assert!(backend.app_url().ends_with("/app"));
320
+ }
321
+
322
+ #[test]
323
+ fn an_unresolved_command_says_why_instead_of_going_blank() {
324
+ let backend = DesktopBackend::with_probe(Arc::new(StaticProbe::new())).expect("backend");
325
+ let mut status = WorkerStatus::idle(4825, true);
326
+ assert_eq!(backend.command_text(&status), "not resolved yet");
327
+ status.last_error = Some("worker unavailable".into());
328
+ assert_eq!(
329
+ backend.command_text(&status),
330
+ "unavailable: worker unavailable"
331
+ );
332
+ status.command = Some("/usr/bin/python3 -m latticeai.cli.entrypoint".into());
333
+ assert_eq!(
334
+ backend.command_text(&status),
335
+ "/usr/bin/python3 -m latticeai.cli.entrypoint"
336
+ );
337
+ }
338
+
339
+ #[test]
340
+ fn the_status_keeps_every_field_the_frontend_has_ever_read() {
341
+ let backend = DesktopBackend::with_probe(Arc::new(StaticProbe::new())).expect("backend");
342
+ let rendered = backend.render(&WorkerStatus::idle(4825, true));
343
+ let value = serde_json::to_value(&rendered).expect("serialise");
344
+ for key in ["origin", "command", "cwd", "running", "pid", "last_error"] {
345
+ assert!(value.get(key).is_some(), "missing legacy field {key}");
346
+ }
347
+ assert!(value["command"].is_string(), "command is never null");
348
+ }
349
+ }
@@ -0,0 +1,33 @@
1
+ //! The native folder picker behind the `select_folder` command.
2
+ //!
3
+ //! Unchanged from the pre-11.4.0 shell: macOS gets an AppleScript `choose
4
+ //! folder` dialog, every other platform answers `None` and the frontend falls
5
+ //! back to its Electron bridge or to typing a path.
6
+
7
+ /// Ask the user for a directory. `None` when they cancel, or when the platform
8
+ /// has no picker here.
9
+ #[cfg(target_os = "macos")]
10
+ pub fn select_folder() -> Option<String> {
11
+ let output = std::process::Command::new("osascript")
12
+ .args([
13
+ "-e",
14
+ r#"POSIX path of (choose folder with prompt "Choose a folder for Lattice AI")"#,
15
+ ])
16
+ .output()
17
+ .ok()?;
18
+ if !output.status.success() {
19
+ return None;
20
+ }
21
+ let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
22
+ if path.is_empty() {
23
+ None
24
+ } else {
25
+ Some(path)
26
+ }
27
+ }
28
+
29
+ /// No native picker outside macOS; the caller falls back.
30
+ #[cfg(not(target_os = "macos"))]
31
+ pub fn select_folder() -> Option<String> {
32
+ None
33
+ }