Policy editor now lists the toolkit's actual tools (grouped integration -> connection -> tool) via the shared ToolTree, with a per-tool approve/require_approval/block menu, instead of free-text glob inputs. Setting an action stores a 3-segment integration.connection.tool pattern (matching the toolkit runtime matcher); ToolTree's 4-segment display form is bridged in/out. Integration icons across the page (cards, Access list, preview rail) now use the canonical IntegrationFavicon + integrationPresetIconUrl/integrationInferredUrl + useIntegrationPlugins pattern, so real favicons render instead of the generic box (the old sourceId-only call resolved only for the executor built-in).
Unified · Split
| @@ -16,9 +16,10 @@ import { useEffect, useMemo, useState } from "react"; · 15 unmodified lines | ||
| 16 | 16 | import { useAtomSet, useAtomValue } from "@effect/atom-react"; |
| 17 | 17 | import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"; |
| 18 | 18 | import * as Exit from "effect/Exit"; |
| 19 | −import { usePluginNavigate, usePluginRoute } from "@executor-js/sdk/client"; | |
| 19 | +import { useIntegrationPlugins, usePluginNavigate, usePluginRoute } from "@executor-js/sdk/client"; | |
| 20 | +import { effectivePolicyFromSorted, PolicyId } from "@executor-js/sdk/shared"; | |
| 20 | 21 | |
| 21 | −import { connectionsAllAtom, integrationsAtom } from "@executor-js/react/api/atoms"; | |
| 22 | +import { connectionsAllAtom, integrationsAtom, toolsAllAtom } from "@executor-js/react/api/atoms"; | |
| 22 | 23 | import { Button } from "@executor-js/react/components/button"; |
| 23 | 24 | import { Input } from "@executor-js/react/components/input"; |
| 24 | 25 | import { Textarea } from "@executor-js/react/components/textarea"; |
| @@ -26,13 +27,12 @@ import { Switch } from "@executor-js/react/components/switch"; · 1 unmodified line | ||
| 26 | 27 | import { Label } from "@executor-js/react/components/label"; |
| 27 | 28 | import { Badge } from "@executor-js/react/components/badge"; |
| 28 | 29 | import { |
| 29 | − Select, | |
| 30 | − SelectContent, | |
| 31 | − SelectItem, | |
| 32 | − SelectTrigger, | |
| 33 | − SelectValue, | |
| 34 | −} from "@executor-js/react/components/select"; | |
| 35 | −import { IntegrationFavicon } from "@executor-js/react/components/integration-favicon"; | |
| 30 | + IntegrationFavicon, | |
| 31 | + integrationInferredUrl, | |
| 32 | + integrationPresetIconUrl, | |
| 33 | +} from "@executor-js/react/components/integration-favicon"; | |
| 34 | +import { ToolTree, type ToolSummary } from "@executor-js/react/components/tool-tree"; | |
| 35 | +import { toPolicyPattern } from "@executor-js/react/lib/policy-pattern"; | |
| 36 | 36 | import { copyToClipboard } from "@executor-js/react/lib/clipboard"; |
| 37 | 37 | import { cn } from "@executor-js/react/lib/utils"; |
| 38 | 38 | |
| @@ -65,23 +65,72 @@ import { · 26 unmodified lines | ||
| 65 | 65 | // read, so this file doesn't pin a specific API success-type name. |
| 66 | 66 | // --------------------------------------------------------------------------- |
| 67 | 67 | |
| 68 | −interface Integ { | |
| 68 | +interface IntegrationInfo { | |
| 69 | 69 | readonly slug: string; |
| 70 | 70 | readonly name: string; |
| 71 | + readonly kind: string; | |
| 72 | + readonly displayUrl?: string; | |
| 71 | 73 | } |
| 72 | 74 | |
| 75 | +type ToolRow = { | |
| 76 | + readonly integration: string; | |
| 77 | + readonly connection: string; | |
| 78 | + readonly name: string; | |
| 79 | + readonly description: string; | |
| 80 | + readonly requiresApproval?: boolean; | |
| 81 | +}; | |
| 82 | + | |
| 73 | 83 | const ACCESS_OPTIONS: ReadonlyArray<{ value: ToolkitAccess; label: string }> = [ |
| 74 | 84 | { value: "off", label: "Off" }, |
| 75 | 85 | { value: "read", label: "Read" }, |
| 76 | 86 | { value: "full", label: "Full" }, |
| 77 | 87 | ]; |
| 78 | 88 | |
| 79 | −const ACTION_LABEL: Record<ToolkitPolicyAction, string> = { | |
| 80 | − approve: "Auto-approve", | |
| 81 | − require_approval: "Needs approval", | |
| 82 | − block: "Block", | |
| 89 | +/** Toolkit rules match `integration.connection.tool` — undo ToolTree's 4-segment bridge. */ | |
| 90 | +const fromToolTreePattern = (pattern: string): string => { | |
| 91 | + const match = pattern.match(/^([^.]+)\.\*\.\*\.(.+)$/); | |
| 92 | + if (match) return `${match[1]}.${match[2]}`; | |
| 93 | + return pattern; | |
| 83 | 94 | }; |
| 84 | 95 | |
| 96 | +const sortToolkitPolicies = ( | |
| 97 | + policies: ReadonlyArray<ToolkitPolicy>, | |
| 98 | +): ReadonlyArray<ToolkitPolicy> => | |
| 99 | + [...policies].sort((a, b) => { | |
| 100 | + const aWild = a.pattern.endsWith("*"); | |
| 101 | + const bWild = b.pattern.endsWith("*"); | |
| 102 | + if (aWild !== bWild) return aWild ? 1 : -1; | |
| 103 | + return b.pattern.length - a.pattern.length; | |
| 104 | + }); | |
| 105 | + | |
| 106 | +function IntegrationIcon(props: { | |
| 107 | + readonly slug: string; | |
| 108 | + readonly size: number; | |
| 109 | + readonly integration: IntegrationInfo | undefined; | |
| 110 | + readonly integrationPlugins: ReturnType<typeof useIntegrationPlugins>; | |
| 111 | +}) { | |
| 112 | + const name = props.integration?.name ?? props.slug; | |
| 113 | + return ( | |
| 114 | + <IntegrationFavicon | |
| 115 | + icon={integrationPresetIconUrl( | |
| 116 | + { | |
| 117 | + id: props.slug, | |
| 118 | + kind: props.integration?.kind ?? "", | |
| 119 | + name, | |
| 120 | + url: props.integration?.displayUrl, | |
| 121 | + }, | |
| 122 | + props.integrationPlugins, | |
| 123 | + )} | |
| 124 | + url={ | |
| 125 | + props.integration?.displayUrl ?? | |
| 126 | + integrationInferredUrl({ id: props.slug, name }) ?? | |
| 127 | + undefined | |
| 128 | + } | |
| 129 | + size={props.size} | |
| 130 | + /> | |
| 131 | + ); | |
| 132 | +} | |
| 133 | + | |
| 85 | 134 | // --------------------------------------------------------------------------- |
| 86 | 135 | // Access segmented control (off / read / full) |
| 87 | 136 | // --------------------------------------------------------------------------- |
| @@ -112,6 +161,8 @@ function AccessSeg(props: { value: ToolkitAccess; onChange: (v: ToolkitAccess) = · 24 unmodified lines | ||
| 112 | 161 | function ToolkitCard(props: { |
| 113 | 162 | toolkit: ToolkitView; |
| 114 | 163 | integrationName: (slug: string) => string; |
| 164 | + integrationBySlug: ReadonlyMap<string, IntegrationInfo>; | |
| 165 | + integrationPlugins: ReturnType<typeof useIntegrationPlugins>; | |
| 115 | 166 | onOpen: () => void; |
| 116 | 167 | }) { |
| 117 | 168 | const active = props.toolkit.connections.filter((c) => c.access !== "off"); |
| @@ -138,7 +189,12 @@ function ToolkitCard(props: { · 20 unmodified lines | ||
| 138 | 189 | key={slug} |
| 139 | 190 | className="inline-flex items-center gap-1.5 rounded-md bg-muted px-2 py-0.5 text-[12px]" |
| 140 | 191 | > |
| 141 | − <IntegrationFavicon sourceId={slug} size={14} /> | |
| 192 | + <IntegrationIcon | |
| 193 | + slug={slug} | |
| 194 | + size={14} | |
| 195 | + integration={props.integrationBySlug.get(slug)} | |
| 196 | + integrationPlugins={props.integrationPlugins} | |
| 197 | + /> | |
| 142 | 198 | {props.integrationName(slug)} |
| 143 | 199 | </span> |
| 144 | 200 | )) |
| @@ -156,6 +212,8 @@ function ListSection(props: { · 11 unmodified lines | ||
| 156 | 212 | empty: string; |
| 157 | 213 | toolkits: ReadonlyArray<ToolkitView>; |
| 158 | 214 | integrationName: (slug: string) => string; |
| 215 | + integrationBySlug: ReadonlyMap<string, IntegrationInfo>; | |
| 216 | + integrationPlugins: ReturnType<typeof useIntegrationPlugins>; | |
| 159 | 217 | onOpen: (id: string) => void; |
| 160 | 218 | onCreate: () => void; |
| 161 | 219 | }) { |
| @@ -178,6 +236,8 @@ function ListSection(props: { · 16 unmodified lines | ||
| 178 | 236 | key={tk.id} |
| 179 | 237 | toolkit={tk} |
| 180 | 238 | integrationName={props.integrationName} |
| 239 | + integrationBySlug={props.integrationBySlug} | |
| 240 | + integrationPlugins={props.integrationPlugins} | |
| 181 | 241 | onOpen={() => props.onOpen(tk.id)} |
| 182 | 242 | /> |
| 183 | 243 | ))} |
| @@ -197,6 +257,8 @@ interface EditorProps { · 13 unmodified lines | ||
| 197 | 257 | readonly takenSlugs: ReadonlySet<string>; |
| 198 | 258 | readonly connections: ReadonlyArray<Conn>; |
| 199 | 259 | readonly integrationName: (slug: string) => string; |
| 260 | + readonly integrationBySlug: ReadonlyMap<string, IntegrationInfo>; | |
| 261 | + readonly integrationPlugins: ReturnType<typeof useIntegrationPlugins>; | |
| 200 | 262 | readonly onBack: () => void; |
| 201 | 263 | readonly onCreated: () => void; |
| 202 | 264 | readonly onRemoved: () => void; |
| @@ -243,15 +305,86 @@ function ToolkitEditor(props: EditorProps) { · 40 unmodified lines | ||
| 243 | 305 | setNotes((m) => ({ ...m, [key]: v })); |
| 244 | 306 | mark(); |
| 245 | 307 | }; |
| 246 | − const setPolicy = (i: number, patch: Partial<ToolkitPolicy>) => { | |
| 247 | − setPolicies((ps) => ps.map((p, j) => (j === i ? { ...p, ...patch } : p))); | |
| 308 | + const upsertPolicy = (pattern: string, action: ToolkitPolicyAction) => { | |
| 309 | + const toolkitPattern = fromToolTreePattern(pattern); | |
| 310 | + setPolicies((ps) => { | |
| 311 | + const i = ps.findIndex((p) => p.pattern === toolkitPattern); | |
| 312 | + if (i >= 0) return ps.map((p, j) => (j === i ? { ...p, action } : p)); | |
| 313 | + return [...ps, { pattern: toolkitPattern, action }]; | |
| 314 | + }); | |
| 315 | + mark(); | |
| 316 | + }; | |
| 317 | + const removePolicy = (pattern: string) => { | |
| 318 | + const toolkitPattern = fromToolTreePattern(pattern); | |
| 319 | + setPolicies((ps) => ps.filter((p) => p.pattern !== toolkitPattern)); | |
| 248 | 320 | mark(); |
| 249 | 321 | }; |
| 250 | 322 | |
| 323 | + const toolsResult = useAtomValue(toolsAllAtom); | |
| 324 | + | |
| 251 | 325 | // The connections this toolkit may draw on: workspace = org-owned only; |
| 252 | 326 | // personal = org + the caller's own. |
| 253 | 327 | const tiers = useMemo(() => tiersForScope(props.connections, scope), [props.connections, scope]); |
| 254 | 328 | |
| 329 | + const activeScopes = useMemo(() => { | |
| 330 | + const pinned = new Map<string, Set<string>>(); | |
| 331 | + const wildcards = new Set<string>(); | |
| 332 | + for (const [key, a] of Object.entries(access)) { | |
| 333 | + if (a === "off") continue; | |
| 334 | + const space = key.indexOf(" "); | |
| 335 | + const integration = key.slice(0, space); | |
| 336 | + const connection = key.slice(space + 1); | |
| 337 | + if (connection === "*") { | |
| 338 | + wildcards.add(integration); | |
| 339 | + } else { | |
| 340 | + const conns = pinned.get(integration) ?? new Set<string>(); | |
| 341 | + conns.add(connection); | |
| 342 | + pinned.set(integration, conns); | |
| 343 | + } | |
| 344 | + } | |
| 345 | + return { pinned, wildcards }; | |
| 346 | + }, [access]); | |
| 347 | + | |
| 348 | + const hasActiveConnections = useMemo( | |
| 349 | + () => activeScopes.wildcards.size > 0 || activeScopes.pinned.size > 0, | |
| 350 | + [activeScopes], | |
| 351 | + ); | |
| 352 | + | |
| 353 | + const sortedPolicies = useMemo(() => sortToolkitPolicies(policies), [policies]); | |
| 354 | + const treePolicies = useMemo( | |
| 355 | + () => | |
| 356 | + sortedPolicies.map((p) => ({ | |
| 357 | + pattern: toPolicyPattern(p.pattern), | |
| 358 | + action: p.action, | |
| 359 | + })), | |
| 360 | + [sortedPolicies], | |
| 361 | + ); | |
| 362 | + | |
| 363 | + const toolSummaries: ReadonlyArray<ToolSummary> = useMemo(() => { | |
| 364 | + if (!AsyncResult.isSuccess(toolsResult)) return []; | |
| 365 | + const matchesTool = (t: ToolRow) => { | |
| 366 | + if (activeScopes.wildcards.has(t.integration)) return true; | |
| 367 | + const conns = activeScopes.pinned.get(t.integration); | |
| 368 | + return conns?.has(t.connection) ?? false; | |
| 369 | + }; | |
| 370 | + const rows: ToolSummary[] = []; | |
| 371 | + for (const tool of toolsResult.value as readonly ToolRow[]) { | |
| 372 | + if (!matchesTool(tool)) continue; | |
| 373 | + const id = `${tool.integration}.${tool.connection}.${tool.name}`; | |
| 374 | + rows.push({ | |
| 375 | + id, | |
| 376 | + name: id, | |
| 377 | + description: tool.description, | |
| 378 | + policy: effectivePolicyFromSorted( | |
| 379 | + id, | |
| 380 | + sortedPolicies.map((p) => ({ ...p, id: PolicyId.make(p.pattern) })), | |
| 381 | + tool.requiresApproval, | |
| 382 | + ), | |
| 383 | + }); | |
| 384 | + } | |
| 385 | + return rows; | |
| 386 | + }, [toolsResult, activeScopes, sortedPolicies]); | |
| 387 | + | |
| 255 | 388 | const buildEntries = () => entriesFromAccess(props.connections, scope, access, notes); |
| 256 | 389 | |
| 257 | 390 | const cleanPolicies = (): ReadonlyArray<ToolkitPolicy> => |
| @@ -414,7 +547,12 @@ function ToolkitEditor(props: EditorProps) { · 156 unmodified lines | ||
| 414 | 547 | groupByIntegration(tier.conns).map(([intSlug, conns]) => ( |
| 415 | 548 | <div key={intSlug} className="px-4 pb-2 pt-1.5"> |
| 416 | 549 | <div className="flex items-center gap-2 pb-1"> |
| 417 | − <IntegrationFavicon sourceId={intSlug} size={16} /> | |
| 550 | + <IntegrationIcon | |
| 551 | + slug={intSlug} | |
| 552 | + size={16} | |
| 553 | + integration={props.integrationBySlug.get(intSlug)} | |
| 554 | + integrationPlugins={props.integrationPlugins} | |
| 555 | + /> | |
| 418 | 556 | <span className="text-[12.5px] font-semibold text-foreground"> |
| 419 | 557 | {props.integrationName(intSlug)} |
| 420 | 558 | </span> |
| @@ -491,70 +629,33 @@ function ToolkitEditor(props: EditorProps) { · 70 unmodified lines | ||
| 491 | 629 | : "Workspace guardrails are off — this toolkit is governed only by its own rules below."} |
| 492 | 630 | </p> |
| 493 | 631 | </div> |
| 494 | − <div className="border-t border-border px-4 py-2.5"> | |
| 495 | − <div className="flex items-center gap-2 pb-1"> | |
| 496 | − <span className="text-[12.5px] font-semibold text-foreground">Toolkit rules</span> | |
| 497 | − <span className="ml-auto text-[11px] text-muted-foreground"> | |
| 498 | − {policies.length} {policies.length === 1 ? "rule" : "rules"} | |
| 499 | − </span> | |
| 500 | − </div> | |
| 501 | − {policies.map((p, i) => ( | |
| 502 | − <div key={i} className="flex flex-wrap items-center gap-2 py-1.5"> | |
| 503 | − <Input | |
| 504 | − value={p.pattern} | |
| 505 | − onChange={(e) => | |
| 506 | − setPolicy(i, { | |
| 507 | − pattern: (e.target as HTMLInputElement).value, | |
| 508 | − }) | |
| 509 | − } | |
| 510 | − placeholder="slack.*.post_message" | |
| 511 | − className="h-8 min-w-[190px] flex-1 font-mono text-[12px]" | |
| 512 | − /> | |
| 513 | − <Select | |
| 514 | − value={p.action} | |
| 515 | − onValueChange={(v) => setPolicy(i, { action: v as ToolkitPolicyAction })} | |
| 516 | − > | |
| 517 | − <SelectTrigger className="h-8 w-[150px] text-[12px]"> | |
| 518 | − <SelectValue /> | |
| 519 | − </SelectTrigger> | |
| 520 | − <SelectContent> | |
| 521 | − <SelectItem value="approve">{ACTION_LABEL.approve}</SelectItem> | |
| 522 | − <SelectItem value="require_approval"> | |
| 523 | − {ACTION_LABEL.require_approval} | |
| 524 | − </SelectItem> | |
| 525 | − <SelectItem value="block">{ACTION_LABEL.block}</SelectItem> | |
| 526 | − </SelectContent> | |
| 527 | − </Select> | |
| 528 | − <Button | |
| 529 | − type="button" | |
| 530 | − size="sm" | |
| 531 | − variant="ghost" | |
| 532 | − className="h-8 px-2 text-destructive/70 hover:text-destructive" | |
| 533 | − onClick={() => { | |
| 534 | − setPolicies((ps) => ps.filter((_, j) => j !== i)); | |
| 535 | − mark(); | |
| 536 | − }} | |
| 537 | − > | |
| 538 | − ✕ | |
| 539 | − </Button> | |
| 540 | − </div> | |
| 541 | − ))} | |
| 542 | − <div className="flex items-center gap-3 pt-1"> | |
| 543 | − <Button | |
| 544 | − type="button" | |
| 545 | − size="sm" | |
| 546 | − variant="ghost" | |
| 547 | − onClick={() => { | |
| 548 | − setPolicies((ps) => [...ps, { pattern: "", action: "approve" }]); | |
| 549 | − mark(); | |
| 550 | − }} | |
| 551 | − > | |
| 552 | − + Add rule | |
| 553 | − </Button> | |
| 554 | − <span className="text-[11px] text-muted-foreground"> | |
| 555 | − Block hides matching tools entirely · approve / needs-approval set gating. | |
| 556 | − </span> | |
| 557 | − </div> | |
| 632 | + <div className="border-t border-border"> | |
| 633 | + {!hasActiveConnections ? ( | |
| 634 | + <p className="px-4 py-3 text-[12px] text-muted-foreground"> | |
| 635 | + Add a connection above to set tool policies. | |
| 636 | + </p> | |
| 637 | + ) : ( | |
| 638 | + AsyncResult.match(toolsResult, { | |
| 639 | + onInitial: () => ( | |
| 640 | + <p className="px-4 py-3 text-[12px] text-muted-foreground">Loading tools…</p> | |
| 641 | + ), | |
| 642 | + onFailure: () => ( | |
| 643 | + <p className="px-4 py-3 text-[12px] text-destructive">Failed to load tools</p> | |
| 644 | + ), | |
| 645 | + onSuccess: () => ( | |
| 646 | + <div className="h-80 min-h-0"> | |
| 647 | + <ToolTree | |
| 648 | + tools={toolSummaries} | |
| 649 | + selectedToolId={null} | |
| 650 | + onSelect={() => {}} | |
| 651 | + policies={treePolicies} | |
| 652 | + onSetPolicy={upsertPolicy} | |
| 653 | + onClearPolicy={removePolicy} | |
| 654 | + /> | |
| 655 | + </div> | |
| 656 | + ), | |
| 657 | + }) | |
| 658 | + )} | |
| 558 | 659 | </div> |
| 559 | 660 | </div> |
| 560 | 661 | |
| @@ -629,7 +730,12 @@ function ToolkitEditor(props: EditorProps) { · 68 unmodified lines | ||
| 629 | 730 | {previewGroups.map(([intSlug, conns]) => ( |
| 630 | 731 | <div key={intSlug}> |
| 631 | 732 | <div className="flex items-center gap-2"> |
| 632 | − <IntegrationFavicon sourceId={intSlug} size={15} /> | |
| 733 | + <IntegrationIcon | |
| 734 | + slug={intSlug} | |
| 735 | + size={15} | |
| 736 | + integration={props.integrationBySlug.get(intSlug)} | |
| 737 | + integrationPlugins={props.integrationPlugins} | |
| 738 | + /> | |
| 633 | 739 | <span className="text-[12.5px] font-semibold text-foreground"> |
| 634 | 740 | {props.integrationName(intSlug)} |
| 635 | 741 | </span> |
| @@ -694,6 +800,7 @@ function viewFromSubpath(subpath: string): View { · 58 unmodified lines | ||
| 694 | 800 | export function ToolkitsPage() { |
| 695 | 801 | const { subpath } = usePluginRoute(); |
| 696 | 802 | const navigate = usePluginNavigate(); |
| 803 | + const integrationPlugins = useIntegrationPlugins(); | |
| 697 | 804 | const view = viewFromSubpath(subpath); |
| 698 | 805 | const toolkitsResult = useAtomValue(toolkitsAtom); |
| 699 | 806 | const connectionsResult = useAtomValue(connectionsAllAtom); |
| @@ -716,18 +823,22 @@ export function ToolkitsPage() { · 16 unmodified lines | ||
| 716 | 823 | }, |
| 717 | 824 | ); |
| 718 | 825 | const integrations = AsyncResult.match( |
| 719 | − integrationsResult as AsyncResult.AsyncResult<ReadonlyArray<Integ>, unknown>, | |
| 826 | + integrationsResult as AsyncResult.AsyncResult<ReadonlyArray<IntegrationInfo>, unknown>, | |
| 720 | 827 | { |
| 721 | − onInitial: () => [] as ReadonlyArray<Integ>, | |
| 722 | − onFailure: () => [] as ReadonlyArray<Integ>, | |
| 828 | + onInitial: () => [] as ReadonlyArray<IntegrationInfo>, | |
| 829 | + onFailure: () => [] as ReadonlyArray<IntegrationInfo>, | |
| 723 | 830 | onSuccess: ({ value }) => value, |
| 724 | 831 | }, |
| 725 | 832 | ); |
| 726 | 833 | |
| 834 | + const integrationBySlug = useMemo( | |
| 835 | + () => new Map(integrations.map((i) => [i.slug, i])), | |
| 836 | + [integrations], | |
| 837 | + ); | |
| 838 | + | |
| 727 | 839 | const integrationName = useMemo(() => { |
| 728 | − const bySlug = new Map(integrations.map((i) => [i.slug, i.name])); | |
| 729 | − return (slug: string) => bySlug.get(slug) ?? slug; | |
| 730 | − }, [integrations]); | |
| 840 | + return (slug: string) => integrationBySlug.get(slug)?.name ?? slug; | |
| 841 | + }, [integrationBySlug]); | |
| 731 | 842 | |
| 732 | 843 | const takenSlugs = useMemo(() => new Set((toolkits ?? []).map((t) => t.slug)), [toolkits]); |
| 733 | 844 | |
| @@ -750,6 +861,8 @@ export function ToolkitsPage() { · 16 unmodified lines | ||
| 750 | 861 | takenSlugs={takenSlugs} |
| 751 | 862 | connections={connections} |
| 752 | 863 | integrationName={integrationName} |
| 864 | + integrationBySlug={integrationBySlug} | |
| 865 | + integrationPlugins={integrationPlugins} | |
| 753 | 866 | onBack={() => navigate("")} |
| 754 | 867 | onCreated={() => navigate("")} |
| 755 | 868 | onRemoved={() => navigate("")} |
| @@ -777,6 +890,8 @@ export function ToolkitsPage() { · 21 unmodified lines | ||
| 777 | 890 | empty="No workspace toolkits yet — these draw on shared workspace connections." |
| 778 | 891 | toolkits={workspace} |
| 779 | 892 | integrationName={integrationName} |
| 893 | + integrationBySlug={integrationBySlug} | |
| 894 | + integrationPlugins={integrationPlugins} | |
| 780 | 895 | onOpen={(id) => navigate(id)} |
| 781 | 896 | onCreate={() => navigate("new/workspace")} |
| 782 | 897 | /> |
| @@ -785,6 +900,8 @@ export function ToolkitsPage() { · 2 unmodified lines | ||
| 785 | 900 | empty="No personal toolkits yet — these can use workspace and personal connections." |
| 786 | 901 | toolkits={personal} |
| 787 | 902 | integrationName={integrationName} |
| 903 | + integrationBySlug={integrationBySlug} | |
| 904 | + integrationPlugins={integrationPlugins} | |
| 788 | 905 | onOpen={(id) => navigate(id)} |
| 789 | 906 | onCreate={() => navigate("new/personal")} |
| 790 | 907 | /> |