Skip to content

Commit 20de84f

Browse files
committed
Refactor Tool Explorer and View Components for Enhanced Tool Detail Management
- Introduce a new `hasRenderableToolDetails` function to streamline the determination of tool detail availability in the ToolExplorer component. - Update loading state handling in the ToolExplorer and ToolsView components to improve user experience during data fetching. - Refactor the display logic for tool details in the ToolDetail component, ensuring accurate representation of input and output hints. - Enhance memoization strategies in the ToolsView component to optimize performance and reduce unnecessary re-renders. - Adjust the handling of tool source names and visibility in the ToolsView component for better clarity and organization.
1 parent 7790037 commit 20de84f

21 files changed

Lines changed: 463 additions & 155 deletions

‎executor/apps/web/src/components/tools/explorer-groups.tsx‎

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -198,15 +198,15 @@ export function GroupNode({
198198
/>
199199
);
200200
})
201-
: isLoading
202-
? (
203-
<ToolLoadingRows
204-
source={displayLabel}
205-
count={group.loadingPlaceholderCount ?? 0}
206-
depth={depth + 1}
207-
/>
208-
)
209-
: null}
201+
: null}
202+
203+
{isLoading ? (
204+
<ToolLoadingRows
205+
source={displayLabel}
206+
count={group.loadingPlaceholderCount ?? 3}
207+
depth={depth + 1}
208+
/>
209+
) : null}
210210
</CollapsibleContent>
211211
</Collapsible>
212212
);

‎executor/apps/web/src/components/tools/explorer.tsx‎

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,18 @@ export function ToolExplorer({
9595
inventoryState,
9696
inventoryError,
9797
}: ToolExplorerProps) {
98+
const hasRenderableToolDetails = useCallback((tool: Pick<ToolDescriptor, "description" | "display" | "typing">) => {
99+
const description = tool.description?.trim() ?? "";
100+
const inputHint = tool.display?.input?.trim() ?? "";
101+
const outputHint = tool.display?.output?.trim() ?? "";
102+
const required = tool.typing?.requiredInputKeys ?? [];
103+
104+
const hasInputHint = inputHint.length > 0 && inputHint !== "{}" && inputHint.toLowerCase() !== "unknown";
105+
const hasOutputHint = outputHint.length > 0 && outputHint.toLowerCase() !== "unknown";
106+
107+
return description.length > 0 || hasInputHint || hasOutputHint || required.length > 0;
108+
}, []);
109+
98110
const [searchInput, setSearchInput] = useState("");
99111
const search = useDeferredValue(searchInput);
100112
const [viewMode, setViewMode] = useState<ViewMode>("tree");
@@ -149,8 +161,21 @@ export function ToolExplorer({
149161
set.add(sourceName);
150162
}
151163
}
164+
165+
if (set.size === 0 && loading && searchInput.length === 0 && filteredTools.length === 0) {
166+
if (resolvedActiveSource) {
167+
set.add(resolvedActiveSource);
168+
} else {
169+
for (const source of sources) {
170+
if (source.enabled) {
171+
set.add(source.name);
172+
}
173+
}
174+
}
175+
}
176+
152177
return set;
153-
}, [loadingSources, sourceLoadingMoreTools]);
178+
}, [filteredTools.length, loading, loadingSources, resolvedActiveSource, searchInput.length, sourceLoadingMoreTools, sources]);
154179

155180
const visibleLoadingSources = useMemo(() => {
156181
if (loadingSourceSet.size === 0) {
@@ -362,12 +387,7 @@ export function ToolExplorer({
362387
return;
363388
}
364389

365-
const hasDetails = Boolean(
366-
tool.description
367-
|| tool.display?.input
368-
|| tool.display?.output
369-
|| (tool.typing?.requiredInputKeys?.length ?? 0) > 0,
370-
);
390+
const hasDetails = hasRenderableToolDetails(tool);
371391

372392
if (hasDetails || toolDetailsByPath[tool.path]) {
373393
return;
@@ -396,7 +416,7 @@ export function ToolExplorer({
396416
return next;
397417
});
398418
}
399-
}, [loadingDetailPaths, onLoadToolDetails, toolDetailsByPath]);
419+
}, [hasRenderableToolDetails, loadingDetailPaths, onLoadToolDetails, toolDetailsByPath]);
400420

401421
const flatLoadingRows = useMemo(() => {
402422
if (search.length > 0 || viewMode !== "flat") {

‎executor/apps/web/src/components/tools/explorer/tool-detail.tsx‎

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,14 @@ export function ToolDetail({
2020
loading?: boolean;
2121
}) {
2222
const insetLeft = depth * 20 + 8 + 16 + 8;
23-
const inputHint = tool.display?.input?.trim();
24-
const outputHint = tool.display?.output?.trim();
23+
const description = tool.description?.trim() ?? "";
24+
const inputHint = tool.display?.input?.trim() ?? "";
25+
const outputHint = tool.display?.output?.trim() ?? "";
2526
const required = tool.typing?.requiredInputKeys ?? [];
26-
const hasDetails = Boolean(tool.description || inputHint || outputHint || required.length > 0);
27-
const showLoading = loading || !hasDetails;
27+
const hasInputHint = inputHint.length > 0 && inputHint !== "{}" && inputHint.toLowerCase() !== "unknown";
28+
const hasOutputHint = outputHint.length > 0 && outputHint.toLowerCase() !== "unknown";
29+
const hasDetails = description.length > 0 || hasInputHint || hasOutputHint || required.length > 0;
30+
const showLoading = Boolean(loading);
2831

2932
return (
3033
<div className="space-y-2.5 pb-3 pt-1 pr-2" style={{ paddingLeft: insetLeft }}>
@@ -48,14 +51,18 @@ export function ToolDetail({
4851
</div>
4952
) : null}
5053

51-
{tool.description && (
54+
{description && (
5255
<div className="tool-description text-[12px] leading-relaxed text-muted-foreground">
53-
<Streamdown plugins={{ code: codePlugin }}>{tool.description}</Streamdown>
56+
<Streamdown plugins={{ code: codePlugin }}>{description}</Streamdown>
5457
</div>
5558
)}
5659

57-
{inputHint && <TypeSignature raw={inputHint} label="Arguments" />}
58-
{outputHint && <TypeSignature raw={outputHint} label="Returns" />}
60+
{hasInputHint && <TypeSignature raw={inputHint} label="Arguments" />}
61+
{hasOutputHint && <TypeSignature raw={outputHint} label="Returns" />}
62+
63+
{!showLoading && !hasDetails ? (
64+
<p className="text-[11px] text-muted-foreground/60">No description or type signatures available yet.</p>
65+
) : null}
5966
</div>
6067
);
6168
}

‎executor/apps/web/src/components/tools/view.tsx‎

Lines changed: 71 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -188,22 +188,52 @@ export function ToolsView({
188188
toast.error(message, { id: INVENTORY_REGENERATION_TOAST_ID });
189189
}, []);
190190

191-
const toolSourceNames = useMemo(
191+
const serverToolSourceNames = useMemo(
192192
() => new Set(tools.map((tool) => sourceLabel(tool.source))),
193193
[tools],
194194
);
195195

196196
// Prune stale ops: if the server already reflects an add/remove, drop it.
197197
const optimisticOps = useMemo(
198-
() => pruneStaleOps(rawOptimisticOps, serverSourceItems, toolSourceNames),
199-
[rawOptimisticOps, serverSourceItems, toolSourceNames],
198+
() => pruneStaleOps(rawOptimisticOps, serverSourceItems, serverToolSourceNames),
199+
[rawOptimisticOps, serverSourceItems, serverToolSourceNames],
200200
);
201201

202202
const sourceItems = useMemo(
203203
() => applyOptimisticOps(serverSourceItems, optimisticOps),
204204
[serverSourceItems, optimisticOps],
205205
);
206206

207+
const visibleSourceNames = useMemo(
208+
() => new Set(sourceItems.map((source) => source.name)),
209+
[sourceItems],
210+
);
211+
212+
const visibleTools = useMemo(
213+
() => tools.filter((tool) => {
214+
const sourceName = sourceLabel(tool.source);
215+
return sourceName === "system" || sourceName === "built-in" || visibleSourceNames.has(sourceName);
216+
}),
217+
[tools, visibleSourceNames],
218+
);
219+
220+
const toolSourceNames = useMemo(
221+
() => {
222+
const names = new Set<string>(["system", "built-in"]);
223+
for (const source of sourceItems) {
224+
names.add(source.name);
225+
}
226+
for (const tool of visibleTools) {
227+
const sourceName = sourceLabel(tool.source);
228+
if (sourceName === "system" || sourceName === "built-in") {
229+
names.add(sourceName);
230+
}
231+
}
232+
return names;
233+
},
234+
[sourceItems, visibleTools],
235+
);
236+
207237
// Source names that are optimistically loading (just added, tools not fetched yet)
208238
const optimisticallyLoadingNames = useMemo(
209239
() => optimisticOps
@@ -254,6 +284,38 @@ export function ToolsView({
254284
return combined;
255285
}, [hasGlobalInventoryWarning, loadingSources, optimisticallyLoadingNames, sourceItems, toolSourceNames]);
256286

287+
const visibleLoadingSources = useMemo(
288+
() => mergedLoadingSources.filter((name) =>
289+
visibleSourceNames.has(name) || name === "system" || name === "built-in" || optimisticallyLoadingNames.includes(name)
290+
),
291+
[mergedLoadingSources, optimisticallyLoadingNames, visibleSourceNames],
292+
);
293+
294+
const visibleSourceCounts = useMemo(() => {
295+
const sourceCounts = inventoryStatus?.sourceToolCounts;
296+
if (!sourceCounts) {
297+
return undefined;
298+
}
299+
300+
const counts: Record<string, number> = {};
301+
for (const [sourceName, rawCount] of Object.entries(sourceCounts)) {
302+
const count =
303+
typeof rawCount === "number"
304+
? rawCount
305+
: typeof rawCount === "string"
306+
? Number(rawCount)
307+
: Number.NaN;
308+
if (!Number.isFinite(count)) {
309+
continue;
310+
}
311+
if (visibleSourceNames.has(sourceName) || sourceName === "system" || sourceName === "built-in") {
312+
counts[sourceName] = count;
313+
}
314+
}
315+
316+
return counts;
317+
}, [inventoryStatus?.sourceToolCounts, visibleSourceNames]);
318+
257319
const existingSourceNames = useMemo(() => new Set(sourceItems.map((source) => source.name)), [sourceItems]);
258320
const warningsBySource = useMemo(() => warningsBySourceName(warnings), [warnings]);
259321
const sourceDialogMeta = useMemo(() => {
@@ -407,11 +469,11 @@ export function ToolsView({
407469
<CardContent className="pt-0 min-h-0 flex-1 flex flex-col gap-3">
408470
<div className="min-h-0 flex-1">
409471
<ToolExplorer
410-
tools={tools}
472+
tools={visibleTools}
411473
sources={sourceItems}
412-
loadingSources={mergedLoadingSources}
413-
sourceCountsOverride={inventoryStatus?.sourceToolCounts}
414-
totalTools={totalTools}
474+
loadingSources={visibleLoadingSources}
475+
sourceCountsOverride={visibleSourceCounts}
476+
totalTools={visibleTools.length}
415477
hasMoreTools={hasMoreTools}
416478
loadingMoreTools={loadingMoreTools}
417479
onLoadMoreTools={loadMoreTools}
@@ -470,7 +532,7 @@ export function ToolsView({
470532

471533
<TabsContent value="policies" className="mt-4">
472534
<PoliciesPanel
473-
tools={tools}
535+
tools={visibleTools}
474536
loadingTools={loadingTools}
475537
/>
476538
</TabsContent>
@@ -485,7 +547,7 @@ export function ToolsView({
485547
sources={sourceItems}
486548
credentials={credentialItems}
487549
sourceAuthProfiles={sourceAuthProfiles}
488-
loadingSourceNames={loadingSources}
550+
loadingSourceNames={visibleLoadingSources}
489551
/>
490552
</div>
491553
);

‎executor/apps/web/src/hooks/use/workspace-tools.ts‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,13 @@ interface UseWorkspaceToolsOptions {
4444

4545
type ToolDetailDescriptor = Pick<ToolDescriptor, "path" | "description" | "display" | "typing">;
4646

47+
type GetToolDetailsMutation = (args: {
48+
workspaceId: Id<"workspaces">;
49+
sessionId?: string;
50+
clientId?: string;
51+
toolPaths: string[];
52+
}) => Promise<Record<string, ToolDetailDescriptor>>;
53+
4754
type ListToolsWithWarningsAction = (args: {
4855
workspaceId: Id<"workspaces">;
4956
accountId?: string;
@@ -71,10 +78,11 @@ export function useWorkspaceTools(
7178
context: WorkspaceContext | null,
7279
options: UseWorkspaceToolsOptions = {},
7380
) {
74-
const includeDetails = options.includeDetails ?? true;
81+
const includeDetails = options.includeDetails ?? false;
7582
const listToolsWithWarningsRaw = useAction(convexApi.executorNode.listToolsWithWarnings);
7683
const listToolsWithWarnings = listToolsWithWarningsRaw as ListToolsWithWarningsAction;
77-
const listToolDetails = useMutation(convexApi.workspace.getToolDetails);
84+
const listToolDetailsRaw = useMutation(convexApi.workspace.getToolDetails);
85+
const listToolDetails = listToolDetailsRaw as GetToolDetailsMutation;
7886
const detailsCacheRef = useRef<Map<string, ToolDetailDescriptor>>(new Map());
7987

8088
// Watch inventory progress reactively so we invalidate when generation state changes.

‎executor/packages/core/src/declaration-generation.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ export function generateToolInventory(tools: ToolDescriptor[]): string {
2929
"You have access to these tool namespaces:",
3030
` ${namespaces.join(", ")}`,
3131
"",
32-
"Prefer one broad lookup over many small ones: use `tools.catalog.namespaces({})` and `tools.catalog.tools({ namespace?, query?, compact: false, depth: 2, limit: 20 })` first.",
33-
"Then use `tools.discover({ query, depth?, limit?, compact? })` when you need ranking. It returns `{ bestPath, results, total }` (not an array).",
32+
"Prefer one broad lookup over many small ones: use `tools.catalog.namespaces({})` and `tools.catalog.tools({ namespace?, query?, depth: 2, limit: 20 })` first.",
33+
"Then use `tools.discover({ query, depth?, limit? })` when you need ranking. It returns `{ bestPath, results, total }` (not an array).",
3434
"Prefer `bestPath` when present; each result includes `{ path, input, output }` to help you form a correct call.",
3535
"For migration/ETL tasks: discover once, then execute in small batches and return compact summaries (counts, IDs, top-N samples).",
3636
"Never shadow the global `tools` object (do NOT write `const tools = ...`).",

‎executor/packages/core/src/mcp/server-formatting.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export function buildRunCodeDescription(tools?: ToolDescriptor[]): string {
5555
? `\n\nTop-level tool keys: ${topLevelKeys.join(", ")}`
5656
: "";
5757
const hasGraphqlTools = toolList.some((tool) => tool.path.endsWith(".graphql"));
58-
const discoverNote = "\n\nTooling tip: avoid repeated tiny discovery calls. Start with a single broad inventory pass via `tools.catalog.namespaces({})` and `tools.catalog.tools({ namespace?, query?, compact: true, depth: 1, limit: 20 })`, then do at most one focused `tools.discover({ query, compact: true, depth: 1, limit: 12 })`. If you still need full signatures for a final pick, rerun only that shortlist with `compact: false`. `discover` returns `{ bestPath, results, schemas?, total }`; prefer `bestPath` when present, otherwise copy `results[i].exampleCall`. Do not assign to `const tools = ...`; use a different variable name (e.g. `const discovered = ...`).";
58+
const discoverNote = "\n\nTooling tip: avoid repeated tiny discovery calls. Start with a single broad inventory pass via `tools.catalog.namespaces({})` and `tools.catalog.tools({ namespace?, query?, depth: 1, limit: 20 })`, then do at most one focused `tools.discover({ query, depth: 1, limit: 12 })`. `discover` returns `{ bestPath, results, schemas?, total }`; prefer `bestPath` when present, otherwise copy `results[i].exampleCall`. Do not assign to `const tools = ...`; use a different variable name (e.g. `const discovered = ...`).";
5959
const executionNote = "\n\nExecution tip: for migration/ETL-style tasks, discover once, then run in small batches and `return` compact summaries (counts, IDs, and top-N samples) instead of full objects.";
6060
const graphqlNote = hasGraphqlTools
6161
? "\n\nGraphQL tip: prefer `source.query.*` / `source.mutation.*` helper paths when available; GraphQL tools return `{ data, errors }`."

‎executor/packages/core/src/openapi/real-specs.test.ts‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,4 +266,39 @@ describe("real-world OpenAPI specs", () => {
266266
},
267267
300_000,
268268
);
269+
270+
test(
271+
"openai: create batch hints stay non-lossy in inventory mode",
272+
async () => {
273+
const openAiUrl = "https://app.stainless.com/api/spec/documented/openai/openapi.documented.yml";
274+
const prepared = await prepareOpenApiSpec(openAiUrl, "openai", {
275+
includeDts: false,
276+
profile: "inventory",
277+
});
278+
279+
const tools = buildOpenApiToolsFromPrepared(
280+
{
281+
type: "openapi",
282+
name: "openai",
283+
spec: openAiUrl,
284+
baseUrl: prepared.servers[0] || "https://api.openai.com",
285+
},
286+
prepared,
287+
);
288+
289+
const tool = tools.find((t) => t.path === "openai.batch.create_batch");
290+
expect(tool).toBeDefined();
291+
292+
const inputHint = tool!.typing?.inputHint ?? "";
293+
const outputHint = tool!.typing?.outputHint ?? "";
294+
295+
expect(inputHint).toContain("input_file_id");
296+
expect(inputHint).toContain("output_expires_after");
297+
expect(outputHint).toContain("errors?: {");
298+
expect(outputHint).toContain("message?: string");
299+
expect(inputHint.includes("...")).toBe(false);
300+
expect(outputHint.includes("...")).toBe(false);
301+
},
302+
300_000,
303+
);
269304
});

‎executor/packages/core/src/openapi/schema-hints.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -954,7 +954,7 @@ export function jsonSchemaTypeHintFallback(
954954
}
955955
return "Record<string, unknown>";
956956
}
957-
const maxInlineProps = 12;
957+
const maxInlineProps = 64;
958958
const isTruncated = propEntries.length > maxInlineProps;
959959
const inner = propEntries
960960
.slice(0, maxInlineProps)

0 commit comments

Comments
 (0)