ltcai 4.0.0 → 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 (195) hide show
  1. package/README.md +42 -33
  2. package/desktop/electron/main.cjs +44 -0
  3. package/docs/CHANGELOG.md +106 -0
  4. package/docs/REALTIME_COLLABORATION.md +3 -3
  5. package/docs/V3_FRONTEND.md +9 -8
  6. package/docs/V4_1_FRONTEND_ARCHITECTURE_REVIEW.md +65 -0
  7. package/docs/V4_1_FRONTEND_MIGRATION_REPORT.md +70 -0
  8. package/docs/V4_1_VALIDATION_REPORT.md +47 -0
  9. package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +95 -45
  10. package/docs/kg-schema.md +6 -2
  11. package/docs/spec-vs-impl.md +10 -10
  12. package/frontend/index.html +24 -0
  13. package/frontend/openapi.json +14190 -0
  14. package/frontend/src/App.tsx +184 -0
  15. package/frontend/src/api/client.ts +317 -0
  16. package/frontend/src/api/openapi.ts +16637 -0
  17. package/frontend/src/components/primitives.tsx +204 -0
  18. package/frontend/src/components/ui/badge.tsx +27 -0
  19. package/frontend/src/components/ui/button.tsx +37 -0
  20. package/frontend/src/components/ui/card.tsx +22 -0
  21. package/frontend/src/components/ui/input.tsx +16 -0
  22. package/frontend/src/components/ui/textarea.tsx +16 -0
  23. package/frontend/src/lib/utils.ts +33 -0
  24. package/frontend/src/main.tsx +23 -0
  25. package/frontend/src/pages/Act.tsx +245 -0
  26. package/frontend/src/pages/Ask.tsx +200 -0
  27. package/frontend/src/pages/Brain.tsx +267 -0
  28. package/frontend/src/pages/Capture.tsx +158 -0
  29. package/frontend/src/pages/Library.tsx +187 -0
  30. package/frontend/src/pages/System.tsx +344 -0
  31. package/frontend/src/routes.ts +85 -0
  32. package/frontend/src/store/appStore.ts +54 -0
  33. package/frontend/src/styles.css +107 -0
  34. package/kg_schema.py +2 -603
  35. package/knowledge_graph.py +37 -4958
  36. package/latticeai/__init__.py +1 -1
  37. package/latticeai/api/admin.py +15 -16
  38. package/latticeai/api/agents.py +13 -6
  39. package/latticeai/api/auth.py +19 -11
  40. package/latticeai/api/invitations.py +100 -0
  41. package/latticeai/api/knowledge_graph.py +4 -11
  42. package/latticeai/api/plugins.py +3 -6
  43. package/latticeai/api/realtime.py +4 -7
  44. package/latticeai/api/setup.py +5 -4
  45. package/latticeai/api/static_routes.py +13 -16
  46. package/latticeai/api/ui_redirects.py +26 -0
  47. package/latticeai/api/workflow_designer.py +39 -6
  48. package/latticeai/api/workspace.py +24 -10
  49. package/latticeai/app_factory.py +88 -17
  50. package/latticeai/brain/_kg_common.py +1123 -0
  51. package/latticeai/brain/discovery.py +1455 -0
  52. package/latticeai/brain/documents.py +218 -0
  53. package/latticeai/brain/ingest.py +644 -0
  54. package/latticeai/brain/projection.py +561 -0
  55. package/latticeai/brain/provenance.py +401 -0
  56. package/latticeai/brain/retrieval.py +1316 -0
  57. package/latticeai/brain/schema.py +640 -0
  58. package/latticeai/brain/store.py +216 -0
  59. package/latticeai/brain/write_master.py +225 -0
  60. package/latticeai/core/invitations.py +131 -0
  61. package/latticeai/core/marketplace.py +1 -1
  62. package/latticeai/core/multi_agent.py +1 -1
  63. package/latticeai/core/policy.py +54 -0
  64. package/latticeai/core/realtime.py +65 -44
  65. package/latticeai/core/sessions.py +31 -5
  66. package/latticeai/core/users.py +147 -0
  67. package/latticeai/core/workspace_os.py +420 -20
  68. package/latticeai/services/agent_runtime.py +242 -4
  69. package/latticeai/services/run_executor.py +328 -0
  70. package/latticeai/services/workspace_service.py +27 -19
  71. package/package.json +54 -27
  72. package/scripts/build_frontend_assets.mjs +38 -0
  73. package/scripts/bump_version.py +1 -1
  74. package/scripts/export_openapi.py +31 -0
  75. package/scripts/lint_frontend.mjs +86 -0
  76. package/scripts/run_python.mjs +47 -0
  77. package/src-tauri/Cargo.lock +4833 -0
  78. package/src-tauri/Cargo.toml +19 -0
  79. package/src-tauri/build.rs +3 -0
  80. package/src-tauri/capabilities/default.json +7 -0
  81. package/src-tauri/src/main.rs +78 -0
  82. package/src-tauri/tauri.conf.json +36 -0
  83. package/static/app/asset-manifest.json +32 -0
  84. package/static/app/assets/core-CwxXejkd.js +2 -0
  85. package/static/app/assets/core-CwxXejkd.js.map +1 -0
  86. package/static/app/assets/index-CJRAzNnf.js +333 -0
  87. package/static/app/assets/index-CJRAzNnf.js.map +1 -0
  88. package/static/app/assets/index-CSwBBgf4.css +2 -0
  89. package/static/app/index.html +25 -0
  90. package/static/manifest.json +2 -2
  91. package/static/sw.js +4 -4
  92. package/scripts/build_v3_assets.mjs +0 -170
  93. package/scripts/lint_v3.mjs +0 -97
  94. package/static/account.html +0 -113
  95. package/static/activity.html +0 -73
  96. package/static/admin.html +0 -486
  97. package/static/agents.html +0 -139
  98. package/static/chat.html +0 -841
  99. package/static/css/reference/account.css +0 -439
  100. package/static/css/reference/admin.css +0 -610
  101. package/static/css/reference/base.css +0 -1661
  102. package/static/css/reference/chat.css +0 -4623
  103. package/static/css/reference/graph.css +0 -1016
  104. package/static/css/responsive.css +0 -861
  105. package/static/graph.html +0 -122
  106. package/static/platform.css +0 -104
  107. package/static/plugins.html +0 -136
  108. package/static/scripts/account.js +0 -238
  109. package/static/scripts/admin.js +0 -1614
  110. package/static/scripts/chat.js +0 -5081
  111. package/static/scripts/graph.js +0 -1804
  112. package/static/scripts/platform.js +0 -64
  113. package/static/scripts/ux.js +0 -167
  114. package/static/scripts/workspace.js +0 -948
  115. package/static/v3/asset-manifest.json +0 -56
  116. package/static/v3/css/lattice.base.49deefb5.css +0 -128
  117. package/static/v3/css/lattice.base.css +0 -128
  118. package/static/v3/css/lattice.components.cde18231.css +0 -472
  119. package/static/v3/css/lattice.components.css +0 -472
  120. package/static/v3/css/lattice.shell.29d36d85.css +0 -452
  121. package/static/v3/css/lattice.shell.css +0 -452
  122. package/static/v3/css/lattice.tokens.304cbc40.css +0 -135
  123. package/static/v3/css/lattice.tokens.css +0 -135
  124. package/static/v3/css/lattice.views.0a18b6c5.css +0 -360
  125. package/static/v3/css/lattice.views.css +0 -360
  126. package/static/v3/index.html +0 -68
  127. package/static/v3/js/app.356e6452.js +0 -26
  128. package/static/v3/js/app.js +0 -26
  129. package/static/v3/js/core/api.7a308b89.js +0 -568
  130. package/static/v3/js/core/api.js +0 -568
  131. package/static/v3/js/core/components.f25b3b93.js +0 -230
  132. package/static/v3/js/core/components.js +0 -230
  133. package/static/v3/js/core/dom.a2773eb0.js +0 -148
  134. package/static/v3/js/core/dom.js +0 -148
  135. package/static/v3/js/core/router.584570f2.js +0 -37
  136. package/static/v3/js/core/router.js +0 -37
  137. package/static/v3/js/core/routes.7222343d.js +0 -93
  138. package/static/v3/js/core/routes.js +0 -93
  139. package/static/v3/js/core/shell.a1657f20.js +0 -391
  140. package/static/v3/js/core/shell.js +0 -391
  141. package/static/v3/js/core/store.204a08b2.js +0 -113
  142. package/static/v3/js/core/store.js +0 -113
  143. package/static/v3/js/views/admin-audit.660a1fb1.js +0 -185
  144. package/static/v3/js/views/admin-audit.js +0 -185
  145. package/static/v3/js/views/admin-permissions.a7ae5f09.js +0 -177
  146. package/static/v3/js/views/admin-permissions.js +0 -177
  147. package/static/v3/js/views/admin-policies.3658fd86.js +0 -102
  148. package/static/v3/js/views/admin-policies.js +0 -102
  149. package/static/v3/js/views/admin-private-vpc.7d342d36.js +0 -135
  150. package/static/v3/js/views/admin-private-vpc.js +0 -135
  151. package/static/v3/js/views/admin-security.07c66b72.js +0 -180
  152. package/static/v3/js/views/admin-security.js +0 -180
  153. package/static/v3/js/views/admin-users.03bac88c.js +0 -168
  154. package/static/v3/js/views/admin-users.js +0 -168
  155. package/static/v3/js/views/agents.014d0b74.js +0 -541
  156. package/static/v3/js/views/agents.js +0 -541
  157. package/static/v3/js/views/chat.e6dd7dd0.js +0 -601
  158. package/static/v3/js/views/chat.js +0 -601
  159. package/static/v3/js/views/files.adad14c1.js +0 -365
  160. package/static/v3/js/views/files.js +0 -365
  161. package/static/v3/js/views/graph-canvas.17c15d65.js +0 -509
  162. package/static/v3/js/views/graph-canvas.js +0 -509
  163. package/static/v3/js/views/home.24f8b8ae.js +0 -200
  164. package/static/v3/js/views/home.js +0 -200
  165. package/static/v3/js/views/hooks.37895880.js +0 -220
  166. package/static/v3/js/views/hooks.js +0 -220
  167. package/static/v3/js/views/hybrid-search.2fb63ed9.js +0 -194
  168. package/static/v3/js/views/hybrid-search.js +0 -194
  169. package/static/v3/js/views/knowledge-graph.5e40cbeb.js +0 -509
  170. package/static/v3/js/views/knowledge-graph.js +0 -509
  171. package/static/v3/js/views/marketplace.ab0583d4.js +0 -141
  172. package/static/v3/js/views/marketplace.js +0 -141
  173. package/static/v3/js/views/mcp.99b5c6a7.js +0 -114
  174. package/static/v3/js/views/mcp.js +0 -114
  175. package/static/v3/js/views/memory.4ebdf474.js +0 -147
  176. package/static/v3/js/views/memory.js +0 -147
  177. package/static/v3/js/views/models.a1ffa147.js +0 -256
  178. package/static/v3/js/views/models.js +0 -256
  179. package/static/v3/js/views/my-computer.d9d9ae1c.js +0 -463
  180. package/static/v3/js/views/my-computer.js +0 -463
  181. package/static/v3/js/views/pipeline.c522f1ce.js +0 -157
  182. package/static/v3/js/views/pipeline.js +0 -157
  183. package/static/v3/js/views/planning.9ac3e313.js +0 -153
  184. package/static/v3/js/views/planning.js +0 -153
  185. package/static/v3/js/views/settings.8631fa5e.js +0 -318
  186. package/static/v3/js/views/settings.js +0 -318
  187. package/static/v3/js/views/skills.c6c2f965.js +0 -109
  188. package/static/v3/js/views/skills.js +0 -109
  189. package/static/v3/js/views/tools.e4f11276.js +0 -108
  190. package/static/v3/js/views/tools.js +0 -108
  191. package/static/v3/js/views/workflows.26c57290.js +0 -128
  192. package/static/v3/js/views/workflows.js +0 -128
  193. package/static/workflows.html +0 -146
  194. package/static/workspace.css +0 -1121
  195. package/static/workspace.html +0 -357
@@ -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
+ }