ltcai 4.4.0 → 4.5.1

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 (61) hide show
  1. package/README.md +46 -18
  2. package/docs/CHANGELOG.md +85 -0
  3. package/docs/V4_5_0_GEMMA_RUNTIME_COMPATIBILITY_REPORT.md +49 -0
  4. package/docs/V4_5_0_GRAPH_UX_REPORT.md +34 -0
  5. package/docs/V4_5_0_MODEL_RUNTIME_UX_REPORT.md +40 -0
  6. package/docs/V4_5_0_ONBOARDING_REPORT.md +31 -0
  7. package/docs/V4_5_0_PRODUCT_EXPERIENCE_RECOVERY_REPORT.md +49 -0
  8. package/docs/V4_5_0_VALIDATION_REPORT.md +60 -0
  9. package/docs/V4_5_1_GRAPH_EXPERIENCE_REPORT.md +33 -0
  10. package/docs/V4_5_1_MODEL_EXPERIENCE_REPORT.md +37 -0
  11. package/docs/V4_5_1_NAVIGATION_REPORT.md +37 -0
  12. package/docs/V4_5_1_ONBOARDING_REPORT.md +29 -0
  13. package/docs/V4_5_1_PRODUCT_REIMAGINING_REPORT.md +61 -0
  14. package/docs/V4_5_1_RC_ARTIFACTS.md +44 -0
  15. package/docs/V4_5_1_UX_REPORT.md +45 -0
  16. package/docs/V4_5_1_VALIDATION_REPORT.md +54 -0
  17. package/docs/V4_5_1_VISUAL_DESIGN_REPORT.md +30 -0
  18. package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +16 -16
  19. package/docs/architecture.md +8 -4
  20. package/frontend/src/App.tsx +152 -91
  21. package/frontend/src/api/client.ts +83 -1
  22. package/frontend/src/components/FirstRunGuide.tsx +99 -0
  23. package/frontend/src/components/primitives.tsx +131 -25
  24. package/frontend/src/components/ui/badge.tsx +2 -2
  25. package/frontend/src/components/ui/button.tsx +7 -7
  26. package/frontend/src/components/ui/card.tsx +5 -5
  27. package/frontend/src/components/ui/input.tsx +1 -1
  28. package/frontend/src/components/ui/textarea.tsx +1 -1
  29. package/frontend/src/pages/Act.tsx +58 -28
  30. package/frontend/src/pages/Ask.tsx +51 -19
  31. package/frontend/src/pages/Brain.tsx +60 -42
  32. package/frontend/src/pages/Capture.tsx +24 -24
  33. package/frontend/src/pages/Library.tsx +222 -32
  34. package/frontend/src/pages/System.tsx +56 -34
  35. package/frontend/src/routes.ts +15 -13
  36. package/frontend/src/store/appStore.ts +8 -1
  37. package/frontend/src/styles.css +666 -36
  38. package/lattice_brain/__init__.py +1 -1
  39. package/lattice_brain/runtime/multi_agent.py +1 -1
  40. package/latticeai/__init__.py +1 -1
  41. package/latticeai/api/models.py +107 -18
  42. package/latticeai/core/marketplace.py +1 -1
  43. package/latticeai/core/model_compat.py +250 -0
  44. package/latticeai/core/workspace_os.py +1 -1
  45. package/latticeai/models/router.py +136 -32
  46. package/latticeai/services/model_catalog.py +2 -2
  47. package/latticeai/services/model_recommendation.py +8 -1
  48. package/latticeai/services/model_runtime.py +18 -3
  49. package/package.json +1 -1
  50. package/scripts/build_frontend_assets.mjs +12 -1
  51. package/src-tauri/Cargo.lock +1 -1
  52. package/src-tauri/Cargo.toml +1 -1
  53. package/src-tauri/tauri.conf.json +1 -1
  54. package/static/app/asset-manifest.json +5 -5
  55. package/static/app/assets/index-3G8qcrIS.js +336 -0
  56. package/static/app/assets/index-3G8qcrIS.js.map +1 -0
  57. package/static/app/assets/index-C0wYZp7k.css +2 -0
  58. package/static/app/index.html +2 -2
  59. package/static/app/assets/index-CHHal8Zl.css +0 -2
  60. package/static/app/assets/index-pdzil9ac.js +0 -333
  61. package/static/app/assets/index-pdzil9ac.js.map +0 -1
@@ -1,12 +1,13 @@
1
1
  import * as React from "react";
2
2
  import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
3
- import { Boxes, Cpu, PackagePlus, Plug, Puzzle } from "lucide-react";
3
+ import { Boxes, CheckCircle2, Cpu, Download, PackagePlus, PlayCircle, Plug, ShieldAlert } from "lucide-react";
4
4
  import { latticeApi } from "@/api/client";
5
- import { ActionButton, DataPanel, EntityList, OperationResult, StructuredView, Tabs } from "@/components/primitives";
5
+ import { ActionButton, DataPanel, EmptyState, EntityList, OperationResult, StatGrid, StructuredView, Tabs, ValuePreview } from "@/components/primitives";
6
6
  import { Badge } from "@/components/ui/badge";
7
7
  import { Button } from "@/components/ui/button";
8
8
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
9
9
  import { Input } from "@/components/ui/input";
10
+ import { useAppStore } from "@/store/appStore";
10
11
  import { asArray } from "@/lib/utils";
11
12
 
12
13
  type LibraryTab = "models" | "skills" | "mcp" | "marketplace";
@@ -14,23 +15,25 @@ type LibraryTab = "models" | "skills" | "mcp" | "marketplace";
14
15
  const tabs: Array<{ id: LibraryTab; label: string }> = [
15
16
  { id: "models", label: "Models" },
16
17
  { id: "skills", label: "Skills" },
17
- { id: "mcp", label: "MCP" },
18
+ { id: "mcp", label: "Connections" },
18
19
  { id: "marketplace", label: "Marketplace" },
19
20
  ];
20
21
 
21
22
  export function LibraryPage({ initialTab }: { initialTab?: string }) {
23
+ const mode = useAppStore((state) => state.mode);
22
24
  const [tab, setTab] = React.useState<LibraryTab>((initialTab as LibraryTab) || "models");
23
25
  React.useEffect(() => {
24
26
  if (tabs.some((item) => item.id === initialTab)) setTab(initialTab as LibraryTab);
25
27
  }, [initialTab]);
28
+ const visibleTabs = tabs.map((item) => item.id === "mcp" && mode !== "basic" ? { ...item, label: "MCP / tools" } : item);
26
29
  return (
27
- <div className="space-y-4">
28
- <header>
29
- <div className="flex items-center gap-2 text-sm text-primary"><Boxes className="h-4 w-4" /> Replaceable runtime assets</div>
30
- <h1 className="mt-2 text-3xl font-semibold">Library</h1>
31
- <p className="mt-2 max-w-3xl text-sm text-muted-foreground">Models, skills, MCP servers, plugins, and templates are managed by local backend registries.</p>
30
+ <div className="space-y-5">
31
+ <header className="page-hero">
32
+ <div className="page-kicker"><Boxes className="h-4 w-4" /> Library</div>
33
+ <h1 className="page-title">Choose what powers Lattice.</h1>
34
+ <p className="page-copy">Pick a private local model, add skills, and connect tools without learning runtime internals.</p>
32
35
  </header>
33
- <Tabs tabs={tabs} value={tab} onChange={(id) => setTab(id as LibraryTab)} />
36
+ <Tabs tabs={visibleTabs} value={tab} onChange={(id) => setTab(id as LibraryTab)} />
34
37
  {tab === "models" ? <ModelsPanel /> : null}
35
38
  {tab === "skills" ? <SkillsPanel /> : null}
36
39
  {tab === "mcp" ? <McpPanel /> : null}
@@ -40,15 +43,115 @@ export function LibraryPage({ initialTab }: { initialTab?: string }) {
40
43
  }
41
44
 
42
45
  function ModelsPanel() {
46
+ const qc = useQueryClient();
47
+ const mode = useAppStore((state) => state.mode);
43
48
  const models = useQuery({ queryKey: ["models"], queryFn: latticeApi.models });
49
+ const recs = useQuery({ queryKey: ["modelRecommendations", "local_mlx"], queryFn: () => latticeApi.modelRecommendations("local_mlx") });
44
50
  const emb = useQuery({ queryKey: ["embeddings"], queryFn: latticeApi.embeddingsStatus });
51
+ const [consent, setConsent] = React.useState(false);
52
+ const [activeModel, setActiveModel] = React.useState<string | null>(null);
53
+ const [progress, setProgress] = React.useState<Record<string, unknown>[]>([]);
54
+ const [lastResult, setLastResult] = React.useState<Record<string, unknown> | null>(null);
55
+ const [lastError, setLastError] = React.useState<Record<string, unknown> | null>(null);
56
+ const [busy, setBusy] = React.useState(false);
45
57
  const catalog = [
46
58
  ...asArray<Record<string, unknown>>((models.data?.data as Record<string, unknown>)?.catalog),
47
59
  ...asArray<Record<string, unknown>>((models.data?.data as Record<string, unknown>)?.recommended),
48
60
  ];
61
+ const recommendationRows = asArray<Record<string, unknown>>(
62
+ ((recs.data?.data as Record<string, unknown>)?.recommendations as Record<string, unknown> | undefined)?.models,
63
+ );
64
+ const recommendationById = new Map(recommendationRows.map((item) => [String(item.id), item]));
65
+ const loadedIds = asArray<string>((models.data?.data as Record<string, unknown> | undefined)?.loaded);
66
+ const currentId = String((models.data?.data as Record<string, unknown> | undefined)?.current || "");
67
+ const current = catalog.find((model) => loadedIds.includes(String(model.id)) || String(model.id) === currentId);
68
+ const topPick = (((recs.data?.data as Record<string, unknown> | undefined)?.recommendations as Record<string, unknown> | undefined)?.top_pick || null) as Record<string, unknown> | null;
69
+ const latestProgress = progress[progress.length - 1] || null;
70
+
71
+ const modelMessage = React.useCallback((message: unknown) => {
72
+ const text = String(message || "This model is not ready to load yet.");
73
+ if (mode !== "basic") return text;
74
+ return text
75
+ .replace(/gemma4_unified/gi, "this local model format")
76
+ .replace(/mlx[-_ ]?vlm|mlx[-_ ]?lm|local_mlx|\bmlx\b|\bgguf\b|\bollama\b|hugging face/gi, "local model support")
77
+ .replace(/runtime/gi, "model support")
78
+ .replace(/model_type/gi, "model format")
79
+ .replace(/this local model format local model support format/gi, "this local model format")
80
+ .replace(/local model support model support/gi, "local model support");
81
+ }, [mode]);
82
+
83
+ async function prepareModel(loadId: string, engine: string, allowDownload: boolean) {
84
+ setBusy(true);
85
+ setActiveModel(loadId);
86
+ setProgress([]);
87
+ setLastResult(null);
88
+ setLastError(null);
89
+ const result = await latticeApi.streamModelPrepare(
90
+ { model: loadId, engine: engine || "local_mlx", allow_download: allowDownload },
91
+ {
92
+ onProgress: (event) => setProgress((items) => [...items.slice(-8), event]),
93
+ onDone: (event) => setLastResult(event),
94
+ onError: (event) => setLastError(event),
95
+ },
96
+ );
97
+ setBusy(false);
98
+ await qc.invalidateQueries({ queryKey: ["models"] });
99
+ await qc.invalidateQueries({ queryKey: ["modelRecommendations", "local_mlx"] });
100
+ if (!result.ok && !lastError) setLastError(result.data as Record<string, unknown>);
101
+ }
102
+
49
103
  return (
50
104
  <div className="grid gap-4 xl:grid-cols-[1.2fr_0.8fr]">
51
- <DataPanel title="Model catalog" result={models.data}>
105
+ <div className="space-y-4">
106
+ <DataPanel title="Guided model setup" description="Analyze this Mac, recommend a model, install only with consent, validate it, then load it." result={recs.data}>
107
+ {(data) => {
108
+ const recommendation = (data as Record<string, unknown>).recommendations as Record<string, unknown> | undefined;
109
+ const profile = (data as Record<string, unknown>).profile as Record<string, unknown> | undefined;
110
+ return (
111
+ <div className="space-y-4">
112
+ <StatGrid stats={[
113
+ { label: "Computer", value: profile?.os ? `${String(profile.os)} ${String(profile.arch || "")}` : "detected" },
114
+ { label: "Memory", value: recommendation?.ram_gb ? `${String(recommendation.ram_gb)} GB` : "checking" },
115
+ { label: "Top pick", value: topPick?.name || topPick?.id || "choose below" },
116
+ { label: "Current", value: current?.name || currentId || "none" },
117
+ ]} />
118
+ <div className="grid gap-2 md:grid-cols-3 xl:grid-cols-6">
119
+ {[
120
+ ["Environment Analysis", true, Cpu],
121
+ ["Recommended Models", Boolean(topPick || catalog.length), CheckCircle2],
122
+ ["Install", Boolean(current || latestProgress?.stage === "engine"), PackagePlus],
123
+ ["Download Progress", Boolean(current || latestProgress?.stage === "download"), Download],
124
+ ["Validate", Boolean(current || latestProgress?.stage === "smoke_test"), ShieldAlert],
125
+ ["Load / Ready", Boolean(current || lastResult), PlayCircle],
126
+ ].map(([label, done, Icon]) => (
127
+ <div key={String(label)} className="rounded-lg border border-border bg-background/55 p-3">
128
+ {React.createElement(Icon as typeof Cpu, { className: "h-4 w-4 text-primary" })}
129
+ <div className="mt-2 text-sm font-medium">{String(label)}</div>
130
+ <Badge variant={done ? "success" : "muted"}>{done ? "ready" : "pending"}</Badge>
131
+ </div>
132
+ ))}
133
+ </div>
134
+ <label className="flex items-start gap-2 rounded-lg border border-border bg-background/55 p-3 text-sm leading-6">
135
+ <input className="mt-1" type="checkbox" checked={consent} onChange={(event) => setConsent(event.target.checked)} />
136
+ <span>
137
+ Allow Lattice to install a missing local model component or download model files for this action.
138
+ </span>
139
+ </label>
140
+ {latestProgress ? (
141
+ <div className="rounded-lg border border-border bg-background/55 p-3 text-sm">
142
+ <div className="font-medium">{String(latestProgress.message || "Preparing model")}</div>
143
+ <div className="mt-2 h-2 overflow-hidden rounded-full bg-muted">
144
+ <div className="h-full bg-primary" style={{ width: `${Number(latestProgress.percent || 8)}%` }} />
145
+ </div>
146
+ {latestProgress.detail ? <div className="mt-2 text-xs text-muted-foreground">{String(latestProgress.detail)}</div> : null}
147
+ </div>
148
+ ) : null}
149
+ {lastError ? <ModelRecovery error={lastError} /> : null}
150
+ </div>
151
+ );
152
+ }}
153
+ </DataPanel>
154
+ <DataPanel title="Recommended models" result={models.data}>
52
155
  {(data) => (
53
156
  <div className="grid gap-2">
54
157
  {(catalog.length ? catalog : asArray<Record<string, unknown>>((data as Record<string, unknown>).loaded)).slice(0, 14).map((model, index) => {
@@ -56,34 +159,120 @@ function ModelsPanel() {
56
159
  const loaded = asArray<string>((data as Record<string, unknown>).loaded).includes(id) || (data as Record<string, unknown>).current === id || model.state === "loaded";
57
160
  const loadId = String(model.recommended_load_id || id);
58
161
  const engine = String(model.recommended_engine || model.engine || "");
59
- const loadAvailable = Boolean(model.load_available) || loaded;
162
+ const recommendation = recommendationById.get(id) || recommendationById.get(loadId) || {};
163
+ const compatibility = (model.runtime_compatibility || recommendation.runtime_compatibility || {}) as Record<string, unknown>;
164
+ const fallbackAvailable = String(compatibility.status || "") === "fallback_available";
165
+ const unsupported = model.load_status === "unsupported" || compatibility.supported === false;
166
+ const downloadRequired = Boolean(model.download_required);
167
+ const loadAvailable = (Boolean(model.load_available) || loaded) && !unsupported;
60
168
  const loadStatus = String(model.load_status || (loaded ? "loaded" : "unavailable"));
61
- const unavailableReason = String(model.unavailable_reason || "Unavailable until the backend reports a local model/runtime ready.");
169
+ const unavailableReason = modelMessage(model.unavailable_reason || "This model is not ready to load yet.");
170
+ const runtimeLabel = String(model.runtime_label || compatibility.preferred_runtime || engine || "local_mlx");
171
+ const actionLabel = String(compatibility.action || loadStatus.replace(/_/g, " "));
172
+ const badgeLabel = unsupported && mode === "basic" ? "needs attention" : unsupported ? actionLabel : loadStatus;
173
+ const canPrepare = loadAvailable || downloadRequired;
62
174
  return (
63
- <div key={id} className="flex flex-wrap items-center justify-between gap-3 rounded-md border border-border bg-background p-3">
64
- <div>
65
- <div className="font-medium">{String(model.name || id)}</div>
66
- <div className="text-sm text-muted-foreground">{String(model.family || model.engine || model.recommended_engine || "local")}</div>
67
- {!loaded && !loadAvailable ? <div className="mt-1 text-xs text-muted-foreground">{unavailableReason}</div> : null}
175
+ <div key={id} className="grid gap-3 rounded-lg border border-border bg-background/55 p-4 md:grid-cols-[1fr_auto]">
176
+ <div className="min-w-0">
177
+ <div className="flex flex-wrap items-center gap-2">
178
+ <div className="text-base font-semibold">{String(model.name || id)}</div>
179
+ {topPick?.id === id ? <Badge variant="success">recommended</Badge> : null}
180
+ </div>
181
+ <div className="mt-1 text-sm text-muted-foreground">
182
+ {mode === "basic"
183
+ ? [
184
+ modelMessage(model.family || recommendation.family || "Local model"),
185
+ /mlx|gguf|ollama/i.test(String(model.size || recommendation.size || "")) ? "" : model.size || recommendation.size,
186
+ ].filter(Boolean).map(String).join(" · ")
187
+ : [model.family || recommendation.family || "local", model.size || recommendation.size].filter(Boolean).map(String).join(" · ")}
188
+ </div>
189
+ {unsupported ? (
190
+ <div className="mt-3 rounded-lg border border-amber-500/30 bg-amber-500/10 p-3 text-sm">
191
+ <div className="font-medium">{mode === "basic" ? "Needs attention before loading" : actionLabel}</div>
192
+ <div className="text-muted-foreground">{modelMessage(compatibility.user_message || unavailableReason)}</div>
193
+ </div>
194
+ ) : fallbackAvailable ? (
195
+ <div className="mt-3 rounded-lg border border-amber-500/30 bg-amber-500/10 p-3 text-sm">
196
+ <div className="font-medium">{mode === "basic" ? "Compatible loading path available" : "Runtime fallback available"}</div>
197
+ <div className="text-muted-foreground">{modelMessage(compatibility.user_message || "Lattice will try the compatible local runtime path before showing this model as unsupported.")}</div>
198
+ </div>
199
+ ) : !loaded && !loadAvailable ? <div className="mt-1 text-xs text-muted-foreground">{unavailableReason}</div> : null}
200
+ {mode !== "basic" ? (
201
+ <div className="mt-2 text-xs text-muted-foreground">
202
+ {runtimeLabel} · {loadId}
203
+ </div>
204
+ ) : null}
205
+ {unsupported || fallbackAvailable ? <AlternativeModels compatibility={compatibility} /> : null}
68
206
  </div>
69
- <div className="flex items-center gap-2">
70
- <Badge variant={loaded ? "success" : loadAvailable ? "muted" : "warning"}>{loaded ? "loaded" : loadStatus}</Badge>
71
- <ActionButton
72
- label={loaded ? "Unload" : "Load"}
73
- action={() => loaded ? latticeApi.unloadModel(loadId) : latticeApi.loadModel(loadId, engine, false)}
74
- invalidate={["models"]}
75
- disabled={!loaded && !loadAvailable}
76
- />
207
+ <div className="flex flex-wrap items-center gap-2 md:justify-end">
208
+ <Badge variant={loaded ? "success" : loadAvailable ? "muted" : "warning"}>{loaded ? "loaded" : badgeLabel}</Badge>
209
+ {loaded ? (
210
+ <ActionButton label="Unload" action={() => latticeApi.unloadModel(loadId)} invalidate={["models"]} />
211
+ ) : (
212
+ <Button
213
+ variant="outline"
214
+ disabled={busy || unsupported || !canPrepare || (downloadRequired && !consent)}
215
+ onClick={() => prepareModel(loadId, engine || "local_mlx", consent)}
216
+ >
217
+ {activeModel === loadId && busy ? "Preparing" : downloadRequired ? "Install & Load" : "Validate & Load"}
218
+ </Button>
219
+ )}
77
220
  </div>
78
221
  </div>
79
222
  );
80
223
  })}
81
224
  </div>
82
225
  )}
83
- </DataPanel>
84
- <DataPanel title="Embedding provider" result={emb.data}>
85
- {(data) => <StructuredView value={data} />}
86
- </DataPanel>
226
+ </DataPanel>
227
+ </div>
228
+ <div className="space-y-4">
229
+ <DataPanel title={mode === "basic" ? "Memory search support" : "Embedding provider"} result={emb.data}>
230
+ {(data) => mode === "basic" ? <ValuePreview value={(data as Record<string, unknown>).state || "ready"} /> : <StructuredView value={data} />}
231
+ </DataPanel>
232
+ <DataPanel title="Model validation" result={models.data}>
233
+ {(data) => {
234
+ const profiles = asArray<Record<string, unknown>>((data as Record<string, unknown>).compat_profiles);
235
+ return profiles.length ? (
236
+ <EntityList items={profiles.map((profile) => ({
237
+ ...profile,
238
+ name: mode === "basic" ? profile.name || profile.display_name || "Loaded model" : profile.name || profile.display_name || profile.model_id,
239
+ status: profile.quality_status || profile.load_status || "checked",
240
+ }))} titleKey="name" metaKey="status" limit={6} />
241
+ ) : (
242
+ <EmptyState title="No model checked yet" detail="Load a model to confirm it can answer before you start using it." />
243
+ );
244
+ }}
245
+ </DataPanel>
246
+ </div>
247
+ </div>
248
+ );
249
+ }
250
+
251
+ function AlternativeModels({ compatibility }: { compatibility: Record<string, unknown> }) {
252
+ const mode = useAppStore((state) => state.mode);
253
+ const alternatives = asArray<Record<string, unknown>>(compatibility.alternatives);
254
+ if (!alternatives.length) return null;
255
+ return (
256
+ <div className="mt-2 flex flex-wrap gap-1">
257
+ {alternatives.slice(0, 3).map((item) => (
258
+ <Badge key={String(item.id || item.name)} variant="muted">
259
+ {mode === "basic" && /mlx|gguf|ollama|lm studio/i.test(String(item.name || item.id)) ? "Compatible alternative" : String(item.name || item.id)}
260
+ </Badge>
261
+ ))}
262
+ </div>
263
+ );
264
+ }
265
+
266
+ function ModelRecovery({ error }: { error: Record<string, unknown> }) {
267
+ const guidance = asArray<string>(error.recovery_guidance);
268
+ return (
269
+ <div className="rounded-lg border border-amber-500/30 bg-amber-500/10 p-3 text-sm">
270
+ <div className="font-medium">{String(error.user_message || "Model setup needs attention.")}</div>
271
+ {guidance.length ? (
272
+ <ul className="mt-2 list-inside list-disc text-muted-foreground">
273
+ {guidance.slice(0, 3).map((item) => <li key={item}>{item}</li>)}
274
+ </ul>
275
+ ) : null}
87
276
  </div>
88
277
  );
89
278
  }
@@ -133,18 +322,19 @@ function SkillsPanel() {
133
322
  }
134
323
 
135
324
  function McpPanel() {
325
+ const mode = useAppStore((state) => state.mode);
136
326
  const [query, setQuery] = React.useState("github");
137
327
  const tools = useQuery({ queryKey: ["mcpTools"], queryFn: latticeApi.mcpTools });
138
328
  const rec = useMutation({ mutationFn: () => latticeApi.mcpRecommend(query) });
139
329
  return (
140
330
  <div className="grid gap-4 xl:grid-cols-[1fr_1fr]">
141
- <DataPanel title="MCP tools" result={tools.data}>
331
+ <DataPanel title={mode === "basic" ? "Tool connections" : "MCP tools"} result={tools.data}>
142
332
  {(data) => <EntityList items={(data as Record<string, unknown>).tools || (data as Record<string, unknown>).installed_mcps} titleKey="name" metaKey="status" />}
143
333
  </DataPanel>
144
334
  <Card>
145
335
  <CardHeader>
146
336
  <CardTitle className="flex items-center gap-2"><Plug className="h-4 w-4" /> Recommend connector</CardTitle>
147
- <CardDescription>Calls `/mcp/recommend`; returned installability depends on available connectors.</CardDescription>
337
+ <CardDescription>Describe what you want to connect and Lattice will suggest a connector.</CardDescription>
148
338
  </CardHeader>
149
339
  <CardContent className="space-y-3">
150
340
  <div className="flex gap-2">
@@ -176,7 +366,7 @@ function MarketplacePanel() {
176
366
  <Card className="xl:col-span-3">
177
367
  <CardHeader>
178
368
  <CardTitle className="flex items-center gap-2"><PackagePlus className="h-4 w-4" /> Template install</CardTitle>
179
- <CardDescription>Install controls are enabled only for template records returned by the backend.</CardDescription>
369
+ <CardDescription>Start from a reusable workspace pattern.</CardDescription>
180
370
  </CardHeader>
181
371
  <CardContent>
182
372
  {asArray<Record<string, unknown>>((templates.data?.data as Record<string, unknown>)?.templates).length ? (
@@ -2,7 +2,7 @@ import * as React from "react";
2
2
  import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
3
3
  import { Network, ShieldCheck, UserCircle, Users } from "lucide-react";
4
4
  import { latticeApi } from "@/api/client";
5
- import { ActionButton, DataPanel, EmptyState, EntityList, KeyValueList, OperationResult, StatGrid, StructuredView, Tabs } from "@/components/primitives";
5
+ import { ActionButton, DataPanel, EmptyState, EntityList, KeyValueList, ModeGate, OperationResult, StatGrid, StructuredView, Tabs } from "@/components/primitives";
6
6
  import { Badge } from "@/components/ui/badge";
7
7
  import { Button } from "@/components/ui/button";
8
8
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
@@ -14,27 +14,28 @@ type SystemTab = "account" | "workspaces" | "snapshots" | "activity" | "network"
14
14
 
15
15
  const tabs: Array<{ id: SystemTab; label: string }> = [
16
16
  { id: "account", label: "Account" },
17
- { id: "workspaces", label: "Workspaces" },
17
+ { id: "workspaces", label: "Spaces" },
18
18
  { id: "snapshots", label: "Snapshots" },
19
- { id: "activity", label: "Activity" },
20
- { id: "network", label: "Network" },
21
- { id: "settings", label: "Settings" },
19
+ { id: "activity", label: "History" },
20
+ { id: "network", label: "Devices" },
21
+ { id: "settings", label: "Preferences" },
22
22
  { id: "admin", label: "Admin" },
23
23
  ];
24
24
 
25
25
  export function SystemPage({ initialTab }: { initialTab?: string }) {
26
+ const mode = useAppStore((state) => state.mode);
26
27
  const [tab, setTab] = React.useState<SystemTab>((initialTab as SystemTab) || "account");
27
28
  React.useEffect(() => {
28
29
  if (tabs.some((item) => item.id === initialTab)) setTab(initialTab as SystemTab);
29
30
  }, [initialTab]);
30
31
  return (
31
- <div className="space-y-4">
32
- <header>
33
- <div className="flex items-center gap-2 text-sm text-primary"><ShieldCheck className="h-4 w-4" /> Local-first control plane</div>
34
- <h1 className="mt-2 text-3xl font-semibold">System</h1>
35
- <p className="mt-2 max-w-3xl text-sm text-muted-foreground">Identity, workspaces, snapshots, activity, network exchange, runtime settings, and admin status.</p>
32
+ <div className="space-y-5">
33
+ <header className="page-hero">
34
+ <div className="page-kicker"><ShieldCheck className="h-4 w-4" /> Care</div>
35
+ <h1 className="page-title">Keep your brain safe and portable.</h1>
36
+ <p className="page-copy">Manage identity, spaces, backups, trusted devices, and safeguards from one calm place.</p>
36
37
  </header>
37
- <Tabs tabs={tabs} value={tab} onChange={(id) => setTab(id as SystemTab)} />
38
+ <Tabs tabs={mode === "basic" ? tabs.filter((item) => item.id !== "admin") : tabs} value={tab} onChange={(id) => setTab(id as SystemTab)} />
38
39
  {tab === "account" ? <AccountPanel /> : null}
39
40
  {tab === "workspaces" ? <WorkspacePanel /> : null}
40
41
  {tab === "snapshots" ? <SnapshotsPanel /> : null}
@@ -66,8 +67,8 @@ function AccountPanel() {
66
67
  </DataPanel>
67
68
  <Card>
68
69
  <CardHeader>
69
- <CardTitle className="flex items-center gap-2"><UserCircle className="h-4 w-4" /> Token-native account</CardTitle>
70
- <CardDescription>Login, registration, profile update, and password change all call existing auth endpoints.</CardDescription>
70
+ <CardTitle className="flex items-center gap-2"><UserCircle className="h-4 w-4" /> Account</CardTitle>
71
+ <CardDescription>Sign in, create a local account, and keep your profile current.</CardDescription>
71
72
  </CardHeader>
72
73
  <CardContent className="grid gap-3">
73
74
  <Input value={email} onChange={(e) => setEmail(e.target.value)} placeholder="email" />
@@ -89,7 +90,7 @@ function AccountPanel() {
89
90
  ))}
90
91
  </CardContent>
91
92
  </Card>
92
- <DataPanel title="SSO config" result={sso.data} className="xl:col-span-2">
93
+ <DataPanel title="Sign-in options" result={sso.data} className="xl:col-span-2">
93
94
  {(data) => <StructuredView value={data} />}
94
95
  </DataPanel>
95
96
  </div>
@@ -110,7 +111,7 @@ function WorkspacePanel() {
110
111
  const workspaces = asArray<Record<string, unknown>>((registry.data?.data as Record<string, unknown>)?.workspaces);
111
112
  return (
112
113
  <div className="grid gap-4 xl:grid-cols-[1.1fr_0.9fr]">
113
- <DataPanel title="Workspace registry" result={registry.data}>
114
+ <DataPanel title="Your workspaces" result={registry.data}>
114
115
  {() => (
115
116
  <div className="grid gap-2">
116
117
  {workspaces.map((workspace) => {
@@ -137,7 +138,7 @@ function WorkspacePanel() {
137
138
  <Card>
138
139
  <CardHeader>
139
140
  <CardTitle className="flex items-center gap-2"><Users className="h-4 w-4" /> Organizations and invitations</CardTitle>
140
- <CardDescription>Local invite tokens and workspace creation are backend-owned.</CardDescription>
141
+ <CardDescription>Create or join a workspace before adding knowledge.</CardDescription>
141
142
  </CardHeader>
142
143
  <CardContent className="grid gap-3">
143
144
  <Input value={orgName} onChange={(e) => setOrgName(e.target.value)} placeholder="New organization name" />
@@ -187,7 +188,7 @@ function SnapshotsPanel() {
187
188
  <Card>
188
189
  <CardHeader>
189
190
  <CardTitle>Snapshot actions</CardTitle>
190
- <CardDescription>Create and compare through Workspace OS endpoints.</CardDescription>
191
+ <CardDescription>Create checkpoints and compare changes over time.</CardDescription>
191
192
  </CardHeader>
192
193
  <CardContent className="grid gap-3">
193
194
  <Input value={name} onChange={(e) => setName(e.target.value)} placeholder="snapshot name" />
@@ -240,14 +241,14 @@ function NetworkPanel() {
240
241
  </DataPanel>
241
242
  <Card>
242
243
  <CardHeader>
243
- <CardTitle className="flex items-center gap-2"><Network className="h-4 w-4" /> Pair peer</CardTitle>
244
- <CardDescription>Manual peer pairing for signed workspace bundle exchange.</CardDescription>
244
+ <CardTitle className="flex items-center gap-2"><Network className="h-4 w-4" /> Pair device</CardTitle>
245
+ <CardDescription>Pair a trusted device for workspace exchange.</CardDescription>
245
246
  </CardHeader>
246
247
  <CardContent className="grid gap-3">
247
- <Input value={name} onChange={(e) => setName(e.target.value)} placeholder="peer name" />
248
- <Input value={baseUrl} onChange={(e) => setBaseUrl(e.target.value)} placeholder="http://peer.local:8765" />
248
+ <Input value={name} onChange={(e) => setName(e.target.value)} placeholder="device name" />
249
+ <Input value={baseUrl} onChange={(e) => setBaseUrl(e.target.value)} placeholder="trusted device address" />
249
250
  <Input value={publicKey} onChange={(e) => setPublicKey(e.target.value)} placeholder="trusted public key" />
250
- <Button disabled={!name || !baseUrl || !publicKey || pair.isPending} onClick={() => pair.mutate()}>Pair</Button>
251
+ <Button disabled={!name || !baseUrl || !publicKey || pair.isPending} onClick={() => pair.mutate()}>Pair device</Button>
251
252
  {pair.data ? <OperationResult result={pair.data} successLabel="Peer pairing request completed" /> : null}
252
253
  </CardContent>
253
254
  </Card>
@@ -344,8 +345,15 @@ function SettingsPanel() {
344
345
  <DataPanel title="Server health" result={health.data}>
345
346
  {(data) => <HealthView data={data as Record<string, unknown>} />}
346
347
  </DataPanel>
347
- <DataPanel title="Host telemetry" result={sys.data}>
348
- {(data) => <StructuredView value={data} />}
348
+ <DataPanel title={mode === "basic" ? "Computer readiness" : "Host telemetry"} result={sys.data}>
349
+ {(data) => mode === "basic" ? (
350
+ <StatGrid stats={[
351
+ { label: "CPU", value: `${String((data as Record<string, unknown>).cpu_pct || "0")}%` },
352
+ { label: "Memory", value: `${String((data as Record<string, unknown>).ram_pct || "0")}%` },
353
+ { label: "GPU memory", value: `${String((data as Record<string, unknown>).gpu_mem_pct || "0")}%` },
354
+ { label: "Local status", value: "ready" },
355
+ ]} />
356
+ ) : <StructuredView value={data} />}
349
357
  </DataPanel>
350
358
  <DataPanel title="Brain storage" result={storage.data} className="xl:col-span-3">
351
359
  {(data) => <StorageView data={data as Record<string, unknown>} />}
@@ -356,7 +364,7 @@ function SettingsPanel() {
356
364
  <Card className="xl:col-span-3">
357
365
  <CardHeader>
358
366
  <CardTitle>.latticebrain portability</CardTitle>
359
- <CardDescription>Encrypted export, inspect, verify, dry-run restore, and confirmed restore use the Brain portability API.</CardDescription>
367
+ <CardDescription>Create an encrypted portable brain file, verify one, or preview a restore before applying it.</CardDescription>
360
368
  </CardHeader>
361
369
  <CardContent className="grid gap-3">
362
370
  <div className="grid gap-2 sm:grid-cols-[1fr_1fr]">
@@ -386,15 +394,15 @@ function SettingsPanel() {
386
394
  ))}
387
395
  </CardContent>
388
396
  </Card>
389
- <Card className="xl:col-span-3">
397
+ {mode !== "basic" ? <Card className="xl:col-span-3">
390
398
  <CardHeader>
391
- <CardTitle>Postgres scale mode</CardTitle>
392
- <CardDescription>Opt-in migration and Docker setup; SQLite remains the default.</CardDescription>
399
+ <CardTitle>Scale mode</CardTitle>
400
+ <CardDescription>Optional advanced storage. Local SQLite remains the default.</CardDescription>
393
401
  </CardHeader>
394
402
  <CardContent className="grid gap-3">
395
403
  <div className="grid gap-2 sm:grid-cols-[1fr_220px]">
396
- <Input value={dsn} onChange={(e) => setDsn(e.target.value)} placeholder="postgresql://user:pass@127.0.0.1:5432/lattice_brain" />
397
- <Input value={schema} onChange={(e) => setSchema(e.target.value)} placeholder="schema" />
404
+ <Input value={dsn} onChange={(e) => setDsn(e.target.value)} placeholder="Postgres connection string" />
405
+ <Input value={schema} onChange={(e) => setSchema(e.target.value)} placeholder="database schema" />
398
406
  </div>
399
407
  <div className="flex flex-wrap gap-2">
400
408
  <Button variant="outline" onClick={() => docker.mutate(false)} disabled={docker.isPending}>Docker plan</Button>
@@ -408,7 +416,7 @@ function SettingsPanel() {
408
416
  {docker.data ? <OperationResult result={docker.data} successLabel="Docker setup request completed" /> : null}
409
417
  {migration.data ? <OperationResult result={migration.data} successLabel="Migration plan completed" /> : null}
410
418
  </CardContent>
411
- </Card>
419
+ </Card> : null}
412
420
  <DataPanel title="Computer memory" result={comp.data} className="xl:col-span-3">
413
421
  {(data) => (
414
422
  <div className="space-y-3">
@@ -441,7 +449,16 @@ function PresenceView({ data }: { data: Record<string, unknown> }) {
441
449
  }
442
450
 
443
451
  function DeviceIdentityView({ data }: { data: Record<string, unknown> }) {
452
+ const mode = useAppStore((state) => state.mode);
444
453
  const publicKey = textValue(data.public_key, "");
454
+ if (mode === "basic") {
455
+ return (
456
+ <div className="space-y-3">
457
+ <StatusCard title="This Mac" status="trusted" detail="This device can participate in local workspace exchange when you pair another trusted device." />
458
+ <Badge variant="muted">{textValue(data.algorithm, "local identity")}</Badge>
459
+ </div>
460
+ );
461
+ }
445
462
  return (
446
463
  <div className="space-y-3">
447
464
  <div className="flex flex-wrap items-center gap-2">
@@ -458,15 +475,16 @@ function DeviceIdentityView({ data }: { data: Record<string, unknown> }) {
458
475
  }
459
476
 
460
477
  function HealthView({ data }: { data: Record<string, unknown> }) {
478
+ const mode = useAppStore((state) => state.mode);
461
479
  return (
462
480
  <div className="space-y-3">
463
481
  <StatGrid stats={[
464
482
  { label: "Status", value: data.status || data.ok || "reported" },
465
483
  { label: "Version", value: data.version || "not reported" },
466
484
  { label: "Mode", value: data.mode || data.environment || "local" },
467
- { label: "Port", value: data.port || data.backend_port || "configured" },
485
+ ...(mode === "basic" ? [] : [{ label: "Port", value: data.port || data.backend_port || "configured" }]),
468
486
  ]} />
469
- <StructuredView value={data} />
487
+ {mode === "basic" ? null : <StructuredView value={data} />}
470
488
  </div>
471
489
  );
472
490
  }
@@ -543,7 +561,7 @@ function HardeningView({ data }: { data: Record<string, unknown> }) {
543
561
  { label: "Backups", value: backup.count || backup.available || "reported" },
544
562
  ]} />
545
563
  <div className="grid gap-3 md:grid-cols-2">
546
- <StatusCard title="Startup" status={startup.network_exposed ? "network exposed" : "local-only"} detail={`Host ${textValue(startup.host, "127.0.0.1")} on port ${textValue(startup.port, "configured")}.`} />
564
+ <StatusCard title="Startup" status={startup.network_exposed ? "network exposed" : "local-only"} detail="Lattice starts locally by default and reports when network access is enabled." />
547
565
  <StatusCard title="Integrations" status={privacy.local_only_default === false ? "review required" : "opt-in"} detail="External integrations remain disabled until the user explicitly enables them." />
548
566
  <StatusCard title="Device identity" status={textValue(identity.algorithm || identity.fingerprint, "reported")} detail={textValue(identity.storage, "Stored locally and used for signed bundle exchange.")} />
549
567
  <StatusCard title="Permissions" status={permissions.destructive_restore_requires_confirmation === false ? "review required" : "guarded"} detail="Export, import, and destructive restore permissions are surfaced through admin status." />
@@ -569,6 +587,7 @@ function SecurityView({ data }: { data: Record<string, unknown> }) {
569
587
  }
570
588
 
571
589
  function AdminPanel() {
590
+ const mode = useAppStore((state) => state.mode);
572
591
  const summary = useQuery({ queryKey: ["adminSummary"], queryFn: latticeApi.adminSummary });
573
592
  const users = useQuery({ queryKey: ["adminUsers"], queryFn: latticeApi.adminUsers });
574
593
  const audit = useQuery({ queryKey: ["adminAudit"], queryFn: latticeApi.adminAudit });
@@ -577,6 +596,9 @@ function AdminPanel() {
577
596
  const hardening = useQuery({ queryKey: ["adminProductHardening"], queryFn: latticeApi.adminProductHardening });
578
597
  const security = useQuery({ queryKey: ["adminSecurity"], queryFn: latticeApi.adminSecurity });
579
598
  const vpc = useQuery({ queryKey: ["vpcStatus"], queryFn: latticeApi.vpcStatus });
599
+ if (mode !== "admin") {
600
+ return <ModeGate title="Admin controls" detail="Switch to Admin mode to review users, audit events, policies, security posture, and private networking diagnostics." target="admin" />;
601
+ }
580
602
  return (
581
603
  <div className="grid gap-4 xl:grid-cols-2">
582
604
  <DataPanel title="Admin summary" result={summary.data}>{(data) => <KeyValueList data={data as Record<string, unknown>} />}</DataPanel>
@@ -15,16 +15,17 @@ import {
15
15
  export type PrimaryRoute = "brain" | "ask" | "capture" | "act" | "library" | "system";
16
16
 
17
17
  export const primaryRoutes = [
18
- { id: "brain", label: "Brain", icon: Brain, description: "Knowledge graph, retrieval, memory, provenance" },
19
- { id: "ask", label: "Ask", icon: MessageSquare, description: "Chat, conversations, context trace" },
20
- { id: "capture", label: "Capture", icon: FolderInput, description: "Documents, local folders, browser ingestion" },
21
- { id: "act", label: "Act", icon: Workflow, description: "Agents, workflows, approvals, hooks" },
22
- { id: "library", label: "Library", icon: Library, description: "Models, skills, MCP, templates" },
23
- { id: "system", label: "System", icon: Settings, description: "Account, workspaces, snapshots, network, admin" },
18
+ { id: "brain", label: "Home", icon: Brain, description: "The living map of what Lattice knows" },
19
+ { id: "ask", label: "Ask", icon: MessageSquare, description: "Think with remembered context" },
20
+ { id: "capture", label: "Add", icon: FolderInput, description: "Bring in files, folders, and pages" },
21
+ { id: "act", label: "Automate", icon: Workflow, description: "Turn goals into supervised runs" },
22
+ { id: "library", label: "Library", icon: Library, description: "Choose models, skills, and tools" },
23
+ { id: "system", label: "Care", icon: Settings, description: "Keep your brain safe and portable" },
24
24
  ] as const;
25
25
 
26
26
  export const routeAliases: Record<string, { primary: PrimaryRoute; tab?: string }> = {
27
27
  home: { primary: "brain", tab: "overview" },
28
+ onboarding: { primary: "system", tab: "account" },
28
29
  "knowledge-graph": { primary: "brain", tab: "graph" },
29
30
  "hybrid-search": { primary: "brain", tab: "search" },
30
31
  memory: { primary: "brain", tab: "memory" },
@@ -57,16 +58,17 @@ export const routeAliases: Record<string, { primary: PrimaryRoute; tab?: string
57
58
  };
58
59
 
59
60
  export const commandRoutes = [
60
- { key: "brain", label: "Brain", icon: Brain },
61
- { key: "knowledge-graph", label: "Knowledge Graph", icon: Network },
62
- { key: "hybrid-search", label: "Hybrid Search", icon: Zap },
61
+ { key: "brain", label: "Home", icon: Brain },
62
+ { key: "onboarding", label: "First 10 Minutes", icon: Settings },
63
+ { key: "knowledge-graph", label: "Memory Map", icon: Network },
64
+ { key: "hybrid-search", label: "Search Everything", icon: Zap },
63
65
  { key: "memory", label: "Memory", icon: Database },
64
66
  { key: "chat", label: "Ask", icon: MessageSquare },
65
- { key: "files", label: "Capture Files", icon: FolderInput },
66
- { key: "agents", label: "Agents", icon: Workflow },
67
- { key: "workflows", label: "Workflows", icon: Workflow },
67
+ { key: "files", label: "Add Files", icon: FolderInput },
68
+ { key: "agents", label: "Start a Run", icon: Workflow },
69
+ { key: "workflows", label: "Automations", icon: Workflow },
68
70
  { key: "models", label: "Models", icon: Library },
69
- { key: "network", label: "Brain Network", icon: Network },
71
+ { key: "network", label: "Trusted Devices", icon: Network },
70
72
  { key: "activity", label: "Activity", icon: Activity },
71
73
  { key: "admin/security", label: "Security", icon: Shield },
72
74
  ];