@stigmer/react 0.5.0 → 0.5.1
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/composer/ContextChip.d.ts +7 -2
- package/composer/ContextChip.d.ts.map +1 -1
- package/composer/ContextChip.js +2 -1
- package/composer/ContextChip.js.map +1 -1
- package/composer/SessionComposer.d.ts +11 -0
- package/composer/SessionComposer.d.ts.map +1 -1
- package/composer/SessionComposer.js +33 -4
- package/composer/SessionComposer.js.map +1 -1
- package/environment/usePersonalEnvironment.d.ts.map +1 -1
- package/environment/usePersonalEnvironment.js +1 -0
- package/environment/usePersonalEnvironment.js.map +1 -1
- package/index.d.ts +2 -2
- package/index.d.ts.map +1 -1
- package/index.js +1 -1
- package/index.js.map +1 -1
- package/inline-edit/InlineEditKeyValue.d.ts +5 -1
- package/inline-edit/InlineEditKeyValue.d.ts.map +1 -1
- package/inline-edit/InlineEditKeyValue.js +3 -3
- package/inline-edit/InlineEditKeyValue.js.map +1 -1
- package/internal/useFetch.js +2 -2
- package/internal/useFetch.js.map +1 -1
- package/mcp-server/McpServerDetailView.d.ts.map +1 -1
- package/mcp-server/McpServerDetailView.js +145 -46
- package/mcp-server/McpServerDetailView.js.map +1 -1
- package/models/ModelRegistryContext.d.ts +2 -0
- package/models/ModelRegistryContext.d.ts.map +1 -1
- package/models/ModelRegistryContext.js +1 -0
- package/models/ModelRegistryContext.js.map +1 -1
- package/models/ModelSelector.d.ts.map +1 -1
- package/models/ModelSelector.js +2 -2
- package/models/ModelSelector.js.map +1 -1
- package/models/__tests__/useModelRegistry.test.js +4 -3
- package/models/__tests__/useModelRegistry.test.js.map +1 -1
- package/models/useModelRegistry.d.ts +2 -0
- package/models/useModelRegistry.d.ts.map +1 -1
- package/models/useModelRegistry.js +3 -2
- package/models/useModelRegistry.js.map +1 -1
- package/package.json +4 -4
- package/provider.d.ts.map +1 -1
- package/provider.js +69 -22
- package/provider.js.map +1 -1
- package/session/__tests__/session-spec-converters.test.d.ts +2 -0
- package/session/__tests__/session-spec-converters.test.d.ts.map +1 -0
- package/session/__tests__/session-spec-converters.test.js +162 -0
- package/session/__tests__/session-spec-converters.test.js.map +1 -0
- package/session/__tests__/useNewSessionFlow.test.js +2 -2
- package/session/__tests__/useNewSessionFlow.test.js.map +1 -1
- package/session/__tests__/usePersistedModel.test.js +1 -1
- package/session/__tests__/usePersistedModel.test.js.map +1 -1
- package/session/group-sessions.d.ts +17 -0
- package/session/group-sessions.d.ts.map +1 -1
- package/session/group-sessions.js +46 -0
- package/session/group-sessions.js.map +1 -1
- package/session/index.d.ts +4 -2
- package/session/index.d.ts.map +1 -1
- package/session/index.js +2 -1
- package/session/index.js.map +1 -1
- package/session/session-spec-converters.d.ts +24 -0
- package/session/session-spec-converters.d.ts.map +1 -0
- package/session/session-spec-converters.js +72 -0
- package/session/session-spec-converters.js.map +1 -0
- package/session/useSessionConversation.d.ts.map +1 -1
- package/session/useSessionConversation.js +1 -56
- package/session/useSessionConversation.js.map +1 -1
- package/session/useSessionPageFlow.d.ts +5 -0
- package/session/useSessionPageFlow.d.ts.map +1 -1
- package/session/useSessionPageFlow.js +20 -6
- package/session/useSessionPageFlow.js.map +1 -1
- package/session/useSessionSearch.d.ts +57 -0
- package/session/useSessionSearch.d.ts.map +1 -0
- package/session/useSessionSearch.js +94 -0
- package/session/useSessionSearch.js.map +1 -0
- package/src/composer/ContextChip.tsx +20 -11
- package/src/composer/SessionComposer.tsx +52 -3
- package/src/environment/usePersonalEnvironment.ts +1 -0
- package/src/index.ts +5 -0
- package/src/inline-edit/InlineEditKeyValue.tsx +23 -0
- package/src/internal/useFetch.ts +2 -2
- package/src/mcp-server/McpServerDetailView.tsx +429 -55
- package/src/models/ModelRegistryContext.ts +3 -0
- package/src/models/ModelSelector.tsx +25 -2
- package/src/models/__tests__/useModelRegistry.test.tsx +5 -3
- package/src/models/useModelRegistry.ts +5 -2
- package/src/provider.tsx +69 -18
- package/src/session/__tests__/session-spec-converters.test.ts +185 -0
- package/src/session/__tests__/useNewSessionFlow.test.tsx +2 -2
- package/src/session/__tests__/usePersistedModel.test.tsx +1 -1
- package/src/session/group-sessions.ts +65 -0
- package/src/session/index.ts +8 -2
- package/src/session/session-spec-converters.ts +86 -0
- package/src/session/useSessionConversation.ts +5 -64
- package/src/session/useSessionPageFlow.ts +28 -7
- package/src/session/useSessionSearch.ts +149 -0
- package/styles.css +1 -1
|
@@ -253,20 +253,16 @@ export function McpServerDetailView({
|
|
|
253
253
|
credentials.refetch();
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
-
setShowCredentialForm(false);
|
|
257
|
-
|
|
258
256
|
if (mcpServer?.metadata?.id) {
|
|
259
257
|
const envKeys = Object.keys(mcpServer.spec?.env ?? {});
|
|
260
258
|
const connectOrg = activeOrg ?? org;
|
|
261
|
-
|
|
262
|
-
await connection.connect(mcpServer.metadata.id, connectOrg, undefined, envKeys);
|
|
263
|
-
} else {
|
|
264
|
-
await connection.connect(mcpServer.metadata.id, connectOrg, values, envKeys);
|
|
265
|
-
}
|
|
259
|
+
await connection.connect(mcpServer.metadata.id, connectOrg, values, envKeys);
|
|
266
260
|
refetch();
|
|
267
261
|
}
|
|
262
|
+
|
|
263
|
+
setShowCredentialForm(false);
|
|
268
264
|
} catch {
|
|
269
|
-
// error state is managed by the hooks
|
|
265
|
+
// error state is managed by the hooks — form stays open for retry
|
|
270
266
|
}
|
|
271
267
|
},
|
|
272
268
|
[credentials, mcpServer, connection, refetch],
|
|
@@ -443,8 +439,6 @@ export function McpServerDetailView({
|
|
|
443
439
|
onConnect={handleConnectClick}
|
|
444
440
|
onClearConnectionError={combinedClearError}
|
|
445
441
|
hasDiscoveredTools={hasDiscoveredTools}
|
|
446
|
-
toolCount={tools.length}
|
|
447
|
-
policyCount={totalPolicyCount}
|
|
448
442
|
credentialsLoading={credentials.isLoading}
|
|
449
443
|
oauthPhase={oauth.phase}
|
|
450
444
|
authMode={credentials.authMode}
|
|
@@ -635,8 +629,6 @@ function ConnectBar({
|
|
|
635
629
|
onConnect,
|
|
636
630
|
onClearConnectionError,
|
|
637
631
|
hasDiscoveredTools,
|
|
638
|
-
toolCount,
|
|
639
|
-
policyCount,
|
|
640
632
|
credentialsLoading,
|
|
641
633
|
oauthPhase,
|
|
642
634
|
authMode,
|
|
@@ -670,8 +662,6 @@ function ConnectBar({
|
|
|
670
662
|
readonly onConnect: () => void;
|
|
671
663
|
readonly onClearConnectionError: () => void;
|
|
672
664
|
readonly hasDiscoveredTools: boolean;
|
|
673
|
-
readonly toolCount: number;
|
|
674
|
-
readonly policyCount: number;
|
|
675
665
|
readonly credentialsLoading: boolean;
|
|
676
666
|
readonly oauthPhase: OAuthConnectPhase;
|
|
677
667
|
readonly authMode: "manual" | "oauth";
|
|
@@ -743,7 +733,7 @@ function ConnectBar({
|
|
|
743
733
|
}
|
|
744
734
|
if (isOrgOAuthApp && showOAuthPrimary) return "Using your OAuth app";
|
|
745
735
|
if (manualOverride) return "Entering token manually";
|
|
746
|
-
if (hasDiscoveredTools) return
|
|
736
|
+
if (hasDiscoveredTools) return "Connected";
|
|
747
737
|
return "Not connected yet";
|
|
748
738
|
})();
|
|
749
739
|
|
|
@@ -1086,13 +1076,6 @@ function oauthPhaseLabel(phase: OAuthConnectPhase): string {
|
|
|
1086
1076
|
}
|
|
1087
1077
|
}
|
|
1088
1078
|
|
|
1089
|
-
function formatConnectionSummary(toolCount: number, policyCount: number): string {
|
|
1090
|
-
const toolLabel = `${toolCount} tool${toolCount !== 1 ? "s" : ""}`;
|
|
1091
|
-
if (policyCount === 0) return toolLabel;
|
|
1092
|
-
const policyLabel = `${policyCount} ${policyCount !== 1 ? "policies" : "policy"}`;
|
|
1093
|
-
return `${toolLabel}, ${policyLabel}`;
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
1079
|
function formatTokenExpiry(expiresAtSeconds: bigint): string | null {
|
|
1097
1080
|
if (expiresAtSeconds === BigInt(0)) return null;
|
|
1098
1081
|
const nowSeconds = BigInt(Math.floor(Date.now() / 1000));
|
|
@@ -1289,6 +1272,20 @@ function ServerConfigSection({
|
|
|
1289
1272
|
value: import("@stigmer/sdk").McpServerInput[K],
|
|
1290
1273
|
) => Promise<boolean>;
|
|
1291
1274
|
}) {
|
|
1275
|
+
const [headersEditing, setHeadersEditing] = useState(false);
|
|
1276
|
+
const [queryParamsEditing, setQueryParamsEditing] = useState(false);
|
|
1277
|
+
|
|
1278
|
+
const currentHttpConfig = useMemo(() => {
|
|
1279
|
+
if (serverType?.case !== "http") return null;
|
|
1280
|
+
const v = serverType.value;
|
|
1281
|
+
return {
|
|
1282
|
+
url: v.url,
|
|
1283
|
+
headers: v.headers && Object.keys(v.headers).length > 0 ? { ...v.headers } : undefined,
|
|
1284
|
+
queryParams: v.queryParams && Object.keys(v.queryParams).length > 0 ? { ...v.queryParams } : undefined,
|
|
1285
|
+
timeoutSeconds: v.timeoutSeconds || undefined,
|
|
1286
|
+
};
|
|
1287
|
+
}, [serverType]);
|
|
1288
|
+
|
|
1292
1289
|
const handleTransportChange = useCallback(
|
|
1293
1290
|
async (newType: string) => {
|
|
1294
1291
|
if (!saveMcpField) return false;
|
|
@@ -1304,6 +1301,46 @@ function ServerConfigSection({
|
|
|
1304
1301
|
[saveMcpField],
|
|
1305
1302
|
);
|
|
1306
1303
|
|
|
1304
|
+
const headerRows: KeyValueRow[] = useMemo(() => {
|
|
1305
|
+
if (!currentHttpConfig?.headers) return [];
|
|
1306
|
+
return Object.entries(currentHttpConfig.headers).map(([key, value]) => ({ key, value }));
|
|
1307
|
+
}, [currentHttpConfig?.headers]);
|
|
1308
|
+
|
|
1309
|
+
const queryParamRows: KeyValueRow[] = useMemo(() => {
|
|
1310
|
+
if (!currentHttpConfig?.queryParams) return [];
|
|
1311
|
+
return Object.entries(currentHttpConfig.queryParams).map(([key, value]) => ({ key, value }));
|
|
1312
|
+
}, [currentHttpConfig?.queryParams]);
|
|
1313
|
+
|
|
1314
|
+
const handleHeadersSave = useCallback(
|
|
1315
|
+
async (rows: KeyValueRow[]) => {
|
|
1316
|
+
if (!saveMcpField || !currentHttpConfig) return false;
|
|
1317
|
+
const headers: Record<string, string> = {};
|
|
1318
|
+
for (const row of rows) {
|
|
1319
|
+
if (row.key.trim()) headers[row.key.trim()] = row.value;
|
|
1320
|
+
}
|
|
1321
|
+
return saveMcpField("http", {
|
|
1322
|
+
...currentHttpConfig,
|
|
1323
|
+
headers: Object.keys(headers).length > 0 ? headers : undefined,
|
|
1324
|
+
});
|
|
1325
|
+
},
|
|
1326
|
+
[saveMcpField, currentHttpConfig],
|
|
1327
|
+
);
|
|
1328
|
+
|
|
1329
|
+
const handleQueryParamsSave = useCallback(
|
|
1330
|
+
async (rows: KeyValueRow[]) => {
|
|
1331
|
+
if (!saveMcpField || !currentHttpConfig) return false;
|
|
1332
|
+
const queryParams: Record<string, string> = {};
|
|
1333
|
+
for (const row of rows) {
|
|
1334
|
+
if (row.key.trim()) queryParams[row.key.trim()] = row.value;
|
|
1335
|
+
}
|
|
1336
|
+
return saveMcpField("http", {
|
|
1337
|
+
...currentHttpConfig,
|
|
1338
|
+
queryParams: Object.keys(queryParams).length > 0 ? queryParams : undefined,
|
|
1339
|
+
});
|
|
1340
|
+
},
|
|
1341
|
+
[saveMcpField, currentHttpConfig],
|
|
1342
|
+
);
|
|
1343
|
+
|
|
1307
1344
|
return (
|
|
1308
1345
|
<Section title="Server Configuration">
|
|
1309
1346
|
<div className="flex flex-col gap-2 p-3">
|
|
@@ -1390,10 +1427,7 @@ function ServerConfigSection({
|
|
|
1390
1427
|
<InlineEditText
|
|
1391
1428
|
value={serverType.value.url}
|
|
1392
1429
|
onSave={async (v) =>
|
|
1393
|
-
saveMcpField("http", {
|
|
1394
|
-
url: v,
|
|
1395
|
-
timeoutSeconds: serverType.value.timeoutSeconds || undefined,
|
|
1396
|
-
})
|
|
1430
|
+
saveMcpField("http", { ...currentHttpConfig!, url: v })
|
|
1397
1431
|
}
|
|
1398
1432
|
isSaving={isSaving}
|
|
1399
1433
|
placeholder="https://example.com/mcp"
|
|
@@ -1414,7 +1448,7 @@ function ServerConfigSection({
|
|
|
1414
1448
|
value={serverType.value.timeoutSeconds > 0 ? String(serverType.value.timeoutSeconds) : ""}
|
|
1415
1449
|
onSave={async (v) =>
|
|
1416
1450
|
saveMcpField("http", {
|
|
1417
|
-
|
|
1451
|
+
...currentHttpConfig!,
|
|
1418
1452
|
timeoutSeconds: v ? Number(v) : undefined,
|
|
1419
1453
|
})
|
|
1420
1454
|
}
|
|
@@ -1435,10 +1469,144 @@ function ServerConfigSection({
|
|
|
1435
1469
|
</>
|
|
1436
1470
|
)}
|
|
1437
1471
|
</div>
|
|
1472
|
+
|
|
1473
|
+
{serverType?.case === "http" && (editable || headerRows.length > 0) && (
|
|
1474
|
+
<HttpKeyValueSubsection
|
|
1475
|
+
title="Headers"
|
|
1476
|
+
count={headerRows.length}
|
|
1477
|
+
rows={headerRows}
|
|
1478
|
+
editable={editable}
|
|
1479
|
+
isSaving={isSaving}
|
|
1480
|
+
editing={headersEditing}
|
|
1481
|
+
onEditingChange={setHeadersEditing}
|
|
1482
|
+
onSave={handleHeadersSave}
|
|
1483
|
+
keyLabel="Header name"
|
|
1484
|
+
/>
|
|
1485
|
+
)}
|
|
1486
|
+
|
|
1487
|
+
{serverType?.case === "http" && (editable || queryParamRows.length > 0) && (
|
|
1488
|
+
<HttpKeyValueSubsection
|
|
1489
|
+
title="Query Parameters"
|
|
1490
|
+
count={queryParamRows.length}
|
|
1491
|
+
rows={queryParamRows}
|
|
1492
|
+
editable={editable}
|
|
1493
|
+
isSaving={isSaving}
|
|
1494
|
+
editing={queryParamsEditing}
|
|
1495
|
+
onEditingChange={setQueryParamsEditing}
|
|
1496
|
+
onSave={handleQueryParamsSave}
|
|
1497
|
+
keyLabel="Parameter name"
|
|
1498
|
+
/>
|
|
1499
|
+
)}
|
|
1438
1500
|
</Section>
|
|
1439
1501
|
);
|
|
1440
1502
|
}
|
|
1441
1503
|
|
|
1504
|
+
/** Renders a key-value subsection (headers or query params) within ServerConfigSection. */
|
|
1505
|
+
function HttpKeyValueSubsection({
|
|
1506
|
+
title,
|
|
1507
|
+
count,
|
|
1508
|
+
rows,
|
|
1509
|
+
editable,
|
|
1510
|
+
isSaving,
|
|
1511
|
+
editing,
|
|
1512
|
+
onEditingChange,
|
|
1513
|
+
onSave,
|
|
1514
|
+
keyLabel,
|
|
1515
|
+
}: {
|
|
1516
|
+
readonly title: string;
|
|
1517
|
+
readonly count: number;
|
|
1518
|
+
readonly rows: readonly KeyValueRow[];
|
|
1519
|
+
readonly editable?: boolean;
|
|
1520
|
+
readonly isSaving?: boolean;
|
|
1521
|
+
readonly editing: boolean;
|
|
1522
|
+
readonly onEditingChange: (v: boolean) => void;
|
|
1523
|
+
readonly onSave: (rows: KeyValueRow[]) => Promise<boolean>;
|
|
1524
|
+
readonly keyLabel: string;
|
|
1525
|
+
}) {
|
|
1526
|
+
return (
|
|
1527
|
+
<div className="border-t border-border">
|
|
1528
|
+
<div className="flex items-center justify-between px-3 py-2">
|
|
1529
|
+
<div className="flex items-center gap-1.5">
|
|
1530
|
+
<span className="text-xs font-medium text-muted-foreground">{title}</span>
|
|
1531
|
+
{count > 0 && (
|
|
1532
|
+
<span className="inline-flex min-w-[1.25rem] items-center justify-center rounded-full bg-muted px-1 py-px text-[10px] font-medium leading-none text-muted-foreground">
|
|
1533
|
+
{count}
|
|
1534
|
+
</span>
|
|
1535
|
+
)}
|
|
1536
|
+
</div>
|
|
1537
|
+
{editable && (
|
|
1538
|
+
<button
|
|
1539
|
+
type="button"
|
|
1540
|
+
onClick={() => onEditingChange(!editing)}
|
|
1541
|
+
className="text-[11px] text-muted-foreground underline decoration-muted-foreground/40 underline-offset-2 hover:text-foreground hover:decoration-foreground"
|
|
1542
|
+
>
|
|
1543
|
+
{editing ? "Done" : "Edit"}
|
|
1544
|
+
</button>
|
|
1545
|
+
)}
|
|
1546
|
+
</div>
|
|
1547
|
+
{editable && editing ? (
|
|
1548
|
+
<InlineEditKeyValue
|
|
1549
|
+
value={[...rows]}
|
|
1550
|
+
onSave={onSave}
|
|
1551
|
+
isSaving={isSaving}
|
|
1552
|
+
editing={editing}
|
|
1553
|
+
onEditingChange={onEditingChange}
|
|
1554
|
+
keyLabel={keyLabel}
|
|
1555
|
+
showValue
|
|
1556
|
+
valueLabel="Value"
|
|
1557
|
+
/>
|
|
1558
|
+
) : (
|
|
1559
|
+
<div className="flex flex-col divide-y divide-border">
|
|
1560
|
+
{rows.map((row) => (
|
|
1561
|
+
<div key={row.key} className="flex items-start gap-2 px-3 py-1.5">
|
|
1562
|
+
<code className="shrink-0 font-mono text-xs font-medium text-foreground">
|
|
1563
|
+
{row.key}
|
|
1564
|
+
</code>
|
|
1565
|
+
<span className="min-w-0 break-all font-mono text-xs text-muted-foreground">
|
|
1566
|
+
{renderHeaderValue(row.value)}
|
|
1567
|
+
</span>
|
|
1568
|
+
</div>
|
|
1569
|
+
))}
|
|
1570
|
+
</div>
|
|
1571
|
+
)}
|
|
1572
|
+
</div>
|
|
1573
|
+
);
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
const ENV_VAR_PLACEHOLDER = /\$\{([^}]+)\}/g;
|
|
1577
|
+
|
|
1578
|
+
/** Renders a header value, highlighting ${VAR} placeholders with a variable badge. */
|
|
1579
|
+
function renderHeaderValue(value: string): React.ReactNode {
|
|
1580
|
+
if (!ENV_VAR_PLACEHOLDER.test(value)) return value;
|
|
1581
|
+
|
|
1582
|
+
ENV_VAR_PLACEHOLDER.lastIndex = 0;
|
|
1583
|
+
const parts: React.ReactNode[] = [];
|
|
1584
|
+
let lastIndex = 0;
|
|
1585
|
+
let match: RegExpExecArray | null;
|
|
1586
|
+
|
|
1587
|
+
while ((match = ENV_VAR_PLACEHOLDER.exec(value)) !== null) {
|
|
1588
|
+
if (match.index > lastIndex) {
|
|
1589
|
+
parts.push(value.slice(lastIndex, match.index));
|
|
1590
|
+
}
|
|
1591
|
+
parts.push(
|
|
1592
|
+
<span
|
|
1593
|
+
key={match.index}
|
|
1594
|
+
className="inline-flex items-center gap-0.5 rounded bg-primary-subtle px-1 py-px text-[10px] font-medium text-primary"
|
|
1595
|
+
title={`Resolved from environment variable: ${match[1]}`}
|
|
1596
|
+
>
|
|
1597
|
+
{match[0]}
|
|
1598
|
+
</span>,
|
|
1599
|
+
);
|
|
1600
|
+
lastIndex = match.index + match[0].length;
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
if (lastIndex < value.length) {
|
|
1604
|
+
parts.push(value.slice(lastIndex));
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
return <>{parts}</>;
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1442
1610
|
function SourceSection({
|
|
1443
1611
|
spec,
|
|
1444
1612
|
}: {
|
|
@@ -1669,6 +1837,19 @@ function ToolsTabContent({
|
|
|
1669
1837
|
}: {
|
|
1670
1838
|
readonly tools: readonly DiscoveredTool[];
|
|
1671
1839
|
}) {
|
|
1840
|
+
const [search, setSearch] = useState("");
|
|
1841
|
+
const [expandedTool, setExpandedTool] = useState<string | null>(null);
|
|
1842
|
+
|
|
1843
|
+
const filtered = useMemo(() => {
|
|
1844
|
+
if (!search.trim()) return tools;
|
|
1845
|
+
const q = search.toLowerCase();
|
|
1846
|
+
return tools.filter(
|
|
1847
|
+
(t) =>
|
|
1848
|
+
t.name.toLowerCase().includes(q) ||
|
|
1849
|
+
t.description?.toLowerCase().includes(q),
|
|
1850
|
+
);
|
|
1851
|
+
}, [tools, search]);
|
|
1852
|
+
|
|
1672
1853
|
if (tools.length === 0) {
|
|
1673
1854
|
return (
|
|
1674
1855
|
<div className="px-3 py-8 text-center">
|
|
@@ -1680,20 +1861,97 @@ function ToolsTabContent({
|
|
|
1680
1861
|
);
|
|
1681
1862
|
}
|
|
1682
1863
|
|
|
1864
|
+
const isFiltered = search.trim().length > 0;
|
|
1865
|
+
|
|
1683
1866
|
return (
|
|
1684
|
-
<div className="flex flex-col
|
|
1685
|
-
|
|
1686
|
-
<div
|
|
1687
|
-
<
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1867
|
+
<div className="flex flex-col">
|
|
1868
|
+
<div className="flex items-center gap-2 px-3 pb-2">
|
|
1869
|
+
<div className="relative flex-1">
|
|
1870
|
+
<SearchIcon className="pointer-events-none absolute left-2 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground" />
|
|
1871
|
+
<input
|
|
1872
|
+
type="text"
|
|
1873
|
+
value={search}
|
|
1874
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
1875
|
+
placeholder="Search tools…"
|
|
1876
|
+
aria-label="Search tools"
|
|
1877
|
+
className="w-full rounded-md border border-border bg-background py-1.5 pl-7 pr-7 text-xs text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
|
1878
|
+
/>
|
|
1879
|
+
{isFiltered && (
|
|
1880
|
+
<button
|
|
1881
|
+
type="button"
|
|
1882
|
+
onClick={() => setSearch("")}
|
|
1883
|
+
aria-label="Clear search"
|
|
1884
|
+
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
|
1885
|
+
>
|
|
1886
|
+
<CloseIcon className="size-3" />
|
|
1887
|
+
</button>
|
|
1694
1888
|
)}
|
|
1695
1889
|
</div>
|
|
1696
|
-
|
|
1890
|
+
<span className="shrink-0 text-[10px] text-muted-foreground">
|
|
1891
|
+
{isFiltered ? `${filtered.length} of ${tools.length}` : tools.length}
|
|
1892
|
+
</span>
|
|
1893
|
+
</div>
|
|
1894
|
+
|
|
1895
|
+
{filtered.length === 0 ? (
|
|
1896
|
+
<div className="px-3 py-6 text-center">
|
|
1897
|
+
<p className="text-xs text-muted-foreground">
|
|
1898
|
+
No tools matching “{search}”
|
|
1899
|
+
</p>
|
|
1900
|
+
</div>
|
|
1901
|
+
) : (
|
|
1902
|
+
<div className="max-h-96 overflow-y-auto">
|
|
1903
|
+
<div className="flex flex-col divide-y divide-border">
|
|
1904
|
+
{filtered.map((tool) => {
|
|
1905
|
+
const isExpanded = expandedTool === tool.name;
|
|
1906
|
+
const hasSchema =
|
|
1907
|
+
tool.inputSchema != null &&
|
|
1908
|
+
Object.keys(tool.inputSchema).length > 0;
|
|
1909
|
+
|
|
1910
|
+
return (
|
|
1911
|
+
<div key={tool.name}>
|
|
1912
|
+
<button
|
|
1913
|
+
type="button"
|
|
1914
|
+
onClick={() =>
|
|
1915
|
+
setExpandedTool(isExpanded ? null : tool.name)
|
|
1916
|
+
}
|
|
1917
|
+
className="flex w-full items-start gap-2 px-3 py-2.5 text-left transition-colors hover:bg-muted-faint"
|
|
1918
|
+
aria-expanded={isExpanded}
|
|
1919
|
+
>
|
|
1920
|
+
<ChevronIcon
|
|
1921
|
+
className={cn(
|
|
1922
|
+
"mt-0.5 size-3 shrink-0 text-muted-foreground transition-transform",
|
|
1923
|
+
isExpanded && "rotate-90",
|
|
1924
|
+
)}
|
|
1925
|
+
/>
|
|
1926
|
+
<div className="min-w-0 flex-1">
|
|
1927
|
+
<code className="font-mono text-sm font-medium text-foreground">
|
|
1928
|
+
{tool.name}
|
|
1929
|
+
</code>
|
|
1930
|
+
{tool.description && (
|
|
1931
|
+
<p className="mt-0.5 text-xs text-muted-foreground">
|
|
1932
|
+
{tool.description}
|
|
1933
|
+
</p>
|
|
1934
|
+
)}
|
|
1935
|
+
</div>
|
|
1936
|
+
{hasSchema && (
|
|
1937
|
+
<span className="mt-0.5 shrink-0 rounded bg-muted px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground">
|
|
1938
|
+
schema
|
|
1939
|
+
</span>
|
|
1940
|
+
)}
|
|
1941
|
+
</button>
|
|
1942
|
+
{isExpanded && hasSchema && (
|
|
1943
|
+
<div className="border-t border-border bg-muted-faint px-3 py-2">
|
|
1944
|
+
<pre className="max-h-64 overflow-auto whitespace-pre-wrap break-words rounded border border-border bg-background p-2 font-mono text-[11px] text-foreground">
|
|
1945
|
+
{JSON.stringify(tool.inputSchema, null, 2)}
|
|
1946
|
+
</pre>
|
|
1947
|
+
</div>
|
|
1948
|
+
)}
|
|
1949
|
+
</div>
|
|
1950
|
+
);
|
|
1951
|
+
})}
|
|
1952
|
+
</div>
|
|
1953
|
+
</div>
|
|
1954
|
+
)}
|
|
1697
1955
|
</div>
|
|
1698
1956
|
);
|
|
1699
1957
|
}
|
|
@@ -1707,9 +1965,33 @@ function PoliciesTabContent({
|
|
|
1707
1965
|
readonly classifiedPolicies: readonly ToolApprovalPolicy[];
|
|
1708
1966
|
readonly hasDiscoveredTools: boolean;
|
|
1709
1967
|
}) {
|
|
1710
|
-
const
|
|
1711
|
-
|
|
1712
|
-
const
|
|
1968
|
+
const [search, setSearch] = useState("");
|
|
1969
|
+
|
|
1970
|
+
const totalCount = pinnedPolicies.length + classifiedPolicies.length;
|
|
1971
|
+
const hasAnyPolicies = totalCount > 0;
|
|
1972
|
+
|
|
1973
|
+
const filteredPinned = useMemo(() => {
|
|
1974
|
+
if (!search.trim()) return pinnedPolicies;
|
|
1975
|
+
const q = search.toLowerCase();
|
|
1976
|
+
return pinnedPolicies.filter(
|
|
1977
|
+
(p) =>
|
|
1978
|
+
p.toolName.toLowerCase().includes(q) ||
|
|
1979
|
+
p.message?.toLowerCase().includes(q),
|
|
1980
|
+
);
|
|
1981
|
+
}, [pinnedPolicies, search]);
|
|
1982
|
+
|
|
1983
|
+
const filteredClassified = useMemo(() => {
|
|
1984
|
+
if (!search.trim()) return classifiedPolicies;
|
|
1985
|
+
const q = search.toLowerCase();
|
|
1986
|
+
return classifiedPolicies.filter(
|
|
1987
|
+
(p) =>
|
|
1988
|
+
p.toolName.toLowerCase().includes(q) ||
|
|
1989
|
+
p.message?.toLowerCase().includes(q),
|
|
1990
|
+
);
|
|
1991
|
+
}, [classifiedPolicies, search]);
|
|
1992
|
+
|
|
1993
|
+
const filteredTotal = filteredPinned.length + filteredClassified.length;
|
|
1994
|
+
const isFiltered = search.trim().length > 0;
|
|
1713
1995
|
|
|
1714
1996
|
if (!hasAnyPolicies) {
|
|
1715
1997
|
return (
|
|
@@ -1726,19 +2008,56 @@ function PoliciesTabContent({
|
|
|
1726
2008
|
|
|
1727
2009
|
return (
|
|
1728
2010
|
<div className="flex flex-col">
|
|
1729
|
-
|
|
1730
|
-
<
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
2011
|
+
<div className="flex items-center gap-2 px-3 pb-2">
|
|
2012
|
+
<div className="relative flex-1">
|
|
2013
|
+
<SearchIcon className="pointer-events-none absolute left-2 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground" />
|
|
2014
|
+
<input
|
|
2015
|
+
type="text"
|
|
2016
|
+
value={search}
|
|
2017
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
2018
|
+
placeholder="Search policies…"
|
|
2019
|
+
aria-label="Search policies"
|
|
2020
|
+
className="w-full rounded-md border border-border bg-background py-1.5 pl-7 pr-7 text-xs text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
|
2021
|
+
/>
|
|
2022
|
+
{isFiltered && (
|
|
2023
|
+
<button
|
|
2024
|
+
type="button"
|
|
2025
|
+
onClick={() => setSearch("")}
|
|
2026
|
+
aria-label="Clear search"
|
|
2027
|
+
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
|
2028
|
+
>
|
|
2029
|
+
<CloseIcon className="size-3" />
|
|
2030
|
+
</button>
|
|
2031
|
+
)}
|
|
2032
|
+
</div>
|
|
2033
|
+
<span className="shrink-0 text-[10px] text-muted-foreground">
|
|
2034
|
+
{isFiltered ? `${filteredTotal} of ${totalCount}` : totalCount}
|
|
2035
|
+
</span>
|
|
2036
|
+
</div>
|
|
2037
|
+
|
|
2038
|
+
{filteredTotal === 0 ? (
|
|
2039
|
+
<div className="px-3 py-6 text-center">
|
|
2040
|
+
<p className="text-xs text-muted-foreground">
|
|
2041
|
+
No policies matching “{search}”
|
|
2042
|
+
</p>
|
|
2043
|
+
</div>
|
|
2044
|
+
) : (
|
|
2045
|
+
<div className="max-h-96 overflow-y-auto">
|
|
2046
|
+
{filteredPinned.length > 0 && (
|
|
2047
|
+
<PolicyGroup
|
|
2048
|
+
icon={<PinIcon className="size-3.5" />}
|
|
2049
|
+
label="Pinned"
|
|
2050
|
+
policies={filteredPinned}
|
|
2051
|
+
/>
|
|
2052
|
+
)}
|
|
2053
|
+
{filteredClassified.length > 0 && (
|
|
2054
|
+
<PolicyGroup
|
|
2055
|
+
icon={<SparklesIcon className="size-3.5" />}
|
|
2056
|
+
label="Auto-classified"
|
|
2057
|
+
policies={filteredClassified}
|
|
2058
|
+
/>
|
|
2059
|
+
)}
|
|
2060
|
+
</div>
|
|
1742
2061
|
)}
|
|
1743
2062
|
</div>
|
|
1744
2063
|
);
|
|
@@ -1771,7 +2090,10 @@ function PolicyGroup({
|
|
|
1771
2090
|
<code className="font-mono text-sm font-medium text-foreground">
|
|
1772
2091
|
{policy.toolName}
|
|
1773
2092
|
</code>
|
|
1774
|
-
<
|
|
2093
|
+
<span className="inline-flex items-center gap-1 rounded bg-amber-500/10 px-1.5 py-0.5 text-[10px] font-medium text-amber-600 dark:text-amber-400">
|
|
2094
|
+
<ShieldIcon className="size-2.5" />
|
|
2095
|
+
requires approval
|
|
2096
|
+
</span>
|
|
1775
2097
|
</div>
|
|
1776
2098
|
{policy.message && (
|
|
1777
2099
|
<p className="mt-0.5 text-xs text-muted-foreground">
|
|
@@ -2055,6 +2377,58 @@ function ExternalLinkIcon({ className }: { readonly className?: string }) {
|
|
|
2055
2377
|
);
|
|
2056
2378
|
}
|
|
2057
2379
|
|
|
2380
|
+
function SearchIcon({ className }: { readonly className?: string }) {
|
|
2381
|
+
return (
|
|
2382
|
+
<svg
|
|
2383
|
+
className={className}
|
|
2384
|
+
viewBox="0 0 16 16"
|
|
2385
|
+
fill="none"
|
|
2386
|
+
stroke="currentColor"
|
|
2387
|
+
strokeWidth="1.5"
|
|
2388
|
+
strokeLinecap="round"
|
|
2389
|
+
strokeLinejoin="round"
|
|
2390
|
+
aria-hidden="true"
|
|
2391
|
+
>
|
|
2392
|
+
<circle cx="7" cy="7" r="4.5" />
|
|
2393
|
+
<path d="m10.5 10.5 3 3" />
|
|
2394
|
+
</svg>
|
|
2395
|
+
);
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
function CloseIcon({ className }: { readonly className?: string }) {
|
|
2399
|
+
return (
|
|
2400
|
+
<svg
|
|
2401
|
+
className={className}
|
|
2402
|
+
viewBox="0 0 16 16"
|
|
2403
|
+
fill="none"
|
|
2404
|
+
stroke="currentColor"
|
|
2405
|
+
strokeWidth="2"
|
|
2406
|
+
strokeLinecap="round"
|
|
2407
|
+
strokeLinejoin="round"
|
|
2408
|
+
aria-hidden="true"
|
|
2409
|
+
>
|
|
2410
|
+
<path d="m4 4 8 8M12 4l-8 8" />
|
|
2411
|
+
</svg>
|
|
2412
|
+
);
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
function ChevronIcon({ className }: { readonly className?: string }) {
|
|
2416
|
+
return (
|
|
2417
|
+
<svg
|
|
2418
|
+
className={className}
|
|
2419
|
+
viewBox="0 0 16 16"
|
|
2420
|
+
fill="none"
|
|
2421
|
+
stroke="currentColor"
|
|
2422
|
+
strokeWidth="2"
|
|
2423
|
+
strokeLinecap="round"
|
|
2424
|
+
strokeLinejoin="round"
|
|
2425
|
+
aria-hidden="true"
|
|
2426
|
+
>
|
|
2427
|
+
<path d="m6 4 4 4-4 4" />
|
|
2428
|
+
</svg>
|
|
2429
|
+
);
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2058
2432
|
function Spinner() {
|
|
2059
2433
|
return (
|
|
2060
2434
|
<svg
|
|
@@ -8,6 +8,8 @@ export interface ModelRegistryState {
|
|
|
8
8
|
readonly models: readonly ModelInfo[];
|
|
9
9
|
readonly isLoading: boolean;
|
|
10
10
|
readonly error: Error | null;
|
|
11
|
+
/** Retry fetching the model registry. No-op while a fetch is in flight. */
|
|
12
|
+
readonly refetch: () => void;
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
/**
|
|
@@ -21,6 +23,7 @@ export const ModelRegistryContext = createContext<ModelRegistryState>({
|
|
|
21
23
|
models: [],
|
|
22
24
|
isLoading: true,
|
|
23
25
|
error: null,
|
|
26
|
+
refetch: () => {},
|
|
24
27
|
});
|
|
25
28
|
|
|
26
29
|
/**
|
|
@@ -119,7 +119,7 @@ export function ModelSelector({
|
|
|
119
119
|
}
|
|
120
120
|
}, [initialHarness, isHarnessLocked]);
|
|
121
121
|
|
|
122
|
-
const { models, featured, defaultModel, getModel, byProvider } = useModelRegistry(
|
|
122
|
+
const { models, featured, defaultModel, getModel, byProvider, isLoading, error, refetch } = useModelRegistry(
|
|
123
123
|
{ harness: activeHarness },
|
|
124
124
|
);
|
|
125
125
|
|
|
@@ -393,7 +393,30 @@ export function ModelSelector({
|
|
|
393
393
|
aria-label="Available models"
|
|
394
394
|
className="max-h-72 overflow-y-auto p-1"
|
|
395
395
|
>
|
|
396
|
-
{visibleModels.length === 0 && (
|
|
396
|
+
{visibleModels.length === 0 && isLoading && (
|
|
397
|
+
<div className="flex items-center justify-center gap-2 px-2 py-3">
|
|
398
|
+
<div className="size-3 animate-spin rounded-full border border-muted border-t-primary" />
|
|
399
|
+
<span className="text-xs text-muted-foreground">Loading models…</span>
|
|
400
|
+
</div>
|
|
401
|
+
)}
|
|
402
|
+
|
|
403
|
+
{visibleModels.length === 0 && !isLoading && error != null && (
|
|
404
|
+
<div className="flex flex-col items-center gap-1.5 px-2 py-3">
|
|
405
|
+
<span className="text-xs text-muted-foreground">Failed to load models</span>
|
|
406
|
+
<button
|
|
407
|
+
type="button"
|
|
408
|
+
className={cn(
|
|
409
|
+
"rounded-md border border-border bg-background px-2.5 py-1 text-xs text-foreground",
|
|
410
|
+
"hover:bg-accent-hover transition-colors cursor-pointer",
|
|
411
|
+
)}
|
|
412
|
+
onClick={refetch}
|
|
413
|
+
>
|
|
414
|
+
Retry
|
|
415
|
+
</button>
|
|
416
|
+
</div>
|
|
417
|
+
)}
|
|
418
|
+
|
|
419
|
+
{visibleModels.length === 0 && !isLoading && error == null && (
|
|
397
420
|
<div className="px-2 py-3 text-center text-xs text-muted-foreground">
|
|
398
421
|
No models found
|
|
399
422
|
</div>
|