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.
- package/README.md +42 -33
- package/desktop/electron/main.cjs +44 -0
- package/docs/CHANGELOG.md +106 -0
- package/docs/REALTIME_COLLABORATION.md +3 -3
- package/docs/V3_FRONTEND.md +9 -8
- package/docs/V4_1_FRONTEND_ARCHITECTURE_REVIEW.md +65 -0
- package/docs/V4_1_FRONTEND_MIGRATION_REPORT.md +70 -0
- package/docs/V4_1_VALIDATION_REPORT.md +47 -0
- package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +95 -45
- package/docs/kg-schema.md +6 -2
- package/docs/spec-vs-impl.md +10 -10
- package/frontend/index.html +24 -0
- package/frontend/openapi.json +14190 -0
- package/frontend/src/App.tsx +184 -0
- package/frontend/src/api/client.ts +317 -0
- package/frontend/src/api/openapi.ts +16637 -0
- package/frontend/src/components/primitives.tsx +204 -0
- package/frontend/src/components/ui/badge.tsx +27 -0
- package/frontend/src/components/ui/button.tsx +37 -0
- package/frontend/src/components/ui/card.tsx +22 -0
- package/frontend/src/components/ui/input.tsx +16 -0
- package/frontend/src/components/ui/textarea.tsx +16 -0
- package/frontend/src/lib/utils.ts +33 -0
- package/frontend/src/main.tsx +23 -0
- package/frontend/src/pages/Act.tsx +245 -0
- package/frontend/src/pages/Ask.tsx +200 -0
- package/frontend/src/pages/Brain.tsx +267 -0
- package/frontend/src/pages/Capture.tsx +158 -0
- package/frontend/src/pages/Library.tsx +187 -0
- package/frontend/src/pages/System.tsx +344 -0
- package/frontend/src/routes.ts +85 -0
- package/frontend/src/store/appStore.ts +54 -0
- package/frontend/src/styles.css +107 -0
- package/kg_schema.py +2 -603
- package/knowledge_graph.py +37 -4958
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/admin.py +15 -16
- package/latticeai/api/agents.py +13 -6
- package/latticeai/api/auth.py +19 -11
- package/latticeai/api/invitations.py +100 -0
- package/latticeai/api/knowledge_graph.py +4 -11
- package/latticeai/api/plugins.py +3 -6
- package/latticeai/api/realtime.py +4 -7
- package/latticeai/api/setup.py +5 -4
- package/latticeai/api/static_routes.py +13 -16
- package/latticeai/api/ui_redirects.py +26 -0
- package/latticeai/api/workflow_designer.py +39 -6
- package/latticeai/api/workspace.py +24 -10
- package/latticeai/app_factory.py +88 -17
- package/latticeai/brain/_kg_common.py +1123 -0
- package/latticeai/brain/discovery.py +1455 -0
- package/latticeai/brain/documents.py +218 -0
- package/latticeai/brain/ingest.py +644 -0
- package/latticeai/brain/projection.py +561 -0
- package/latticeai/brain/provenance.py +401 -0
- package/latticeai/brain/retrieval.py +1316 -0
- package/latticeai/brain/schema.py +640 -0
- package/latticeai/brain/store.py +216 -0
- package/latticeai/brain/write_master.py +225 -0
- package/latticeai/core/invitations.py +131 -0
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/multi_agent.py +1 -1
- package/latticeai/core/policy.py +54 -0
- package/latticeai/core/realtime.py +65 -44
- package/latticeai/core/sessions.py +31 -5
- package/latticeai/core/users.py +147 -0
- package/latticeai/core/workspace_os.py +420 -20
- package/latticeai/services/agent_runtime.py +242 -4
- package/latticeai/services/run_executor.py +328 -0
- package/latticeai/services/workspace_service.py +27 -19
- package/package.json +54 -27
- package/scripts/build_frontend_assets.mjs +38 -0
- package/scripts/bump_version.py +1 -1
- package/scripts/export_openapi.py +31 -0
- package/scripts/lint_frontend.mjs +86 -0
- package/scripts/run_python.mjs +47 -0
- package/src-tauri/Cargo.lock +4833 -0
- package/src-tauri/Cargo.toml +19 -0
- package/src-tauri/build.rs +3 -0
- package/src-tauri/capabilities/default.json +7 -0
- package/src-tauri/src/main.rs +78 -0
- package/src-tauri/tauri.conf.json +36 -0
- package/static/app/asset-manifest.json +32 -0
- package/static/app/assets/core-CwxXejkd.js +2 -0
- package/static/app/assets/core-CwxXejkd.js.map +1 -0
- package/static/app/assets/index-CJRAzNnf.js +333 -0
- package/static/app/assets/index-CJRAzNnf.js.map +1 -0
- package/static/app/assets/index-CSwBBgf4.css +2 -0
- package/static/app/index.html +25 -0
- package/static/manifest.json +2 -2
- package/static/sw.js +4 -4
- package/scripts/build_v3_assets.mjs +0 -170
- package/scripts/lint_v3.mjs +0 -97
- package/static/account.html +0 -113
- package/static/activity.html +0 -73
- package/static/admin.html +0 -486
- package/static/agents.html +0 -139
- package/static/chat.html +0 -841
- package/static/css/reference/account.css +0 -439
- package/static/css/reference/admin.css +0 -610
- package/static/css/reference/base.css +0 -1661
- package/static/css/reference/chat.css +0 -4623
- package/static/css/reference/graph.css +0 -1016
- package/static/css/responsive.css +0 -861
- package/static/graph.html +0 -122
- package/static/platform.css +0 -104
- package/static/plugins.html +0 -136
- package/static/scripts/account.js +0 -238
- package/static/scripts/admin.js +0 -1614
- package/static/scripts/chat.js +0 -5081
- package/static/scripts/graph.js +0 -1804
- package/static/scripts/platform.js +0 -64
- package/static/scripts/ux.js +0 -167
- package/static/scripts/workspace.js +0 -948
- package/static/v3/asset-manifest.json +0 -56
- package/static/v3/css/lattice.base.49deefb5.css +0 -128
- package/static/v3/css/lattice.base.css +0 -128
- package/static/v3/css/lattice.components.cde18231.css +0 -472
- package/static/v3/css/lattice.components.css +0 -472
- package/static/v3/css/lattice.shell.29d36d85.css +0 -452
- package/static/v3/css/lattice.shell.css +0 -452
- package/static/v3/css/lattice.tokens.304cbc40.css +0 -135
- package/static/v3/css/lattice.tokens.css +0 -135
- package/static/v3/css/lattice.views.0a18b6c5.css +0 -360
- package/static/v3/css/lattice.views.css +0 -360
- package/static/v3/index.html +0 -68
- package/static/v3/js/app.356e6452.js +0 -26
- package/static/v3/js/app.js +0 -26
- package/static/v3/js/core/api.7a308b89.js +0 -568
- package/static/v3/js/core/api.js +0 -568
- package/static/v3/js/core/components.f25b3b93.js +0 -230
- package/static/v3/js/core/components.js +0 -230
- package/static/v3/js/core/dom.a2773eb0.js +0 -148
- package/static/v3/js/core/dom.js +0 -148
- package/static/v3/js/core/router.584570f2.js +0 -37
- package/static/v3/js/core/router.js +0 -37
- package/static/v3/js/core/routes.7222343d.js +0 -93
- package/static/v3/js/core/routes.js +0 -93
- package/static/v3/js/core/shell.a1657f20.js +0 -391
- package/static/v3/js/core/shell.js +0 -391
- package/static/v3/js/core/store.204a08b2.js +0 -113
- package/static/v3/js/core/store.js +0 -113
- package/static/v3/js/views/admin-audit.660a1fb1.js +0 -185
- package/static/v3/js/views/admin-audit.js +0 -185
- package/static/v3/js/views/admin-permissions.a7ae5f09.js +0 -177
- package/static/v3/js/views/admin-permissions.js +0 -177
- package/static/v3/js/views/admin-policies.3658fd86.js +0 -102
- package/static/v3/js/views/admin-policies.js +0 -102
- package/static/v3/js/views/admin-private-vpc.7d342d36.js +0 -135
- package/static/v3/js/views/admin-private-vpc.js +0 -135
- package/static/v3/js/views/admin-security.07c66b72.js +0 -180
- package/static/v3/js/views/admin-security.js +0 -180
- package/static/v3/js/views/admin-users.03bac88c.js +0 -168
- package/static/v3/js/views/admin-users.js +0 -168
- package/static/v3/js/views/agents.014d0b74.js +0 -541
- package/static/v3/js/views/agents.js +0 -541
- package/static/v3/js/views/chat.e6dd7dd0.js +0 -601
- package/static/v3/js/views/chat.js +0 -601
- package/static/v3/js/views/files.adad14c1.js +0 -365
- package/static/v3/js/views/files.js +0 -365
- package/static/v3/js/views/graph-canvas.17c15d65.js +0 -509
- package/static/v3/js/views/graph-canvas.js +0 -509
- package/static/v3/js/views/home.24f8b8ae.js +0 -200
- package/static/v3/js/views/home.js +0 -200
- package/static/v3/js/views/hooks.37895880.js +0 -220
- package/static/v3/js/views/hooks.js +0 -220
- package/static/v3/js/views/hybrid-search.2fb63ed9.js +0 -194
- package/static/v3/js/views/hybrid-search.js +0 -194
- package/static/v3/js/views/knowledge-graph.5e40cbeb.js +0 -509
- package/static/v3/js/views/knowledge-graph.js +0 -509
- package/static/v3/js/views/marketplace.ab0583d4.js +0 -141
- package/static/v3/js/views/marketplace.js +0 -141
- package/static/v3/js/views/mcp.99b5c6a7.js +0 -114
- package/static/v3/js/views/mcp.js +0 -114
- package/static/v3/js/views/memory.4ebdf474.js +0 -147
- package/static/v3/js/views/memory.js +0 -147
- package/static/v3/js/views/models.a1ffa147.js +0 -256
- package/static/v3/js/views/models.js +0 -256
- package/static/v3/js/views/my-computer.d9d9ae1c.js +0 -463
- package/static/v3/js/views/my-computer.js +0 -463
- package/static/v3/js/views/pipeline.c522f1ce.js +0 -157
- package/static/v3/js/views/pipeline.js +0 -157
- package/static/v3/js/views/planning.9ac3e313.js +0 -153
- package/static/v3/js/views/planning.js +0 -153
- package/static/v3/js/views/settings.8631fa5e.js +0 -318
- package/static/v3/js/views/settings.js +0 -318
- package/static/v3/js/views/skills.c6c2f965.js +0 -109
- package/static/v3/js/views/skills.js +0 -109
- package/static/v3/js/views/tools.e4f11276.js +0 -108
- package/static/v3/js/views/tools.js +0 -108
- package/static/v3/js/views/workflows.26c57290.js +0 -128
- package/static/v3/js/views/workflows.js +0 -128
- package/static/workflows.html +0 -146
- package/static/workspace.css +0 -1121
- 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
|
+
}
|