ltcai 4.0.1 → 4.1.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 (150) hide show
  1. package/README.md +28 -23
  2. package/desktop/electron/main.cjs +44 -0
  3. package/docs/CHANGELOG.md +42 -0
  4. package/docs/V4_1_FRONTEND_ARCHITECTURE_REVIEW.md +65 -0
  5. package/docs/V4_1_FRONTEND_MIGRATION_REPORT.md +70 -0
  6. package/docs/V4_1_VALIDATION_REPORT.md +47 -0
  7. package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +26 -19
  8. package/frontend/index.html +24 -0
  9. package/frontend/openapi.json +14190 -0
  10. package/frontend/src/App.tsx +184 -0
  11. package/frontend/src/api/client.ts +317 -0
  12. package/frontend/src/api/openapi.ts +16637 -0
  13. package/frontend/src/components/primitives.tsx +204 -0
  14. package/frontend/src/components/ui/badge.tsx +27 -0
  15. package/frontend/src/components/ui/button.tsx +37 -0
  16. package/frontend/src/components/ui/card.tsx +22 -0
  17. package/frontend/src/components/ui/input.tsx +16 -0
  18. package/frontend/src/components/ui/textarea.tsx +16 -0
  19. package/frontend/src/lib/utils.ts +33 -0
  20. package/frontend/src/main.tsx +23 -0
  21. package/frontend/src/pages/Act.tsx +245 -0
  22. package/frontend/src/pages/Ask.tsx +200 -0
  23. package/frontend/src/pages/Brain.tsx +267 -0
  24. package/frontend/src/pages/Capture.tsx +158 -0
  25. package/frontend/src/pages/Library.tsx +187 -0
  26. package/frontend/src/pages/System.tsx +344 -0
  27. package/frontend/src/routes.ts +85 -0
  28. package/frontend/src/store/appStore.ts +54 -0
  29. package/frontend/src/styles.css +107 -0
  30. package/latticeai/__init__.py +1 -1
  31. package/latticeai/api/setup.py +5 -4
  32. package/latticeai/api/static_routes.py +4 -4
  33. package/latticeai/core/marketplace.py +1 -1
  34. package/latticeai/core/multi_agent.py +1 -1
  35. package/latticeai/core/workspace_os.py +1 -1
  36. package/package.json +54 -15
  37. package/scripts/build_frontend_assets.mjs +38 -0
  38. package/scripts/bump_version.py +1 -1
  39. package/scripts/export_openapi.py +31 -0
  40. package/scripts/lint_frontend.mjs +86 -0
  41. package/scripts/run_python.mjs +47 -0
  42. package/src-tauri/Cargo.lock +4833 -0
  43. package/src-tauri/Cargo.toml +19 -0
  44. package/src-tauri/build.rs +3 -0
  45. package/src-tauri/capabilities/default.json +7 -0
  46. package/src-tauri/src/main.rs +78 -0
  47. package/src-tauri/tauri.conf.json +36 -0
  48. package/static/app/asset-manifest.json +32 -0
  49. package/static/app/assets/core-CwxXejkd.js +2 -0
  50. package/static/app/assets/core-CwxXejkd.js.map +1 -0
  51. package/static/app/assets/index-CJRAzNnf.js +333 -0
  52. package/static/app/assets/index-CJRAzNnf.js.map +1 -0
  53. package/static/app/assets/index-CSwBBgf4.css +2 -0
  54. package/static/app/index.html +25 -0
  55. package/static/manifest.json +2 -2
  56. package/static/sw.js +4 -4
  57. package/scripts/build_v3_assets.mjs +0 -170
  58. package/scripts/lint_v3.mjs +0 -120
  59. package/static/v3/asset-manifest.json +0 -63
  60. package/static/v3/css/lattice.base.49deefb5.css +0 -128
  61. package/static/v3/css/lattice.base.css +0 -128
  62. package/static/v3/css/lattice.components.cde18231.css +0 -472
  63. package/static/v3/css/lattice.components.css +0 -472
  64. package/static/v3/css/lattice.shell.29d36d85.css +0 -452
  65. package/static/v3/css/lattice.shell.css +0 -452
  66. package/static/v3/css/lattice.tokens.304cbc40.css +0 -135
  67. package/static/v3/css/lattice.tokens.css +0 -135
  68. package/static/v3/css/lattice.views.0a18b6c5.css +0 -360
  69. package/static/v3/css/lattice.views.css +0 -360
  70. package/static/v3/index.html +0 -68
  71. package/static/v3/js/app.c5c80c46.js +0 -26
  72. package/static/v3/js/app.js +0 -26
  73. package/static/v3/js/core/api.ba0fbf14.js +0 -625
  74. package/static/v3/js/core/api.js +0 -625
  75. package/static/v3/js/core/components.f25b3b93.js +0 -230
  76. package/static/v3/js/core/components.js +0 -230
  77. package/static/v3/js/core/dom.a2773eb0.js +0 -148
  78. package/static/v3/js/core/dom.js +0 -148
  79. package/static/v3/js/core/i18n.880e1fec.js +0 -575
  80. package/static/v3/js/core/i18n.js +0 -575
  81. package/static/v3/js/core/router.584570f2.js +0 -37
  82. package/static/v3/js/core/router.js +0 -37
  83. package/static/v3/js/core/routes.37522821.js +0 -101
  84. package/static/v3/js/core/routes.js +0 -101
  85. package/static/v3/js/core/shell.e3f6bbfa.js +0 -420
  86. package/static/v3/js/core/shell.js +0 -420
  87. package/static/v3/js/core/store.7b2aa044.js +0 -123
  88. package/static/v3/js/core/store.js +0 -123
  89. package/static/v3/js/views/account.eff40715.js +0 -143
  90. package/static/v3/js/views/account.js +0 -143
  91. package/static/v3/js/views/activity.0d271ef9.js +0 -67
  92. package/static/v3/js/views/activity.js +0 -67
  93. package/static/v3/js/views/admin-audit.660a1fb1.js +0 -185
  94. package/static/v3/js/views/admin-audit.js +0 -185
  95. package/static/v3/js/views/admin-permissions.a7ae5f09.js +0 -177
  96. package/static/v3/js/views/admin-permissions.js +0 -177
  97. package/static/v3/js/views/admin-policies.3658fd86.js +0 -102
  98. package/static/v3/js/views/admin-policies.js +0 -102
  99. package/static/v3/js/views/admin-private-vpc.7d342d36.js +0 -135
  100. package/static/v3/js/views/admin-private-vpc.js +0 -135
  101. package/static/v3/js/views/admin-security.07c66b72.js +0 -180
  102. package/static/v3/js/views/admin-security.js +0 -180
  103. package/static/v3/js/views/admin-users.f7ac7b43.js +0 -166
  104. package/static/v3/js/views/admin-users.js +0 -166
  105. package/static/v3/js/views/agents.17c5288d.js +0 -564
  106. package/static/v3/js/views/agents.js +0 -564
  107. package/static/v3/js/views/chat.e250e2cc.js +0 -624
  108. package/static/v3/js/views/chat.js +0 -624
  109. package/static/v3/js/views/files.adad14c1.js +0 -365
  110. package/static/v3/js/views/files.js +0 -365
  111. package/static/v3/js/views/graph-canvas.17c15d65.js +0 -509
  112. package/static/v3/js/views/graph-canvas.js +0 -509
  113. package/static/v3/js/views/home.24f8b8ae.js +0 -200
  114. package/static/v3/js/views/home.js +0 -200
  115. package/static/v3/js/views/hooks.37895880.js +0 -220
  116. package/static/v3/js/views/hooks.js +0 -220
  117. package/static/v3/js/views/hybrid-search.2fb63ed9.js +0 -194
  118. package/static/v3/js/views/hybrid-search.js +0 -194
  119. package/static/v3/js/views/knowledge-graph.4d09c537.js +0 -529
  120. package/static/v3/js/views/knowledge-graph.js +0 -529
  121. package/static/v3/js/views/marketplace.ab0583d4.js +0 -141
  122. package/static/v3/js/views/marketplace.js +0 -141
  123. package/static/v3/js/views/mcp.99b5c6a7.js +0 -114
  124. package/static/v3/js/views/mcp.js +0 -114
  125. package/static/v3/js/views/memory.4ebdf474.js +0 -147
  126. package/static/v3/js/views/memory.js +0 -147
  127. package/static/v3/js/views/models.a1ffa147.js +0 -256
  128. package/static/v3/js/views/models.js +0 -256
  129. package/static/v3/js/views/my-computer.d9d9ae1c.js +0 -463
  130. package/static/v3/js/views/my-computer.js +0 -463
  131. package/static/v3/js/views/network.52a4f181.js +0 -97
  132. package/static/v3/js/views/network.js +0 -97
  133. package/static/v3/js/views/pipeline.c522f1ce.js +0 -157
  134. package/static/v3/js/views/pipeline.js +0 -157
  135. package/static/v3/js/views/planning.4876fd77.js +0 -174
  136. package/static/v3/js/views/planning.js +0 -174
  137. package/static/v3/js/views/runs.b63b2afa.js +0 -144
  138. package/static/v3/js/views/runs.js +0 -144
  139. package/static/v3/js/views/settings.b7140634.js +0 -317
  140. package/static/v3/js/views/settings.js +0 -317
  141. package/static/v3/js/views/skills.c6c2f965.js +0 -109
  142. package/static/v3/js/views/skills.js +0 -109
  143. package/static/v3/js/views/snapshots.6f5db095.js +0 -135
  144. package/static/v3/js/views/snapshots.js +0 -135
  145. package/static/v3/js/views/tools.e4f11276.js +0 -108
  146. package/static/v3/js/views/tools.js +0 -108
  147. package/static/v3/js/views/workflows.7752225a.js +0 -213
  148. package/static/v3/js/views/workflows.js +0 -213
  149. package/static/v3/js/views/workspace-admin.c466029b.js +0 -156
  150. package/static/v3/js/views/workspace-admin.js +0 -156
@@ -0,0 +1,184 @@
1
+ import * as React from "react";
2
+ import { useQuery } from "@tanstack/react-query";
3
+ import { Command, Menu, Moon, Search, Sun, X } from "lucide-react";
4
+ import { latticeApi } from "@/api/client";
5
+ import { Button } from "@/components/ui/button";
6
+ import { Input } from "@/components/ui/input";
7
+ import { useAppStore } from "@/store/appStore";
8
+ import { commandRoutes, go, parseHash, primaryRoutes, PrimaryRoute } from "@/routes";
9
+ import { BrainPage } from "@/pages/Brain";
10
+ import { AskPage } from "@/pages/Ask";
11
+ import { CapturePage } from "@/pages/Capture";
12
+ import { ActPage } from "@/pages/Act";
13
+ import { LibraryPage } from "@/pages/Library";
14
+ import { SystemPage } from "@/pages/System";
15
+ import { cn } from "@/lib/utils";
16
+
17
+ function useRoute() {
18
+ const [route, setRoute] = React.useState(parseHash);
19
+ React.useEffect(() => {
20
+ const onHash = () => setRoute(parseHash());
21
+ window.addEventListener("hashchange", onHash);
22
+ return () => window.removeEventListener("hashchange", onHash);
23
+ }, []);
24
+ return route;
25
+ }
26
+
27
+ function Page({ primary, tab }: { primary: PrimaryRoute; tab?: string }) {
28
+ if (primary === "ask") return <AskPage />;
29
+ if (primary === "capture") return <CapturePage initialTab={tab} />;
30
+ if (primary === "act") return <ActPage initialTab={tab} />;
31
+ if (primary === "library") return <LibraryPage initialTab={tab} />;
32
+ if (primary === "system") return <SystemPage initialTab={tab} />;
33
+ return <BrainPage initialTab={tab} />;
34
+ }
35
+
36
+ function CommandPalette({ open, onClose }: { open: boolean; onClose: () => void }) {
37
+ const [query, setQuery] = React.useState("");
38
+ const matches = commandRoutes.filter((route) => route.label.toLowerCase().includes(query.toLowerCase()) || route.key.includes(query.toLowerCase()));
39
+ React.useEffect(() => {
40
+ if (!open) return;
41
+ const onKey = (event: KeyboardEvent) => {
42
+ if (event.key === "Escape") onClose();
43
+ };
44
+ window.addEventListener("keydown", onKey);
45
+ return () => window.removeEventListener("keydown", onKey);
46
+ }, [open, onClose]);
47
+ if (!open) return null;
48
+ return (
49
+ <div className="fixed inset-0 z-50 bg-background/80 p-4 backdrop-blur-sm" role="dialog" aria-modal="true">
50
+ <div className="mx-auto mt-16 max-w-xl rounded-lg border border-border bg-card shadow-xl">
51
+ <div className="flex items-center gap-2 border-b border-border p-3">
52
+ <Search className="h-4 w-4 text-muted-foreground" />
53
+ <Input value={query} onChange={(e) => setQuery(e.target.value)} autoFocus placeholder="Jump to a capability" />
54
+ <Button variant="ghost" size="icon" onClick={onClose}><X className="h-4 w-4" /></Button>
55
+ </div>
56
+ <div className="max-h-96 overflow-auto p-2">
57
+ {matches.map((route) => {
58
+ const Icon = route.icon;
59
+ return (
60
+ <button
61
+ key={route.key}
62
+ onClick={() => {
63
+ go(route.key);
64
+ onClose();
65
+ }}
66
+ className="flex w-full items-center gap-3 rounded-md px-3 py-2 text-left text-sm hover:bg-muted"
67
+ >
68
+ <Icon className="h-4 w-4 text-primary" />
69
+ {route.label}
70
+ </button>
71
+ );
72
+ })}
73
+ </div>
74
+ </div>
75
+ </div>
76
+ );
77
+ }
78
+
79
+ export default function App() {
80
+ const route = useRoute();
81
+ const { theme, setTheme, mode, setMode } = useAppStore();
82
+ const [drawer, setDrawer] = React.useState(false);
83
+ const [palette, setPalette] = React.useState(false);
84
+ const health = useQuery({ queryKey: ["health"], queryFn: latticeApi.health });
85
+ const workspace = useQuery({ queryKey: ["workspaceOs"], queryFn: latticeApi.workspaceOs });
86
+
87
+ React.useEffect(() => {
88
+ document.documentElement.dataset.theme = theme;
89
+ }, [theme]);
90
+ React.useEffect(() => {
91
+ const onKey = (event: KeyboardEvent) => {
92
+ if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") {
93
+ event.preventDefault();
94
+ setPalette(true);
95
+ }
96
+ };
97
+ window.addEventListener("keydown", onKey);
98
+ return () => window.removeEventListener("keydown", onKey);
99
+ }, []);
100
+
101
+ const rail = (
102
+ <aside className="flex h-full w-64 shrink-0 flex-col border-r border-border bg-card">
103
+ <div className="flex h-16 items-center gap-3 border-b border-border px-4">
104
+ <div className="grid h-9 w-9 place-items-center rounded-md bg-primary text-primary-foreground font-bold">LA</div>
105
+ <div>
106
+ <div className="font-semibold">Lattice AI</div>
107
+ <div className="text-xs text-muted-foreground">Digital Brain Desktop</div>
108
+ </div>
109
+ </div>
110
+ <nav className="flex-1 space-y-1 overflow-auto p-3">
111
+ {primaryRoutes.map((item) => {
112
+ const Icon = item.icon;
113
+ const active = route.primary === item.id;
114
+ return (
115
+ <button
116
+ key={item.id}
117
+ onClick={() => {
118
+ go(item.id);
119
+ setDrawer(false);
120
+ }}
121
+ className={cn(
122
+ "flex min-h-14 w-full items-center gap-3 rounded-md px-3 py-2 text-left transition",
123
+ active ? "bg-primary/14 text-foreground" : "text-muted-foreground hover:bg-muted hover:text-foreground",
124
+ )}
125
+ >
126
+ <Icon className="h-5 w-5" />
127
+ <span>
128
+ <span className="block text-sm font-medium">{item.label}</span>
129
+ <span className="block text-xs">{item.description}</span>
130
+ </span>
131
+ </button>
132
+ );
133
+ })}
134
+ </nav>
135
+ <div className="border-t border-border p-3 text-xs text-muted-foreground">
136
+ <div>Server: {health.data?.ok ? "online" : "unavailable"}</div>
137
+ <div>Workspace: {String((workspace.data?.data as Record<string, unknown>)?.active_workspace || "local")}</div>
138
+ </div>
139
+ </aside>
140
+ );
141
+
142
+ return (
143
+ <div className="min-h-screen bg-background text-foreground">
144
+ <CommandPalette open={palette} onClose={() => setPalette(false)} />
145
+ <div className="hidden lg:fixed lg:inset-y-0 lg:left-0 lg:block">{rail}</div>
146
+ {drawer ? (
147
+ <div className="fixed inset-0 z-40 lg:hidden">
148
+ <button className="absolute inset-0 bg-background/70" aria-label="Close navigation" onClick={() => setDrawer(false)} />
149
+ <div className="relative h-full">{rail}</div>
150
+ </div>
151
+ ) : null}
152
+ <div className="lg:pl-64">
153
+ <header className="sticky top-0 z-30 flex h-16 items-center justify-between gap-3 border-b border-border bg-background/95 px-4 backdrop-blur">
154
+ <div className="flex min-w-0 items-center gap-2">
155
+ <Button variant="ghost" size="icon" className="lg:hidden" onClick={() => setDrawer(true)}><Menu className="h-5 w-5" /></Button>
156
+ <div className="min-w-0">
157
+ <div className="truncate text-sm text-muted-foreground">v4.1.0 Release Candidate</div>
158
+ <div className="truncate font-medium">{primaryRoutes.find((item) => item.id === route.primary)?.label}</div>
159
+ </div>
160
+ </div>
161
+ <div className="flex items-center gap-2">
162
+ <Button variant="outline" onClick={() => setPalette(true)}><Command className="h-4 w-4" /> Search</Button>
163
+ <select
164
+ value={mode}
165
+ onChange={(e) => setMode(e.target.value as "basic" | "advanced" | "admin")}
166
+ className="h-9 rounded-md border border-border bg-background px-2 text-sm"
167
+ aria-label="Workspace mode"
168
+ >
169
+ <option value="basic">Basic</option>
170
+ <option value="advanced">Advanced</option>
171
+ <option value="admin">Admin</option>
172
+ </select>
173
+ <Button variant="outline" size="icon" onClick={() => setTheme(theme === "dark" ? "light" : "dark")} aria-label="Toggle theme">
174
+ {theme === "dark" ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
175
+ </Button>
176
+ </div>
177
+ </header>
178
+ <main className="p-4 lg:p-6">
179
+ <Page primary={route.primary} tab={route.tab} />
180
+ </main>
181
+ </div>
182
+ </div>
183
+ );
184
+ }
@@ -0,0 +1,317 @@
1
+ import createClient from "openapi-fetch";
2
+ import type { paths } from "./openapi";
3
+ import { useAppStore } from "@/store/appStore";
4
+
5
+ export type ApiResult<T = unknown> = {
6
+ ok: boolean;
7
+ status: number;
8
+ data: T;
9
+ source: "live" | "unavailable";
10
+ error?: string;
11
+ };
12
+
13
+ type HttpMethod = "GET" | "POST" | "PATCH" | "DELETE";
14
+ type Query = Record<string, string | number | boolean | null | undefined>;
15
+
16
+ const TIMEOUT_MS = 10_000;
17
+ const clients = new Map<string, ReturnType<typeof createClient<paths>>>();
18
+ let desktopBase: Promise<string | null> | null = null;
19
+
20
+ declare global {
21
+ interface Window {
22
+ __TAURI_INTERNALS__?: unknown;
23
+ }
24
+ }
25
+
26
+ function sameOriginBase() {
27
+ return "";
28
+ }
29
+
30
+ async function tauriBackendOrigin(): Promise<string | null> {
31
+ if (!window.__TAURI_INTERNALS__) return null;
32
+ if (!desktopBase) {
33
+ desktopBase = import("@tauri-apps/api/core")
34
+ .then(({ invoke }) => invoke<string>("backend_origin"))
35
+ .then((origin) => origin || null)
36
+ .catch(() => null);
37
+ }
38
+ return desktopBase;
39
+ }
40
+
41
+ async function apiBase() {
42
+ const stateBase = useAppStore.getState().apiBase;
43
+ if (stateBase) return stateBase;
44
+ const desktop = await tauriBackendOrigin();
45
+ if (desktop) {
46
+ useAppStore.getState().setApiBase(desktop);
47
+ return desktop;
48
+ }
49
+ return sameOriginBase();
50
+ }
51
+
52
+ function clientFor(baseUrl: string) {
53
+ if (!clients.has(baseUrl)) {
54
+ clients.set(baseUrl, createClient<paths>({ baseUrl, credentials: "same-origin" }));
55
+ }
56
+ return clients.get(baseUrl)!;
57
+ }
58
+
59
+ function emptyFor<T>(shape: T): T {
60
+ if (Array.isArray(shape)) return [] as T;
61
+ if (shape && typeof shape === "object") return { ...(shape as Record<string, unknown>) } as T;
62
+ return shape;
63
+ }
64
+
65
+ function workspaceHeaders(): Record<string, string> {
66
+ const workspaceId = useAppStore.getState().workspaceId;
67
+ return workspaceId ? { "X-Workspace-Id": workspaceId } : {};
68
+ }
69
+
70
+ async function apiJson<T>(
71
+ method: HttpMethod,
72
+ path: string,
73
+ opts: { body?: unknown; query?: Query; headers?: Record<string, string>; shape: T },
74
+ ): Promise<ApiResult<T>> {
75
+ const base = await apiBase();
76
+ const client = clientFor(base);
77
+ const ctrl = new AbortController();
78
+ const timer = window.setTimeout(() => ctrl.abort(), TIMEOUT_MS);
79
+ try {
80
+ const request = {
81
+ body: opts.body,
82
+ params: { query: opts.query || {} },
83
+ headers: { ...workspaceHeaders(), ...(opts.headers || {}) },
84
+ signal: ctrl.signal,
85
+ } as never;
86
+ const call =
87
+ method === "GET" ? client.GET :
88
+ method === "POST" ? client.POST :
89
+ method === "PATCH" ? client.PATCH :
90
+ client.DELETE;
91
+ const result = await (call as unknown as (p: never, r: never) => Promise<{ data?: unknown; error?: unknown; response: Response }>)(path as never, request);
92
+ const { data, error, response } = result;
93
+ if (response.ok && data !== undefined) {
94
+ return { ok: true, status: response.status, data: data as T, source: "live" };
95
+ }
96
+ return {
97
+ ok: false,
98
+ status: response.status,
99
+ data: emptyFor(opts.shape),
100
+ source: "unavailable",
101
+ error: error ? JSON.stringify(error) : response.statusText,
102
+ };
103
+ } catch (err) {
104
+ return {
105
+ ok: false,
106
+ status: 0,
107
+ data: emptyFor(opts.shape),
108
+ source: "unavailable",
109
+ error: err instanceof Error ? err.message : String(err),
110
+ };
111
+ } finally {
112
+ window.clearTimeout(timer);
113
+ }
114
+ }
115
+
116
+ function get<T>(path: string, shape: T, query?: Query) {
117
+ return apiJson<T>("GET", path, { query, shape });
118
+ }
119
+
120
+ function post<T>(path: string, body: unknown, shape: T) {
121
+ return apiJson<T>("POST", path, { body, shape });
122
+ }
123
+
124
+ function patch<T>(path: string, body: unknown, shape: T) {
125
+ return apiJson<T>("PATCH", path, { body, shape });
126
+ }
127
+
128
+ function del<T>(path: string, shape: T) {
129
+ return apiJson<T>("DELETE", path, { shape });
130
+ }
131
+
132
+ async function uploadDocument(file: File): Promise<ApiResult<Record<string, unknown> | null>> {
133
+ const base = await apiBase();
134
+ const form = new FormData();
135
+ form.append("file", file);
136
+ try {
137
+ const res = await fetch(`${base}/upload/document`, {
138
+ method: "POST",
139
+ credentials: "same-origin",
140
+ headers: { Accept: "application/json", ...workspaceHeaders() } satisfies HeadersInit,
141
+ body: form,
142
+ });
143
+ const data = await res.json().catch(() => null);
144
+ return { ok: res.ok, status: res.status, data, source: res.ok ? "live" : "unavailable" };
145
+ } catch (err) {
146
+ return { ok: false, status: 0, data: null, source: "unavailable", error: String(err) };
147
+ }
148
+ }
149
+
150
+ export type ChatEventHandlers = {
151
+ onChunk?: (delta: string, fullText: string) => void;
152
+ onTrace?: (trace: unknown) => void;
153
+ signal?: AbortSignal;
154
+ };
155
+
156
+ async function streamChat(body: Record<string, unknown>, handlers: ChatEventHandlers = {}) {
157
+ const base = await apiBase();
158
+ const res = await fetch(`${base}/chat`, {
159
+ method: "POST",
160
+ credentials: "same-origin",
161
+ signal: handlers.signal,
162
+ headers: {
163
+ "Content-Type": "application/json",
164
+ Accept: "text/event-stream",
165
+ ...workspaceHeaders(),
166
+ } satisfies HeadersInit,
167
+ body: JSON.stringify({ stream: true, max_tokens: 2048, temperature: 0.2, ...body }),
168
+ });
169
+ if (!res.ok || !res.body || !(res.headers.get("content-type") || "").includes("text/event-stream")) {
170
+ const payload = await res.json().catch(() => null);
171
+ return { source: "live", text: "", trace: null, error: payload?.error || payload?.detail || res.statusText };
172
+ }
173
+ const reader = res.body.getReader();
174
+ const decoder = new TextDecoder();
175
+ let buffer = "";
176
+ let text = "";
177
+ let trace: unknown = null;
178
+ for (;;) {
179
+ const { done, value } = await reader.read();
180
+ if (done) break;
181
+ buffer += decoder.decode(value, { stream: true });
182
+ const parts = buffer.split("\n\n");
183
+ buffer = parts.pop() || "";
184
+ for (const part of parts) {
185
+ const line = part.split("\n").find((item) => item.startsWith("data:"));
186
+ if (!line) continue;
187
+ const raw = line.slice(5).trim();
188
+ if (raw === "[DONE]") return { source: "live", text, trace };
189
+ const data = JSON.parse(raw);
190
+ const delta = data.chunk || data.text || "";
191
+ if (delta) {
192
+ text += delta;
193
+ handlers.onChunk?.(delta, text);
194
+ }
195
+ if (data.trace) {
196
+ trace = data.trace;
197
+ handlers.onTrace?.(trace);
198
+ }
199
+ }
200
+ }
201
+ return { source: "live", text, trace };
202
+ }
203
+
204
+ export const latticeApi = {
205
+ raw: get,
206
+ health: () => get("/health", {}),
207
+ workspaceOs: () => get("/workspace/os", { counts: {}, models: {}, workspace_registry: { workspaces: [] } }),
208
+ indexStatus: () => get("/api/index/status", {}),
209
+ rebuildIndex: () => post("/api/index/rebuild", { full: false, include_nodes: true, include_chunks: true }, {}),
210
+ graph: () => get("/knowledge-graph/graph", { nodes: [], edges: [] }),
211
+ graphStats: () => get("/knowledge-graph/stats", { nodes: {}, edges: {}, total_nodes: 0, total_edges: 0 }),
212
+ graphPortability: () => get("/api/knowledge-graph/portability", {}),
213
+ graphProvenance: (limit = 50) => get("/api/knowledge-graph/provenance", { items: [] }, { limit }),
214
+ graphCoverage: () => get("/knowledge-graph/provenance/coverage", {}),
215
+ graphExport: () => post("/api/knowledge-graph/export", {}, {}),
216
+ graphBackup: () => post("/api/knowledge-graph/backup", {}, {}),
217
+ graphImport: (artifact: unknown, dry_run = true) => post("/api/knowledge-graph/import", { artifact, mode: "merge", dry_run }, {}),
218
+ hybridSearch: async (query: string, weights?: unknown) => {
219
+ const res = await post<Record<string, unknown>>("/api/search/hybrid", { query, ...(weights ? { weights } : {}) }, { matches: [] });
220
+ const data = res.data as Record<string, unknown>;
221
+ if (res.ok && !Array.isArray(data.matches) && Array.isArray(data.results)) {
222
+ return { ...res, data: { ...data, matches: data.results } };
223
+ }
224
+ return res;
225
+ },
226
+ browserReadUrl: (url: string) => post("/api/browser/read-url", { url }, {}),
227
+ memoryManager: () => get("/api/memory/manager", { sources: [], tiers: [], usage: {} }),
228
+ memoryRecall: (query: string, limit = 20) => post("/api/memory/recall", { query, limit }, { matches: [] }),
229
+ memoryCompact: () => post("/api/memory/compact", {}, {}),
230
+ memoryRebuild: () => post("/api/memory/rebuild", { target: "vector" }, {}),
231
+ chatHistory: () => get("/history/conversations", []),
232
+ conversation: (id: string) => get(`/history/conversations/${encodeURIComponent(id)}`, { messages: [] }),
233
+ deleteConversation: (id: string) => del(`/history/conversations/${encodeURIComponent(id)}`, {}),
234
+ streamChat,
235
+ uploadDocument,
236
+ documents: (limit = 200) => get("/knowledge-graph/documents", { documents: [] }, { limit }),
237
+ localSources: () => get("/knowledge-graph/local/sources", { sources: [], watch: { available: false, active: {} } }),
238
+ localAgent: () => get("/api/local-agent/status", { agent: { online: false }, sources: [] }),
239
+ connectFolder: (path: string) => post("/knowledge-graph/local/index", { path, approved: true, watch_enabled: true, consent: { approved: true, source: "desktop-spa" } }, {}),
240
+ localWatchStop: (source_id: string) => post("/knowledge-graph/local/watch/stop", { source_id }, {}),
241
+ models: () => get("/models", { catalog: [], loaded: [], recommended: [] }),
242
+ loadModel: (model_id: string, engine?: string) => post("/models/load", { model_id, engine: engine || null }, {}),
243
+ unloadModel: (model_id: string) => del(`/models/unload/${encodeURIComponent(model_id)}`, {}),
244
+ embeddingsStatus: () => get("/api/embeddings/status", {}),
245
+ agentRuntime: () => get("/agents/api/runtime/status", { runtime: {}, agents: [], runs: [] }),
246
+ runAgent: (goal: string, roles: string[]) => post("/agents/api/run", { goal, roles }, {}),
247
+ agentRun: (id: string) => get(`/agents/api/runs/${encodeURIComponent(id)}`, {}),
248
+ stopAgentRun: (id: string) => post(`/agents/api/runs/${encodeURIComponent(id)}/stop`, {}, {}),
249
+ agentRegistry: () => get("/agents/api/registry", { agents: [] }),
250
+ agentCapabilities: () => get("/agents/api/registry/capabilities", { capabilities: {} }),
251
+ registerAgent: (body: unknown) => post("/agents/api/registry", body, {}),
252
+ updateAgent: (id: string, body: unknown) => patch(`/agents/api/registry/${encodeURIComponent(id)}`, body, {}),
253
+ removeAgent: (id: string) => del(`/agents/api/registry/${encodeURIComponent(id)}`, {}),
254
+ workflowDefinitions: () => get("/workflows/api/definitions", { workflows: [] }),
255
+ workflowRuns: () => get("/workflows/api/runs", { runs: [] }),
256
+ workflowTriggers: () => get("/workflows/api/triggers", { armed: [] }),
257
+ runWorkflow: (id: string) => post(`/workflows/api/definitions/${encodeURIComponent(id)}/run`, {}, {}),
258
+ updateWorkflow: (id: string, body: unknown) => patch(`/workflows/api/definitions/${encodeURIComponent(id)}`, body, {}),
259
+ stopWorkflowRun: (id: string) => post(`/workflows/api/runs/${encodeURIComponent(id)}/stop`, {}, {}),
260
+ resumeWorkflowRun: (id: string, approved: boolean) => post(`/workflows/api/runs/${encodeURIComponent(id)}/resume`, { approved }, {}),
261
+ hooks: () => get("/api/hooks", { hooks: [] }),
262
+ hookRuns: () => get("/api/hooks/runs", { runs: [] }, { limit: 50 }),
263
+ hookRun: (body: unknown) => post("/api/hooks/run", body, {}),
264
+ permissionsPending: () => get("/permissions/pending", { pending: {}, count: 0 }),
265
+ approvePermission: (token: string) => post(`/permissions/approve/${encodeURIComponent(token)}`, {}, {}),
266
+ denyPermission: (token: string) => post(`/permissions/deny/${encodeURIComponent(token)}`, {}, {}),
267
+ skills: () => get("/workspace/skills", { skills: [] }),
268
+ skillToggle: (skill: string, enabled: boolean) => post(enabled ? "/workspace/skills/disable" : "/workspace/skills/enable", { skill }, {}),
269
+ skillsMarketplace: () => get("/skills/marketplace", { skills: [] }),
270
+ skillInstall: (skill: string, plugin?: string) => post("/workspace/skills/install", { skill, plugin: plugin || "" }, {}),
271
+ mcpTools: () => get("/mcp/tools", { tools: [], installed_mcps: [] }),
272
+ mcpRecommend: (query: string) => post("/mcp/recommend", { query, limit: 6 }, {}),
273
+ templates: () => get("/marketplace/templates", { templates: [], kinds: [] }),
274
+ installTemplate: (data: unknown) => post("/marketplace/templates/install", { data }, {}),
275
+ pluginsRegistry: () => get("/plugins/registry", { plugins: [] }),
276
+ pluginsDirectory: () => get("/plugins/directory", { plugins: [] }),
277
+ profile: () => get("/account/profile", {}),
278
+ login: (email: string, password: string) => post("/login", { email, password }, {}),
279
+ register: (body: unknown) => post("/register", body, {}),
280
+ logout: () => post("/logout", {}, {}),
281
+ updateProfile: (body: unknown) => patch("/account/profile", body, {}),
282
+ changePassword: (current_password: string, new_password: string) => post("/account/change-password", { current_password, new_password }, {}),
283
+ ssoConfig: () => get("/auth/sso/config", { enabled: false, providers: [] }),
284
+ workspaceRegistry: () => get("/workspace/registry", { workspaces: [] }),
285
+ createOrg: (name: string) => post("/workspace/orgs", { name }, {}),
286
+ activateWorkspace: (workspace_id: string) => post("/workspace/activate", { workspace_id }, {}),
287
+ archiveWorkspace: (workspace_id: string) => post(`/workspace/orgs/${encodeURIComponent(workspace_id)}/archive`, {}, {}),
288
+ addWorkspaceMember: (workspace_id: string, user_id: string, role: string) => post(`/workspace/orgs/${encodeURIComponent(workspace_id)}/members`, { user_id, role }, {}),
289
+ removeWorkspaceMember: (workspace_id: string, user_id: string) => del(`/workspace/orgs/${encodeURIComponent(workspace_id)}/members/${encodeURIComponent(user_id)}`, {}),
290
+ invitations: () => get("/invitations", { invitations: [] }),
291
+ createInvitation: (body: unknown) => post("/invitations", body, {}),
292
+ acceptInvitation: (token: string) => post(`/invitations/${encodeURIComponent(token)}/accept`, {}, {}),
293
+ snapshots: () => get("/workspace/snapshots", { snapshots: [] }),
294
+ createSnapshot: (name: string) => post("/workspace/snapshots", { name }, {}),
295
+ compareSnapshots: (before_id: string, after_id: string) => post("/workspace/snapshots/compare", { before_id, after_id }, {}),
296
+ restoreSnapshot: (id: string) => post(`/workspace/snapshots/${encodeURIComponent(id)}/restore`, {}, {}),
297
+ exportSnapshot: (id: string) => post(`/workspace/snapshots/${encodeURIComponent(id)}/export`, {}, {}),
298
+ timeMachine: () => get("/workspace/time-machine", { events: [] }, { limit: 100 }),
299
+ realtimeFeed: () => get("/realtime/feed", { events: [] }, { limit: 80 }),
300
+ presence: () => get("/realtime/presence", { presence: [] }),
301
+ networkIdentity: () => get("/network/identity", {}),
302
+ networkPeers: () => get("/network/peers", { peers: [] }),
303
+ pairPeer: (body: unknown) => post("/network/peers", body, {}),
304
+ unpairPeer: (id: string) => del(`/network/peers/${encodeURIComponent(id)}`, {}),
305
+ pushPeer: (id: string, workspace_id?: string | null) => post(`/network/push/${encodeURIComponent(id)}`, { workspace_id }, {}),
306
+ sysinfo: () => get("/local/sysinfo", {}),
307
+ computerMemory: () => get("/workspace/computer-memory", {}),
308
+ setComputerMemory: (enabled: boolean) => post("/workspace/computer-memory", { enabled, consent: { approved: enabled } }, {}),
309
+ adminSummary: () => get("/admin/summary", {}),
310
+ adminUsers: () => get("/admin/users", []),
311
+ adminAudit: () => get("/admin/audit", { recent_events: [] }),
312
+ adminRoles: () => get("/admin/roles", { roles: [] }),
313
+ adminPolicies: () => get("/admin/policies", { policies: [] }),
314
+ adminSecurity: () => get("/admin/security/overview", {}),
315
+ vpcStatus: () => get("/vpc/status", {}),
316
+ toolPermissions: () => get("/tools/permissions", { permissions: [] }),
317
+ };