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,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
+ }