@stigmer/react 3.4.1 → 3.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.
Files changed (99) hide show
  1. package/agent/AgentDetailView.d.ts +14 -4
  2. package/agent/AgentDetailView.d.ts.map +1 -1
  3. package/agent/AgentDetailView.js +73 -11
  4. package/agent/AgentDetailView.js.map +1 -1
  5. package/agent/internal/agentToInput.d.ts +3 -0
  6. package/agent/internal/agentToInput.d.ts.map +1 -1
  7. package/agent/internal/agentToInput.js +21 -1
  8. package/agent/internal/agentToInput.js.map +1 -1
  9. package/api-key/ApiKeyListPanel.d.ts +8 -1
  10. package/api-key/ApiKeyListPanel.d.ts.map +1 -1
  11. package/api-key/ApiKeyListPanel.js +9 -7
  12. package/api-key/ApiKeyListPanel.js.map +1 -1
  13. package/dependency-graph/DependencyTreeNode.d.ts.map +1 -1
  14. package/dependency-graph/DependencyTreeNode.js +8 -0
  15. package/dependency-graph/DependencyTreeNode.js.map +1 -1
  16. package/dependency-graph/types.d.ts +20 -8
  17. package/dependency-graph/types.d.ts.map +1 -1
  18. package/dependency-graph/useDependencyGraph.d.ts.map +1 -1
  19. package/dependency-graph/useDependencyGraph.js +21 -1
  20. package/dependency-graph/useDependencyGraph.js.map +1 -1
  21. package/index.d.ts +2 -0
  22. package/index.d.ts.map +1 -1
  23. package/index.js +4 -0
  24. package/index.js.map +1 -1
  25. package/internal/scroll-area.d.ts +4 -0
  26. package/internal/scroll-area.d.ts.map +1 -0
  27. package/internal/scroll-area.js +15 -0
  28. package/internal/scroll-area.js.map +1 -0
  29. package/internal/tooltip.d.ts +7 -0
  30. package/internal/tooltip.d.ts.map +1 -0
  31. package/internal/tooltip.js +31 -0
  32. package/internal/tooltip.js.map +1 -0
  33. package/mcp-server/StdioSandboxNotice.d.ts +12 -11
  34. package/mcp-server/StdioSandboxNotice.d.ts.map +1 -1
  35. package/mcp-server/StdioSandboxNotice.js +13 -12
  36. package/mcp-server/StdioSandboxNotice.js.map +1 -1
  37. package/mcp-server/steps/IdentityTransportStep.d.ts.map +1 -1
  38. package/mcp-server/steps/IdentityTransportStep.js +1 -1
  39. package/mcp-server/steps/IdentityTransportStep.js.map +1 -1
  40. package/package.json +4 -4
  41. package/resource-creation/templates/mcp-server-templates.d.ts.map +1 -1
  42. package/resource-creation/templates/mcp-server-templates.js +17 -14
  43. package/resource-creation/templates/mcp-server-templates.js.map +1 -1
  44. package/session/useSessionExecutions.d.ts +14 -0
  45. package/session/useSessionExecutions.d.ts.map +1 -1
  46. package/session/useSessionExecutions.js +27 -1
  47. package/session/useSessionExecutions.js.map +1 -1
  48. package/sidebar/SettingsSidebar.d.ts +62 -0
  49. package/sidebar/SettingsSidebar.d.ts.map +1 -0
  50. package/sidebar/SettingsSidebar.js +60 -0
  51. package/sidebar/SettingsSidebar.js.map +1 -0
  52. package/sidebar/WorkspaceSidebar.d.ts +87 -0
  53. package/sidebar/WorkspaceSidebar.d.ts.map +1 -0
  54. package/sidebar/WorkspaceSidebar.js +95 -0
  55. package/sidebar/WorkspaceSidebar.js.map +1 -0
  56. package/sidebar/chrome.d.ts +48 -0
  57. package/sidebar/chrome.d.ts.map +1 -0
  58. package/sidebar/chrome.js +32 -0
  59. package/sidebar/chrome.js.map +1 -0
  60. package/sidebar/index.d.ts +6 -0
  61. package/sidebar/index.d.ts.map +1 -0
  62. package/sidebar/index.js +9 -0
  63. package/sidebar/index.js.map +1 -0
  64. package/sidebar/types.d.ts +54 -0
  65. package/sidebar/types.d.ts.map +1 -0
  66. package/sidebar/types.js +2 -0
  67. package/sidebar/types.js.map +1 -0
  68. package/src/agent/AgentDetailView.tsx +198 -11
  69. package/src/agent/__tests__/AgentDetailView.datastores.test.tsx +166 -0
  70. package/src/agent/internal/__tests__/agentToInput.test.ts +154 -0
  71. package/src/agent/internal/agentToInput.ts +25 -7
  72. package/src/api-key/ApiKeyListPanel.tsx +17 -4
  73. package/src/dependency-graph/DependencyTreeNode.tsx +8 -0
  74. package/src/dependency-graph/__tests__/useDependencyGraph.test.ts +108 -0
  75. package/src/dependency-graph/types.ts +20 -8
  76. package/src/dependency-graph/useDependencyGraph.ts +23 -1
  77. package/src/index.ts +13 -0
  78. package/src/internal/scroll-area.tsx +37 -0
  79. package/src/internal/tooltip.tsx +66 -0
  80. package/src/mcp-server/StdioSandboxNotice.tsx +16 -14
  81. package/src/mcp-server/__tests__/StdioSandboxNotice.test.tsx +3 -2
  82. package/src/mcp-server/steps/IdentityTransportStep.tsx +7 -0
  83. package/src/resource-creation/templates/mcp-server-templates.ts +17 -14
  84. package/src/session/__tests__/useSessionExecutions.test.ts +63 -0
  85. package/src/session/useSessionExecutions.ts +27 -1
  86. package/src/sidebar/SettingsSidebar.tsx +173 -0
  87. package/src/sidebar/WorkspaceSidebar.tsx +386 -0
  88. package/src/sidebar/__tests__/SettingsSidebar.test.tsx +107 -0
  89. package/src/sidebar/__tests__/WorkspaceSidebar.test.tsx +236 -0
  90. package/src/sidebar/chrome.tsx +129 -0
  91. package/src/sidebar/index.ts +16 -0
  92. package/src/sidebar/types.ts +55 -0
  93. package/src/test/__tests__/samples.test.ts +2 -0
  94. package/src/test/samples.ts +48 -0
  95. package/styles.css +1 -1
  96. package/test/samples.d.ts +21 -0
  97. package/test/samples.d.ts.map +1 -1
  98. package/test/samples.js +32 -0
  99. package/test/samples.js.map +1 -1
@@ -11,9 +11,10 @@ type McpServerType = McpServerSpec["serverType"];
11
11
  /**
12
12
  * Whether an MCP server's transport is stdio while connected to Stigmer Cloud.
13
13
  *
14
- * stdio servers run as a subprocess inside an isolated Daytona sandbox in the
15
- * cloud not on the user's own computer. This predicate identifies exactly
16
- * that combination so the connect UI can set accurate expectations. HTTP
14
+ * stdio is local-runner-only: cloud-hosted sessions refuse stdio servers at
15
+ * execution create, and the cloud connect flow refuses to spawn them. This
16
+ * predicate identifies exactly that combination so the UI can explain the
17
+ * policy and its remediation (run the session on a local runner). HTTP
17
18
  * servers and any transport in local mode return `false`.
18
19
  *
19
20
  * Pure and framework-free so it can be unit-tested without rendering.
@@ -34,15 +35,15 @@ export interface StdioSandboxNoticeProps {
34
35
  }
35
36
 
36
37
  /**
37
- * Sets accurate expectations for connecting an **stdio** MCP server from
38
- * **Stigmer Cloud**.
38
+ * Explains the local-runner-only policy for an **stdio** MCP server viewed
39
+ * from **Stigmer Cloud**.
39
40
  *
40
- * stdio servers are a supported cloud capability Stigmer runs them in an
41
- * isolated Daytona sandbox. But that sandbox is not the user's own machine,
42
- * so a server that expects access to local files, applications, or a private
43
- * network (e.g. a filesystem server) won't behave as the user intends. This
44
- * notice explains that up front; Connect itself stays enabled because
45
- * self-contained servers work fine.
41
+ * stdio servers spawn subprocesses on the machine that runs the agent, so
42
+ * they run only on local runners Stigmer-managed cloud compute refuses
43
+ * them (at execution create, and in the connect flow). The server remains
44
+ * fully usable on sessions that execute on a local runner (desktop app,
45
+ * `stigmer server`), where tools are discovered automatically at session
46
+ * start. This notice states that up front so the refusal never surprises.
46
47
  *
47
48
  * Self-gating: reads the deployment mode from context and renders `null`
48
49
  * unless the server is stdio and the backend is cloud. Callers render it
@@ -68,9 +69,10 @@ export function StdioSandboxNotice({
68
69
  >
69
70
  <SandboxIcon className="mt-0.5 size-4 shrink-0" />
70
71
  <p className="text-xs leading-relaxed">
71
- This stdio server runs in an isolated cloud sandbox, not on your own
72
- computer. Tools that need access to your local files, applications, or
73
- private network won&apos;t be available here.
72
+ This stdio server runs only on local runners it won&apos;t be
73
+ available to sessions on Stigmer-managed cloud compute. Run the
74
+ session on a local runner (desktop app or CLI) to use it, or choose
75
+ a remote (HTTP) server instead.
74
76
  </p>
75
77
  </div>
76
78
  );
@@ -44,10 +44,11 @@ function withMode(mode: "cloud" | "local", children: ReactNode) {
44
44
  }
45
45
 
46
46
  describe("StdioSandboxNotice", () => {
47
- it("renders the sandbox-isolation caveat for stdio in cloud", () => {
47
+ it("renders the local-runner-only policy for stdio in cloud", () => {
48
48
  render(withMode("cloud", <StdioSandboxNotice serverType={STDIO} />));
49
49
  expect(screen.getByRole("status")).toBeTruthy();
50
- expect(screen.getByText(/isolated cloud sandbox/i)).toBeTruthy();
50
+ expect(screen.getByText(/only on local runners/i)).toBeTruthy();
51
+ expect(screen.getByText(/remote \(HTTP\) server/i)).toBeTruthy();
51
52
  });
52
53
 
53
54
  it("does not suggest the CLI (avoids inaccurate guidance)", () => {
@@ -223,6 +223,13 @@ export function IdentityTransportStep({
223
223
  onChange={() => updateData({ transportType: "stdio" })}
224
224
  />
225
225
  </div>
226
+ {data.transportType === "stdio" && (
227
+ <p className="text-xs text-muted-foreground">
228
+ Stdio servers run only on local runners (desktop app or CLI).
229
+ Sessions on Stigmer-managed cloud compute require a remote (HTTP)
230
+ server.
231
+ </p>
232
+ )}
226
233
 
227
234
  {/* HTTP fields */}
228
235
  {data.transportType === "http" && (
@@ -62,41 +62,44 @@ export const MCP_SERVER_TEMPLATES: readonly ResourceTemplate<McpServerWizardData
62
62
  },
63
63
  },
64
64
  {
65
- id: "postgresql",
66
- name: "PostgreSQL",
65
+ id: "neon",
66
+ name: "Neon",
67
67
  description:
68
- "Connect to a PostgreSQL database for querying, schema inspection, and data management.",
68
+ "Connect to Neon's hosted MCP endpoint for serverless PostgreSQL — querying, schema inspection, and branch management.",
69
69
  category: "integration",
70
- tags: ["postgres", "postgresql", "database", "sql", "data"],
70
+ tags: ["neon", "postgres", "postgresql", "database", "sql", "data"],
71
71
  data: {
72
- name: "PostgreSQL",
72
+ name: "Neon",
73
73
  description:
74
- "PostgreSQL database connection for querying, schema inspection, and data management.",
75
- transportType: "stdio",
76
- stdioCommand: "npx",
77
- stdioArgs: "-y @modelcontextprotocol/server-postgres",
74
+ "Neon serverless PostgreSQL querying, schema inspection, database provisioning, and branch management.",
75
+ transportType: "http",
76
+ httpUrl: "https://mcp.neon.tech/mcp",
78
77
  env: [
79
78
  {
80
- key: "DATABASE_URL",
79
+ key: "NEON_API_KEY",
81
80
  description:
82
- "PostgreSQL connection string (e.g. postgresql://user:pass@host:5432/db).",
81
+ "Neon API key (generate at console.neon.tech/app/settings/api-keys).",
83
82
  isSecret: true,
84
83
  optional: false,
85
84
  },
86
85
  ],
87
86
  },
88
87
  },
88
+ // Deliberately stdio: the canonical example of a tool that must run on
89
+ // the user's own machine. Stdio servers are local-runner-only — the
90
+ // wizard's transport step states this, and cloud-targeted sessions
91
+ // refuse them at execution create.
89
92
  {
90
93
  id: "filesystem",
91
- name: "Filesystem",
94
+ name: "Filesystem (local runners)",
92
95
  description:
93
- "Provide read and write access to a local directory for file management and content operations.",
96
+ "Provide read and write access to a local directory for file management and content operations. Runs on local runners only.",
94
97
  category: "general",
95
98
  tags: ["filesystem", "files", "local", "directory"],
96
99
  data: {
97
100
  name: "Filesystem",
98
101
  description:
99
- "Local filesystem access for reading, writing, and managing files within a directory.",
102
+ "Local filesystem access for reading, writing, and managing files within a directory. Stdio transport — runs on local runners only.",
100
103
  transportType: "stdio",
101
104
  stdioCommand: "npx",
102
105
  stdioArgs:
@@ -0,0 +1,63 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { create } from "@bufbuild/protobuf";
3
+ import {
4
+ AgentExecutionSchema,
5
+ type AgentExecution,
6
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
7
+ import { ApiResourceMetadataSchema } from "@stigmer/protos/ai/stigmer/commons/apiresource/metadata_pb";
8
+ import { sortChronologically } from "../useSessionExecutions";
9
+
10
+ function exec(id: string): AgentExecution {
11
+ const e = create(AgentExecutionSchema);
12
+ const metadata = create(ApiResourceMetadataSchema);
13
+ metadata.id = id;
14
+ e.metadata = metadata;
15
+ return e;
16
+ }
17
+
18
+ function ids(list: readonly AgentExecution[]): string[] {
19
+ return list.map((e) => e.metadata?.id ?? "");
20
+ }
21
+
22
+ describe("sortChronologically", () => {
23
+ it("orders executions oldest-first by their ULID id", () => {
24
+ // Real ids from the pilot session, returned by the server in scrambled
25
+ // heap order — the afternoon turns (…vk…) interleaved with the evening
26
+ // turns (…w3…/…w4…). Chronological order is ascending ULID.
27
+ const scrambled = [
28
+ exec("aex_01kyw3zeq8cre6cc8gznf31y13"),
29
+ exec("aex_01kyvk1hxyrhch1q23p5j177pb"),
30
+ exec("aex_01kyw416w9mg6ezzpkc6q9y71x"),
31
+ exec("aex_01kyvk2sh0vjc69c7z3q4c9r1m"),
32
+ exec("aex_01kyw45qkf88x28pf9bxmrr3bf"),
33
+ exec("aex_01kyw3y0rfx9vd7dhnawgqgnb0"),
34
+ exec("aex_01kyvk3v34vh9phedx2ezajxtr"),
35
+ ];
36
+
37
+ expect(ids(sortChronologically(scrambled))).toEqual([
38
+ "aex_01kyvk1hxyrhch1q23p5j177pb",
39
+ "aex_01kyvk2sh0vjc69c7z3q4c9r1m",
40
+ "aex_01kyvk3v34vh9phedx2ezajxtr",
41
+ "aex_01kyw3y0rfx9vd7dhnawgqgnb0",
42
+ "aex_01kyw3zeq8cre6cc8gznf31y13",
43
+ "aex_01kyw416w9mg6ezzpkc6q9y71x",
44
+ "aex_01kyw45qkf88x28pf9bxmrr3bf",
45
+ ]);
46
+ });
47
+
48
+ it("does not mutate the input array", () => {
49
+ const input = [exec("aex_02"), exec("aex_01")];
50
+ const before = ids(input);
51
+ sortChronologically(input);
52
+ expect(ids(input)).toEqual(before);
53
+ });
54
+
55
+ it("sorts entries missing an id last", () => {
56
+ const list = [exec("aex_02"), exec(""), exec("aex_01")];
57
+ expect(ids(sortChronologically(list))).toEqual(["aex_01", "aex_02", ""]);
58
+ });
59
+
60
+ it("returns an empty array unchanged", () => {
61
+ expect(sortChronologically([])).toEqual([]);
62
+ });
63
+ });
@@ -71,6 +71,32 @@ export interface UseSessionExecutionsReturn {
71
71
  * const { executions } = useSessionExecutions(sessionId ?? null);
72
72
  * ```
73
73
  */
74
+ /**
75
+ * Returns the executions in chronological (oldest-first) order — the order
76
+ * the conversation thread renders top-to-bottom.
77
+ *
78
+ * Defense-in-depth against an unordered list response: the executions ARE the
79
+ * transcript, so a scrambled order drops the newest turns out of view (they no
80
+ * longer sort to the bottom). The server orders this list, but the thread must
81
+ * never depend on that alone. Resource ids are time-sortable ULIDs
82
+ * (`aex_01k…`), so an ascending id sort is creation order without parsing
83
+ * timestamps; entries missing an id sort last but keep a stable relative order.
84
+ *
85
+ * @internal Exported for testing — not part of the public API.
86
+ */
87
+ export function sortChronologically(
88
+ executions: readonly AgentExecution[],
89
+ ): AgentExecution[] {
90
+ return [...executions].sort((a, b) => {
91
+ const aId = a.metadata?.id ?? "";
92
+ const bId = b.metadata?.id ?? "";
93
+ if (aId === bId) return 0;
94
+ if (!aId) return 1;
95
+ if (!bId) return -1;
96
+ return aId < bId ? -1 : 1;
97
+ });
98
+ }
99
+
74
100
  export function useSessionExecutions(
75
101
  sessionId: string | null,
76
102
  options?: UseSessionExecutionsOptions,
@@ -87,7 +113,7 @@ export function useSessionExecutions(
87
113
  pageSize: 100,
88
114
  }),
89
115
  )
90
- .then((result) => result.entries)
116
+ .then((result) => sortChronologically(result.entries))
91
117
  : null,
92
118
  [sessionId, stigmer],
93
119
  [] as AgentExecution[],
@@ -0,0 +1,173 @@
1
+ "use client";
2
+
3
+ import type { ReactNode } from "react";
4
+ import { ArrowLeft } from "lucide-react";
5
+ import type { Organization } from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/api_pb";
6
+ import type { SettingsNavGroup } from "../settings/settings-nav.js";
7
+ import { SidebarChrome, SidebarSeparator, navRowClassName } from "./chrome.js";
8
+ import type { RenderSidebarLink } from "./types.js";
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Public API
12
+ // ---------------------------------------------------------------------------
13
+
14
+ /** Props for {@link SettingsSidebar}. */
15
+ export interface SettingsSidebarProps {
16
+ /**
17
+ * Grouped settings navigation to render. The console passes
18
+ * `useSettingsNavGroups()` (permission-aware); deterministic hosts
19
+ * pass `SETTINGS_NAV_GROUPS` or a fixture subset.
20
+ */
21
+ readonly groups: readonly SettingsNavGroup[];
22
+ /**
23
+ * Current pathname for active-row matching: a row is active when the
24
+ * path equals its `href` or sits under `href + "/"`. Omit for hosts
25
+ * with no location (deterministic embeds pass the depicted route).
26
+ */
27
+ readonly activePath?: string | null;
28
+ /** Renders each interactive row. See {@link RenderSidebarLink}. */
29
+ readonly renderLink: RenderSidebarLink;
30
+ /**
31
+ * "Back to Sessions" target — the last workspace-zone location, so
32
+ * leaving settings restores where the user was. @default "/"
33
+ */
34
+ readonly backHref?: string;
35
+ /** Footer content — typically the host app's `UserMenu` wrapper. */
36
+ readonly footer: ReactNode;
37
+ /** Reflected as `aria-expanded` on the collapse toggle. @default true */
38
+ readonly isOpen?: boolean;
39
+ /** Called when the collapse toggle is pressed. */
40
+ readonly onCollapse?: () => void;
41
+ /** Passed through to the embedded `OrgSwitcher`. */
42
+ readonly onOrgChanged?: (org: Organization) => void;
43
+ }
44
+
45
+ /**
46
+ * The console's settings-zone (management) sidebar: org switcher, a
47
+ * "Back to Sessions" exit, and grouped settings navigation driven by
48
+ * the SDK's settings-nav model, with a user footer.
49
+ *
50
+ * Like {@link WorkspaceSidebar}, this is the exact component the web
51
+ * console, desktop app, and documentation tours all render; hosts vary
52
+ * only `renderLink`, `groups`, and the `footer` slot.
53
+ *
54
+ * Row identifiers passed to `renderLink` are the settings route
55
+ * basenames (`"api-keys"`, `"members"`, …) plus `"back-to-sessions"`,
56
+ * so consumers can address specific rows without parsing hrefs.
57
+ *
58
+ * @example
59
+ * ```tsx
60
+ * <SettingsSidebar
61
+ * groups={useSettingsNavGroups()}
62
+ * activePath={usePathname()}
63
+ * backHref={lastSessionZonePath ?? "/"}
64
+ * renderLink={({ href, children, ...rest }) => (
65
+ * <Link href={href} {...rest}>{children}</Link>
66
+ * )}
67
+ * footer={<UserMenu />}
68
+ * />
69
+ * ```
70
+ */
71
+ export function SettingsSidebar({
72
+ groups,
73
+ activePath = null,
74
+ renderLink,
75
+ backHref = "/",
76
+ footer,
77
+ isOpen = true,
78
+ onCollapse,
79
+ onOrgChanged,
80
+ }: SettingsSidebarProps) {
81
+ return (
82
+ <SidebarChrome
83
+ ariaLabel="Management navigation"
84
+ isOpen={isOpen}
85
+ onCollapse={onCollapse}
86
+ onOrgChanged={onOrgChanged}
87
+ footer={footer}
88
+ >
89
+ {/* Back to Sessions */}
90
+ <div className="flex-none px-3 py-1">
91
+ {renderLink({
92
+ id: "back-to-sessions",
93
+ href: backHref,
94
+ active: false,
95
+ className: navRowClassName(false, { muted: true }),
96
+ "aria-current": undefined,
97
+ children: (
98
+ <>
99
+ <ArrowLeft className="size-4 shrink-0" />
100
+ Back to Sessions
101
+ </>
102
+ ),
103
+ })}
104
+ </div>
105
+
106
+ <SidebarSeparator />
107
+
108
+ {/* Settings nav links — grouped, permission-aware via `groups` */}
109
+ <div className="flex flex-col gap-4 px-3 py-1">
110
+ {groups.map((group) => (
111
+ <div key={group.label} className="flex flex-col gap-0.5">
112
+ <span className="text-sidebar-muted-foreground px-2 pb-1 text-[11px] font-medium tracking-wider uppercase">
113
+ {group.label}
114
+ </span>
115
+ {group.items.map((item) => {
116
+ const active =
117
+ activePath != null &&
118
+ (activePath === item.href ||
119
+ activePath.startsWith(`${item.href}/`));
120
+
121
+ return (
122
+ <SettingsNavRow
123
+ key={item.href}
124
+ href={item.href}
125
+ label={item.label}
126
+ icon={<item.icon className="size-4 shrink-0" />}
127
+ active={active}
128
+ renderLink={renderLink}
129
+ />
130
+ );
131
+ })}
132
+ </div>
133
+ ))}
134
+ </div>
135
+
136
+ {/* Spacer pushes the footer to the bottom (no scrollable middle). */}
137
+ <div className="flex-1" />
138
+ </SidebarChrome>
139
+ );
140
+ }
141
+
142
+ // ---------------------------------------------------------------------------
143
+ // Internals
144
+ // ---------------------------------------------------------------------------
145
+
146
+ function SettingsNavRow({
147
+ href,
148
+ label,
149
+ icon,
150
+ active,
151
+ renderLink,
152
+ }: {
153
+ readonly href: string;
154
+ readonly label: string;
155
+ readonly icon: ReactNode;
156
+ readonly active: boolean;
157
+ readonly renderLink: RenderSidebarLink;
158
+ }) {
159
+ return renderLink({
160
+ // Route basename as the stable id: "/settings/api-keys" → "api-keys".
161
+ id: href.slice(href.lastIndexOf("/") + 1),
162
+ href,
163
+ active,
164
+ className: navRowClassName(active),
165
+ "aria-current": active ? "page" : undefined,
166
+ children: (
167
+ <>
168
+ {icon}
169
+ {label}
170
+ </>
171
+ ),
172
+ });
173
+ }