@anchrd/intel-ui 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,18 +1,59 @@
1
1
  # @anchrd/intel-ui
2
2
 
3
- The source-built Intel browser application. Install it next to `@anchrd/intel`, then run:
3
+ The Intel browser application: Knowledge, Flows and Tools for the
4
+ [`@anchrd/intel-api`](https://www.npmjs.com/package/@anchrd/intel-api) server.
5
+
6
+ This package ships **source, not a bundle**. You compile it yourself so your own theme, logo and
7
+ languages are applied at build time and the output belongs to you — no customer build artifact ever
8
+ lives in `node_modules`.
9
+
10
+ ## Install
11
+
12
+ Install it next to `@anchrd/intel-api`, then let the CLI build it:
4
13
 
5
14
  ```bash
15
+ npm install @anchrd/intel-api @anchrd/intel-ui
6
16
  npx intel build
7
17
  ```
8
18
 
9
- The CLI applies `intel.json` at build time and writes the customer-owned static output to
10
- `.intel/ui`. Theme CSS changes shadcn semantic variables, so customer design never requires editing
11
- React components.
19
+ The CLI applies `intel.json` and writes the static output to `.intel/ui`, which the Intel Worker
20
+ serves through its `ASSETS` binding.
21
+
22
+ ## Theming
23
+
24
+ Theme CSS overrides shadcn semantic variables such as `--primary`, `--background`, `--sidebar` and
25
+ `--radius`. It loads after Intel's defaults, so customer tokens win without forking a single React
26
+ component. Every product component uses semantic tokens rather than palette colours, which is
27
+ enforced by a lint rule.
28
+
29
+ Point `intel.json` at your files:
30
+
31
+ ```json
32
+ {
33
+ "ui": {
34
+ "theme": "./branding/theme.css",
35
+ "logo": "./branding/logo.svg",
36
+ "favicon": "./branding/favicon.svg",
37
+ "defaultLanguage": "en",
38
+ "languages": {}
39
+ }
40
+ }
41
+ ```
42
+
43
+ Missing assets, non-SVG branding and incomplete language catalogs fail the build rather than
44
+ silently producing a half-branded application.
45
+
46
+ ## What is inside
47
+
48
+ Vite, React, TanStack Router and Query, with shadcn components. Knowledge uses BlockNote for editing
49
+ and Sigma for the graph, Flows uses React Flow, and Tools presents the discovered MCP catalog.
50
+ Route-level code splitting keeps both editors out of the initial bundle.
51
+
52
+ Server state lives only in TanStack Query, and one data provider is the sole module that talks to the
53
+ Intel HTTP surface. The application uses same-origin `HttpOnly` sessions established by the Intel
54
+ Worker against Gate OAuth: it holds no service key and stores no access token in the browser.
12
55
 
13
- The browser application uses same-origin `HttpOnly` sessions established by the Intel Worker against
14
- Gate OAuth. It contains no service key or browser-side access-token storage.
56
+ ## License
15
57
 
16
- Knowledge uses BlockNote, Flows uses React Flow, and Tools presents the MCP catalog. Route-level
17
- code splitting keeps both editors outside the initial application bundle. Every product component
18
- uses shadcn semantic tokens so an `intel.json` theme can replace the customer design safely.
58
+ Proprietary. Published publicly for installation convenience; this is not an open-source license and
59
+ grants no right to use, copy, modify or redistribute the software. Contact Anchrd for licensing.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anchrd/intel-ui",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -28,7 +28,7 @@
28
28
  "typecheck": "tsc --noEmit"
29
29
  },
30
30
  "dependencies": {
31
- "@anchrd/intel-contract": "^0.1.0",
31
+ "@anchrd/intel-contract": "^0.2.0",
32
32
  "@blocknote/core": "^0.52.1",
33
33
  "@blocknote/react": "^0.52.1",
34
34
  "@blocknote/shadcn": "^0.52.1",
@@ -4,9 +4,7 @@ import {
4
4
  CreateFlowInput,
5
5
  CreateKnowledgeLinkInput,
6
6
  CreateKnowledgeNodeInput,
7
- CreateToolSourceInput,
8
7
  DeleteKnowledgeLinkInput,
9
- DiscoverToolSourceInput,
10
8
  Flow,
11
9
  FlowDocument,
12
10
  FlowList,
@@ -36,8 +34,6 @@ import {
36
34
  StartFlowRunInput,
37
35
  TestToolInput,
38
36
  ToolCatalog,
39
- ToolSource,
40
- ToolSourceList,
41
37
  ToolTestResult,
42
38
  UpdateKnowledgeNodeInput,
43
39
  } from "@anchrd/intel-contract";
@@ -294,26 +290,6 @@ export function createIntelDataProvider(
294
290
  body: JSON.stringify(parsed),
295
291
  });
296
292
  },
297
- async listToolSources() {
298
- return await request("/tool-sources", ToolSourceList);
299
- },
300
- async createToolSource(input) {
301
- return await request("/tool-sources", ToolSource, {
302
- method: "POST",
303
- body: JSON.stringify(CreateToolSourceInput.parse(input)),
304
- });
305
- },
306
- async discoverToolSource(input) {
307
- const parsed = DiscoverToolSourceInput.parse(input);
308
- return await request(
309
- `/tool-sources/${encodeURIComponent(parsed.sourceId)}/discover`,
310
- ToolCatalog,
311
- {
312
- method: "POST",
313
- body: JSON.stringify(parsed),
314
- },
315
- );
316
- },
317
293
  async listTools() {
318
294
  return await request("/tools", ToolCatalog);
319
295
  },
@@ -324,8 +300,8 @@ export function createIntelDataProvider(
324
300
  });
325
301
  },
326
302
 
327
- toolSourceConnectUrl(sourceId, returnTo = "/tools") {
328
- return `${baseUrl}/auth/connect/${encodeURIComponent(sourceId)}?returnTo=${encodeURIComponent(returnTo)}`;
303
+ portalConnectUrl(returnTo = "/tools") {
304
+ return `${baseUrl}/auth/connect?returnTo=${encodeURIComponent(returnTo)}`;
329
305
  },
330
306
  async logout() {
331
307
  const response = await doFetch(`${baseUrl}/auth/logout`, {
@@ -4,9 +4,7 @@ import type {
4
4
  CreateFlowInput,
5
5
  CreateKnowledgeLinkInput,
6
6
  CreateKnowledgeNodeInput,
7
- CreateToolSourceInput,
8
7
  DeleteKnowledgeLinkInput,
9
- DiscoverToolSourceInput,
10
8
  Flow,
11
9
  FlowDocument,
12
10
  FlowList,
@@ -35,8 +33,6 @@ import type {
35
33
  StartFlowRunInput,
36
34
  TestToolInput,
37
35
  ToolCatalog,
38
- ToolSource,
39
- ToolSourceList,
40
36
  ToolTestResult,
41
37
  UpdateKnowledgeNodeInput,
42
38
  } from "@anchrd/intel-contract";
@@ -57,7 +53,7 @@ export interface IntelDataProvider {
57
53
  saveKnowledge(input: SaveKnowledgeVersionInput): Promise<KnowledgeDocument>;
58
54
  saveKnowledgeAttachment(input: SaveKnowledgeAttachmentInput): Promise<KnowledgeDocument>;
59
55
  getKnowledgeAttachment(nodeId: string): Promise<Blob>;
60
- toolSourceConnectUrl(sourceId: string, returnTo?: string): string;
56
+ portalConnectUrl(returnTo?: string): string;
61
57
  listKnowledgeVersions(nodeId: string): Promise<KnowledgeVersionList>;
62
58
  updateKnowledge(input: UpdateKnowledgeNodeInput): Promise<KnowledgeNode>;
63
59
  archiveKnowledge(input: ArchiveKnowledgeNodeInput): Promise<KnowledgeNode>;
@@ -76,9 +72,6 @@ export interface IntelDataProvider {
76
72
  startFlow(input: StartFlowRunInput): Promise<FlowRunStep>;
77
73
  getFlowRun(runId: string): Promise<FlowRunStep>;
78
74
  completeFlowStep(input: CompleteFlowRunStepInput): Promise<FlowRunStep>;
79
- listToolSources(): Promise<ToolSourceList>;
80
- createToolSource(input: CreateToolSourceInput): Promise<ToolSource>;
81
- discoverToolSource(input: DiscoverToolSourceInput): Promise<ToolCatalog>;
82
75
  listTools(): Promise<ToolCatalog>;
83
76
  testTool(input: TestToolInput): Promise<ToolTestResult>;
84
77
  logout(): Promise<void>;
@@ -198,7 +198,6 @@ function newNode(
198
198
  ...common,
199
199
  kind,
200
200
  configuration: {
201
- sourceId: "select-a-source",
202
201
  toolName: "select-a-tool",
203
202
  fingerprint: null,
204
203
  arguments: {},
@@ -599,10 +598,7 @@ function NodeInspector({
599
598
  }: {
600
599
  node: CanvasNode | null;
601
600
  update(fn: (node: FlowNode) => FlowNode): void;
602
- tools: Array<{
603
- source: { id: string; name: string };
604
- capability: { name: string; title: string | null; fingerprint: string };
605
- }>;
601
+ tools: Array<{ name: string; title: string | null; fingerprint: string }>;
606
602
  knowledge: KnowledgeTreeNode[];
607
603
  }) {
608
604
  const { i18n } = useIntelRouterContext();
@@ -726,23 +722,18 @@ function NodeInspector({
726
722
  <label className="block text-sm font-medium">
727
723
  {i18n.t("flows.tool")}
728
724
  <select
729
- value={`${contract.configuration.sourceId}:${contract.configuration.toolName}`}
725
+ value={contract.configuration.toolName}
730
726
  onChange={(event) => {
731
- const [sourceId, ...name] = event.target.value.split(":");
732
- const toolName = name.join(":");
727
+ const toolName = event.target.value;
733
728
  update((value) =>
734
- value.kind === "tool" && sourceId
729
+ value.kind === "tool"
735
730
  ? {
736
731
  ...value,
737
732
  configuration: {
738
733
  ...value.configuration,
739
- sourceId,
740
734
  toolName,
741
735
  fingerprint:
742
- tools.find(
743
- (entry) =>
744
- entry.source.id === sourceId && entry.capability.name === toolName,
745
- )?.capability.fingerprint ?? null,
736
+ tools.find((entry) => entry.name === toolName)?.fingerprint ?? null,
746
737
  },
747
738
  }
748
739
  : value,
@@ -750,13 +741,10 @@ function NodeInspector({
750
741
  }}
751
742
  className="mt-2 w-full rounded-md border bg-background px-3 py-2 outline-none focus-visible:ring-2 focus-visible:ring-ring"
752
743
  >
753
- <option value="select-a-source:select-a-tool">{i18n.t("flows.selectTool")}</option>
744
+ <option value="select-a-tool">{i18n.t("flows.selectTool")}</option>
754
745
  {tools.map((entry) => (
755
- <option
756
- key={`${entry.source.id}:${entry.capability.name}`}
757
- value={`${entry.source.id}:${entry.capability.name}`}
758
- >
759
- {entry.source.name} · {entry.capability.title ?? entry.capability.name}
746
+ <option key={entry.name} value={entry.name}>
747
+ {entry.title ?? entry.name}
760
748
  </option>
761
749
  ))}
762
750
  </select>
package/src/i18n/en.json CHANGED
@@ -57,30 +57,23 @@
57
57
  "knowledge.saveConflict": "This document changed elsewhere. Reload it before saving again.",
58
58
  "knowledge.saveError": "This document could not be saved. Reload and try again.",
59
59
  "tools.title": "Company tools",
60
- "tools.description": "Governed MCP capabilities available to people and AI.",
61
- "tools.empty": "No MCP source registered",
62
- "tools.emptyHelp": "Register the aggregated Cloudflare Portal endpoint. Intel discovers its tools while Gate remains the credential and permission boundary.",
63
- "tools.addSource": "Add MCP source",
64
- "tools.endpoint": "Streamable HTTP endpoint",
65
- "tools.connectionHandle": "Gate connection handle",
66
- "tools.connect": "Connect with Gate",
67
- "tools.sourceHelp": "Intel registers the handle/connect interface in Gate. Grant it to the right roles and keep the credential in Gate.",
68
- "tools.discover": "Discover tools",
69
- "tools.notDiscovered": "No tools cached yet. Connect the source in Gate, then run discovery.",
60
+ "tools.description": "The MCP tools you can reach through the company portal.",
61
+ "tools.connect": "Connect the portal",
62
+ "tools.reconnect": "Reconnect",
63
+ "tools.disconnected": "Portal not connected",
64
+ "tools.disconnectedHelp": "Sign in to the company MCP portal once. The portal decides which servers you may use and holds their credentials; Intel never sees them.",
65
+ "tools.empty": "No tools available to you",
66
+ "tools.emptyHelp": "The portal returned no tools for your account. An administrator configures which servers it offers.",
70
67
  "tools.testTitle": "Tool playground",
71
- "tools.select": "Select a discovered tool to inspect and test it.",
68
+ "tools.select": "Select a tool to inspect and test it.",
72
69
  "tools.arguments": "Arguments (JSON object)",
73
70
  "tools.runTest": "Run controlled test",
74
71
  "tools.invalidJson": "Arguments must be a valid JSON object.",
75
- "tools.destructive": "This source marks the tool as potentially destructive. Review the arguments before running it.",
76
- "tools.connectionRequired": "Your Gate connection is missing, forbidden, or invalid.",
72
+ "tools.destructive": "The portal marks this tool as potentially destructive. Review the arguments before running it.",
73
+ "tools.destructiveShort": "Destructive",
77
74
  "tools.testUnsafe": "The playground only runs tools explicitly marked read-only and non-destructive.",
78
- "tools.operationFailed": "The MCP operation failed. Check the source and try again.",
79
- "tools.connectFailed": "The MCP source could not be connected with your current access.",
80
- "tools.status.connected": "Connected",
81
- "tools.status.missing": "Not connected",
82
- "tools.status.forbidden": "No access",
83
- "tools.status.invalid": "Invalid",
75
+ "tools.operationFailed": "The tool list could not be loaded. Reconnect the portal and try again.",
76
+ "tools.connectFailed": "The portal could not be connected with your current access.",
84
77
  "flows.title": "Company flows",
85
78
  "flows.description": "Model-agnostic processes that connect instructions, Knowledge, tools, and people.",
86
79
  "flows.new": "New flow",
@@ -1,56 +1,17 @@
1
- import type { ToolCatalogEntry } from "@anchrd/intel-contract";
2
- import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
3
- import {
4
- AlertTriangle,
5
- CheckCircle2,
6
- CircleOff,
7
- KeyRound,
8
- LogIn,
9
- Play,
10
- Plus,
11
- RefreshCw,
12
- ShieldAlert,
13
- Wrench,
14
- } from "lucide-react";
15
- import { useMemo, useState } from "react";
16
- import { Modal } from "@/modal/modal.tsx";
1
+ import type { ToolCapability } from "@anchrd/intel-contract";
2
+ import { useMutation, useQuery } from "@tanstack/react-query";
3
+ import { AlertTriangle, LogIn, Play, ShieldAlert, Wrench } from "lucide-react";
4
+ import { useState } from "react";
17
5
  import { useIntelRouterContext } from "@/router/router-context.ts";
18
6
 
19
- const statusIcon = {
20
- connected: CheckCircle2,
21
- missing: KeyRound,
22
- forbidden: ShieldAlert,
23
- invalid: CircleOff,
24
- } as const;
25
-
26
7
  export function Tools() {
27
8
  const { data, i18n } = useIntelRouterContext();
28
- const queryClient = useQueryClient();
29
- const sources = useQuery({ queryKey: ["tool-sources"], queryFn: () => data.listToolSources() });
30
9
  const catalog = useQuery({ queryKey: ["tools"], queryFn: () => data.listTools() });
31
- const [registering, setRegistering] = useState(false);
32
- const [selected, setSelected] = useState<ToolCatalogEntry | null>(null);
10
+ const [selected, setSelected] = useState<ToolCapability | null>(null);
33
11
  const connectError =
34
12
  typeof window === "undefined"
35
13
  ? null
36
14
  : new URLSearchParams(window.location.search).get("connectError");
37
- const grouped = useMemo(() => {
38
- const entries = catalog.data?.items ?? [];
39
- return (sources.data?.items ?? []).map((source) => ({
40
- source,
41
- entries: entries.filter((entry) => entry.source.id === source.id),
42
- }));
43
- }, [catalog.data, sources.data]);
44
- const discover = useMutation({
45
- mutationFn: (sourceId: string) =>
46
- data.discoverToolSource({ sourceId, idempotencyKey: crypto.randomUUID() }),
47
- onSuccess: async () => {
48
- await Promise.all([
49
- queryClient.invalidateQueries({ queryKey: ["tool-sources"] }),
50
- queryClient.invalidateQueries({ queryKey: ["tools"] }),
51
- ]);
52
- },
53
- });
54
15
 
55
16
  return (
56
17
  <div className="min-h-screen">
@@ -59,26 +20,19 @@ export function Tools() {
59
20
  <h1 className="text-xl font-semibold tracking-tight">{i18n.t("tools.title")}</h1>
60
21
  <p className="mt-1 text-sm text-muted-foreground">{i18n.t("tools.description")}</p>
61
22
  </div>
62
- <button
63
- type="button"
64
- onClick={() => setRegistering(true)}
65
- className="inline-flex items-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"
66
- >
67
- <Plus aria-hidden="true" className="size-4" />
68
- {i18n.t("tools.addSource")}
69
- </button>
23
+ {catalog.data?.portalConnected && (
24
+ <a
25
+ href={data.portalConnectUrl("/tools")}
26
+ className="inline-flex items-center gap-2 rounded-md border bg-background px-3 py-2 text-sm outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring"
27
+ >
28
+ <LogIn aria-hidden="true" className="size-4" />
29
+ {i18n.t("tools.reconnect")}
30
+ </a>
31
+ )}
70
32
  </header>
71
33
 
72
34
  <div className="grid gap-5 p-8 xl:grid-cols-[minmax(0,1fr)_24rem]">
73
35
  <div className="space-y-5">
74
- {discover.isError && (
75
- <p
76
- role="alert"
77
- className="rounded-md border border-destructive/30 p-3 text-sm text-destructive"
78
- >
79
- {i18n.t("tools.operationFailed")}
80
- </p>
81
- )}
82
36
  {connectError && (
83
37
  <p
84
38
  role="alert"
@@ -87,9 +41,28 @@ export function Tools() {
87
41
  {i18n.t("tools.connectFailed")}
88
42
  </p>
89
43
  )}
90
- {sources.isPending || catalog.isPending ? (
44
+ {catalog.isPending ? (
91
45
  <p className="text-sm text-muted-foreground">{i18n.t("common.loading")}</p>
92
- ) : grouped.length === 0 ? (
46
+ ) : catalog.isError ? (
47
+ <p role="alert" className="text-sm text-destructive">
48
+ {i18n.t("tools.operationFailed")}
49
+ </p>
50
+ ) : !catalog.data?.portalConnected ? (
51
+ <section className="rounded-xl border border-dashed bg-card p-12 text-center">
52
+ <Wrench aria-hidden="true" className="mx-auto size-8 text-muted-foreground" />
53
+ <h2 className="mt-4 font-semibold">{i18n.t("tools.disconnected")}</h2>
54
+ <p className="mx-auto mt-2 max-w-lg text-sm text-muted-foreground">
55
+ {i18n.t("tools.disconnectedHelp")}
56
+ </p>
57
+ <a
58
+ href={data.portalConnectUrl("/tools")}
59
+ className="mt-5 inline-flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground outline-none hover:bg-primary/90 focus-visible:ring-2 focus-visible:ring-ring"
60
+ >
61
+ <LogIn aria-hidden="true" className="size-4" />
62
+ {i18n.t("tools.connect")}
63
+ </a>
64
+ </section>
65
+ ) : catalog.data.items.length === 0 ? (
93
66
  <section className="rounded-xl border border-dashed bg-card p-12 text-center">
94
67
  <Wrench aria-hidden="true" className="mx-auto size-8 text-muted-foreground" />
95
68
  <h2 className="mt-4 font-semibold">{i18n.t("tools.empty")}</h2>
@@ -98,120 +71,81 @@ export function Tools() {
98
71
  </p>
99
72
  </section>
100
73
  ) : (
101
- grouped.map(({ source, entries }) => (
102
- <section
103
- key={source.id}
104
- className="overflow-hidden rounded-xl border bg-card shadow-sm"
105
- >
106
- <div className="flex items-start justify-between gap-5 border-b bg-muted/20 px-5 py-4">
107
- <div className="min-w-0">
108
- <h2 className="font-semibold">{source.name}</h2>
109
- <p className="mt-1 truncate text-xs text-muted-foreground">{source.url}</p>
110
- <p className="mt-2 text-xs text-muted-foreground">
111
- {i18n.t("tools.connectionHandle")}: {source.connectionHandle}
112
- </p>
113
- </div>
114
- <div className="flex shrink-0 items-center gap-2">
115
- <a
116
- href={data.toolSourceConnectUrl(source.id, "/tools")}
117
- className="inline-flex items-center gap-2 rounded-md border bg-background px-3 py-2 text-xs font-medium outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring"
118
- >
119
- <LogIn aria-hidden="true" className="size-3.5" />
120
- {i18n.t("tools.connect")}
121
- </a>
74
+ <section className="overflow-hidden rounded-xl border bg-card shadow-sm">
75
+ <ul className="divide-y">
76
+ {catalog.data.items.map((capability) => (
77
+ <li key={capability.name}>
122
78
  <button
123
79
  type="button"
124
- disabled={discover.isPending && discover.variables === source.id}
125
- onClick={() => discover.mutate(source.id)}
126
- className="inline-flex items-center gap-2 rounded-md border bg-background px-3 py-2 text-xs font-medium outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50"
80
+ onClick={() => setSelected(capability)}
81
+ className="flex w-full items-start justify-between gap-5 px-5 py-4 text-left outline-none hover:bg-muted/50 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring"
127
82
  >
128
- <RefreshCw aria-hidden="true" className="size-3.5" />
129
- {i18n.t("tools.discover")}
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
+ )}
130
102
  </button>
131
- </div>
132
- </div>
133
- {entries.length === 0 ? (
134
- <p className="p-5 text-sm text-muted-foreground">
135
- {i18n.t("tools.notDiscovered")}
136
- </p>
137
- ) : (
138
- <ul className="divide-y">
139
- {entries.map((entry) => {
140
- const Status = statusIcon[entry.connectionStatus];
141
- return (
142
- <li key={entry.capability.name}>
143
- <button
144
- type="button"
145
- onClick={() => setSelected(entry)}
146
- className="flex w-full items-start justify-between gap-5 px-5 py-4 text-left outline-none hover:bg-muted/50 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring"
147
- >
148
- <span className="min-w-0">
149
- <span className="block text-sm font-medium">
150
- {entry.capability.title ?? entry.capability.name}
151
- </span>
152
- <span className="mt-1 block truncate font-mono text-xs text-muted-foreground">
153
- {entry.capability.name}
154
- </span>
155
- {entry.capability.description && (
156
- <span className="mt-2 line-clamp-2 block text-xs text-muted-foreground">
157
- {entry.capability.description}
158
- </span>
159
- )}
160
- </span>
161
- <span className="inline-flex shrink-0 items-center gap-1.5 rounded-full bg-muted px-2 py-1 text-xs text-muted-foreground">
162
- <Status aria-hidden="true" className="size-3.5" />
163
- {i18n.t(`tools.status.${entry.connectionStatus}`)}
164
- </span>
165
- </button>
166
- </li>
167
- );
168
- })}
169
- </ul>
170
- )}
171
- </section>
172
- ))
103
+ </li>
104
+ ))}
105
+ </ul>
106
+ </section>
173
107
  )}
174
108
  </div>
175
- <ToolTester entry={selected} />
109
+ <ToolTester capability={selected} />
176
110
  </div>
177
- {registering && <RegisterSource close={() => setRegistering(false)} />}
178
111
  </div>
179
112
  );
180
113
  }
181
114
 
182
- function ToolTester({ entry }: { entry: ToolCatalogEntry | null }) {
115
+ function ToolTester({ capability }: { capability: ToolCapability | null }) {
183
116
  const { data, i18n } = useIntelRouterContext();
184
117
  const [argumentsText, setArgumentsText] = useState("{}");
185
118
  const [parseError, setParseError] = useState(false);
186
119
  const safeToTest =
187
- entry?.capability.annotations.readOnlyHint === true &&
188
- entry.capability.annotations.destructiveHint !== true;
120
+ capability?.annotations.readOnlyHint === true &&
121
+ capability.annotations.destructiveHint !== true;
189
122
  const test = useMutation({
190
123
  mutationFn: async () => {
191
124
  const parsed: unknown = JSON.parse(argumentsText);
192
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("object");
125
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
126
+ throw new SyntaxError("Arguments must be a JSON object");
127
+ }
193
128
  return await data.testTool({
194
- sourceId: entry?.source.id ?? "",
195
- name: entry?.capability.name ?? "",
129
+ name: capability?.name ?? "",
196
130
  arguments: parsed as Record<string, unknown>,
197
131
  });
198
132
  },
199
133
  onMutate: () => setParseError(false),
200
- onError: (error) => setParseError(error instanceof SyntaxError || error.message === "object"),
134
+ onError: (error) => setParseError(error instanceof SyntaxError),
201
135
  });
202
136
 
203
137
  return (
204
138
  <aside className="sticky top-5 h-fit rounded-xl border bg-card p-5 shadow-sm">
205
139
  <h2 className="font-semibold">{i18n.t("tools.testTitle")}</h2>
206
- {!entry ? (
140
+ {!capability ? (
207
141
  <p className="mt-3 text-sm text-muted-foreground">{i18n.t("tools.select")}</p>
208
142
  ) : (
209
143
  <div className="mt-4 space-y-4">
210
144
  <div>
211
- <p className="text-sm font-medium">{entry.capability.title ?? entry.capability.name}</p>
212
- <p className="mt-1 font-mono text-xs text-muted-foreground">{entry.capability.name}</p>
145
+ <p className="text-sm font-medium">{capability.title ?? capability.name}</p>
146
+ <p className="mt-1 font-mono text-xs text-muted-foreground">{capability.name}</p>
213
147
  </div>
214
- {entry.capability.annotations.destructiveHint && (
148
+ {capability.annotations.destructiveHint && (
215
149
  <p className="flex gap-2 rounded-md bg-destructive/10 p-3 text-xs text-destructive">
216
150
  <AlertTriangle aria-hidden="true" className="size-4 shrink-0" />
217
151
  {i18n.t("tools.destructive")}
@@ -223,11 +157,6 @@ function ToolTester({ entry }: { entry: ToolCatalogEntry | null }) {
223
157
  {i18n.t("tools.testUnsafe")}
224
158
  </p>
225
159
  )}
226
- {entry.connectionStatus !== "connected" && (
227
- <p className="rounded-md bg-muted p-3 text-xs text-muted-foreground">
228
- {i18n.t("tools.connectionRequired")}
229
- </p>
230
- )}
231
160
  <label className="block text-xs font-medium">
232
161
  {i18n.t("tools.arguments")}
233
162
  <textarea
@@ -241,7 +170,7 @@ function ToolTester({ entry }: { entry: ToolCatalogEntry | null }) {
241
170
  {parseError && <p className="text-xs text-destructive">{i18n.t("tools.invalidJson")}</p>}
242
171
  <button
243
172
  type="button"
244
- disabled={test.isPending || entry.connectionStatus !== "connected" || !safeToTest}
173
+ disabled={test.isPending || !safeToTest}
245
174
  onClick={() => test.mutate()}
246
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"
247
176
  >
@@ -261,86 +190,3 @@ function ToolTester({ entry }: { entry: ToolCatalogEntry | null }) {
261
190
  </aside>
262
191
  );
263
192
  }
264
-
265
- function RegisterSource({ close }: { close(): void }) {
266
- const { data, i18n } = useIntelRouterContext();
267
- const queryClient = useQueryClient();
268
- const [name, setName] = useState("");
269
- const [url, setUrl] = useState("");
270
- const [connectionHandle, setConnectionHandle] = useState("intel-tools");
271
- const create = useMutation({
272
- mutationFn: () =>
273
- data.createToolSource({
274
- name,
275
- url,
276
- connectionHandle,
277
- idempotencyKey: crypto.randomUUID(),
278
- }),
279
- onSuccess: async () => {
280
- await queryClient.invalidateQueries({ queryKey: ["tool-sources"] });
281
- close();
282
- },
283
- });
284
- return (
285
- <Modal title={i18n.t("tools.addSource")} close={close} className="max-w-lg">
286
- <form
287
- className="space-y-4"
288
- onSubmit={(event) => {
289
- event.preventDefault();
290
- if (create.isPending) return;
291
- create.mutate();
292
- }}
293
- >
294
- <Field label={i18n.t("common.title")} value={name} setValue={setName} />
295
- <Field label={i18n.t("tools.endpoint")} value={url} setValue={setUrl} type="url" />
296
- <Field
297
- label={i18n.t("tools.connectionHandle")}
298
- value={connectionHandle}
299
- setValue={setConnectionHandle}
300
- pattern="[a-z0-9][a-z0-9-]{1,62}"
301
- />
302
- <p className="text-xs text-muted-foreground">{i18n.t("tools.sourceHelp")}</p>
303
- {create.isError && (
304
- <p role="alert" className="text-sm text-destructive">
305
- {i18n.t("tools.operationFailed")}
306
- </p>
307
- )}
308
- <button
309
- type="submit"
310
- disabled={create.isPending}
311
- className="w-full rounded-md bg-primary px-4 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"
312
- >
313
- {create.isPending ? i18n.t("common.saving") : i18n.t("common.create")}
314
- </button>
315
- </form>
316
- </Modal>
317
- );
318
- }
319
-
320
- function Field({
321
- label,
322
- value,
323
- setValue,
324
- type = "text",
325
- pattern,
326
- }: {
327
- label: string;
328
- value: string;
329
- setValue(value: string): void;
330
- type?: string;
331
- pattern?: string;
332
- }) {
333
- return (
334
- <label className="block text-sm font-medium">
335
- {label}
336
- <input
337
- required
338
- type={type}
339
- pattern={pattern}
340
- value={value}
341
- onChange={(event) => setValue(event.target.value)}
342
- className="mt-2 w-full rounded-md border bg-background px-3 py-2 outline-none focus-visible:ring-2 focus-visible:ring-ring"
343
- />
344
- </label>
345
- );
346
- }