@stigmer/react 0.0.68 → 0.0.69

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 (108) hide show
  1. package/README.md +1 -1
  2. package/composer/SessionComposer.d.ts.map +1 -1
  3. package/composer/SessionComposer.js +12 -3
  4. package/composer/SessionComposer.js.map +1 -1
  5. package/demo/__tests__/demo-client.test.d.ts +2 -0
  6. package/demo/__tests__/demo-client.test.d.ts.map +1 -0
  7. package/demo/__tests__/demo-client.test.js +133 -0
  8. package/demo/__tests__/demo-client.test.js.map +1 -0
  9. package/demo/__tests__/fixtures.test.d.ts +2 -0
  10. package/demo/__tests__/fixtures.test.d.ts.map +1 -0
  11. package/demo/__tests__/fixtures.test.js +135 -0
  12. package/demo/__tests__/fixtures.test.js.map +1 -0
  13. package/demo/__tests__/samples.test.d.ts +2 -0
  14. package/demo/__tests__/samples.test.d.ts.map +1 -0
  15. package/demo/__tests__/samples.test.js +152 -0
  16. package/demo/__tests__/samples.test.js.map +1 -0
  17. package/demo/client.d.ts +29 -0
  18. package/demo/client.d.ts.map +1 -0
  19. package/demo/client.js +52 -0
  20. package/demo/client.js.map +1 -0
  21. package/demo/fixtures.d.ts +190 -0
  22. package/demo/fixtures.d.ts.map +1 -0
  23. package/demo/fixtures.js +263 -0
  24. package/demo/fixtures.js.map +1 -0
  25. package/demo/index.d.ts +6 -0
  26. package/demo/index.d.ts.map +1 -0
  27. package/demo/index.js +6 -0
  28. package/demo/index.js.map +1 -0
  29. package/demo/samples.d.ts +166 -0
  30. package/demo/samples.d.ts.map +1 -0
  31. package/demo/samples.js +308 -0
  32. package/demo/samples.js.map +1 -0
  33. package/demo/transport.d.ts +59 -0
  34. package/demo/transport.d.ts.map +1 -0
  35. package/demo/transport.js +75 -0
  36. package/demo/transport.js.map +1 -0
  37. package/demo/types.d.ts +62 -0
  38. package/demo/types.d.ts.map +1 -0
  39. package/demo/types.js +16 -0
  40. package/demo/types.js.map +1 -0
  41. package/environment/EnvVarForm.d.ts.map +1 -1
  42. package/environment/EnvVarForm.js +1 -1
  43. package/environment/EnvVarForm.js.map +1 -1
  44. package/environment/__tests__/systemEnvVars.test.d.ts +2 -0
  45. package/environment/__tests__/systemEnvVars.test.d.ts.map +1 -0
  46. package/environment/__tests__/systemEnvVars.test.js +76 -0
  47. package/environment/__tests__/systemEnvVars.test.js.map +1 -0
  48. package/environment/index.d.ts +1 -0
  49. package/environment/index.d.ts.map +1 -1
  50. package/environment/index.js +1 -0
  51. package/environment/index.js.map +1 -1
  52. package/environment/systemEnvVars.d.ts +52 -0
  53. package/environment/systemEnvVars.d.ts.map +1 -0
  54. package/environment/systemEnvVars.js +91 -0
  55. package/environment/systemEnvVars.js.map +1 -0
  56. package/execution/ApprovalCard.d.ts.map +1 -1
  57. package/execution/ApprovalCard.js +3 -3
  58. package/execution/ApprovalCard.js.map +1 -1
  59. package/index.d.ts +1 -1
  60. package/index.d.ts.map +1 -1
  61. package/index.js +2 -2
  62. package/index.js.map +1 -1
  63. package/internal/Tabs.d.ts +41 -0
  64. package/internal/Tabs.d.ts.map +1 -0
  65. package/internal/Tabs.js +65 -0
  66. package/internal/Tabs.js.map +1 -0
  67. package/mcp-server/McpServerDetailView.d.ts +33 -7
  68. package/mcp-server/McpServerDetailView.d.ts.map +1 -1
  69. package/mcp-server/McpServerDetailView.js +53 -37
  70. package/mcp-server/McpServerDetailView.js.map +1 -1
  71. package/mcp-server/useMcpServerCredentials.d.ts.map +1 -1
  72. package/mcp-server/useMcpServerCredentials.js +2 -1
  73. package/mcp-server/useMcpServerCredentials.js.map +1 -1
  74. package/models/index.d.ts +1 -1
  75. package/models/index.d.ts.map +1 -1
  76. package/models/index.js +1 -1
  77. package/models/index.js.map +1 -1
  78. package/models/registry.d.ts +10 -0
  79. package/models/registry.d.ts.map +1 -1
  80. package/models/registry.js +13 -0
  81. package/models/registry.js.map +1 -1
  82. package/models/useModelRegistry.d.ts.map +1 -1
  83. package/models/useModelRegistry.js +7 -3
  84. package/models/useModelRegistry.js.map +1 -1
  85. package/package.json +9 -5
  86. package/src/composer/SessionComposer.tsx +21 -3
  87. package/src/demo/__tests__/demo-client.test.tsx +213 -0
  88. package/src/demo/__tests__/fixtures.test.ts +214 -0
  89. package/src/demo/__tests__/samples.test.ts +171 -0
  90. package/src/demo/client.ts +78 -0
  91. package/src/demo/fixtures.ts +401 -0
  92. package/src/demo/index.ts +12 -0
  93. package/src/demo/samples.ts +470 -0
  94. package/src/demo/transport.ts +116 -0
  95. package/src/demo/types.ts +69 -0
  96. package/src/environment/EnvVarForm.tsx +1 -0
  97. package/src/environment/__tests__/systemEnvVars.test.ts +120 -0
  98. package/src/environment/index.ts +6 -0
  99. package/src/environment/systemEnvVars.ts +104 -0
  100. package/src/execution/ApprovalCard.tsx +4 -0
  101. package/src/index.ts +5 -1
  102. package/src/internal/Tabs.tsx +166 -0
  103. package/src/mcp-server/McpServerDetailView.tsx +273 -204
  104. package/src/mcp-server/useMcpServerCredentials.ts +4 -1
  105. package/src/models/index.ts +1 -1
  106. package/src/models/registry.ts +14 -0
  107. package/src/models/useModelRegistry.ts +7 -2
  108. package/styles.css +1 -1
@@ -0,0 +1,120 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ toGrpcAddress,
4
+ buildSystemEnvVars,
5
+ SYSTEM_ENV_VAR_KEYS,
6
+ } from "../systemEnvVars";
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // toGrpcAddress
10
+ // ---------------------------------------------------------------------------
11
+
12
+ describe("toGrpcAddress", () => {
13
+ it("extracts host and explicit port from http URL", () => {
14
+ expect(toGrpcAddress("http://localhost:7234")).toBe("localhost:7234");
15
+ });
16
+
17
+ it("defaults to port 443 for https without explicit port", () => {
18
+ expect(toGrpcAddress("https://api.stigmer.ai")).toBe(
19
+ "api.stigmer.ai:443",
20
+ );
21
+ });
22
+
23
+ it("preserves explicit port on https URL", () => {
24
+ expect(toGrpcAddress("https://api.stigmer.ai:8443")).toBe(
25
+ "api.stigmer.ai:8443",
26
+ );
27
+ });
28
+
29
+ it("defaults to port 80 for http without explicit port", () => {
30
+ expect(toGrpcAddress("http://api.local")).toBe("api.local:80");
31
+ });
32
+
33
+ it("handles IPv6 localhost", () => {
34
+ expect(toGrpcAddress("http://[::1]:7234")).toBe("[::1]:7234");
35
+ });
36
+
37
+ it("returns input unchanged for non-URL strings", () => {
38
+ expect(toGrpcAddress("not-a-url")).toBe("not-a-url");
39
+ });
40
+
41
+ it("handles trailing slash", () => {
42
+ expect(toGrpcAddress("http://localhost:7234/")).toBe("localhost:7234");
43
+ });
44
+
45
+ it("strips path components", () => {
46
+ expect(toGrpcAddress("https://api.stigmer.ai/v1/rpc")).toBe(
47
+ "api.stigmer.ai:443",
48
+ );
49
+ });
50
+ });
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // buildSystemEnvVars
54
+ // ---------------------------------------------------------------------------
55
+
56
+ describe("buildSystemEnvVars", () => {
57
+ it("returns both system env vars", () => {
58
+ const result = buildSystemEnvVars(
59
+ "http://localhost:7234",
60
+ "test-token",
61
+ );
62
+
63
+ expect(Object.keys(result)).toHaveLength(2);
64
+ expect(result).toHaveProperty("STIGMER_SERVER_ADDRESS");
65
+ expect(result).toHaveProperty("STIGMER_API_KEY");
66
+ });
67
+
68
+ it("derives gRPC address from baseUrl", () => {
69
+ const result = buildSystemEnvVars(
70
+ "https://api.stigmer.ai",
71
+ "tok",
72
+ );
73
+
74
+ expect(result.STIGMER_SERVER_ADDRESS.value).toBe(
75
+ "api.stigmer.ai:443",
76
+ );
77
+ expect(result.STIGMER_SERVER_ADDRESS.isSecret).toBe(false);
78
+ });
79
+
80
+ it("uses credential as API key value", () => {
81
+ const result = buildSystemEnvVars(
82
+ "http://localhost:7234",
83
+ "my-api-key",
84
+ );
85
+
86
+ expect(result.STIGMER_API_KEY.value).toBe("my-api-key");
87
+ expect(result.STIGMER_API_KEY.isSecret).toBe(true);
88
+ });
89
+
90
+ it('uses "unused" placeholder when credential is null', () => {
91
+ const result = buildSystemEnvVars("http://localhost:7234", null);
92
+
93
+ expect(result.STIGMER_API_KEY.value).toBe("unused");
94
+ });
95
+
96
+ it('uses "unused" placeholder when credential is empty string', () => {
97
+ const result = buildSystemEnvVars("http://localhost:7234", "");
98
+
99
+ expect(result.STIGMER_API_KEY.value).toBe("unused");
100
+ });
101
+
102
+ it("keys match SYSTEM_ENV_VAR_KEYS constant", () => {
103
+ const result = buildSystemEnvVars("http://localhost:7234", "tok");
104
+ const resultKeys = new Set(Object.keys(result));
105
+
106
+ expect(resultKeys).toEqual(SYSTEM_ENV_VAR_KEYS);
107
+ });
108
+ });
109
+
110
+ // ---------------------------------------------------------------------------
111
+ // SYSTEM_ENV_VAR_KEYS
112
+ // ---------------------------------------------------------------------------
113
+
114
+ describe("SYSTEM_ENV_VAR_KEYS", () => {
115
+ it("contains exactly the two expected keys", () => {
116
+ expect(SYSTEM_ENV_VAR_KEYS.size).toBe(2);
117
+ expect(SYSTEM_ENV_VAR_KEYS.has("STIGMER_SERVER_ADDRESS")).toBe(true);
118
+ expect(SYSTEM_ENV_VAR_KEYS.has("STIGMER_API_KEY")).toBe(true);
119
+ });
120
+ });
@@ -41,3 +41,9 @@ export type {
41
41
  SessionEnvPoolInput,
42
42
  UseSessionEnvPoolReturn,
43
43
  } from "./useSessionEnvPool";
44
+ export {
45
+ SYSTEM_ENV_VAR_KEYS,
46
+ toGrpcAddress,
47
+ buildSystemEnvVars,
48
+ resolveSystemEnvVarValues,
49
+ } from "./systemEnvVars";
@@ -0,0 +1,104 @@
1
+ import type { EnvVarInput, Stigmer } from "@stigmer/sdk";
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Well-known Stigmer platform environment variable keys
5
+ //
6
+ // These env vars configure MCP server subprocesses (and agents) to
7
+ // communicate back to the Stigmer backend. Because the SDK client
8
+ // already knows the server address and auth credential, the setup
9
+ // hooks can skip prompting users for these values and inject them
10
+ // automatically at session creation time.
11
+ // ---------------------------------------------------------------------------
12
+
13
+ const STIGMER_SERVER_ADDRESS = "STIGMER_SERVER_ADDRESS";
14
+ const STIGMER_API_KEY = "STIGMER_API_KEY";
15
+
16
+ /**
17
+ * Environment variable keys that the SDK can resolve automatically
18
+ * from the current {@link Stigmer} client context.
19
+ *
20
+ * Used by setup hooks to exclude these keys from the "missing
21
+ * variables" prompt and by the session composer to inject their
22
+ * values into runtime env at submit time.
23
+ *
24
+ * Platform builders who manage setup hooks directly can use this
25
+ * set to extend their own `poolKeys`.
26
+ */
27
+ export const SYSTEM_ENV_VAR_KEYS: ReadonlySet<string> = new Set([
28
+ STIGMER_SERVER_ADDRESS,
29
+ STIGMER_API_KEY,
30
+ ]);
31
+
32
+ /**
33
+ * Convert an HTTP(S) base URL to a gRPC host:port address.
34
+ *
35
+ * The Stigmer server serves both gRPC and gRPC-Web on the same
36
+ * endpoint, so stripping the protocol and extracting host:port
37
+ * produces a valid gRPC dial target.
38
+ *
39
+ * @example
40
+ * ```ts
41
+ * toGrpcAddress("http://localhost:7234") // "localhost:7234"
42
+ * toGrpcAddress("https://api.stigmer.ai") // "api.stigmer.ai:443"
43
+ * toGrpcAddress("https://api.stigmer.ai:8443") // "api.stigmer.ai:8443"
44
+ * ```
45
+ */
46
+ export function toGrpcAddress(httpUrl: string): string {
47
+ try {
48
+ const url = new URL(httpUrl);
49
+ const host = url.hostname;
50
+ const port =
51
+ url.port || (url.protocol === "https:" ? "443" : "80");
52
+ return `${host}:${port}`;
53
+ } catch {
54
+ return httpUrl;
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Build system env var entries from raw connection parameters.
60
+ *
61
+ * Pure function — no side effects, no async. Suitable for unit
62
+ * testing without a live Stigmer client.
63
+ *
64
+ * @param baseUrl - The Stigmer client's base URL (HTTP).
65
+ * @param credential - Current auth credential, or `null` for
66
+ * unauthenticated (OSS) backends. When `null`, a placeholder
67
+ * value is used so the MCP server env var is always populated.
68
+ */
69
+ export function buildSystemEnvVars(
70
+ baseUrl: string,
71
+ credential: string | null,
72
+ ): Record<string, EnvVarInput> {
73
+ return {
74
+ [STIGMER_SERVER_ADDRESS]: {
75
+ value: toGrpcAddress(baseUrl),
76
+ isSecret: false,
77
+ description:
78
+ "Auto-resolved from the current Stigmer connection.",
79
+ },
80
+ [STIGMER_API_KEY]: {
81
+ value: credential || "unused",
82
+ isSecret: true,
83
+ description:
84
+ "Auto-resolved from the current Stigmer auth context.",
85
+ },
86
+ };
87
+ }
88
+
89
+ /**
90
+ * Resolve system env var values from a live {@link Stigmer} client.
91
+ *
92
+ * Calls {@link Stigmer.getAuthCredential} to obtain the current
93
+ * credential, then delegates to {@link buildSystemEnvVars}.
94
+ *
95
+ * Intended for use at session submit time — the returned values
96
+ * are injected into `runtimeEnv` at the **lowest priority** so
97
+ * any user-provided values (personal env, manual secrets) win.
98
+ */
99
+ export async function resolveSystemEnvVarValues(
100
+ stigmer: Stigmer,
101
+ ): Promise<Record<string, EnvVarInput>> {
102
+ const credential = await stigmer.getAuthCredential();
103
+ return buildSystemEnvVars(stigmer.baseUrl, credential);
104
+ }
@@ -173,6 +173,7 @@ export function ApprovalCard({
173
173
  isSubmitting={isSubmitting}
174
174
  onClick={handleAction}
175
175
  variant="approve"
176
+ cursorTarget="approve-button"
176
177
  />
177
178
  <ActionButton
178
179
  label="Skip"
@@ -207,6 +208,7 @@ function ActionButton({
207
208
  isSubmitting,
208
209
  onClick,
209
210
  variant,
211
+ cursorTarget,
210
212
  }: {
211
213
  label: string;
212
214
  action: ApprovalAction;
@@ -214,6 +216,7 @@ function ActionButton({
214
216
  isSubmitting: boolean;
215
217
  onClick: (action: ApprovalAction) => void;
216
218
  variant: "approve" | "skip" | "reject";
219
+ cursorTarget?: string;
217
220
  }) {
218
221
  const isActive = activeAction === action;
219
222
  const disabled = isSubmitting;
@@ -239,6 +242,7 @@ function ActionButton({
239
242
  disabled={disabled}
240
243
  onClick={() => onClick(action)}
241
244
  aria-label={label}
245
+ data-cursor-target={cursorTarget}
242
246
  className={cn(
243
247
  "inline-flex items-center justify-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium transition-colors",
244
248
  "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
package/src/index.ts CHANGED
@@ -327,7 +327,7 @@ export type {
327
327
  UseDefaultAgentReturn,
328
328
  } from "./agent";
329
329
 
330
- // Environment — data hooks, list hook, personal convenience hook, secret reveal, variable management, env var form, and styled components
330
+ // Environment — data hooks, list hook, personal convenience hook, secret reveal, variable management, env var form, system env vars, and styled components
331
331
  export {
332
332
  useEnvironment,
333
333
  useEnvironmentList,
@@ -342,6 +342,10 @@ export {
342
342
  CreateEnvironmentForm,
343
343
  EnvVarForm,
344
344
  useSessionEnvPool,
345
+ SYSTEM_ENV_VAR_KEYS,
346
+ toGrpcAddress,
347
+ buildSystemEnvVars,
348
+ resolveSystemEnvVarValues,
345
349
  } from "./environment";
346
350
  export type {
347
351
  UseEnvironmentReturn,
@@ -0,0 +1,166 @@
1
+ "use client";
2
+
3
+ import { useCallback, useId, useRef, type KeyboardEvent, type ReactNode } from "react";
4
+ import { cn } from "@stigmer/theme";
5
+
6
+ export interface TabItem {
7
+ /** Unique identifier for the tab, used as the `activeTab` value. */
8
+ readonly id: string;
9
+ /** Display label shown in the tab trigger. */
10
+ readonly label: string;
11
+ /** Optional numeric badge rendered next to the label (e.g. item count). */
12
+ readonly badge?: number;
13
+ }
14
+
15
+ export interface TabsProps {
16
+ /** Ordered list of tabs to render. Tabs with no matching content are still clickable. */
17
+ readonly tabs: readonly TabItem[];
18
+ /** The `id` of the currently active tab. */
19
+ readonly activeTab: string;
20
+ /** Called when the user selects a different tab. */
21
+ readonly onTabChange: (tabId: string) => void;
22
+ /**
23
+ * Content to render in the active tab panel.
24
+ * Typically a switch/map over `activeTab`.
25
+ */
26
+ readonly children: ReactNode;
27
+ /** Accessible label for the tab list (e.g. "Capability sections"). */
28
+ readonly "aria-label"?: string;
29
+ /** Additional CSS classes for the root container. */
30
+ readonly className?: string;
31
+ }
32
+
33
+ /**
34
+ * Accessible tabbed panel with badge support.
35
+ *
36
+ * Implements the WAI-ARIA Tabs pattern:
37
+ * - `role="tablist"` / `role="tab"` / `role="tabpanel"`
38
+ * - Arrow-key navigation (Left/Right), Home/End to jump
39
+ * - `aria-selected`, `aria-controls`, `aria-labelledby` wiring
40
+ *
41
+ * All visual properties flow through `--stgm-*` design tokens.
42
+ * No external dependencies — safe for platform builder embedding.
43
+ *
44
+ * @internal — not yet part of the public `@stigmer/react` API.
45
+ */
46
+ export function Tabs({
47
+ tabs,
48
+ activeTab,
49
+ onTabChange,
50
+ children,
51
+ "aria-label": ariaLabel,
52
+ className,
53
+ }: TabsProps) {
54
+ const instanceId = useId();
55
+ const tabRefsMap = useRef<Map<string, HTMLButtonElement>>(new Map());
56
+
57
+ const tabId = (id: string) => `${instanceId}-tab-${id}`;
58
+ const panelId = (id: string) => `${instanceId}-panel-${id}`;
59
+
60
+ const focusTab = useCallback(
61
+ (id: string) => {
62
+ tabRefsMap.current.get(id)?.focus();
63
+ onTabChange(id);
64
+ },
65
+ [onTabChange],
66
+ );
67
+
68
+ const handleKeyDown = useCallback(
69
+ (e: KeyboardEvent<HTMLDivElement>) => {
70
+ const currentIndex = tabs.findIndex((t) => t.id === activeTab);
71
+ if (currentIndex === -1) return;
72
+
73
+ let nextIndex: number | null = null;
74
+
75
+ switch (e.key) {
76
+ case "ArrowRight":
77
+ nextIndex = (currentIndex + 1) % tabs.length;
78
+ break;
79
+ case "ArrowLeft":
80
+ nextIndex = (currentIndex - 1 + tabs.length) % tabs.length;
81
+ break;
82
+ case "Home":
83
+ nextIndex = 0;
84
+ break;
85
+ case "End":
86
+ nextIndex = tabs.length - 1;
87
+ break;
88
+ default:
89
+ return;
90
+ }
91
+
92
+ e.preventDefault();
93
+ focusTab(tabs[nextIndex].id);
94
+ },
95
+ [tabs, activeTab, focusTab],
96
+ );
97
+
98
+ return (
99
+ <div className={cn("flex flex-col", className)}>
100
+ <div
101
+ role="tablist"
102
+ aria-label={ariaLabel}
103
+ onKeyDown={handleKeyDown}
104
+ className="flex border-b border-border"
105
+ >
106
+ {tabs.map((tab) => {
107
+ const isActive = tab.id === activeTab;
108
+ return (
109
+ <button
110
+ key={tab.id}
111
+ ref={(el) => {
112
+ if (el) tabRefsMap.current.set(tab.id, el);
113
+ else tabRefsMap.current.delete(tab.id);
114
+ }}
115
+ id={tabId(tab.id)}
116
+ role="tab"
117
+ type="button"
118
+ aria-selected={isActive}
119
+ aria-controls={panelId(tab.id)}
120
+ tabIndex={isActive ? 0 : -1}
121
+ onClick={() => onTabChange(tab.id)}
122
+ data-cursor-target={`tab-${tab.id}`}
123
+ className={cn(
124
+ "relative inline-flex items-center gap-1.5 px-3 py-2 text-xs font-medium transition-colors",
125
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1",
126
+ isActive
127
+ ? "text-foreground"
128
+ : "text-muted-foreground hover:text-foreground",
129
+ )}
130
+ >
131
+ {tab.label}
132
+ {tab.badge != null && tab.badge > 0 && (
133
+ <span
134
+ className={cn(
135
+ "inline-flex min-w-[1.25rem] items-center justify-center rounded-full px-1 py-px text-[10px] font-medium leading-none",
136
+ isActive
137
+ ? "bg-primary/10 text-primary"
138
+ : "bg-muted text-muted-foreground",
139
+ )}
140
+ >
141
+ {tab.badge}
142
+ </span>
143
+ )}
144
+ {isActive && (
145
+ <span
146
+ className="absolute inset-x-0 -bottom-px h-0.5 bg-primary"
147
+ aria-hidden="true"
148
+ />
149
+ )}
150
+ </button>
151
+ );
152
+ })}
153
+ </div>
154
+
155
+ <div
156
+ id={panelId(activeTab)}
157
+ role="tabpanel"
158
+ aria-labelledby={tabId(activeTab)}
159
+ tabIndex={0}
160
+ className="focus-visible:outline-none"
161
+ >
162
+ {children}
163
+ </div>
164
+ </div>
165
+ );
166
+ }