ltcai 11.2.0 → 11.5.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.
- package/README.md +50 -53
- package/docs/CHANGELOG.md +87 -0
- package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
- package/docs/DEVELOPMENT.md +1 -1
- package/docs/MULTI_AGENT_RUNTIME.md +1 -1
- package/docs/ONBOARDING.md +1 -1
- package/docs/OPERATIONS.md +6 -2
- package/docs/PERMISSION_MODE.md +1 -1
- package/docs/TRUST_MODEL.md +1 -1
- package/docs/WHY_LATTICE.md +1 -1
- package/docs/kg-schema.md +2 -2
- package/docs/v11.3.0_PLAN.md +202 -0
- package/docs/v11.4.0_RUST_FOUNDATION_PLAN.md +181 -0
- package/docs/v11.5.0_RUST_COMPLETE_PLAN.md +145 -0
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/graph/_kg_common/__init__.py +287 -0
- package/lattice_brain/graph/_kg_common/extraction.py +516 -0
- package/lattice_brain/graph/_kg_common/relations.py +161 -0
- package/lattice_brain/graph/_kg_common/text.py +479 -0
- package/lattice_brain/graph/discovery_index/__init__.py +35 -0
- package/lattice_brain/graph/discovery_index/cleanup.py +182 -0
- package/lattice_brain/graph/discovery_index/extract.py +137 -0
- package/lattice_brain/graph/discovery_index/scan.py +411 -0
- package/lattice_brain/graph/discovery_index/upsert.py +495 -0
- package/lattice_brain/graph/projection/__init__.py +42 -0
- package/lattice_brain/graph/projection/curation.py +500 -0
- package/lattice_brain/graph/{projection.py → projection/v2_schema.py} +15 -477
- package/lattice_brain/graph/retrieval/__init__.py +54 -0
- package/lattice_brain/graph/retrieval/context.py +197 -0
- package/lattice_brain/graph/retrieval/graph_view.py +319 -0
- package/lattice_brain/graph/retrieval/hybrid.py +488 -0
- package/lattice_brain/graph/retrieval/maintenance.py +121 -0
- package/lattice_brain/graph/retrieval/signals.py +95 -0
- package/lattice_brain/graph/retrieval_vector/__init__.py +42 -0
- package/lattice_brain/graph/retrieval_vector/fingerprint.py +97 -0
- package/lattice_brain/graph/retrieval_vector/indexing.py +347 -0
- package/lattice_brain/graph/retrieval_vector/search.py +560 -0
- package/lattice_brain/graph/retrieval_vector/status.py +374 -0
- package/lattice_brain/ingestion/__init__.py +130 -0
- package/lattice_brain/ingestion/_contract.py +90 -0
- package/lattice_brain/ingestion/constants.py +127 -0
- package/lattice_brain/ingestion/folder_scan.py +57 -0
- package/lattice_brain/ingestion/folders.py +258 -0
- package/lattice_brain/ingestion/hashing.py +26 -0
- package/lattice_brain/ingestion/jobs_api.py +107 -0
- package/lattice_brain/ingestion/models.py +80 -0
- package/lattice_brain/ingestion/pipeline.py +486 -0
- package/lattice_brain/ingestion/quality.py +209 -0
- package/lattice_brain/ingestion/routing.py +295 -0
- package/lattice_brain/multimodal/__init__.py +164 -0
- package/lattice_brain/multimodal/audio.py +77 -0
- package/lattice_brain/multimodal/common.py +118 -0
- package/lattice_brain/multimodal/images.py +498 -0
- package/lattice_brain/multimodal/ports.py +169 -0
- package/lattice_brain/multimodal/video.py +410 -0
- package/lattice_brain/portability/__init__.py +90 -0
- package/lattice_brain/portability/_contract.py +42 -0
- package/lattice_brain/portability/backups.py +338 -0
- package/lattice_brain/portability/bundles.py +136 -0
- package/lattice_brain/portability/constants.py +93 -0
- package/lattice_brain/portability/fsops.py +138 -0
- package/lattice_brain/portability/service.py +41 -0
- package/lattice_brain/{portability.py → portability/sharing.py} +44 -677
- package/lattice_brain/runtime/__init__.py +1 -1
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/chronicle.py +63 -0
- package/latticeai/api/index_jobs.py +145 -0
- package/latticeai/core/agent/__init__.py +93 -0
- package/latticeai/core/agent/_contract.py +79 -0
- package/latticeai/core/agent/context.py +57 -0
- package/latticeai/core/agent/deps.py +125 -0
- package/latticeai/core/agent/execution.py +622 -0
- package/latticeai/core/agent/planning.py +145 -0
- package/latticeai/core/agent/recovery.py +157 -0
- package/latticeai/core/agent/runtime.py +210 -0
- package/latticeai/core/agent/verification.py +231 -0
- package/latticeai/core/embedding_providers/__init__.py +151 -0
- package/latticeai/core/embedding_providers/base.py +199 -0
- package/latticeai/core/embedding_providers/captions.py +162 -0
- package/latticeai/core/embedding_providers/profiles.py +126 -0
- package/latticeai/core/embedding_providers/text.py +350 -0
- package/latticeai/core/embedding_providers/vision.py +352 -0
- package/latticeai/core/file_generation/__init__.py +115 -0
- package/latticeai/core/file_generation/bundles.py +76 -0
- package/latticeai/core/file_generation/extraction.py +154 -0
- package/latticeai/core/file_generation/inference.py +235 -0
- package/latticeai/core/file_generation/orchestration.py +152 -0
- package/latticeai/core/file_generation/prompting.py +117 -0
- package/latticeai/core/file_generation/repair.py +114 -0
- package/latticeai/core/file_generation/sanitize.py +61 -0
- package/latticeai/core/file_generation/validation.py +201 -0
- package/latticeai/core/legacy_compatibility.py +1 -1
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/messages.py +14 -0
- package/latticeai/core/workspace_os_constants.py +1 -1
- package/latticeai/integrations/telegram_bot/__init__.py +123 -0
- package/latticeai/integrations/telegram_bot/__main__.py +17 -0
- package/latticeai/integrations/telegram_bot/config.py +86 -0
- package/latticeai/integrations/telegram_bot/dispatch.py +311 -0
- package/latticeai/integrations/telegram_bot/flows.py +478 -0
- package/latticeai/integrations/telegram_bot/helpers.py +322 -0
- package/latticeai/integrations/telegram_bot/screens.py +394 -0
- package/latticeai/models/router/__init__.py +88 -0
- package/latticeai/models/router/_contract.py +66 -0
- package/latticeai/models/router/branding.py +56 -0
- package/latticeai/models/router/catalog.py +69 -0
- package/latticeai/models/router/documents.py +199 -0
- package/latticeai/models/router/errors.py +37 -0
- package/latticeai/models/router/generation.py +258 -0
- package/latticeai/models/router/loading.py +291 -0
- package/latticeai/models/router/local_models.py +85 -0
- package/latticeai/models/router/registry.py +147 -0
- package/latticeai/runtime/build_phases/__init__.py +82 -0
- package/latticeai/runtime/build_phases/features.py +421 -0
- package/latticeai/runtime/build_phases/foundation.py +555 -0
- package/latticeai/runtime/build_phases/web.py +492 -0
- package/latticeai/runtime/runtime_context.py +1 -0
- package/latticeai/services/architecture_readiness.py +48 -19
- package/latticeai/services/brain_intelligence/__init__.py +58 -0
- package/latticeai/services/brain_intelligence/_contract.py +71 -0
- package/latticeai/services/brain_intelligence/consistency.py +193 -0
- package/latticeai/services/brain_intelligence/constants.py +47 -0
- package/latticeai/services/brain_intelligence/digest.py +258 -0
- package/latticeai/services/brain_intelligence/health.py +331 -0
- package/latticeai/services/brain_intelligence/proposals.py +264 -0
- package/latticeai/services/brain_intelligence/sampling.py +84 -0
- package/latticeai/services/brain_intelligence/service.py +48 -0
- package/latticeai/services/chronicle.py +557 -0
- package/latticeai/services/memory_service/__init__.py +52 -0
- package/latticeai/services/memory_service/_contract.py +100 -0
- package/latticeai/services/memory_service/brief.py +431 -0
- package/latticeai/services/memory_service/constants.py +57 -0
- package/latticeai/services/memory_service/maintenance.py +138 -0
- package/latticeai/services/memory_service/manager.py +186 -0
- package/latticeai/services/memory_service/proof.py +136 -0
- package/latticeai/services/memory_service/recall.py +225 -0
- package/latticeai/services/memory_service/service.py +48 -0
- package/latticeai/services/memory_service/stores.py +110 -0
- package/latticeai/services/model_runtime/__init__.py +322 -0
- package/latticeai/services/model_runtime/cloud.py +87 -0
- package/latticeai/services/model_runtime/download.py +282 -0
- package/latticeai/services/model_runtime/engines.py +341 -0
- package/latticeai/services/model_runtime/loading.py +178 -0
- package/latticeai/services/model_runtime/service.py +129 -0
- package/latticeai/services/model_runtime/state.py +131 -0
- package/latticeai/services/model_runtime/status.py +255 -0
- package/latticeai/services/product_readiness.py +15 -7
- package/latticeai/setup/wizard/__init__.py +126 -0
- package/latticeai/setup/wizard/catalog.py +172 -0
- package/latticeai/setup/wizard/detect.py +323 -0
- package/latticeai/setup/wizard/install.py +348 -0
- package/latticeai/setup/wizard/paths.py +168 -0
- package/latticeai/setup/wizard/plans.py +74 -0
- package/latticeai/setup/wizard/recommend.py +320 -0
- package/package.json +6 -2
- package/scripts/bump_version.py +14 -0
- package/scripts/capture_release_evidence.mjs +33 -21
- package/scripts/check_current_release_docs.mjs +1 -1
- package/scripts/check_i18n_namespace_coverage.mjs +41 -4
- package/scripts/check_max_file_lines.mjs +102 -0
- package/scripts/check_release_evidence_bound.mjs +30 -15
- package/scripts/check_screenshot_pixel_delta.py +34 -4
- package/scripts/check_server_i18n.mjs +2 -0
- package/scripts/chunking_parity_corpus.py +449 -0
- package/scripts/generate_agent_parity_fixtures.py +752 -0
- package/scripts/generate_chunking_parity_fixtures.py +259 -0
- package/scripts/generate_rust_parity_fixtures.py +997 -0
- package/scripts/lib/mock_server_fingerprint.mjs +94 -0
- package/scripts/release_screen_claims.json +42 -2
- package/src-tauri/Cargo.lock +404 -3
- package/src-tauri/Cargo.toml +13 -1
- package/src-tauri/src/backend.rs +460 -0
- package/src-tauri/src/folder.rs +33 -0
- package/src-tauri/src/main.rs +109 -399
- package/src-tauri/src/topology.rs +356 -0
- package/src-tauri/tauri.conf.json +1 -1
- package/static/app/asset-manifest.json +41 -37
- package/static/app/assets/Act-CWnxSCgN.js +1 -0
- package/static/app/assets/AdminConsole-BEQYU6kF.js +1 -0
- package/static/app/assets/{Brain-tuhI4sOC.js → Brain-DWu1BhFg.js} +2 -2
- package/static/app/assets/BrainHome-95Hilr9R.js +2 -0
- package/static/app/assets/BrainSignals-QdeqCpAF.js +1 -0
- package/static/app/assets/Capture-BHpCxnzb.js +1 -0
- package/static/app/assets/Chronicle-B4xYKoed.js +1 -0
- package/static/app/assets/CommandPalette-BVXnttSz.js +1 -0
- package/static/app/assets/Library-DgYcHome.js +1 -0
- package/static/app/assets/{LivingBrain-DBwhto14.js → LivingBrain-CrJLDbf7.js} +1 -1
- package/static/app/assets/ProductFlow-DFlScKoJ.js +1 -0
- package/static/app/assets/ReviewCard-Cy5f48Pj.js +3 -0
- package/static/app/assets/System-NF8IfhTa.js +1 -0
- package/static/app/assets/arrow-left-DwkSYrjR.js +1 -0
- package/static/app/assets/{bot-Cia42c2h.js → bot-CucuhLhm.js} +1 -1
- package/static/app/assets/brain-BBnSryW_.js +1 -0
- package/static/app/assets/{button-2j2Ijzgq.js → button-C2GUj2Ai.js} +1 -1
- package/static/app/assets/circle-check-CxOVPwYq.js +1 -0
- package/static/app/assets/{circle-pause-BEFeWpVW.js → circle-pause-CbkWzBmG.js} +1 -1
- package/static/app/assets/{circle-play-ujXMcHxl.js → circle-play-7lEaqHdJ.js} +1 -1
- package/static/app/assets/{cpu-k4awryFq.js → cpu-DAlCXlIy.js} +1 -1
- package/static/app/assets/{download-DFbLJ_ig.js → download-RNhuuJwh.js} +1 -1
- package/static/app/assets/{folder-open-7y_b6xkM.js → folder-open-CLW4odzM.js} +1 -1
- package/static/app/assets/{hard-drive-Bidh02Kr.js → hard-drive-NKEiDIAJ.js} +1 -1
- package/static/app/assets/{index-DwDl9-8Y.css → index-BLPb5lmE.css} +1 -1
- package/static/app/assets/index-DMurvUuR.js +10 -0
- package/static/app/assets/input-D2UhPC1X.js +1 -0
- package/static/app/assets/link-2-6amKbP_P.js +1 -0
- package/static/app/assets/{permissionCopy-Bpb83Hx9.js → permissionCopy-Cu9TZtdR.js} +1 -1
- package/static/app/assets/primitives-gPsccucr.js +1 -0
- package/static/app/assets/search-Cj_TKk_2.js +1 -0
- package/static/app/assets/{share-2-BH1M-WNi.js → share-2-Bau7KkPq.js} +1 -1
- package/static/app/assets/{shield-alert-BlKdBXcG.js → shield-alert-BufNYypi.js} +1 -1
- package/static/app/assets/{textarea-CCWbUfFB.js → textarea-BQnVWhYs.js} +1 -1
- package/static/app/assets/{useFocusTrap-YdHQ7pJ1.js → useFocusTrap-B3_w60si.js} +1 -1
- package/static/app/assets/useMutation-BHhCflT6.js +1 -0
- package/static/app/assets/{useQuery-CXQiwbVT.js → useQuery-rBWfI-5t.js} +1 -1
- package/static/app/assets/utils-V_5-wxr5.js +4 -0
- package/static/app/assets/workspace-K1zjYUHj.js +1 -0
- package/static/app/index.html +4 -4
- package/static/sw.js +1 -1
- package/lattice_brain/graph/_kg_common.py +0 -1331
- package/lattice_brain/graph/discovery_index.py +0 -1141
- package/lattice_brain/graph/retrieval.py +0 -1120
- package/lattice_brain/graph/retrieval_vector.py +0 -1293
- package/lattice_brain/ingestion.py +0 -1525
- package/lattice_brain/multimodal.py +0 -1258
- package/latticeai/core/agent.py +0 -1465
- package/latticeai/core/embedding_providers.py +0 -1196
- package/latticeai/core/file_generation.py +0 -1047
- package/latticeai/integrations/telegram_bot.py +0 -1390
- package/latticeai/models/router.py +0 -1007
- package/latticeai/runtime/build_phases.py +0 -1450
- package/latticeai/services/brain_intelligence.py +0 -1083
- package/latticeai/services/memory_service.py +0 -1177
- package/latticeai/services/model_runtime.py +0 -1281
- package/latticeai/setup/wizard.py +0 -1310
- package/static/app/assets/Act-AWf0SAKp.js +0 -1
- package/static/app/assets/AdminConsole-D0u8Tiyj.js +0 -1
- package/static/app/assets/BrainHome-Ts7G_Ila.js +0 -2
- package/static/app/assets/BrainSignals-jMYgQ2Ar.js +0 -1
- package/static/app/assets/Capture-CqOSzyPr.js +0 -1
- package/static/app/assets/CommandPalette-DC0Bzh-I.js +0 -1
- package/static/app/assets/Library-CX-bbhmK.js +0 -1
- package/static/app/assets/ProductFlow-BHA2cfKI.js +0 -1
- package/static/app/assets/ReviewCard-BUhCKRNM.js +0 -3
- package/static/app/assets/System-Bu2t5hn1.js +0 -1
- package/static/app/assets/arrow-left-Dzwa5zRb.js +0 -1
- package/static/app/assets/brain-DJMoqrwx.js +0 -1
- package/static/app/assets/index-BpYkzcVm.js +0 -10
- package/static/app/assets/input-DSlJJxRs.js +0 -1
- package/static/app/assets/primitives-BCx6TvfG.js +0 -1
- package/static/app/assets/search-Cgy8cCFJ.js +0 -1
- package/static/app/assets/utils-zqPZJxdx.js +0 -4
- package/static/app/assets/workspace-DXTihhfU.js +0 -1
package/src-tauri/src/main.rs
CHANGED
|
@@ -1,410 +1,137 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
+
//! Since 11.5.0 the shell also *serves* that front door: the gateway runs
|
|
12
|
+
//! in-process on the public port and the worker sits behind it (see
|
|
13
|
+
//! [`topology`] for the arrangement and its escape hatches).
|
|
14
|
+
//!
|
|
15
|
+
//! The five command names and their response shapes are unchanged — the
|
|
16
|
+
//! frontend (`frontend/src/api/base.ts`, `frontend/src/api/client.ts`) invokes
|
|
17
|
+
//! them by name and reads `backend_status` as a plain record. The fields added
|
|
18
|
+
//! in 11.5.0 are additive; nothing was renamed or removed.
|
|
19
|
+
|
|
20
|
+
mod backend;
|
|
21
|
+
mod folder;
|
|
22
|
+
mod topology;
|
|
23
|
+
|
|
24
|
+
use std::sync::Arc;
|
|
25
|
+
|
|
26
|
+
use backend::{BackendStatus, DesktopBackend};
|
|
10
27
|
use tauri::{Manager, State};
|
|
11
28
|
|
|
12
|
-
|
|
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
|
-
|
|
29
|
+
/// `backend_origin` → the origin every frontend request is sent to.
|
|
37
30
|
#[tauri::command]
|
|
38
|
-
fn backend_origin(state: State<'_,
|
|
39
|
-
state.origin.
|
|
31
|
+
fn backend_origin(state: State<'_, Arc<DesktopBackend>>) -> String {
|
|
32
|
+
state.origin().to_string()
|
|
40
33
|
}
|
|
41
34
|
|
|
35
|
+
/// `backend_status` → a live snapshot, health probe included.
|
|
36
|
+
///
|
|
37
|
+
/// Async, and therefore `Result`, because Tauri requires it of a command that
|
|
38
|
+
/// borrows state across an await. It never returns `Err`: the frontend keeps
|
|
39
|
+
/// receiving a resolved record exactly as before.
|
|
42
40
|
#[tauri::command]
|
|
43
|
-
fn backend_status(state: State<'_,
|
|
44
|
-
|
|
41
|
+
async fn backend_status(state: State<'_, Arc<DesktopBackend>>) -> Result<BackendStatus, String> {
|
|
42
|
+
Ok(state.status().await)
|
|
45
43
|
}
|
|
46
44
|
|
|
45
|
+
/// `restart_backend` → stop, start, and report what happened.
|
|
47
46
|
#[tauri::command]
|
|
48
|
-
fn restart_backend(state: State<'_,
|
|
49
|
-
|
|
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)
|
|
47
|
+
async fn restart_backend(state: State<'_, Arc<DesktopBackend>>) -> Result<BackendStatus, String> {
|
|
48
|
+
Ok(state.restart().await)
|
|
61
49
|
}
|
|
62
50
|
|
|
51
|
+
/// `shutdown_backend` → stop the worker and suppress restarts.
|
|
63
52
|
#[tauri::command]
|
|
64
|
-
fn shutdown_backend(state: State<'_,
|
|
65
|
-
|
|
66
|
-
status_from_state(&state)
|
|
53
|
+
async fn shutdown_backend(state: State<'_, Arc<DesktopBackend>>) -> Result<BackendStatus, String> {
|
|
54
|
+
Ok(state.stop().await)
|
|
67
55
|
}
|
|
68
56
|
|
|
57
|
+
/// `select_folder` → the native directory picker.
|
|
69
58
|
#[tauri::command]
|
|
70
59
|
fn select_folder() -> Option<String> {
|
|
71
|
-
|
|
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
|
|
60
|
+
folder::select_folder()
|
|
120
61
|
}
|
|
121
62
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
};
|
|
63
|
+
/// Bring the worker up, then point the webview at `{origin}/app`.
|
|
64
|
+
///
|
|
65
|
+
/// The navigation waits on the worker's own `GET /health` rather than on a TCP
|
|
66
|
+
/// connect: a bound socket only proves something is listening, and landing on
|
|
67
|
+
/// `/app` before the routes exist is what used to show a blank window.
|
|
68
|
+
fn boot(app: &tauri::App) {
|
|
69
|
+
let Some(window) = app.get_webview_window("main") else {
|
|
70
|
+
return;
|
|
71
|
+
};
|
|
72
|
+
let _ = window.set_title("Lattice AI");
|
|
73
|
+
let _ = window.show();
|
|
74
|
+
let _ = window.set_focus();
|
|
75
|
+
|
|
76
|
+
let shell = Arc::clone(&app.state::<Arc<DesktopBackend>>());
|
|
77
|
+
tauri::async_runtime::spawn(async move {
|
|
78
|
+
let status = shell.start().await;
|
|
79
|
+
if !status.healthy && !shell.supervised() {
|
|
80
|
+
// A supervised worker already had its health gate inside `start`;
|
|
81
|
+
// an adopted one (LATTICEAI_DESKTOP_BACKEND_ORIGIN) still needs it.
|
|
82
|
+
// Waiting in both places would double the worst case.
|
|
83
|
+
shell.wait_until_serving().await;
|
|
239
84
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
if
|
|
244
|
-
|
|
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
|
-
}
|
|
85
|
+
// Navigate either way, as the shell always has: a window showing the
|
|
86
|
+
// browser's own "cannot connect" is more use than a window showing
|
|
87
|
+
// nothing while a worker that will never arrive is waited for.
|
|
88
|
+
if let Ok(url) = tauri::Url::parse(&shell.app_url()) {
|
|
89
|
+
let _ = window.navigate(url);
|
|
259
90
|
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
fn
|
|
271
|
-
if let
|
|
272
|
-
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/// Stop the worker and the front door from a synchronous Tauri callback
|
|
95
|
+
/// (window close, app exit).
|
|
96
|
+
///
|
|
97
|
+
/// `block_on` is safe here because these callbacks run on the event loop
|
|
98
|
+
/// thread, outside any async runtime — and the wait is what makes the shutdown
|
|
99
|
+
/// graceful: SIGTERM, then SIGKILL only if the worker ignores it, so the
|
|
100
|
+
/// worker gets to close its SQLite handles instead of being shot mid-write.
|
|
101
|
+
fn stop_worker<R: tauri::Runtime, M: Manager<R>>(manager: &M) {
|
|
102
|
+
if let Some(shell) = manager.try_state::<Arc<DesktopBackend>>() {
|
|
103
|
+
let shell = Arc::clone(&shell);
|
|
104
|
+
let stopping = Arc::clone(&shell);
|
|
105
|
+
tauri::async_runtime::block_on(async move {
|
|
106
|
+
stopping.stop().await;
|
|
107
|
+
});
|
|
108
|
+
// The gateway outlives `shutdown_backend` on purpose (so the window can
|
|
109
|
+
// still read `/host/status`), but not the process.
|
|
110
|
+
shell.stop_gateway();
|
|
273
111
|
}
|
|
274
112
|
}
|
|
275
113
|
|
|
276
|
-
fn
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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());
|
|
114
|
+
fn main() {
|
|
115
|
+
let shell = match DesktopBackend::resolve() {
|
|
116
|
+
Ok(shell) => Arc::new(shell),
|
|
117
|
+
Err(err) => {
|
|
118
|
+
eprintln!("lattice-ai-desktop: cannot prepare the backend: {err}");
|
|
119
|
+
std::process::exit(1);
|
|
321
120
|
}
|
|
322
|
-
|
|
323
|
-
|
|
121
|
+
};
|
|
122
|
+
eprintln!(
|
|
123
|
+
"lattice-ai-desktop: backend {} [{}] ({})",
|
|
124
|
+
shell.origin(),
|
|
125
|
+
shell.topology().as_str(),
|
|
126
|
+
if shell.supervised() {
|
|
127
|
+
"supervised here"
|
|
324
128
|
} else {
|
|
325
|
-
|
|
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
|
-
}
|
|
129
|
+
"external — nothing will be spawned"
|
|
361
130
|
}
|
|
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
|
-
}
|
|
131
|
+
);
|
|
373
132
|
|
|
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
133
|
tauri::Builder::default()
|
|
401
|
-
.manage(
|
|
402
|
-
origin,
|
|
403
|
-
command,
|
|
404
|
-
cwd,
|
|
405
|
-
child: Mutex::new(child),
|
|
406
|
-
last_error: Mutex::new(last_error),
|
|
407
|
-
})
|
|
134
|
+
.manage(shell)
|
|
408
135
|
.invoke_handler(tauri::generate_handler![
|
|
409
136
|
backend_origin,
|
|
410
137
|
backend_status,
|
|
@@ -413,27 +140,12 @@ fn main() {
|
|
|
413
140
|
select_folder
|
|
414
141
|
])
|
|
415
142
|
.setup(|app| {
|
|
416
|
-
|
|
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
|
-
}
|
|
143
|
+
boot(app);
|
|
430
144
|
Ok(())
|
|
431
145
|
})
|
|
432
146
|
.on_window_event(|window, event| {
|
|
433
147
|
if matches!(event, tauri::WindowEvent::CloseRequested { .. }) {
|
|
434
|
-
|
|
435
|
-
kill_backend(&state);
|
|
436
|
-
}
|
|
148
|
+
stop_worker(window);
|
|
437
149
|
}
|
|
438
150
|
})
|
|
439
151
|
.build(tauri::generate_context!())
|
|
@@ -443,9 +155,7 @@ fn main() {
|
|
|
443
155
|
event,
|
|
444
156
|
tauri::RunEvent::ExitRequested { .. } | tauri::RunEvent::Exit
|
|
445
157
|
) {
|
|
446
|
-
|
|
447
|
-
kill_backend(&state);
|
|
448
|
-
}
|
|
158
|
+
stop_worker(app_handle);
|
|
449
159
|
}
|
|
450
160
|
});
|
|
451
161
|
}
|