ltcai 4.0.1 → 4.2.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 (192) hide show
  1. package/README.md +33 -24
  2. package/desktop/electron/main.cjs +44 -0
  3. package/docs/CHANGELOG.md +84 -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_2_BRAIN_CORE_ARCHITECTURE.md +97 -0
  8. package/docs/V4_2_STORAGE_MIGRATION_REPORT.md +91 -0
  9. package/docs/V4_2_VALIDATION_REPORT.md +89 -0
  10. package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +31 -26
  11. package/frontend/index.html +24 -0
  12. package/frontend/openapi.json +14436 -0
  13. package/frontend/src/App.tsx +184 -0
  14. package/frontend/src/api/client.ts +320 -0
  15. package/frontend/src/api/openapi.ts +16921 -0
  16. package/frontend/src/components/primitives.tsx +204 -0
  17. package/frontend/src/components/ui/badge.tsx +27 -0
  18. package/frontend/src/components/ui/button.tsx +37 -0
  19. package/frontend/src/components/ui/card.tsx +22 -0
  20. package/frontend/src/components/ui/input.tsx +16 -0
  21. package/frontend/src/components/ui/textarea.tsx +16 -0
  22. package/frontend/src/lib/utils.ts +33 -0
  23. package/frontend/src/main.tsx +23 -0
  24. package/frontend/src/pages/Act.tsx +245 -0
  25. package/frontend/src/pages/Ask.tsx +200 -0
  26. package/frontend/src/pages/Brain.tsx +267 -0
  27. package/frontend/src/pages/Capture.tsx +158 -0
  28. package/frontend/src/pages/Library.tsx +187 -0
  29. package/frontend/src/pages/System.tsx +378 -0
  30. package/frontend/src/routes.ts +85 -0
  31. package/frontend/src/store/appStore.ts +54 -0
  32. package/frontend/src/styles.css +107 -0
  33. package/kg_schema.py +1 -1
  34. package/knowledge_graph.py +4 -4
  35. package/lattice_brain/__init__.py +70 -0
  36. package/lattice_brain/_kg_common.py +1 -0
  37. package/lattice_brain/archive.py +133 -0
  38. package/lattice_brain/context.py +3 -0
  39. package/lattice_brain/conversations.py +3 -0
  40. package/lattice_brain/core.py +82 -0
  41. package/lattice_brain/discovery.py +1 -0
  42. package/lattice_brain/documents.py +1 -0
  43. package/lattice_brain/embeddings.py +82 -0
  44. package/lattice_brain/identity.py +13 -0
  45. package/lattice_brain/ingest.py +1 -0
  46. package/lattice_brain/memory.py +3 -0
  47. package/lattice_brain/network.py +1 -0
  48. package/lattice_brain/projection.py +1 -0
  49. package/lattice_brain/provenance.py +1 -0
  50. package/lattice_brain/retrieval.py +1 -0
  51. package/lattice_brain/schema.py +1 -0
  52. package/lattice_brain/storage/__init__.py +22 -0
  53. package/lattice_brain/storage/base.py +72 -0
  54. package/lattice_brain/storage/docker.py +105 -0
  55. package/lattice_brain/storage/factory.py +31 -0
  56. package/lattice_brain/storage/migration.py +190 -0
  57. package/lattice_brain/storage/postgres.py +123 -0
  58. package/lattice_brain/storage/sqlite.py +128 -0
  59. package/lattice_brain/store.py +3 -0
  60. package/lattice_brain/write_master.py +1 -0
  61. package/latticeai/__init__.py +1 -1
  62. package/latticeai/api/portability.py +69 -0
  63. package/latticeai/api/setup.py +5 -4
  64. package/latticeai/api/static_routes.py +4 -4
  65. package/latticeai/app_factory.py +17 -10
  66. package/latticeai/brain/__init__.py +6 -6
  67. package/latticeai/brain/_kg_common.py +1 -1
  68. package/latticeai/brain/network.py +1 -1
  69. package/latticeai/brain/retrieval.py +15 -0
  70. package/latticeai/brain/store.py +22 -6
  71. package/latticeai/core/config.py +8 -0
  72. package/latticeai/core/marketplace.py +1 -1
  73. package/latticeai/core/multi_agent.py +1 -1
  74. package/latticeai/core/workspace_os.py +1 -1
  75. package/latticeai/services/kg_portability.py +82 -1
  76. package/package.json +55 -15
  77. package/scripts/build_frontend_assets.mjs +38 -0
  78. package/scripts/bump_version.py +4 -1
  79. package/scripts/export_openapi.py +31 -0
  80. package/scripts/lint_frontend.mjs +91 -0
  81. package/scripts/migrate_brain_storage.py +53 -0
  82. package/scripts/run_python.mjs +47 -0
  83. package/scripts/wheel_smoke.py +3 -0
  84. package/src-tauri/Cargo.lock +4833 -0
  85. package/src-tauri/Cargo.toml +19 -0
  86. package/src-tauri/build.rs +3 -0
  87. package/src-tauri/capabilities/default.json +7 -0
  88. package/src-tauri/src/main.rs +78 -0
  89. package/src-tauri/tauri.conf.json +39 -0
  90. package/static/app/asset-manifest.json +32 -0
  91. package/static/app/assets/core-CwxXejkd.js +2 -0
  92. package/static/app/assets/core-CwxXejkd.js.map +1 -0
  93. package/static/app/assets/index-CDjiH_se.css +2 -0
  94. package/static/app/assets/index-C_HAkbAg.js +333 -0
  95. package/static/app/assets/index-C_HAkbAg.js.map +1 -0
  96. package/static/app/index.html +25 -0
  97. package/static/manifest.json +2 -2
  98. package/static/sw.js +4 -4
  99. package/scripts/build_v3_assets.mjs +0 -170
  100. package/scripts/lint_v3.mjs +0 -120
  101. package/static/v3/asset-manifest.json +0 -63
  102. package/static/v3/css/lattice.base.49deefb5.css +0 -128
  103. package/static/v3/css/lattice.base.css +0 -128
  104. package/static/v3/css/lattice.components.cde18231.css +0 -472
  105. package/static/v3/css/lattice.components.css +0 -472
  106. package/static/v3/css/lattice.shell.29d36d85.css +0 -452
  107. package/static/v3/css/lattice.shell.css +0 -452
  108. package/static/v3/css/lattice.tokens.304cbc40.css +0 -135
  109. package/static/v3/css/lattice.tokens.css +0 -135
  110. package/static/v3/css/lattice.views.0a18b6c5.css +0 -360
  111. package/static/v3/css/lattice.views.css +0 -360
  112. package/static/v3/index.html +0 -68
  113. package/static/v3/js/app.c5c80c46.js +0 -26
  114. package/static/v3/js/app.js +0 -26
  115. package/static/v3/js/core/api.ba0fbf14.js +0 -625
  116. package/static/v3/js/core/api.js +0 -625
  117. package/static/v3/js/core/components.f25b3b93.js +0 -230
  118. package/static/v3/js/core/components.js +0 -230
  119. package/static/v3/js/core/dom.a2773eb0.js +0 -148
  120. package/static/v3/js/core/dom.js +0 -148
  121. package/static/v3/js/core/i18n.880e1fec.js +0 -575
  122. package/static/v3/js/core/i18n.js +0 -575
  123. package/static/v3/js/core/router.584570f2.js +0 -37
  124. package/static/v3/js/core/router.js +0 -37
  125. package/static/v3/js/core/routes.37522821.js +0 -101
  126. package/static/v3/js/core/routes.js +0 -101
  127. package/static/v3/js/core/shell.e3f6bbfa.js +0 -420
  128. package/static/v3/js/core/shell.js +0 -420
  129. package/static/v3/js/core/store.7b2aa044.js +0 -123
  130. package/static/v3/js/core/store.js +0 -123
  131. package/static/v3/js/views/account.eff40715.js +0 -143
  132. package/static/v3/js/views/account.js +0 -143
  133. package/static/v3/js/views/activity.0d271ef9.js +0 -67
  134. package/static/v3/js/views/activity.js +0 -67
  135. package/static/v3/js/views/admin-audit.660a1fb1.js +0 -185
  136. package/static/v3/js/views/admin-audit.js +0 -185
  137. package/static/v3/js/views/admin-permissions.a7ae5f09.js +0 -177
  138. package/static/v3/js/views/admin-permissions.js +0 -177
  139. package/static/v3/js/views/admin-policies.3658fd86.js +0 -102
  140. package/static/v3/js/views/admin-policies.js +0 -102
  141. package/static/v3/js/views/admin-private-vpc.7d342d36.js +0 -135
  142. package/static/v3/js/views/admin-private-vpc.js +0 -135
  143. package/static/v3/js/views/admin-security.07c66b72.js +0 -180
  144. package/static/v3/js/views/admin-security.js +0 -180
  145. package/static/v3/js/views/admin-users.f7ac7b43.js +0 -166
  146. package/static/v3/js/views/admin-users.js +0 -166
  147. package/static/v3/js/views/agents.17c5288d.js +0 -564
  148. package/static/v3/js/views/agents.js +0 -564
  149. package/static/v3/js/views/chat.e250e2cc.js +0 -624
  150. package/static/v3/js/views/chat.js +0 -624
  151. package/static/v3/js/views/files.adad14c1.js +0 -365
  152. package/static/v3/js/views/files.js +0 -365
  153. package/static/v3/js/views/graph-canvas.17c15d65.js +0 -509
  154. package/static/v3/js/views/graph-canvas.js +0 -509
  155. package/static/v3/js/views/home.24f8b8ae.js +0 -200
  156. package/static/v3/js/views/home.js +0 -200
  157. package/static/v3/js/views/hooks.37895880.js +0 -220
  158. package/static/v3/js/views/hooks.js +0 -220
  159. package/static/v3/js/views/hybrid-search.2fb63ed9.js +0 -194
  160. package/static/v3/js/views/hybrid-search.js +0 -194
  161. package/static/v3/js/views/knowledge-graph.4d09c537.js +0 -529
  162. package/static/v3/js/views/knowledge-graph.js +0 -529
  163. package/static/v3/js/views/marketplace.ab0583d4.js +0 -141
  164. package/static/v3/js/views/marketplace.js +0 -141
  165. package/static/v3/js/views/mcp.99b5c6a7.js +0 -114
  166. package/static/v3/js/views/mcp.js +0 -114
  167. package/static/v3/js/views/memory.4ebdf474.js +0 -147
  168. package/static/v3/js/views/memory.js +0 -147
  169. package/static/v3/js/views/models.a1ffa147.js +0 -256
  170. package/static/v3/js/views/models.js +0 -256
  171. package/static/v3/js/views/my-computer.d9d9ae1c.js +0 -463
  172. package/static/v3/js/views/my-computer.js +0 -463
  173. package/static/v3/js/views/network.52a4f181.js +0 -97
  174. package/static/v3/js/views/network.js +0 -97
  175. package/static/v3/js/views/pipeline.c522f1ce.js +0 -157
  176. package/static/v3/js/views/pipeline.js +0 -157
  177. package/static/v3/js/views/planning.4876fd77.js +0 -174
  178. package/static/v3/js/views/planning.js +0 -174
  179. package/static/v3/js/views/runs.b63b2afa.js +0 -144
  180. package/static/v3/js/views/runs.js +0 -144
  181. package/static/v3/js/views/settings.b7140634.js +0 -317
  182. package/static/v3/js/views/settings.js +0 -317
  183. package/static/v3/js/views/skills.c6c2f965.js +0 -109
  184. package/static/v3/js/views/skills.js +0 -109
  185. package/static/v3/js/views/snapshots.6f5db095.js +0 -135
  186. package/static/v3/js/views/snapshots.js +0 -135
  187. package/static/v3/js/views/tools.e4f11276.js +0 -108
  188. package/static/v3/js/views/tools.js +0 -108
  189. package/static/v3/js/views/workflows.7752225a.js +0 -213
  190. package/static/v3/js/views/workflows.js +0 -213
  191. package/static/v3/js/views/workspace-admin.c466029b.js +0 -156
  192. package/static/v3/js/views/workspace-admin.js +0 -156
@@ -0,0 +1,204 @@
1
+ import * as React from "react";
2
+ import { useMutation, useQueryClient } from "@tanstack/react-query";
3
+ import { AlertCircle, CheckCircle2, Loader2 } from "lucide-react";
4
+ import { ApiResult } from "@/api/client";
5
+ import { Badge } from "@/components/ui/badge";
6
+ import { Button } from "@/components/ui/button";
7
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
8
+ import { cn, asArray, fmtNumber, titleize } from "@/lib/utils";
9
+
10
+ export function SourceBadge({ result }: { result?: Pick<ApiResult, "source" | "ok" | "status"> }) {
11
+ if (!result) return <Badge variant="muted">not loaded</Badge>;
12
+ if (result.source === "live" && result.ok) return <Badge variant="success">live API</Badge>;
13
+ return <Badge variant="warning">unavailable</Badge>;
14
+ }
15
+
16
+ export function EmptyState({ title = "Unavailable", detail }: { title?: string; detail?: React.ReactNode }) {
17
+ return (
18
+ <div className="flex min-h-28 flex-col items-center justify-center gap-2 rounded-md border border-dashed border-border bg-muted/30 p-5 text-center text-sm text-muted-foreground">
19
+ <AlertCircle className="h-5 w-5" />
20
+ <div className="font-medium text-foreground">{title}</div>
21
+ {detail ? <div>{detail}</div> : null}
22
+ </div>
23
+ );
24
+ }
25
+
26
+ export function DataPanel<T>({
27
+ title,
28
+ description,
29
+ result,
30
+ children,
31
+ className,
32
+ }: {
33
+ title: string;
34
+ description?: string;
35
+ result?: ApiResult<T>;
36
+ children: (data: T) => React.ReactNode;
37
+ className?: string;
38
+ }) {
39
+ return (
40
+ <Card className={className}>
41
+ <CardHeader className="flex-row items-start justify-between gap-3">
42
+ <div>
43
+ <CardTitle>{title}</CardTitle>
44
+ {description ? <CardDescription>{description}</CardDescription> : null}
45
+ </div>
46
+ <SourceBadge result={result} />
47
+ </CardHeader>
48
+ <CardContent>
49
+ {result?.ok ? children(result.data) : <EmptyState detail={result?.error || "The backend did not return this capability."} />}
50
+ </CardContent>
51
+ </Card>
52
+ );
53
+ }
54
+
55
+ export function LoadingPanel({ title }: { title: string }) {
56
+ return (
57
+ <Card>
58
+ <CardHeader>
59
+ <CardTitle>{title}</CardTitle>
60
+ </CardHeader>
61
+ <CardContent>
62
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
63
+ <Loader2 className="h-4 w-4 animate-spin" /> Loading
64
+ </div>
65
+ </CardContent>
66
+ </Card>
67
+ );
68
+ }
69
+
70
+ export function StatGrid({ stats }: { stats: Array<{ label: string; value: unknown; hint?: string }> }) {
71
+ return (
72
+ <div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
73
+ {stats.map((stat) => (
74
+ <div key={stat.label} className="rounded-md border border-border bg-background p-3">
75
+ <div className="text-xs uppercase tracking-wide text-muted-foreground">{stat.label}</div>
76
+ <div className="mt-1 text-2xl font-semibold">{typeof stat.value === "number" ? fmtNumber(stat.value) : String(stat.value ?? "-")}</div>
77
+ {stat.hint ? <div className="mt-1 text-xs text-muted-foreground">{stat.hint}</div> : null}
78
+ </div>
79
+ ))}
80
+ </div>
81
+ );
82
+ }
83
+
84
+ export function JsonView({ value }: { value: unknown }) {
85
+ return (
86
+ <pre className="max-h-80 overflow-auto rounded-md border border-border bg-muted/40 p-3 text-xs leading-relaxed text-muted-foreground">
87
+ {JSON.stringify(value, null, 2)}
88
+ </pre>
89
+ );
90
+ }
91
+
92
+ export function KeyValueList({ data, limit = 8 }: { data: Record<string, unknown>; limit?: number }) {
93
+ const rows = Object.entries(data || {}).slice(0, limit);
94
+ if (!rows.length) return <EmptyState title="No values" />;
95
+ return (
96
+ <div className="divide-y divide-border rounded-md border border-border">
97
+ {rows.map(([key, value]) => (
98
+ <div key={key} className="grid grid-cols-[minmax(9rem,0.5fr)_1fr] gap-3 p-3 text-sm">
99
+ <span className="font-medium text-muted-foreground">{titleize(key)}</span>
100
+ <span className="break-words">{typeof value === "object" ? JSON.stringify(value) : String(value ?? "-")}</span>
101
+ </div>
102
+ ))}
103
+ </div>
104
+ );
105
+ }
106
+
107
+ export function EntityList({
108
+ items,
109
+ titleKey = "title",
110
+ metaKey = "type",
111
+ limit = 8,
112
+ }: {
113
+ items: unknown;
114
+ titleKey?: string;
115
+ metaKey?: string;
116
+ limit?: number;
117
+ }) {
118
+ const rows = asArray<Record<string, unknown>>(items).slice(0, limit);
119
+ if (!rows.length) return <EmptyState title="No records" detail="The API returned an empty collection." />;
120
+ return (
121
+ <div className="grid gap-2">
122
+ {rows.map((item, index) => (
123
+ <div key={String(item.id || item.name || index)} className="rounded-md border border-border bg-background p-3">
124
+ <div className="flex flex-wrap items-center justify-between gap-2">
125
+ <div className="font-medium">{String(item[titleKey] || item.name || item.id || `Record ${index + 1}`)}</div>
126
+ <Badge variant="muted">{String(item[metaKey] || item.status || item.state || "record")}</Badge>
127
+ </div>
128
+ {item.summary || item.description || item.path ? (
129
+ <p className="mt-1 text-sm text-muted-foreground">{String(item.summary || item.description || item.path)}</p>
130
+ ) : null}
131
+ </div>
132
+ ))}
133
+ </div>
134
+ );
135
+ }
136
+
137
+ export function ActionButton({
138
+ label,
139
+ successLabel = "Done",
140
+ action,
141
+ invalidate,
142
+ variant = "outline",
143
+ disabled,
144
+ }: {
145
+ label: string;
146
+ successLabel?: string;
147
+ action: () => Promise<ApiResult<unknown>>;
148
+ invalidate?: string[];
149
+ variant?: React.ComponentProps<typeof Button>["variant"];
150
+ disabled?: boolean;
151
+ }) {
152
+ const qc = useQueryClient();
153
+ const [result, setResult] = React.useState<string | null>(null);
154
+ const mut = useMutation({
155
+ mutationFn: action,
156
+ onSuccess: async (res) => {
157
+ setResult(res.ok ? successLabel : res.error || "Unavailable");
158
+ if (invalidate) {
159
+ await Promise.all(invalidate.map((key) => qc.invalidateQueries({ queryKey: [key] })));
160
+ }
161
+ },
162
+ });
163
+ return (
164
+ <div className="flex flex-wrap items-center gap-2">
165
+ <Button variant={variant} disabled={disabled || mut.isPending} onClick={() => mut.mutate()}>
166
+ {mut.isPending ? <Loader2 className="h-4 w-4 animate-spin" /> : null}
167
+ {label}
168
+ </Button>
169
+ {result ? (
170
+ <span className={cn("inline-flex items-center gap-1 text-xs", result === successLabel ? "text-emerald-300" : "text-amber-300")}>
171
+ {result === successLabel ? <CheckCircle2 className="h-3.5 w-3.5" /> : <AlertCircle className="h-3.5 w-3.5" />}
172
+ {result}
173
+ </span>
174
+ ) : null}
175
+ </div>
176
+ );
177
+ }
178
+
179
+ export function Tabs({
180
+ tabs,
181
+ value,
182
+ onChange,
183
+ }: {
184
+ tabs: Array<{ id: string; label: string }>;
185
+ value: string;
186
+ onChange: (id: string) => void;
187
+ }) {
188
+ return (
189
+ <div className="flex flex-wrap gap-1 rounded-md border border-border bg-muted/30 p-1">
190
+ {tabs.map((tab) => (
191
+ <button
192
+ key={tab.id}
193
+ onClick={() => onChange(tab.id)}
194
+ className={cn(
195
+ "h-8 rounded px-3 text-sm font-medium transition",
196
+ value === tab.id ? "bg-background text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground",
197
+ )}
198
+ >
199
+ {tab.label}
200
+ </button>
201
+ ))}
202
+ </div>
203
+ );
204
+ }
@@ -0,0 +1,27 @@
1
+ import * as React from "react";
2
+ import { cn } from "@/lib/utils";
3
+
4
+ type BadgeProps = React.HTMLAttributes<HTMLSpanElement> & {
5
+ variant?: "default" | "success" | "warning" | "muted" | "danger";
6
+ };
7
+
8
+ const variants = {
9
+ default: "border-primary/25 bg-primary/12 text-primary",
10
+ success: "border-emerald-500/25 bg-emerald-500/12 text-emerald-300",
11
+ warning: "border-amber-500/25 bg-amber-500/12 text-amber-300",
12
+ muted: "border-border bg-muted text-muted-foreground",
13
+ danger: "border-destructive/30 bg-destructive/12 text-destructive",
14
+ };
15
+
16
+ export function Badge({ className, variant = "default", ...props }: BadgeProps) {
17
+ return (
18
+ <span
19
+ className={cn(
20
+ "inline-flex min-h-6 items-center rounded-md border px-2 py-0.5 text-xs font-medium",
21
+ variants[variant],
22
+ className,
23
+ )}
24
+ {...props}
25
+ />
26
+ );
27
+ }
@@ -0,0 +1,37 @@
1
+ import * as React from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+ import { cn } from "@/lib/utils";
4
+
5
+ const buttonVariants = cva(
6
+ "inline-flex h-9 items-center justify-center gap-2 rounded-md px-3 text-sm font-medium transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-45",
7
+ {
8
+ variants: {
9
+ variant: {
10
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
11
+ secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
12
+ ghost: "hover:bg-muted text-foreground",
13
+ outline: "border border-border bg-background hover:bg-muted",
14
+ destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15
+ },
16
+ size: {
17
+ sm: "h-8 px-2.5 text-xs",
18
+ md: "h-9 px-3",
19
+ icon: "h-9 w-9 px-0",
20
+ },
21
+ },
22
+ defaultVariants: {
23
+ variant: "default",
24
+ size: "md",
25
+ },
26
+ },
27
+ );
28
+
29
+ export type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> &
30
+ VariantProps<typeof buttonVariants>;
31
+
32
+ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
33
+ ({ className, variant, size, ...props }, ref) => (
34
+ <button ref={ref} className={cn(buttonVariants({ variant, size, className }))} {...props} />
35
+ ),
36
+ );
37
+ Button.displayName = "Button";
@@ -0,0 +1,22 @@
1
+ import * as React from "react";
2
+ import { cn } from "@/lib/utils";
3
+
4
+ export function Card({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
5
+ return <section className={cn("rounded-lg border border-border bg-card text-card-foreground shadow-sm", className)} {...props} />;
6
+ }
7
+
8
+ export function CardHeader({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
9
+ return <div className={cn("flex flex-col gap-1.5 p-4", className)} {...props} />;
10
+ }
11
+
12
+ export function CardTitle({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) {
13
+ return <h2 className={cn("text-sm font-semibold tracking-normal", className)} {...props} />;
14
+ }
15
+
16
+ export function CardDescription({ className, ...props }: React.HTMLAttributes<HTMLParagraphElement>) {
17
+ return <p className={cn("text-sm text-muted-foreground", className)} {...props} />;
18
+ }
19
+
20
+ export function CardContent({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
21
+ return <div className={cn("p-4 pt-0", className)} {...props} />;
22
+ }
@@ -0,0 +1,16 @@
1
+ import * as React from "react";
2
+ import { cn } from "@/lib/utils";
3
+
4
+ export const Input = React.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>(
5
+ ({ className, ...props }, ref) => (
6
+ <input
7
+ ref={ref}
8
+ className={cn(
9
+ "h-9 w-full rounded-md border border-input bg-background px-3 text-sm outline-none transition placeholder:text-muted-foreground focus:ring-2 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
10
+ className,
11
+ )}
12
+ {...props}
13
+ />
14
+ ),
15
+ );
16
+ Input.displayName = "Input";
@@ -0,0 +1,16 @@
1
+ import * as React from "react";
2
+ import { cn } from "@/lib/utils";
3
+
4
+ export const Textarea = React.forwardRef<HTMLTextAreaElement, React.TextareaHTMLAttributes<HTMLTextAreaElement>>(
5
+ ({ className, ...props }, ref) => (
6
+ <textarea
7
+ ref={ref}
8
+ className={cn(
9
+ "min-h-24 w-full resize-y rounded-md border border-input bg-background px-3 py-2 text-sm outline-none transition placeholder:text-muted-foreground focus:ring-2 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
10
+ className,
11
+ )}
12
+ {...props}
13
+ />
14
+ ),
15
+ );
16
+ Textarea.displayName = "Textarea";
@@ -0,0 +1,33 @@
1
+ import { clsx, type ClassValue } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
7
+
8
+ export function fmtNumber(value: unknown, fallback = "0") {
9
+ const n = Number(value);
10
+ if (!Number.isFinite(n)) return fallback;
11
+ return new Intl.NumberFormat().format(n);
12
+ }
13
+
14
+ export function pct(value: unknown) {
15
+ const n = Number(value);
16
+ if (!Number.isFinite(n)) return "0%";
17
+ return `${Math.round(n * 100)}%`;
18
+ }
19
+
20
+ export function shortId(value: unknown, length = 10) {
21
+ const text = String(value || "");
22
+ return text.length > length ? `${text.slice(0, length)}...` : text;
23
+ }
24
+
25
+ export function asArray<T = Record<string, unknown>>(value: unknown): T[] {
26
+ return Array.isArray(value) ? (value as T[]) : [];
27
+ }
28
+
29
+ export function titleize(value: unknown) {
30
+ return String(value || "")
31
+ .replace(/[_-]+/g, " ")
32
+ .replace(/\b\w/g, (m) => m.toUpperCase());
33
+ }
@@ -0,0 +1,23 @@
1
+ import React from "react";
2
+ import ReactDOM from "react-dom/client";
3
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
4
+ import App from "./App";
5
+ import "./styles.css";
6
+
7
+ const queryClient = new QueryClient({
8
+ defaultOptions: {
9
+ queries: {
10
+ staleTime: 15_000,
11
+ refetchOnWindowFocus: false,
12
+ retry: 1,
13
+ },
14
+ },
15
+ });
16
+
17
+ ReactDOM.createRoot(document.getElementById("root")!).render(
18
+ <React.StrictMode>
19
+ <QueryClientProvider client={queryClient}>
20
+ <App />
21
+ </QueryClientProvider>
22
+ </React.StrictMode>,
23
+ );
@@ -0,0 +1,245 @@
1
+ import * as React from "react";
2
+ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
3
+ import ReactFlow, { Background, Controls, Edge, Node } from "reactflow";
4
+ import { Bot, GitBranch, PauseCircle, Play, Workflow } from "lucide-react";
5
+ import { latticeApi } from "@/api/client";
6
+ import { ActionButton, DataPanel, EntityList, JsonView, Tabs } from "@/components/primitives";
7
+ import { Badge } from "@/components/ui/badge";
8
+ import { Button } from "@/components/ui/button";
9
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
10
+ import { Input } from "@/components/ui/input";
11
+ import { Textarea } from "@/components/ui/textarea";
12
+ import { asArray, shortId } from "@/lib/utils";
13
+
14
+ type ActTab = "agents" | "runs" | "workflows" | "hooks" | "tools";
15
+
16
+ const tabs: Array<{ id: ActTab; label: string }> = [
17
+ { id: "agents", label: "Agents" },
18
+ { id: "runs", label: "Runs" },
19
+ { id: "workflows", label: "Workflows" },
20
+ { id: "hooks", label: "Hooks" },
21
+ { id: "tools", label: "Tools" },
22
+ ];
23
+
24
+ export function ActPage({ initialTab }: { initialTab?: string }) {
25
+ const [tab, setTab] = React.useState<ActTab>((initialTab as ActTab) || "agents");
26
+ React.useEffect(() => {
27
+ if (tabs.some((item) => item.id === initialTab)) setTab(initialTab as ActTab);
28
+ }, [initialTab]);
29
+ return (
30
+ <div className="space-y-4">
31
+ <header>
32
+ <div className="flex items-center gap-2 text-sm text-primary"><Workflow className="h-4 w-4" /> Durable execution</div>
33
+ <h1 className="mt-2 text-3xl font-semibold">Act</h1>
34
+ <p className="mt-2 max-w-3xl text-sm text-muted-foreground">Agents, workflows, approvals, hooks, and governed tools. Pauses and unavailable states are surfaced honestly.</p>
35
+ </header>
36
+ <Tabs tabs={tabs} value={tab} onChange={(id) => setTab(id as ActTab)} />
37
+ {tab === "agents" ? <AgentsPanel /> : null}
38
+ {tab === "runs" ? <RunsPanel /> : null}
39
+ {tab === "workflows" ? <WorkflowsPanel /> : null}
40
+ {tab === "hooks" ? <HooksPanel /> : null}
41
+ {tab === "tools" ? <ToolsPanel /> : null}
42
+ </div>
43
+ );
44
+ }
45
+
46
+ function AgentsPanel() {
47
+ const qc = useQueryClient();
48
+ const [goal, setGoal] = React.useState("");
49
+ const runtime = useQuery({ queryKey: ["agentRuntime"], queryFn: latticeApi.agentRuntime });
50
+ const registry = useQuery({ queryKey: ["agentRegistry"], queryFn: latticeApi.agentRegistry });
51
+ const caps = useQuery({ queryKey: ["agentCapabilities"], queryFn: latticeApi.agentCapabilities });
52
+ const run = useMutation({
53
+ mutationFn: () => latticeApi.runAgent(goal, ["planner", "executor", "reviewer"]),
54
+ onSuccess: () => qc.invalidateQueries({ queryKey: ["agentRuntime"] }),
55
+ });
56
+ const [agentName, setAgentName] = React.useState("");
57
+ const register = useMutation({
58
+ mutationFn: () => latticeApi.registerAgent({ name: agentName, type: "custom", capabilities: [] }),
59
+ onSuccess: () => qc.invalidateQueries({ queryKey: ["agentRegistry"] }),
60
+ });
61
+ return (
62
+ <div className="grid gap-4 xl:grid-cols-[0.9fr_1.1fr]">
63
+ <Card>
64
+ <CardHeader>
65
+ <CardTitle className="flex items-center gap-2"><Bot className="h-4 w-4" /> Run agent pipeline</CardTitle>
66
+ <CardDescription>POST `/agents/api/run` creates a durable run; mode is determined by backend model availability.</CardDescription>
67
+ </CardHeader>
68
+ <CardContent className="space-y-3">
69
+ <Textarea value={goal} onChange={(e) => setGoal(e.target.value)} placeholder="Describe the objective..." />
70
+ <Button disabled={!goal.trim() || run.isPending} onClick={() => run.mutate()}><Play className="h-4 w-4" /> Run planner/executor/reviewer</Button>
71
+ {run.data ? <JsonView value={run.data.data || run.data.error} /> : null}
72
+ </CardContent>
73
+ </Card>
74
+ <DataPanel title="Runtime status" result={runtime.data}>
75
+ {(data) => <JsonView value={data} />}
76
+ </DataPanel>
77
+ <DataPanel title="Agent registry" result={registry.data}>
78
+ {(data) => (
79
+ <div className="space-y-3">
80
+ <EntityList items={(data as Record<string, unknown>).agents} titleKey="name" metaKey="type" />
81
+ <div className="flex gap-2">
82
+ <Input value={agentName} onChange={(e) => setAgentName(e.target.value)} placeholder="New custom agent name" />
83
+ <Button disabled={!agentName.trim() || register.isPending} onClick={() => register.mutate()}>Register</Button>
84
+ </div>
85
+ </div>
86
+ )}
87
+ </DataPanel>
88
+ <DataPanel title="Agent capabilities" result={caps.data}>
89
+ {(data) => <JsonView value={data} />}
90
+ </DataPanel>
91
+ </div>
92
+ );
93
+ }
94
+
95
+ function RunsPanel() {
96
+ const runtime = useQuery({ queryKey: ["agentRuntime"], queryFn: latticeApi.agentRuntime });
97
+ const workflows = useQuery({ queryKey: ["workflowRuns"], queryFn: latticeApi.workflowRuns });
98
+ const pending = useQuery({ queryKey: ["permissions"], queryFn: latticeApi.permissionsPending });
99
+ const agentRuns = asArray<Record<string, unknown>>((runtime.data?.data as Record<string, unknown>)?.runs);
100
+ const workflowRuns = asArray<Record<string, unknown>>((workflows.data?.data as Record<string, unknown>)?.runs);
101
+ return (
102
+ <div className="grid gap-4 xl:grid-cols-2">
103
+ <DataPanel title="Agent runs" result={runtime.data}>
104
+ {() => <RunList runs={agentRuns} kind="agent" />}
105
+ </DataPanel>
106
+ <DataPanel title="Workflow runs" result={workflows.data}>
107
+ {() => <RunList runs={workflowRuns} kind="workflow" />}
108
+ </DataPanel>
109
+ <DataPanel title="Approval inbox" result={pending.data} className="xl:col-span-2">
110
+ {(data) => {
111
+ const pendingMap = ((data as Record<string, unknown>).pending || {}) as Record<string, unknown>;
112
+ const rows = Object.entries(pendingMap);
113
+ return rows.length ? (
114
+ <div className="grid gap-2">
115
+ {rows.map(([token, value]) => (
116
+ <div key={token} className="flex flex-wrap items-center justify-between gap-3 rounded-md border border-border p-3">
117
+ <div>
118
+ <div className="font-medium">{shortId(token, 16)}</div>
119
+ <div className="text-sm text-muted-foreground">{JSON.stringify(value)}</div>
120
+ </div>
121
+ <div className="flex gap-2">
122
+ <ActionButton label="Approve" action={() => latticeApi.approvePermission(token)} invalidate={["permissions"]} />
123
+ <ActionButton label="Deny" action={() => latticeApi.denyPermission(token)} invalidate={["permissions"]} variant="destructive" />
124
+ </div>
125
+ </div>
126
+ ))}
127
+ </div>
128
+ ) : <EntityList items={[]} />;
129
+ }}
130
+ </DataPanel>
131
+ </div>
132
+ );
133
+ }
134
+
135
+ function RunList({ runs, kind }: { runs: Array<Record<string, unknown>>; kind: "agent" | "workflow" }) {
136
+ if (!runs.length) return <EntityList items={[]} />;
137
+ return (
138
+ <div className="grid gap-2">
139
+ {runs.slice(0, 10).map((run) => {
140
+ const id = String(run.run_id || run.id);
141
+ const status = String(run.status || "unknown");
142
+ return (
143
+ <div key={id} className="rounded-md border border-border bg-background p-3">
144
+ <div className="flex flex-wrap items-center justify-between gap-2">
145
+ <div className="font-medium">{shortId(id, 18)}</div>
146
+ <Badge variant={status === "succeeded" ? "success" : status === "awaiting_approval" ? "warning" : "muted"}>{status}</Badge>
147
+ </div>
148
+ <div className="mt-2 flex flex-wrap gap-2">
149
+ <ActionButton label="Stop" action={() => kind === "agent" ? latticeApi.stopAgentRun(id) : latticeApi.stopWorkflowRun(id)} />
150
+ {status === "awaiting_approval" && kind === "workflow" ? (
151
+ <>
152
+ <ActionButton label="Resume approved" action={() => latticeApi.resumeWorkflowRun(id, true)} />
153
+ <ActionButton label="Resume denied" action={() => latticeApi.resumeWorkflowRun(id, false)} variant="destructive" />
154
+ </>
155
+ ) : null}
156
+ </div>
157
+ </div>
158
+ );
159
+ })}
160
+ </div>
161
+ );
162
+ }
163
+
164
+ function WorkflowsPanel() {
165
+ const defs = useQuery({ queryKey: ["workflowDefinitions"], queryFn: latticeApi.workflowDefinitions });
166
+ const triggers = useQuery({ queryKey: ["workflowTriggers"], queryFn: latticeApi.workflowTriggers });
167
+ const workflows = asArray<Record<string, unknown>>((defs.data?.data as Record<string, unknown>)?.workflows);
168
+ const nodes: Node[] = workflows.slice(0, 12).map((workflow, index) => ({
169
+ id: String(workflow.id || workflow.workflow_id || index),
170
+ position: { x: (index % 4) * 190, y: Math.floor(index / 4) * 120 },
171
+ data: { label: String(workflow.name || workflow.id || `Workflow ${index + 1}`) },
172
+ }));
173
+ const edges: Edge[] = nodes.slice(1).map((node, index) => ({ id: `e-${index}`, source: nodes[index].id, target: node.id }));
174
+ return (
175
+ <div className="grid gap-4 xl:grid-cols-[1.2fr_0.8fr]">
176
+ <Card>
177
+ <CardHeader>
178
+ <CardTitle className="flex items-center gap-2"><GitBranch className="h-4 w-4" /> Workflow graph</CardTitle>
179
+ <CardDescription>React Flow view of workflow definitions. Running a workflow calls its backend run endpoint.</CardDescription>
180
+ </CardHeader>
181
+ <CardContent>
182
+ <div className="h-[440px] rounded-lg border border-border">
183
+ <ReactFlow nodes={nodes} edges={edges} fitView>
184
+ <Background />
185
+ <Controls />
186
+ </ReactFlow>
187
+ </div>
188
+ </CardContent>
189
+ </Card>
190
+ <DataPanel title="Definitions" result={defs.data}>
191
+ {() => (
192
+ <div className="space-y-2">
193
+ {workflows.length ? workflows.map((workflow) => {
194
+ const id = String(workflow.id || workflow.workflow_id);
195
+ return (
196
+ <div key={id} className="rounded-md border border-border p-3">
197
+ <div className="font-medium">{String(workflow.name || id)}</div>
198
+ <div className="mt-2 flex gap-2">
199
+ <ActionButton label="Run" action={() => latticeApi.runWorkflow(id)} invalidate={["workflowRuns"]} />
200
+ </div>
201
+ </div>
202
+ );
203
+ }) : <EntityList items={[]} />}
204
+ </div>
205
+ )}
206
+ </DataPanel>
207
+ <DataPanel title="Trigger configuration" result={triggers.data} className="xl:col-span-2">
208
+ {(data) => <JsonView value={data} />}
209
+ </DataPanel>
210
+ </div>
211
+ );
212
+ }
213
+
214
+ function HooksPanel() {
215
+ const hooks = useQuery({ queryKey: ["hooks"], queryFn: latticeApi.hooks });
216
+ const runs = useQuery({ queryKey: ["hookRuns"], queryFn: latticeApi.hookRuns });
217
+ return (
218
+ <div className="grid gap-4 xl:grid-cols-2">
219
+ <DataPanel title="Hooks" result={hooks.data}>
220
+ {(data) => <EntityList items={(data as Record<string, unknown>).hooks} titleKey="name" metaKey="kind" />}
221
+ </DataPanel>
222
+ <DataPanel title="Hook run log" result={runs.data}>
223
+ {(data) => <EntityList items={(data as Record<string, unknown>).runs} titleKey="hook_id" metaKey="status" />}
224
+ </DataPanel>
225
+ <Card className="xl:col-span-2">
226
+ <CardHeader>
227
+ <CardTitle className="flex items-center gap-2"><PauseCircle className="h-4 w-4" /> Manual hook fire</CardTitle>
228
+ <CardDescription>Uses `/api/hooks/run`; no hook is treated as successful unless the backend records it.</CardDescription>
229
+ </CardHeader>
230
+ <CardContent>
231
+ <ActionButton label="Run all manual hooks" action={() => latticeApi.hookRun({ event: "manual" })} invalidate={["hookRuns"]} />
232
+ </CardContent>
233
+ </Card>
234
+ </div>
235
+ );
236
+ }
237
+
238
+ function ToolsPanel() {
239
+ const tools = useQuery({ queryKey: ["toolPermissions"], queryFn: latticeApi.toolPermissions });
240
+ return (
241
+ <DataPanel title="Tool governance" result={tools.data}>
242
+ {(data) => <JsonView value={data} />}
243
+ </DataPanel>
244
+ );
245
+ }