@anchrd/intel-ui 0.8.2 → 0.8.3
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/package.json
CHANGED
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
FileText,
|
|
7
7
|
Folder,
|
|
8
8
|
FolderOpen,
|
|
9
|
-
Paperclip,
|
|
10
9
|
Plus,
|
|
11
10
|
Table,
|
|
12
11
|
Upload,
|
|
@@ -36,6 +35,7 @@ import {
|
|
|
36
35
|
} from "@/components/ui/sidebar";
|
|
37
36
|
import { flowEntry } from "@/data/intel-data-provider/intel-data-provider.ts";
|
|
38
37
|
import type { TreeEntry } from "@/data/intel-data-provider/intel-data-provider.types.ts";
|
|
38
|
+
import { kindIcons } from "@/kind-icon.ts";
|
|
39
39
|
import { Modal } from "@/modal/modal.tsx";
|
|
40
40
|
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
41
41
|
import { selectedFrom } from "@/router/selection-search.ts";
|
|
@@ -43,14 +43,6 @@ import { selectedFrom } from "@/router/selection-search.ts";
|
|
|
43
43
|
type NewKind = "folder" | "document" | "table" | "flow";
|
|
44
44
|
type Creating = { parentId: string | null; kind: NewKind };
|
|
45
45
|
|
|
46
|
-
const icons = {
|
|
47
|
-
folder: Folder,
|
|
48
|
-
document: FileText,
|
|
49
|
-
attachment: Paperclip,
|
|
50
|
-
table: Table,
|
|
51
|
-
flow: Workflow,
|
|
52
|
-
} as const;
|
|
53
|
-
|
|
54
46
|
// Attachments are inlined as base64, so the browser holds the file twice while it uploads.
|
|
55
47
|
const MaxUploadBytes = 15_000_000;
|
|
56
48
|
|
|
@@ -385,7 +377,7 @@ export function AppTree() {
|
|
|
385
377
|
const expandable = entry.openable;
|
|
386
378
|
const isOpen =
|
|
387
379
|
expandable && expanded.some((open) => open.id === entry.id && open.type === level.type);
|
|
388
|
-
const Icon = isOpen && isFolder ? FolderOpen :
|
|
380
|
+
const Icon = isOpen && isFolder ? FolderOpen : kindIcons[entry.kind];
|
|
389
381
|
const area = entry.type === "flow" ? "/flows" : "/knowledge";
|
|
390
382
|
const isActive = location.select === entry.id && location.pathname === area;
|
|
391
383
|
// A row's plus files into that row's place: inside a folder, beside anything else.
|
|
@@ -10,15 +10,17 @@ import {
|
|
|
10
10
|
TableRow,
|
|
11
11
|
} from "@/components/ui/table.tsx";
|
|
12
12
|
import type { TreeEntry } from "@/data/intel-data-provider/intel-data-provider.types.ts";
|
|
13
|
+
import { kindIcons } from "@/kind-icon.ts";
|
|
14
|
+
import { ResourceMenu, type ResourceTarget } from "@/resource-menu/resource-menu.tsx";
|
|
13
15
|
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
17
|
+
// The same record the menu is handed everywhere else, read off a level row. A re-labelling, never a
|
|
18
|
+
// second source of truth — a rename works on the whole record, not on a summary of it.
|
|
19
|
+
function targetOf(entry: TreeEntry): ResourceTarget {
|
|
20
|
+
return entry.type === "flow"
|
|
21
|
+
? { type: "flow", flow: entry.flow }
|
|
22
|
+
: { type: "knowledge", node: entry.node };
|
|
23
|
+
}
|
|
22
24
|
|
|
23
25
|
/**
|
|
24
26
|
* A folder's own screen: what lies directly in it, as a table.
|
|
@@ -27,7 +29,13 @@ const icons: Record<TreeEntry["kind"], string> = {
|
|
|
27
29
|
* `listTreeChildren`. Not a second call and not a second cache entry: two readers of one folder that
|
|
28
30
|
* fetch separately show it differently the moment one of them is stale, and that is the kind of
|
|
29
31
|
* difference a customer finds before anybody here does. The other half of the bargain is free —
|
|
30
|
-
* every invalidation the tree already does after a create, a move or an archive lands here too
|
|
32
|
+
* every invalidation the tree already does after a create, a move or an archive lands here too, and
|
|
33
|
+
* that now includes the ones this screen's own menu makes: `ResourceMenu` invalidates
|
|
34
|
+
* `["tree", parentId]`, which is this level.
|
|
35
|
+
*
|
|
36
|
+
* ⚠️ There is no "Kind" column since issue 101. The icon says what a row is, in the same picture the
|
|
37
|
+
* sidebar draws two centimetres to its left, and `sr-only` says it in words for everyone who does
|
|
38
|
+
* not read pictures. A column repeating the icon in prose costs the width the title needs.
|
|
31
39
|
*/
|
|
32
40
|
export function FolderContents({ folderId }: { folderId: string }) {
|
|
33
41
|
const { data, i18n } = useIntelRouterContext();
|
|
@@ -61,46 +69,75 @@ export function FolderContents({ folderId }: { folderId: string }) {
|
|
|
61
69
|
}
|
|
62
70
|
|
|
63
71
|
return (
|
|
64
|
-
<div className="min-h-0 flex-1 overflow-y-auto p-
|
|
65
|
-
<
|
|
66
|
-
<
|
|
67
|
-
<
|
|
68
|
-
<
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
<
|
|
78
|
-
{
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
>
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
72
|
+
<div className="min-h-0 flex-1 overflow-y-auto p-6">
|
|
73
|
+
<div className="overflow-hidden rounded-lg border bg-card">
|
|
74
|
+
<Table>
|
|
75
|
+
<TableHeader className="[&_tr]:bg-muted/40">
|
|
76
|
+
<TableRow className="hover:bg-transparent">
|
|
77
|
+
<TableHead className="h-9 px-4 text-xs tracking-wide uppercase">
|
|
78
|
+
{i18n.t("common.title")}
|
|
79
|
+
</TableHead>
|
|
80
|
+
<TableHead className="h-9 px-4 text-right text-xs tracking-wide uppercase">
|
|
81
|
+
{i18n.t("knowledge.changed")}
|
|
82
|
+
</TableHead>
|
|
83
|
+
{/* A `<th>` with nothing in it leaves the column unnamed for anyone reading the table
|
|
84
|
+
by its headers. The word is there; only the eye is spared it. */}
|
|
85
|
+
<TableHead className="h-9 w-14">
|
|
86
|
+
<span className="sr-only">{i18n.t("knowledge.rowActions")}</span>
|
|
87
|
+
</TableHead>
|
|
88
|
+
</TableRow>
|
|
89
|
+
</TableHeader>
|
|
90
|
+
<TableBody>
|
|
91
|
+
{entries.map((entry) => {
|
|
92
|
+
const changed = entry.type === "flow" ? entry.flow.updatedAt : entry.node.updatedAt;
|
|
93
|
+
const Icon = kindIcons[entry.kind];
|
|
94
|
+
return (
|
|
95
|
+
<TableRow key={entry.id} className="group/row hover:bg-muted">
|
|
96
|
+
{/* ⚠️ The button carries the whole cell rather than the cell carrying an onClick.
|
|
97
|
+
A row that only reacts to a mouse is a row nobody can reach with a keyboard, and
|
|
98
|
+
a `<tr onClick>` has no name to read out either. */}
|
|
99
|
+
<TableCell className="p-0">
|
|
100
|
+
<button
|
|
101
|
+
type="button"
|
|
102
|
+
onClick={() =>
|
|
103
|
+
void navigate({
|
|
104
|
+
to: entry.type === "flow" ? "/flows" : "/knowledge",
|
|
105
|
+
search: { select: entry.id },
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
className="flex w-full min-w-0 items-center gap-3 px-4 py-2.5 text-left outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset"
|
|
109
|
+
>
|
|
110
|
+
{/* The icon gets a tile of its own so five kinds under one another read as a
|
|
111
|
+
column, instead of five loose glyphs at five different widths. */}
|
|
112
|
+
<span
|
|
113
|
+
aria-hidden="true"
|
|
114
|
+
className="grid size-8 shrink-0 place-items-center rounded-md border bg-muted/50 text-muted-foreground transition-colors group-hover/row:bg-accent group-hover/row:text-accent-foreground"
|
|
115
|
+
>
|
|
116
|
+
<Icon className="size-4" />
|
|
117
|
+
</span>
|
|
118
|
+
<span className="min-w-0 truncate font-medium group-hover/row:underline">
|
|
119
|
+
{entry.title}
|
|
120
|
+
</span>
|
|
121
|
+
{/* The icon carries the type on screen; this carries it everywhere else, and
|
|
122
|
+
it is why a flow needs no suffix in its name to be told from a document. */}
|
|
123
|
+
<span className="sr-only">{i18n.t(`knowledge.kind.${entry.kind}`)}</span>
|
|
124
|
+
</button>
|
|
125
|
+
</TableCell>
|
|
126
|
+
<TableCell className="px-4 text-right text-sm text-muted-foreground tabular-nums">
|
|
127
|
+
{new Date(changed).toLocaleDateString(i18n.locale)}
|
|
128
|
+
</TableCell>
|
|
129
|
+
{/* ⚠️ The same menu as the title line of the open thing, not a shorter one built
|
|
130
|
+
for here. #58 collected every action in one place so that place is always the
|
|
131
|
+
same; a folder listing offering three of the six would be a second place. */}
|
|
132
|
+
<TableCell className="px-3 text-right">
|
|
133
|
+
<ResourceMenu target={targetOf(entry)} variant="row" />
|
|
134
|
+
</TableCell>
|
|
135
|
+
</TableRow>
|
|
136
|
+
);
|
|
137
|
+
})}
|
|
138
|
+
</TableBody>
|
|
139
|
+
</Table>
|
|
140
|
+
</div>
|
|
104
141
|
</div>
|
|
105
142
|
);
|
|
106
143
|
}
|
package/src/i18n/en.json
CHANGED
|
@@ -85,8 +85,8 @@
|
|
|
85
85
|
"knowledge.select": "Select a document or folder to work with it.",
|
|
86
86
|
"knowledge.folderEmpty": "Nothing in this folder yet. Create something with the plus in the sidebar.",
|
|
87
87
|
"knowledge.folderFailed": "This folder could not be read.",
|
|
88
|
-
"knowledge.kind": "Kind",
|
|
89
88
|
"knowledge.changed": "Changed",
|
|
89
|
+
"knowledge.rowActions": "Actions",
|
|
90
90
|
"knowledge.kind.folder": "Folder",
|
|
91
91
|
"knowledge.kind.document": "Document",
|
|
92
92
|
"knowledge.kind.attachment": "Upload",
|
|
@@ -157,6 +157,9 @@
|
|
|
157
157
|
"tools.inputSchema": "Input schema",
|
|
158
158
|
"tools.outputSchema": "Result schema",
|
|
159
159
|
"tools.destructiveShort": "Destructive",
|
|
160
|
+
"tools.destructiveCount": "{count} destructive",
|
|
161
|
+
"tools.toolCount": "{count} tools",
|
|
162
|
+
"tools.toolCountOne": "1 tool",
|
|
160
163
|
"tools.liveNote": "This list is a live query with your own portal access. Intel stores neither the tools nor who may use them; the portal decides both.",
|
|
161
164
|
"tools.arguments": "Arguments",
|
|
162
165
|
"tools.invalidJson": "The arguments must be a JSON object in curly braces; a list or a single value will not work.",
|
package/src/kind-icon.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { FileText, Folder, type LucideIcon, Paperclip, Table, Workflow } from "lucide-react";
|
|
2
|
+
import type { TreeEntry } from "@/data/intel-data-provider/intel-data-provider.types.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The one place a kind becomes a picture.
|
|
6
|
+
*
|
|
7
|
+
* ⚠️ Two screens draw the same row — the sidebar tree and the folder's own table — and they must
|
|
8
|
+
* draw it the same. A second map beside this one drifts the first time a kind is added: the tree
|
|
9
|
+
* would show the new icon and the folder screen the old one, for the same thing on the same screen.
|
|
10
|
+
* The picture is also only half the answer; both places still name the kind for a reader who cannot
|
|
11
|
+
* see it (`tree.kind.*`, `knowledge.kind.*`), because an icon has no text.
|
|
12
|
+
*/
|
|
13
|
+
export const kindIcons: Record<TreeEntry["kind"], LucideIcon> = {
|
|
14
|
+
folder: Folder,
|
|
15
|
+
document: FileText,
|
|
16
|
+
attachment: Paperclip,
|
|
17
|
+
table: Table,
|
|
18
|
+
flow: Workflow,
|
|
19
|
+
};
|
|
@@ -102,11 +102,14 @@ export function resourceErrorKey(error: unknown): string {
|
|
|
102
102
|
/**
|
|
103
103
|
* The three-dot menu.
|
|
104
104
|
*
|
|
105
|
-
* `variant` is only how the trigger is painted. `title` is
|
|
106
|
-
* the end of a title line, put there by `TitleRow` and by nobody else, so it is
|
|
107
|
-
* thing in that line (#53). `row` is the
|
|
108
|
-
* in it takes focus — and
|
|
109
|
-
*
|
|
105
|
+
* `variant` is only how the trigger is painted, never which entries it holds. `title` is a plain
|
|
106
|
+
* icon button at the end of a title line, put there by `TitleRow` and by nobody else, so it is
|
|
107
|
+
* always the last thing in that line (#53). `row` is the form for a row in a list — quiet until the
|
|
108
|
+
* row is hovered or something in it takes focus — and since issue 101 that list is the folder table.
|
|
109
|
+
*
|
|
110
|
+
* ⚠️ `row` needs a `group/row` on the row it sits in, or it stays invisible to the mouse: the
|
|
111
|
+
* reveal is `group-hover/row`, and a group that is not there never hovers. Below `md` and under
|
|
112
|
+
* focus it is shown regardless, because hover is not a gesture every input has.
|
|
110
113
|
*/
|
|
111
114
|
export function ResourceMenu({
|
|
112
115
|
target,
|
|
@@ -218,7 +221,7 @@ export function ResourceMenu({
|
|
|
218
221
|
aria-label={i18n.t("resource.menu", { title })}
|
|
219
222
|
className={
|
|
220
223
|
variant === "row"
|
|
221
|
-
? "shrink-0 rounded-md
|
|
224
|
+
? "inline-flex size-8 shrink-0 items-center justify-center rounded-md text-muted-foreground outline-none transition hover:bg-accent hover:text-accent-foreground focus-visible:opacity-100 focus-visible:ring-2 focus-visible:ring-ring data-[state=open]:opacity-100 group-focus-within/row:opacity-100 group-hover/row:opacity-100 md:opacity-0"
|
|
222
225
|
: "inline-flex size-8 shrink-0 items-center justify-center rounded-md border bg-background outline-none hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring"
|
|
223
226
|
}
|
|
224
227
|
>
|
package/src/tools/tools.tsx
CHANGED
|
@@ -19,6 +19,30 @@ function toolOrigin(name: string): string | null {
|
|
|
19
19
|
return boundary > 0 ? name.slice(0, boundary) : null;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
interface ToolGroup {
|
|
23
|
+
// `null` is a tool the portal answers without a namespace: its origin is the portal itself.
|
|
24
|
+
origin: string | null;
|
|
25
|
+
items: ToolCapability[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// The catalog arrives as one flat list of a hundred-odd names, and the namespace is the only thing
|
|
29
|
+
// that says which server a tool belongs to. Grouping is therefore not decoration — it is the one
|
|
30
|
+
// piece of structure the wire carries, made visible (#99).
|
|
31
|
+
//
|
|
32
|
+
// The order is the portal's own: a group appears where its first tool appears. Sorting here would
|
|
33
|
+
// replace an answer with a preference, and the portal already returns a server's tools together.
|
|
34
|
+
function groupByOrigin(items: ToolCapability[]): ToolGroup[] {
|
|
35
|
+
const groups = new Map<string, ToolGroup>();
|
|
36
|
+
for (const item of items) {
|
|
37
|
+
const origin = toolOrigin(item.name);
|
|
38
|
+
const key = origin ?? "";
|
|
39
|
+
const group = groups.get(key) ?? { origin, items: [] };
|
|
40
|
+
group.items.push(item);
|
|
41
|
+
groups.set(key, group);
|
|
42
|
+
}
|
|
43
|
+
return [...groups.values()];
|
|
44
|
+
}
|
|
45
|
+
|
|
22
46
|
// Signed in to Intel is signed in to the portal, so the sign-in is attempted at most once per
|
|
23
47
|
// visit. A second attempt after a refusal would be a redirect loop with an unchanging answer, and
|
|
24
48
|
// the marker in the URL only survives until the next navigation — the browser session remembers it
|
|
@@ -140,19 +164,16 @@ export function Tools() {
|
|
|
140
164
|
/>
|
|
141
165
|
) : (
|
|
142
166
|
<>
|
|
143
|
-
<
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
))}
|
|
154
|
-
</ul>
|
|
155
|
-
</section>
|
|
167
|
+
<div className="space-y-3">
|
|
168
|
+
{groupByOrigin(catalog.data.items).map((group) => (
|
|
169
|
+
<ServerGroup
|
|
170
|
+
key={group.origin ?? ""}
|
|
171
|
+
group={group}
|
|
172
|
+
i18n={i18n}
|
|
173
|
+
requested={requested}
|
|
174
|
+
/>
|
|
175
|
+
))}
|
|
176
|
+
</div>
|
|
156
177
|
<p className="text-xs text-muted-foreground">{i18n.t("tools.liveNote")}</p>
|
|
157
178
|
</>
|
|
158
179
|
)}
|
|
@@ -187,6 +208,74 @@ function Notice({
|
|
|
187
208
|
);
|
|
188
209
|
}
|
|
189
210
|
|
|
211
|
+
// One server, one box, closed until asked. Closed is the point: a hundred rows spread over a
|
|
212
|
+
// handful of servers is a list nobody reads, and which server a tool speaks to is the first thing
|
|
213
|
+
// somebody wants to know about it (#99).
|
|
214
|
+
function ServerGroup({
|
|
215
|
+
group,
|
|
216
|
+
i18n,
|
|
217
|
+
requested,
|
|
218
|
+
}: {
|
|
219
|
+
group: ToolGroup;
|
|
220
|
+
i18n: I18n;
|
|
221
|
+
requested: string | null;
|
|
222
|
+
}) {
|
|
223
|
+
// A hit from the header search names a tool, not a server, and the box it sits in has to open
|
|
224
|
+
// with it — otherwise Enter on a hit lands on a screen where nothing happened.
|
|
225
|
+
const holdsRequested = group.items.some((item) => item.name === requested);
|
|
226
|
+
const destructive = group.items.filter((item) => item.annotations.destructiveHint).length;
|
|
227
|
+
const label = group.origin ?? i18n.t("tools.originPortal");
|
|
228
|
+
const count =
|
|
229
|
+
group.items.length === 1
|
|
230
|
+
? i18n.t("tools.toolCountOne")
|
|
231
|
+
: i18n.t("tools.toolCount", { count: group.items.length });
|
|
232
|
+
// The badges beside the name read as one run-on word when a screen reader concatenates them
|
|
233
|
+
// ("wiki1 tool"), so the control says its own name instead of being read off its contents.
|
|
234
|
+
const spoken = [`${i18n.t("tools.origin")}: ${label}`, count];
|
|
235
|
+
if (destructive > 0) spoken.push(i18n.t("tools.destructiveCount", { count: destructive }));
|
|
236
|
+
|
|
237
|
+
return (
|
|
238
|
+
<Collapsible defaultOpen={holdsRequested} asChild>
|
|
239
|
+
<section className="overflow-hidden rounded-xl border bg-card shadow-sm">
|
|
240
|
+
<CollapsibleTrigger
|
|
241
|
+
aria-label={spoken.join(", ")}
|
|
242
|
+
className="group flex w-full items-center gap-3 px-5 py-4 text-left outline-none hover:bg-muted/50 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring"
|
|
243
|
+
>
|
|
244
|
+
<ChevronRight
|
|
245
|
+
aria-hidden="true"
|
|
246
|
+
className="size-4 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-90"
|
|
247
|
+
/>
|
|
248
|
+
<span className="min-w-0 flex-1">
|
|
249
|
+
<span className="block truncate text-sm font-semibold">{label}</span>
|
|
250
|
+
</span>
|
|
251
|
+
{/* Closed, the box hides every destructive badge inside it. The count says so on the lid
|
|
252
|
+
rather than making somebody open each server to find out. */}
|
|
253
|
+
{destructive > 0 && (
|
|
254
|
+
<span className="inline-flex shrink-0 items-center gap-1.5 rounded-full bg-destructive/10 px-2 py-0.5 text-xs text-destructive">
|
|
255
|
+
<AlertTriangle aria-hidden="true" className="size-3.5" />
|
|
256
|
+
{i18n.t("tools.destructiveCount", { count: destructive })}
|
|
257
|
+
</span>
|
|
258
|
+
)}
|
|
259
|
+
<span className="shrink-0 text-xs text-muted-foreground">{count}</span>
|
|
260
|
+
</CollapsibleTrigger>
|
|
261
|
+
<CollapsibleContent>
|
|
262
|
+
<ul className="divide-y border-t">
|
|
263
|
+
{group.items.map((capability) => (
|
|
264
|
+
<li key={capability.name}>
|
|
265
|
+
<ToolEntry
|
|
266
|
+
capability={capability}
|
|
267
|
+
i18n={i18n}
|
|
268
|
+
open={capability.name === requested}
|
|
269
|
+
/>
|
|
270
|
+
</li>
|
|
271
|
+
))}
|
|
272
|
+
</ul>
|
|
273
|
+
</CollapsibleContent>
|
|
274
|
+
</section>
|
|
275
|
+
</Collapsible>
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
190
279
|
// One tool, one disclosure. The row is the only control in the list: it opens the schema and does
|
|
191
280
|
// nothing else — enabling, sharing or permitting a tool happens in the portal, never here.
|
|
192
281
|
function ToolEntry({
|
|
@@ -198,11 +287,9 @@ function ToolEntry({
|
|
|
198
287
|
i18n: I18n;
|
|
199
288
|
open: boolean;
|
|
200
289
|
}) {
|
|
201
|
-
const origin = toolOrigin(capability.name);
|
|
202
|
-
|
|
203
290
|
return (
|
|
204
291
|
<Collapsible defaultOpen={open}>
|
|
205
|
-
<CollapsibleTrigger className="group flex w-full items-start gap-4 px-5 py-
|
|
292
|
+
<CollapsibleTrigger className="group flex w-full items-start gap-4 px-5 py-3 text-left outline-none hover:bg-muted/50 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring">
|
|
206
293
|
<ChevronRight
|
|
207
294
|
aria-hidden="true"
|
|
208
295
|
className="mt-0.5 size-4 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-90"
|
|
@@ -210,10 +297,6 @@ function ToolEntry({
|
|
|
210
297
|
<span className="min-w-0 flex-1">
|
|
211
298
|
<span className="flex flex-wrap items-center gap-2">
|
|
212
299
|
<span className="text-sm font-medium">{capability.title ?? capability.name}</span>
|
|
213
|
-
<span className="inline-flex items-center rounded-full bg-muted px-2 py-0.5 text-xs text-muted-foreground">
|
|
214
|
-
<span className="sr-only">{i18n.t("tools.origin")}: </span>
|
|
215
|
-
{origin ?? i18n.t("tools.originPortal")}
|
|
216
|
-
</span>
|
|
217
300
|
{capability.annotations.destructiveHint && (
|
|
218
301
|
<span className="inline-flex items-center gap-1.5 rounded-full bg-destructive/10 px-2 py-0.5 text-xs text-destructive">
|
|
219
302
|
<AlertTriangle aria-hidden="true" className="size-3.5" />
|
|
@@ -228,14 +311,14 @@ function ToolEntry({
|
|
|
228
311
|
{capability.name}
|
|
229
312
|
</span>
|
|
230
313
|
)}
|
|
231
|
-
{capability.description && (
|
|
232
|
-
<span className="mt-2 block text-xs text-muted-foreground">
|
|
233
|
-
{capability.description}
|
|
234
|
-
</span>
|
|
235
|
-
)}
|
|
236
314
|
</span>
|
|
237
315
|
</CollapsibleTrigger>
|
|
316
|
+
{/* ⚠️ The description belongs here, not in the closed row. Held open it was the whole reason
|
|
317
|
+
the list could not be scanned: a paragraph per tool, a hundred times over. */}
|
|
238
318
|
<CollapsibleContent className="space-y-3 px-5 pb-4 pl-13">
|
|
319
|
+
{capability.description && (
|
|
320
|
+
<p className="text-xs text-muted-foreground">{capability.description}</p>
|
|
321
|
+
)}
|
|
239
322
|
<Schema label={i18n.t("tools.inputSchema")} value={capability.inputSchema} />
|
|
240
323
|
{capability.outputSchema && (
|
|
241
324
|
<Schema label={i18n.t("tools.outputSchema")} value={capability.outputSchema} />
|