@anchrd/intel-ui 0.2.1 → 0.4.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/components.json +7 -1
- package/package.json +3 -1
- package/src/app/app-sidebar/app-sidebar.tsx +56 -0
- package/src/app/app-tree/app-tree.tsx +427 -0
- package/src/app/app.tsx +84 -54
- package/src/app/header-actions/header-actions.tsx +15 -0
- package/src/app/header-search/header-search.tsx +291 -0
- package/src/app/sidebar-preferences/sidebar-preferences.ts +68 -0
- package/src/app/sidebar-preferences/sidebar-preferences.types.ts +15 -0
- package/src/app/sidebar-resize-handle/sidebar-resize-handle.tsx +85 -0
- package/src/app/user-footer/user-footer.tsx +76 -0
- package/src/components/ui/breadcrumb.tsx +102 -0
- package/src/components/ui/button.tsx +64 -0
- package/src/components/ui/collapsible.tsx +20 -0
- package/src/components/ui/command.tsx +160 -0
- package/src/components/ui/dialog.tsx +143 -0
- package/src/components/ui/dropdown-menu.tsx +84 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/separator.tsx +26 -0
- package/src/components/ui/sheet.tsx +136 -0
- package/src/components/ui/sidebar.tsx +693 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/tooltip.tsx +51 -0
- package/src/data/intel-data-provider/intel-data-provider.ts +58 -39
- package/src/data/intel-data-provider/intel-data-provider.types.ts +17 -8
- package/src/flows/flows.tsx +59 -142
- package/src/hooks/use-mobile.ts +19 -0
- package/src/i18n/en.json +48 -29
- package/src/knowledge/knowledge.tsx +133 -423
- package/src/router/router.tsx +4 -0
- package/src/router/selection-search.ts +19 -0
- package/src/styles.css +54 -51
- package/src/tools/tools.tsx +175 -158
package/src/router/router.tsx
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from "@tanstack/react-router";
|
|
8
8
|
import { App } from "@/app/app.tsx";
|
|
9
9
|
import type { RouterContext } from "./router.types.ts";
|
|
10
|
+
import { selectionSearch } from "./selection-search.ts";
|
|
10
11
|
|
|
11
12
|
const rootRoute = createRootRouteWithContext<RouterContext>()({ component: App });
|
|
12
13
|
const redirectRoute = createRoute({
|
|
@@ -17,16 +18,19 @@ const redirectRoute = createRoute({
|
|
|
17
18
|
const knowledgeRoute = createRoute({
|
|
18
19
|
getParentRoute: () => rootRoute,
|
|
19
20
|
path: "/knowledge",
|
|
21
|
+
validateSearch: selectionSearch,
|
|
20
22
|
component: lazyRouteComponent(() => import("@/knowledge/knowledge.tsx"), "Knowledge"),
|
|
21
23
|
});
|
|
22
24
|
const flowsRoute = createRoute({
|
|
23
25
|
getParentRoute: () => rootRoute,
|
|
24
26
|
path: "/flows",
|
|
27
|
+
validateSearch: selectionSearch,
|
|
25
28
|
component: lazyRouteComponent(() => import("@/flows/flows.tsx"), "Flows"),
|
|
26
29
|
});
|
|
27
30
|
const toolsRoute = createRoute({
|
|
28
31
|
getParentRoute: () => rootRoute,
|
|
29
32
|
path: "/tools",
|
|
33
|
+
validateSearch: selectionSearch,
|
|
30
34
|
component: lazyRouteComponent(() => import("@/tools/tools.tsx"), "Tools"),
|
|
31
35
|
});
|
|
32
36
|
const routeTree = rootRoute.addChildren([redirectRoute, knowledgeRoute, flowsRoute, toolsRoute]);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type SelectionSearch = Record<string, unknown> & { select?: string };
|
|
2
|
+
|
|
3
|
+
// One search parameter carries "open this one" into an area: the header search names a hit, the
|
|
4
|
+
// screen for that area selects it. Keeping it in the URL is what makes a hit reachable with Enter
|
|
5
|
+
// alone — the dialog never reaches into a screen's own state.
|
|
6
|
+
//
|
|
7
|
+
// ⚠️ Unknown parameters are passed through rather than dropped. The portal's connect flow returns
|
|
8
|
+
// to `/tools` with its own marker, and a validator that kept only what it knows would delete it.
|
|
9
|
+
export function selectionSearch(search: Record<string, unknown>): SelectionSearch {
|
|
10
|
+
const { select, ...rest } = search;
|
|
11
|
+
return typeof select === "string" && select.length > 0 ? { ...rest, select } : rest;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Screens are lazy route components with no route object at hand, so they read the parameter off
|
|
15
|
+
// the location instead of off a typed route.
|
|
16
|
+
export function selectedFrom(search: unknown): string | null {
|
|
17
|
+
const select = (search as { select?: unknown } | null)?.select;
|
|
18
|
+
return typeof select === "string" && select.length > 0 ? select : null;
|
|
19
|
+
}
|
package/src/styles.css
CHANGED
|
@@ -7,65 +7,68 @@
|
|
|
7
7
|
|
|
8
8
|
@custom-variant dark (&:is(.dark *));
|
|
9
9
|
|
|
10
|
+
/* The default theme is gate's, token for token: an achromatic base whose primary is black rather
|
|
11
|
+
than Intel's former blue (hue 265). Every value here is a default — `theme/custom.css` is loaded
|
|
12
|
+
after this file and a customer theme overrides any of them without touching a component. */
|
|
10
13
|
:root {
|
|
11
14
|
--radius: 0.625rem;
|
|
12
|
-
--background: oklch(0
|
|
13
|
-
--foreground: oklch(0.
|
|
15
|
+
--background: oklch(1 0 0);
|
|
16
|
+
--foreground: oklch(0.145 0 0);
|
|
14
17
|
--card: oklch(1 0 0);
|
|
15
|
-
--card-foreground: oklch(0.
|
|
18
|
+
--card-foreground: oklch(0.145 0 0);
|
|
16
19
|
--popover: oklch(1 0 0);
|
|
17
|
-
--popover-foreground: oklch(0.
|
|
18
|
-
--primary: oklch(0.
|
|
19
|
-
--primary-foreground: oklch(0.
|
|
20
|
-
--secondary: oklch(0.
|
|
21
|
-
--secondary-foreground: oklch(0.
|
|
22
|
-
--muted: oklch(0.
|
|
23
|
-
--muted-foreground: oklch(0.
|
|
24
|
-
--accent: oklch(0.
|
|
25
|
-
--accent-foreground: oklch(0.
|
|
26
|
-
--destructive: oklch(0.
|
|
27
|
-
--destructive-foreground: oklch(0.
|
|
28
|
-
--border: oklch(0.
|
|
29
|
-
--input: oklch(0.
|
|
30
|
-
--ring: oklch(0.
|
|
31
|
-
--sidebar: oklch(0.
|
|
32
|
-
--sidebar-foreground: oklch(0.
|
|
33
|
-
--sidebar-primary: oklch(0.
|
|
34
|
-
--sidebar-primary-foreground: oklch(0.
|
|
35
|
-
--sidebar-accent: oklch(0.
|
|
36
|
-
--sidebar-accent-foreground: oklch(0.
|
|
37
|
-
--sidebar-border: oklch(0.
|
|
38
|
-
--sidebar-ring: oklch(0.
|
|
20
|
+
--popover-foreground: oklch(0.145 0 0);
|
|
21
|
+
--primary: oklch(0.205 0 0);
|
|
22
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
23
|
+
--secondary: oklch(0.97 0 0);
|
|
24
|
+
--secondary-foreground: oklch(0.205 0 0);
|
|
25
|
+
--muted: oklch(0.97 0 0);
|
|
26
|
+
--muted-foreground: oklch(0.556 0 0);
|
|
27
|
+
--accent: oklch(0.97 0 0);
|
|
28
|
+
--accent-foreground: oklch(0.205 0 0);
|
|
29
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
30
|
+
--destructive-foreground: oklch(0.985 0 0);
|
|
31
|
+
--border: oklch(0.922 0 0);
|
|
32
|
+
--input: oklch(0.922 0 0);
|
|
33
|
+
--ring: oklch(0.708 0 0);
|
|
34
|
+
--sidebar: oklch(0.985 0 0);
|
|
35
|
+
--sidebar-foreground: oklch(0.145 0 0);
|
|
36
|
+
--sidebar-primary: oklch(0.205 0 0);
|
|
37
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
38
|
+
--sidebar-accent: oklch(0.97 0 0);
|
|
39
|
+
--sidebar-accent-foreground: oklch(0.205 0 0);
|
|
40
|
+
--sidebar-border: oklch(0.922 0 0);
|
|
41
|
+
--sidebar-ring: oklch(0.708 0 0);
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
.dark {
|
|
42
|
-
--background: oklch(0.
|
|
43
|
-
--foreground: oklch(0.
|
|
44
|
-
--card: oklch(0.
|
|
45
|
-
--card-foreground: oklch(0.
|
|
46
|
-
--popover: oklch(0.
|
|
47
|
-
--popover-foreground: oklch(0.
|
|
48
|
-
--primary: oklch(0.
|
|
49
|
-
--primary-foreground: oklch(0.
|
|
50
|
-
--secondary: oklch(0.
|
|
51
|
-
--secondary-foreground: oklch(0.
|
|
52
|
-
--muted: oklch(0.
|
|
53
|
-
--muted-foreground: oklch(0.
|
|
54
|
-
--accent: oklch(0.
|
|
55
|
-
--accent-foreground: oklch(0.
|
|
56
|
-
--destructive: oklch(0.
|
|
57
|
-
--destructive-foreground: oklch(0.
|
|
58
|
-
--border: oklch(1 0 0 /
|
|
59
|
-
--input: oklch(1 0 0 /
|
|
60
|
-
--ring: oklch(0.
|
|
61
|
-
--sidebar: oklch(0.
|
|
62
|
-
--sidebar-foreground: oklch(0.
|
|
63
|
-
--sidebar-primary: oklch(0.
|
|
64
|
-
--sidebar-primary-foreground: oklch(0.
|
|
65
|
-
--sidebar-accent: oklch(0.
|
|
66
|
-
--sidebar-accent-foreground: oklch(0.
|
|
45
|
+
--background: oklch(0.145 0 0);
|
|
46
|
+
--foreground: oklch(0.985 0 0);
|
|
47
|
+
--card: oklch(0.205 0 0);
|
|
48
|
+
--card-foreground: oklch(0.985 0 0);
|
|
49
|
+
--popover: oklch(0.205 0 0);
|
|
50
|
+
--popover-foreground: oklch(0.985 0 0);
|
|
51
|
+
--primary: oklch(0.922 0 0);
|
|
52
|
+
--primary-foreground: oklch(0.205 0 0);
|
|
53
|
+
--secondary: oklch(0.269 0 0);
|
|
54
|
+
--secondary-foreground: oklch(0.985 0 0);
|
|
55
|
+
--muted: oklch(0.269 0 0);
|
|
56
|
+
--muted-foreground: oklch(0.708 0 0);
|
|
57
|
+
--accent: oklch(0.269 0 0);
|
|
58
|
+
--accent-foreground: oklch(0.985 0 0);
|
|
59
|
+
--destructive: oklch(0.704 0.191 22.216);
|
|
60
|
+
--destructive-foreground: oklch(0.985 0 0);
|
|
61
|
+
--border: oklch(1 0 0 / 10%);
|
|
62
|
+
--input: oklch(1 0 0 / 15%);
|
|
63
|
+
--ring: oklch(0.556 0 0);
|
|
64
|
+
--sidebar: oklch(0.205 0 0);
|
|
65
|
+
--sidebar-foreground: oklch(0.985 0 0);
|
|
66
|
+
--sidebar-primary: oklch(0.922 0 0);
|
|
67
|
+
--sidebar-primary-foreground: oklch(0.205 0 0);
|
|
68
|
+
--sidebar-accent: oklch(0.269 0 0);
|
|
69
|
+
--sidebar-accent-foreground: oklch(0.985 0 0);
|
|
67
70
|
--sidebar-border: oklch(1 0 0 / 10%);
|
|
68
|
-
--sidebar-ring: oklch(0.
|
|
71
|
+
--sidebar-ring: oklch(0.556 0 0);
|
|
69
72
|
}
|
|
70
73
|
|
|
71
74
|
@theme inline {
|
package/src/tools/tools.tsx
CHANGED
|
@@ -1,192 +1,209 @@
|
|
|
1
1
|
import type { ToolCapability } from "@anchrd/intel-contract";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { useQuery } from "@tanstack/react-query";
|
|
3
|
+
import { useRouterState } from "@tanstack/react-router";
|
|
4
|
+
import { AlertTriangle, ChevronRight, LogIn, PlugZap, Wrench } from "lucide-react";
|
|
5
|
+
import type * as React from "react";
|
|
6
|
+
import { HeaderActions } from "@/app/header-actions/header-actions.tsx";
|
|
7
|
+
import { Button, buttonVariants } from "@/components/ui/button";
|
|
8
|
+
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
|
9
|
+
import type { I18n } from "@/i18n/i18n.types.ts";
|
|
5
10
|
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
11
|
+
import { selectedFrom } from "@/router/selection-search.ts";
|
|
6
12
|
|
|
13
|
+
// The portal namespaces every tool as `<server>__<tool>`, and that prefix is the whole of what
|
|
14
|
+
// Intel ever learns about a tool's origin: the portal resolves the real server and holds its
|
|
15
|
+
// credentials (ADR-0003). A name without a namespace has no origin beyond the portal itself.
|
|
16
|
+
const NamespaceSeparator = "__";
|
|
17
|
+
|
|
18
|
+
function toolOrigin(name: string): string | null {
|
|
19
|
+
const boundary = name.indexOf(NamespaceSeparator);
|
|
20
|
+
return boundary > 0 ? name.slice(0, boundary) : null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// The catalog is one live `tools/list` with the signed-in user's own portal token. Nothing here is
|
|
24
|
+
// stored, mirrored or administered — the screen shows what the portal answers and nothing else.
|
|
7
25
|
export function Tools() {
|
|
8
26
|
const { data, i18n } = useIntelRouterContext();
|
|
9
27
|
const catalog = useQuery({ queryKey: ["tools"], queryFn: () => data.listTools() });
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
28
|
+
// A hit from the header search arrives as `?select=<tool name>`, and the row it names opens with
|
|
29
|
+
// the list. Nothing else about the row changes: it is still only a disclosure.
|
|
30
|
+
const requested = useRouterState({ select: (state) => selectedFrom(state.location.search) });
|
|
31
|
+
// The portal connect flow returns here with a marker when it refuses. Only its presence is used:
|
|
32
|
+
// the value is attacker-controlled and could carry provider detail, so it is never rendered.
|
|
33
|
+
const connectFailed =
|
|
34
|
+
typeof window !== "undefined" &&
|
|
35
|
+
new URLSearchParams(window.location.search).has("connectError");
|
|
15
36
|
|
|
16
37
|
return (
|
|
17
|
-
<div className="min-h-
|
|
18
|
-
|
|
19
|
-
<
|
|
20
|
-
<h1 className="text-xl font-semibold tracking-tight">{i18n.t("tools.title")}</h1>
|
|
21
|
-
<p className="mt-1 text-sm text-muted-foreground">{i18n.t("tools.description")}</p>
|
|
22
|
-
</div>
|
|
23
|
-
{catalog.data?.portalConnected && (
|
|
38
|
+
<div className="min-h-full p-8">
|
|
39
|
+
{catalog.data?.portalConnected ? (
|
|
40
|
+
<HeaderActions>
|
|
24
41
|
<a
|
|
25
42
|
href={data.portalConnectUrl("/tools")}
|
|
26
|
-
className=
|
|
43
|
+
className={buttonVariants({ variant: "outline", size: "sm" })}
|
|
27
44
|
>
|
|
28
|
-
<LogIn aria-hidden="true"
|
|
45
|
+
<LogIn aria-hidden="true" />
|
|
29
46
|
{i18n.t("tools.reconnect")}
|
|
30
47
|
</a>
|
|
48
|
+
</HeaderActions>
|
|
49
|
+
) : null}
|
|
50
|
+
|
|
51
|
+
<div className="mx-auto w-full max-w-4xl space-y-5">
|
|
52
|
+
{connectFailed && (
|
|
53
|
+
<p
|
|
54
|
+
role="alert"
|
|
55
|
+
className="rounded-md border border-destructive/30 p-3 text-sm text-destructive"
|
|
56
|
+
>
|
|
57
|
+
{i18n.t("tools.connectFailed")}
|
|
58
|
+
</p>
|
|
31
59
|
)}
|
|
32
|
-
</header>
|
|
33
60
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
<
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
<
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
<h2 className="mt-4 font-semibold">{i18n.t("tools.empty")}</h2>
|
|
69
|
-
<p className="mx-auto mt-2 max-w-lg text-sm text-muted-foreground">
|
|
70
|
-
{i18n.t("tools.emptyHelp")}
|
|
71
|
-
</p>
|
|
72
|
-
</section>
|
|
73
|
-
) : (
|
|
61
|
+
{catalog.isPending ? (
|
|
62
|
+
<p className="text-sm text-muted-foreground">{i18n.t("common.loading")}</p>
|
|
63
|
+
) : catalog.isError ? (
|
|
64
|
+
// Unreachable is not empty: the portal did not answer, so Intel knows nothing about this
|
|
65
|
+
// user's tools rather than knowing there are none.
|
|
66
|
+
<Notice
|
|
67
|
+
alert
|
|
68
|
+
icon={<PlugZap aria-hidden="true" className="mx-auto size-8 text-destructive" />}
|
|
69
|
+
title={i18n.t("tools.unreachable")}
|
|
70
|
+
help={i18n.t("tools.unreachableHelp")}
|
|
71
|
+
>
|
|
72
|
+
<Button variant="outline" onClick={() => void catalog.refetch()}>
|
|
73
|
+
{i18n.t("common.retry")}
|
|
74
|
+
</Button>
|
|
75
|
+
</Notice>
|
|
76
|
+
) : !catalog.data.portalConnected ? (
|
|
77
|
+
<Notice
|
|
78
|
+
icon={<PlugZap aria-hidden="true" className="mx-auto size-8 text-muted-foreground" />}
|
|
79
|
+
title={i18n.t("tools.disconnected")}
|
|
80
|
+
help={i18n.t("tools.disconnectedHelp")}
|
|
81
|
+
>
|
|
82
|
+
<a href={data.portalConnectUrl("/tools")} className={buttonVariants()}>
|
|
83
|
+
<LogIn aria-hidden="true" />
|
|
84
|
+
{i18n.t("tools.connect")}
|
|
85
|
+
</a>
|
|
86
|
+
</Notice>
|
|
87
|
+
) : catalog.data.items.length === 0 ? (
|
|
88
|
+
<Notice
|
|
89
|
+
icon={<Wrench aria-hidden="true" className="mx-auto size-8 text-muted-foreground" />}
|
|
90
|
+
title={i18n.t("tools.empty")}
|
|
91
|
+
help={i18n.t("tools.emptyHelp")}
|
|
92
|
+
/>
|
|
93
|
+
) : (
|
|
94
|
+
<>
|
|
74
95
|
<section className="overflow-hidden rounded-xl border bg-card shadow-sm">
|
|
75
96
|
<ul className="divide-y">
|
|
76
97
|
{catalog.data.items.map((capability) => (
|
|
77
98
|
<li key={capability.name}>
|
|
78
|
-
<
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
<span className="min-w-0">
|
|
84
|
-
<span className="block text-sm font-medium">
|
|
85
|
-
{capability.title ?? capability.name}
|
|
86
|
-
</span>
|
|
87
|
-
<span className="mt-1 block truncate font-mono text-xs text-muted-foreground">
|
|
88
|
-
{capability.name}
|
|
89
|
-
</span>
|
|
90
|
-
{capability.description && (
|
|
91
|
-
<span className="mt-2 line-clamp-2 block text-xs text-muted-foreground">
|
|
92
|
-
{capability.description}
|
|
93
|
-
</span>
|
|
94
|
-
)}
|
|
95
|
-
</span>
|
|
96
|
-
{capability.annotations.destructiveHint && (
|
|
97
|
-
<span className="inline-flex shrink-0 items-center gap-1.5 rounded-full bg-destructive/10 px-2 py-1 text-xs text-destructive">
|
|
98
|
-
<AlertTriangle aria-hidden="true" className="size-3.5" />
|
|
99
|
-
{i18n.t("tools.destructiveShort")}
|
|
100
|
-
</span>
|
|
101
|
-
)}
|
|
102
|
-
</button>
|
|
99
|
+
<ToolEntry
|
|
100
|
+
capability={capability}
|
|
101
|
+
i18n={i18n}
|
|
102
|
+
open={capability.name === requested}
|
|
103
|
+
/>
|
|
103
104
|
</li>
|
|
104
105
|
))}
|
|
105
106
|
</ul>
|
|
106
107
|
</section>
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
108
|
+
<p className="text-xs text-muted-foreground">{i18n.t("tools.liveNote")}</p>
|
|
109
|
+
</>
|
|
110
|
+
)}
|
|
110
111
|
</div>
|
|
111
112
|
</div>
|
|
112
113
|
);
|
|
113
114
|
}
|
|
114
115
|
|
|
115
|
-
function
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
116
|
+
function Notice({
|
|
117
|
+
icon,
|
|
118
|
+
title,
|
|
119
|
+
help,
|
|
120
|
+
alert,
|
|
121
|
+
children,
|
|
122
|
+
}: {
|
|
123
|
+
icon: React.ReactNode;
|
|
124
|
+
title: string;
|
|
125
|
+
help: string;
|
|
126
|
+
alert?: boolean;
|
|
127
|
+
children?: React.ReactNode;
|
|
128
|
+
}) {
|
|
129
|
+
return (
|
|
130
|
+
<section
|
|
131
|
+
{...(alert ? { role: "alert" } : {})}
|
|
132
|
+
className="rounded-xl border border-dashed bg-card p-12 text-center"
|
|
133
|
+
>
|
|
134
|
+
{icon}
|
|
135
|
+
<h2 className="mt-4 font-semibold">{title}</h2>
|
|
136
|
+
<p className="mx-auto mt-2 max-w-lg text-sm text-muted-foreground">{help}</p>
|
|
137
|
+
{children ? <div className="mt-5">{children}</div> : null}
|
|
138
|
+
</section>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// One tool, one disclosure. The row is the only control in the list: it opens the schema and does
|
|
143
|
+
// nothing else — enabling, sharing or permitting a tool happens in the portal, never here.
|
|
144
|
+
function ToolEntry({
|
|
145
|
+
capability,
|
|
146
|
+
i18n,
|
|
147
|
+
open,
|
|
148
|
+
}: {
|
|
149
|
+
capability: ToolCapability;
|
|
150
|
+
i18n: I18n;
|
|
151
|
+
open: boolean;
|
|
152
|
+
}) {
|
|
153
|
+
const origin = toolOrigin(capability.name);
|
|
136
154
|
|
|
137
155
|
return (
|
|
138
|
-
<
|
|
139
|
-
<
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
<
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
onChange={(event) => setArgumentsText(event.target.value)}
|
|
165
|
-
rows={9}
|
|
166
|
-
spellCheck={false}
|
|
167
|
-
className="mt-2 w-full resize-y rounded-md border bg-background p-3 font-mono text-xs outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
168
|
-
/>
|
|
169
|
-
</label>
|
|
170
|
-
{parseError && <p className="text-xs text-destructive">{i18n.t("tools.invalidJson")}</p>}
|
|
171
|
-
<button
|
|
172
|
-
type="button"
|
|
173
|
-
disabled={test.isPending || !safeToTest}
|
|
174
|
-
onClick={() => test.mutate()}
|
|
175
|
-
className="inline-flex w-full items-center justify-center gap-2 rounded-md bg-primary px-3 py-2 text-sm font-medium text-primary-foreground outline-none hover:bg-primary/90 focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50"
|
|
176
|
-
>
|
|
177
|
-
<Play aria-hidden="true" className="size-4" />
|
|
178
|
-
{i18n.t("tools.runTest")}
|
|
179
|
-
</button>
|
|
180
|
-
{test.data && (
|
|
181
|
-
<pre className="max-h-80 overflow-auto rounded-md bg-muted p-3 text-xs">
|
|
182
|
-
{JSON.stringify(test.data, null, 2)}
|
|
183
|
-
</pre>
|
|
156
|
+
<Collapsible defaultOpen={open}>
|
|
157
|
+
<CollapsibleTrigger className="group flex w-full items-start gap-4 px-5 py-4 text-left outline-none hover:bg-muted/50 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring">
|
|
158
|
+
<ChevronRight
|
|
159
|
+
aria-hidden="true"
|
|
160
|
+
className="mt-0.5 size-4 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-90"
|
|
161
|
+
/>
|
|
162
|
+
<span className="min-w-0 flex-1">
|
|
163
|
+
<span className="flex flex-wrap items-center gap-2">
|
|
164
|
+
<span className="text-sm font-medium">{capability.title ?? capability.name}</span>
|
|
165
|
+
<span className="inline-flex items-center rounded-full bg-muted px-2 py-0.5 text-xs text-muted-foreground">
|
|
166
|
+
<span className="sr-only">{i18n.t("tools.origin")}: </span>
|
|
167
|
+
{origin ?? i18n.t("tools.originPortal")}
|
|
168
|
+
</span>
|
|
169
|
+
{capability.annotations.destructiveHint && (
|
|
170
|
+
<span className="inline-flex items-center gap-1.5 rounded-full bg-destructive/10 px-2 py-0.5 text-xs text-destructive">
|
|
171
|
+
<AlertTriangle aria-hidden="true" className="size-3.5" />
|
|
172
|
+
{i18n.t("tools.destructiveShort")}
|
|
173
|
+
</span>
|
|
174
|
+
)}
|
|
175
|
+
</span>
|
|
176
|
+
{/* The namespaced name is what a flow references, so it stays readable — but only where
|
|
177
|
+
it is not already the heading of the row. */}
|
|
178
|
+
{capability.title && (
|
|
179
|
+
<span className="mt-1 block truncate font-mono text-xs text-muted-foreground">
|
|
180
|
+
{capability.name}
|
|
181
|
+
</span>
|
|
184
182
|
)}
|
|
185
|
-
{
|
|
186
|
-
<
|
|
183
|
+
{capability.description && (
|
|
184
|
+
<span className="mt-2 block text-xs text-muted-foreground">
|
|
185
|
+
{capability.description}
|
|
186
|
+
</span>
|
|
187
187
|
)}
|
|
188
|
-
</
|
|
189
|
-
|
|
190
|
-
|
|
188
|
+
</span>
|
|
189
|
+
</CollapsibleTrigger>
|
|
190
|
+
<CollapsibleContent className="space-y-3 px-5 pb-4 pl-13">
|
|
191
|
+
<Schema label={i18n.t("tools.inputSchema")} value={capability.inputSchema} />
|
|
192
|
+
{capability.outputSchema && (
|
|
193
|
+
<Schema label={i18n.t("tools.outputSchema")} value={capability.outputSchema} />
|
|
194
|
+
)}
|
|
195
|
+
</CollapsibleContent>
|
|
196
|
+
</Collapsible>
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function Schema({ label, value }: { label: string; value: Record<string, unknown> }) {
|
|
201
|
+
return (
|
|
202
|
+
<div>
|
|
203
|
+
<p className="text-xs font-medium">{label}</p>
|
|
204
|
+
<pre className="mt-1 max-h-80 overflow-auto rounded-md bg-muted p-3 text-xs">
|
|
205
|
+
{JSON.stringify(value, null, 2)}
|
|
206
|
+
</pre>
|
|
207
|
+
</div>
|
|
191
208
|
);
|
|
192
209
|
}
|